@gangfun/cli 0.1.4 → 0.2.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
@@ -1,8 +1,8 @@
1
1
  # Gang CLI (`gang`)
2
2
 
3
- Operate [gang.fun](https://gang.fun) from a Binance **Agentic Wallet**. Curve buys require a SPACE ID `.ai` Agent ID. This CLI builds calldata; `baw` signs — no keys in this package.
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
- Not an open-source product dump: install and run steps only.
5
+ **Create path (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.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,36 +10,23 @@ Node ≥ 18. Also install Binance Agentic Wallet CLI (`baw`):
10
10
 
11
11
  ```bash
12
12
  npm i -g @binance/agentic-wallet
13
- npm i -g @gangfun/cli@0.1.3 --registry=https://npm.pkg.github.com
13
+ npm i -g @gangfun/cli@0.2.1 --registry=https://npm.pkg.github.com
14
14
  ```
15
15
 
16
- Package: [GitHub Packages `@gangfun/cli`](https://github.com/gangfun/cli/pkgs/npm/cli). GitHub Packages needs auth — one-liner: `npm login --registry=https://npm.pkg.github.com` (use a token with `read:packages`). Or put the token in `~/.npmrc` (or `NODE_AUTH_TOKEN`):
16
+ Package: [GitHub Packages `@gangfun/cli`](https://github.com/gangfun/cli/pkgs/npm/cli). Auth:
17
17
 
18
18
  ```ini
19
19
  @gangfun:registry=https://npm.pkg.github.com
20
20
  //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
21
21
  ```
22
22
 
23
- Then you can omit the `--registry` flag: `npm i -g @gangfun/cli@0.1.3`.
24
-
25
- **Alternate** (no Packages token; installs from the public git repo `main` tip = `@gangfun/cli@0.1.3` / Launchpad v4.1):
26
-
27
- ```bash
28
- npm i -g github:gangfun/cli
29
- # or one-shot:
30
- npx --yes github:gangfun/cli preflight
31
- ```
32
-
33
- If a previous global is stale, clear it then reinstall:
23
+ **Alternate** (public git tip):
34
24
 
35
25
  ```bash
36
- npm uninstall -g @gangfun/cli
37
26
  npm i -g github:gangfun/cli
38
27
  ```
39
28
 
40
- After install, run `gang preflight` — Launchpad must be `0x315Ba2e8952D06094CC636b2B84229FA32fb23A7` (v4.1). If you still see `0x86854…dDcc`, the old global is still in use; uninstall and reinstall.
41
-
42
- Skill install (Claude Code / Cursor / Codex / OpenClaw / Binance AI Pro):
29
+ Skill:
43
30
 
44
31
  ```bash
45
32
  npx skills add gangfun/gang-launchpad/skill
@@ -50,7 +37,7 @@ npx skills add gangfun/gang-launchpad/skill
50
37
  1. Binance App → Wallet → Agentic Wallet → create; claim a `.ai` Agent ID.
51
38
  2. Enable **Developer Mode** and **Trade all tokens**.
52
39
  3. `baw auth signin` → confirm in the App.
53
- 4. `gang preflight`
40
+ 4. `gang preflight` — `createPad` must be LaunchpadV2.
54
41
 
55
42
  ## Commands
56
43
 
@@ -58,20 +45,45 @@ npx skills add gangfun/gang-launchpad/skill
58
45
  gang preflight
59
46
  gang list
60
47
  gang token --token <addr>
61
- gang quote --token <addr> --bnb <n>
48
+ gang quote --token <addr> --bnb <n> # Soft archive: BNB; V2: quote ERC-20 (LaunchpadV2.sol math)
62
49
  gang buy --token <addr> --bnb <n> [--slippage 1]
63
50
  gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1]
64
- gang create --name <n> --symbol <s>
51
+ gang upload-logo --file ./avatar.png # → https://gang.fun/api/logos/<id>.png
52
+ gang create --name <n> --symbol <s> [--logo-uri URL | --logo ./avatar.png] [--taxed] [--pair-or-quote 0x…]
65
53
  gang execute --request-id <id>
66
54
  gang tx --hash <0x..>
67
55
  ```
68
56
 
69
- Buy / sell / create: **preview → you confirm → execute**. `requestId` expires in ~5 minutes. Sells may need App confirmation.
57
+ ### Logo + create
58
+
59
+ Browser UI: https://gang.fun/dev/logo (pre-create tab).
60
+
61
+ ```bash
62
+ # Option A — upload then create
63
+ gang upload-logo --file ./avatar.png
64
+ # → { "logoUrl": "https://gang.fun/api/logos/<id>.png", ... }
65
+ gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/logos/<id>.png
66
+ gang execute --request-id …
67
+
68
+ # Option B — create uploads for you
69
+ gang create --name "My Agent Coin" --symbol MAC --logo ./avatar.png
70
+ gang execute --request-id …
71
+ ```
72
+
73
+ `--logo-uri ""` / omit logo → empty `logoURI` on-chain (allowed). `--taxed` requires `--pair-or-quote`. Untaxed default: `taxed=false`, `pairOrQuote=0x0` (WBNB).
74
+
75
+ Buy / sell / create: **preview → you confirm → execute**. `requestId` expires in ~5 minutes.
76
+
77
+ ## Quote path
78
+
79
+ - **Soft archive tokens:** on-chain `quoteBuy` / `quoteSell`.
80
+ - **LaunchpadV2 (and U/S-shaped pads for legacy trade):** unique off-chain path = `LaunchpadV2.sol` `_prepBuy` / `_tokensOut` / `_quoteOut` (1% fee, wallet cap, graduation clamp). No Soft/U/S create/quote fallback. Output fields `quotePath` + `deterministic: true`.
70
81
 
71
82
  ## Rules (short)
72
83
 
73
- - 1% per-wallet cap on the curve; excess BNB refunded in-tx.
74
- - 20 BNB → graduate to PancakeSwap V2, LP burned.
84
+ - 1% per-wallet cap on the curve; excess input refunded in-tx.
85
+ - **New** creates → ≈15 BNB graduation (LaunchpadV2 / PoolCheck) → PancakeSwap V2, LP burned.
86
+ - Soft legacy (archive): 20 BNB graduation; buy/sell only — no Soft create from this CLI.
75
87
  - Before graduation: no transfers, curve only.
76
88
  - Graduated: trade on PancakeSwap (`pair` from `gang token`).
77
89
 
@@ -79,9 +91,10 @@ Buy / sell / create: **preview → you confirm → execute**. `requestId` expire
79
91
 
80
92
  | Var | Meaning |
81
93
  |---|---|
82
- | `GANG_LAUNCHPAD` | Launchpad address (default v4.1 `0x315Ba2e8952D06094CC636b2B84229FA32fb23A7`; override via env) |
94
+ | `GANG_LAUNCHPAD` | Create target (default LaunchpadV2 `0x1B8F3f69…966E`) |
83
95
  | `GANG_RPC_URL` | BSC HTTP RPC |
96
+ | `GANG_LOGO_API` | Logo upload endpoint (default `https://gang.fun/api/logos`) |
84
97
 
85
98
  ## Version
86
99
 
87
- `0.1.3` · repo [gangfun/cli](https://github.com/gangfun/cli) · distribute via [GitHub Packages](https://github.com/gangfun/cli/pkgs/npm/cli) (not npmjs)
100
+ `0.2.1` · repo [gangfun/cli](https://github.com/gangfun/cli) · distribute via [GitHub Packages](https://github.com/gangfun/cli/pkgs/npm/cli) (not npmjs)
package/README.zh-CN.md CHANGED
@@ -1,56 +1,28 @@
1
1
  # Gang CLI(`gang`)
2
2
 
3
- 用 Binance **Agentic Wallet** 操作 [gang.fun](https://gang.fun)。内盘买入需要 SPACE ID `.ai` Agent ID。本 CLI 只组 calldata,签名走 `baw`,不持有私钥。
3
+ 用 Binance **Agentic Wallet** 操作 [gang.fun](https://gang.fun)。内盘买入/发币需要 SPACE ID `.ai` Agent ID。本 CLI 只组 calldata,签名走 `baw`,不持有私钥。
4
4
 
5
- 这里只写操作步骤,不是开源说明书。
5
+ **发币路径(0.2.1+):** 仅 LaunchpadV2(`0x1B8F3f69DFa0a7F63d308C7EB0Fd38D8b099966E`)。Soft / LaunchpadU / LaunchpadS:**禁止新 create**,仅存量 buy/sell/claim。新币毕业 ≈ **15 BNB**(PoolCheck)。Soft 存量链上仍为 20 BNB 毕业。
6
6
 
7
7
  ## 安装
8
8
 
9
- Node ≥ 18。另需安装 Binance Agentic Wallet CLI(`baw`):
9
+ Node ≥ 18。另需 `baw`:
10
10
 
11
11
  ```bash
12
12
  npm i -g @binance/agentic-wallet
13
- npm i -g @gangfun/cli@0.1.3 --registry=https://npm.pkg.github.com
13
+ npm i -g @gangfun/cli@0.2.1 --registry=https://npm.pkg.github.com
14
14
  ```
15
15
 
16
- 包地址:[GitHub Packages `@gangfun/cli`](https://github.com/gangfun/cli/pkgs/npm/cli)。GitHub Packages 需要登录——一句:`npm login --registry=https://npm.pkg.github.com`(token 需带 `read:packages`)。或把 token 写进 `~/.npmrc`(或环境变量 `NODE_AUTH_TOKEN`):
16
+ 或:`npm i -g github:gangfun/cli`
17
17
 
18
- ```ini
19
- @gangfun:registry=https://npm.pkg.github.com
20
- //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
21
- ```
22
-
23
- 配好后可省略 `--registry`:`npm i -g @gangfun/cli@0.1.3`。
24
-
25
- **备选**(不需要 Packages token;从公开 git 仓 `main` tip 装 = `@gangfun/cli@0.1.3` / Launchpad v4.1):
26
-
27
- ```bash
28
- npm i -g github:gangfun/cli
29
- # 或一次性:
30
- npx --yes github:gangfun/cli preflight
31
- ```
32
-
33
- 若之前装过旧全局包,先卸再装:
34
-
35
- ```bash
36
- npm uninstall -g @gangfun/cli
37
- npm i -g github:gangfun/cli
38
- ```
39
-
40
- 装完跑 `gang preflight`——Launchpad 必须是 `0x315Ba2e8952D06094CC636b2B84229FA32fb23A7`(v4.1)。若仍看到 `0x86854…dDcc`,说明旧全局还在,卸掉重装。
41
-
42
- Skill(Claude Code / Cursor / Codex / OpenClaw / Binance AI Pro):
43
-
44
- ```bash
45
- npx skills add gangfun/gang-launchpad/skill
46
- ```
18
+ Skill:`npx skills add gangfun/gang-launchpad/skill`
47
19
 
48
20
  ## 准备
49
21
 
50
- 1. Binance App → 钱包 → Agentic Wallet → 创建;领取 `.ai` Agent ID。
51
- 2. 打开 **Developer Mode** 和 **Trade all tokens**。
52
- 3. `baw auth signin`,在 App 确认。
53
- 4. `gang preflight`
22
+ 1. 币安 App → 钱包 → Agentic Wallet;领取 `.ai` Agent ID。
23
+ 2. 打开 **Developer Mode** 与 **Trade all tokens**。
24
+ 3. `baw auth signin`
25
+ 4. `gang preflight` — `createPad` 须为 LaunchpadV2。
54
26
 
55
27
  ## 命令
56
28
 
@@ -61,27 +33,45 @@ gang token --token <addr>
61
33
  gang quote --token <addr> --bnb <n>
62
34
  gang buy --token <addr> --bnb <n> [--slippage 1]
63
35
  gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1]
64
- gang create --name <n> --symbol <s>
36
+ gang upload-logo --file ./avatar.png
37
+ gang create --name <n> --symbol <s> [--logo-uri URL | --logo ./avatar.png] [--taxed] [--pair-or-quote 0x…]
65
38
  gang execute --request-id <id>
66
39
  gang tx --hash <0x..>
67
40
  ```
68
41
 
69
- 买 / 卖 / 发币:**预览 → 你确认 → execute**。`requestId` 约 5 分钟过期。卖出可能要在 App 里点确认。
42
+ ### Logo + 发币
43
+
44
+ 站内:https://gang.fun/dev/logo
45
+
46
+ ```bash
47
+ gang upload-logo --file ./avatar.png
48
+ gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/logos/<id>.png
49
+ gang execute --request-id …
50
+
51
+ # 或一步上传
52
+ gang create --name "My Agent Coin" --symbol MAC --logo ./avatar.png
53
+ gang execute --request-id …
54
+ ```
55
+
56
+ ## Quote 路径
57
+
58
+ - Soft 存量:链上 `quoteBuy` / `quoteSell`。
59
+ - **LaunchpadV2:** 唯一路径 = 合约 `LaunchpadV2.sol` 曲线公式(`_prepBuy` / `_tokensOut` / `_quoteOut`),无 Soft/U/S create/quote fallback;输出 `quotePath` + `deterministic: true`。
70
60
 
71
61
  ## 规则(短)
72
62
 
73
- - 内盘每钱包 1% 上限;多付的 BNB 同笔退回。
74
- - 筹满 20 BNB → 毕业到 PancakeSwap V2,LP 销毁。
75
- - 毕业前不可转账,只能走曲线。
76
- - 已毕业:去 PancakeSwap(`gang token` 里的 `pair`)。
63
+ - 内盘每钱包 1%;多付同笔退回。
64
+ - **新**发币 ≈15 BNB 毕业(LaunchpadV2)。Soft 存量归档:20 BNB,仅买卖。
65
+ - 毕业前不可转账;毕业后走 PancakeSwap。
77
66
 
78
67
  ## 环境变量
79
68
 
80
69
  | 变量 | 含义 |
81
70
  |---|---|
82
- | `GANG_LAUNCHPAD` | Launchpad 地址(默认 v4.1 `0x315Ba2e8952D06094CC636b2B84229FA32fb23A7`;可用环境变量覆盖) |
83
- | `GANG_RPC_URL` | BSC HTTP RPC |
71
+ | `GANG_LAUNCHPAD` | 发币目标(默认 V2) |
72
+ | `GANG_RPC_URL` | BSC RPC |
73
+ | `GANG_LOGO_API` | Logo 上传 API(默认 `https://gang.fun/api/logos`) |
84
74
 
85
75
  ## 版本
86
76
 
87
- `0.1.3` · 仓库 [gangfun/cli](https://github.com/gangfun/cli) · 分发走 [GitHub Packages](https://github.com/gangfun/cli/pkgs/npm/cli)(不上 npmjs)
77
+ `0.2.1` · [gangfun/cli](https://github.com/gangfun/cli)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gangfun/cli",
3
- "version": "0.1.4",
4
- "description": "Gang launchpad CLI for Binance Agentic Wallet agents: list, quote, buy, sell, create on the agent-only bonding curve.",
3
+ "version": "0.2.1",
4
+ "description": "Gang launchpad CLI (LaunchpadV2-only create + legacy Soft/U/S trade) for Binance Agentic Wallet agents.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "gang": "./src/index.js"
package/src/abi.js CHANGED
@@ -1,8 +1,7 @@
1
- export const LAUNCHPAD_ABI = [
1
+ /** Soft / legacy BNB-payable pad (create archived; buy/sell/claim only). */
2
+ export const SOFT_ABI = [
2
3
  { type: 'function', name: 'create', stateMutability: 'nonpayable', inputs: [{ name: 'name', type: 'string' }, { name: 'symbol', type: 'string' }], outputs: [{ type: 'address' }] },
3
4
  { type: 'function', name: 'buy', stateMutability: 'payable', inputs: [{ name: 'token', type: 'address' }, { name: 'minTokensOut', type: 'uint256' }], outputs: [{ type: 'uint256' }] },
4
- { type: 'function', name: 'withdraw', stateMutability: 'nonpayable', inputs: [], outputs: [] },
5
- { type: 'function', name: 'pendingWithdrawals', stateMutability: 'view', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] },
6
5
  { type: 'function', name: 'sell', stateMutability: 'nonpayable', inputs: [{ name: 'token', type: 'address' }, { name: 'tokensIn', type: 'uint256' }, { name: 'minBnbOut', type: 'uint256' }], outputs: [{ type: 'uint256' }] },
7
6
  { type: 'function', name: 'quoteBuy', stateMutability: 'view', inputs: [{ name: 'token', type: 'address' }, { name: 'buyer', type: 'address' }, { name: 'bnbGross', type: 'uint256' }], outputs: [{ name: 'tokensOut', type: 'uint256' }, { name: 'bnbIn', type: 'uint256' }, { name: 'fee', type: 'uint256' }, { name: 'refund', type: 'uint256' }] },
8
7
  { type: 'function', name: 'quoteSell', stateMutability: 'view', inputs: [{ name: 'token', type: 'address' }, { name: 'tokensIn', type: 'uint256' }], outputs: [{ name: 'bnbToUser', type: 'uint256' }, { name: 'fee', type: 'uint256' }] },
@@ -20,11 +19,54 @@ export const LAUNCHPAD_ABI = [
20
19
  { type: 'event', name: 'Graduated', inputs: [{ name: 'token', type: 'address', indexed: true }, { name: 'pair', type: 'address', indexed: true }, { name: 'tokenAmount', type: 'uint256', indexed: false }, { name: 'bnbAmount', type: 'uint256', indexed: false }] },
21
20
  ]
22
21
 
22
+ /** LaunchpadV2 (+ U/S-shaped quote pads): create with logoURI; buy/sell take quote ERC-20. */
23
+ export const V2_ABI = [
24
+ { type: 'function', name: 'create', stateMutability: 'nonpayable', inputs: [{ name: 'name', type: 'string' }, { name: 'symbol', type: 'string' }, { name: 'logoURI', type: 'string' }, { name: 'taxed', type: 'bool' }, { name: 'pairOrQuote', type: 'address' }], outputs: [{ type: 'address' }] },
25
+ { type: 'function', name: 'buy', stateMutability: 'nonpayable', inputs: [{ name: 'token', type: 'address' }, { name: 'quoteIn', type: 'uint256' }, { name: 'minTokensOut', type: 'uint256' }], outputs: [{ type: 'uint256' }] },
26
+ { type: 'function', name: 'sell', stateMutability: 'nonpayable', inputs: [{ name: 'token', type: 'address' }, { name: 'tokensIn', type: 'uint256' }, { name: 'minQuoteOut', type: 'uint256' }], outputs: [{ type: 'uint256' }] },
27
+ { type: 'function', name: 'curves', stateMutability: 'view', inputs: [{ name: 'token', type: 'address' }], outputs: [
28
+ { name: 'creator', type: 'address' }, { name: 'quote', type: 'address' }, { name: 'realQuote', type: 'uint128' }, { name: 'sold', type: 'uint128' },
29
+ { name: 'gradAmt', type: 'uint128' }, { name: 'virtualQuote', type: 'uint128' }, { name: 'graduated', type: 'bool' }, { name: 'taxed', type: 'bool' },
30
+ { name: 'pair', type: 'address' }, { name: 'refPair', type: 'address' },
31
+ ] },
32
+ { type: 'function', name: 'allTokens', stateMutability: 'view', inputs: [{ type: 'uint256' }], outputs: [{ type: 'address' }] },
33
+ { type: 'function', name: 'tokenCount', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
34
+ { type: 'function', name: 'isAgent', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'bool' }] },
35
+ { type: 'function', name: 'walletRoom', stateMutability: 'view', inputs: [{ name: 'token', type: 'address' }, { name: 'buyer', type: 'address' }], outputs: [{ type: 'uint256' }] },
36
+ { type: 'function', name: 'MAX_PER_WALLET', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
37
+ { type: 'function', name: 'TARGET_GRAD_BNB', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
38
+ { type: 'function', name: 'logoURIOf', stateMutability: 'view', inputs: [{ type: 'address' }], outputs: [{ type: 'string' }] },
39
+ { type: 'event', name: 'Created', inputs: [
40
+ { name: 'token', type: 'address', indexed: true }, { name: 'creator', type: 'address', indexed: true },
41
+ { name: 'name', type: 'string', indexed: false }, { name: 'symbol', type: 'string', indexed: false },
42
+ { name: 'logoURI', type: 'string', indexed: false }, { name: 'taxed', type: 'bool', indexed: false },
43
+ { name: 'quote', type: 'address', indexed: true }, { name: 'refPair', type: 'address', indexed: false }, { name: 'gradAmt', type: 'uint256', indexed: false },
44
+ ] },
45
+ { type: 'event', name: 'Buy', inputs: [
46
+ { name: 'token', type: 'address', indexed: true }, { name: 'buyer', type: 'address', indexed: true },
47
+ { name: 'quoteIn', type: 'uint256', indexed: false }, { name: 'taxDividend', type: 'uint256', indexed: false },
48
+ { name: 'taxPlatform', type: 'uint256', indexed: false }, { name: 'tokensOut', type: 'uint256', indexed: false }, { name: 'realQuote', type: 'uint256', indexed: false },
49
+ ] },
50
+ { type: 'event', name: 'Sell', inputs: [
51
+ { name: 'token', type: 'address', indexed: true }, { name: 'seller', type: 'address', indexed: true },
52
+ { name: 'tokensIn', type: 'uint256', indexed: false }, { name: 'taxDividend', type: 'uint256', indexed: false },
53
+ { name: 'taxPlatform', type: 'uint256', indexed: false }, { name: 'quoteOut', type: 'uint256', indexed: false }, { name: 'realQuote', type: 'uint256', indexed: false },
54
+ ] },
55
+ { type: 'event', name: 'Graduated', inputs: [
56
+ { name: 'token', type: 'address', indexed: true }, { name: 'pair', type: 'address', indexed: true },
57
+ { name: 'quote', type: 'address', indexed: false }, { name: 'tokenAmount', type: 'uint256', indexed: false }, { name: 'quoteAmount', type: 'uint256', indexed: false },
58
+ ] },
59
+ ]
60
+
23
61
  export const ERC20_ABI = [
24
62
  { type: 'function', name: 'name', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
25
63
  { type: 'function', name: 'symbol', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
64
+ { type: 'function', name: 'decimals', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] },
26
65
  { type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] },
27
66
  { type: 'function', name: 'allowance', stateMutability: 'view', inputs: [{ type: 'address' }, { type: 'address' }], outputs: [{ type: 'uint256' }] },
28
67
  { type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [{ type: 'address' }, { type: 'uint256' }], outputs: [{ type: 'bool' }] },
29
68
  { type: 'function', name: 'locked', stateMutability: 'view', inputs: [], outputs: [{ type: 'bool' }] },
30
69
  ]
70
+
71
+ /** @deprecated alias kept for older imports */
72
+ export const LAUNCHPAD_ABI = V2_ABI
package/src/chain.js CHANGED
@@ -1,89 +1,280 @@
1
- import { createPublicClient, http, formatEther, parseEther, encodeFunctionData, decodeEventLog } from 'viem'
1
+ import { createPublicClient, http, formatEther, parseEther, encodeFunctionData, decodeEventLog, zeroAddress } from 'viem'
2
2
  import { bsc } from 'viem/chains'
3
- import { LAUNCHPAD_ABI, ERC20_ABI } from './abi.js'
3
+ import { SOFT_ABI, V2_ABI, ERC20_ABI } from './abi.js'
4
+
5
+ export const ADDR = {
6
+ v2: '0x1B8F3f69DFa0a7F63d308C7EB0Fd38D8b099966E',
7
+ soft: '0x4E6f13c8A453c1b8b4E20b590082bb233a5b80d7',
8
+ u: '0xF81C4E51c48466c6025B0165b1Ea0F6978c712cb',
9
+ s: '0x60aee38576035E757deD30fF8db8A8DA476430C1',
10
+ wbnb: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
11
+ }
12
+
13
+ const FORBIDDEN_CREATE = new Set([ADDR.soft, ADDR.u, ADDR.s].map((a) => a.toLowerCase()))
4
14
 
5
15
  export const CONFIG = {
6
16
  rpc: process.env.GANG_RPC_URL || 'https://bsc-dataseed.binance.org',
7
- launchpad: process.env.GANG_LAUNCHPAD || '0x315Ba2e8952D06094CC636b2B84229FA32fb23A7',
17
+ launchpad: process.env.GANG_LAUNCHPAD || ADDR.v2,
8
18
  chainId: '56',
9
19
  }
10
20
 
11
21
  export const client = createPublicClient({ chain: bsc, transport: http(CONFIG.rpc) })
12
- const pad = (functionName, args = []) => client.readContract({ address: CONFIG.launchpad, abi: LAUNCHPAD_ABI, functionName, args })
13
- const erc = (address, functionName, args = []) => client.readContract({ address, abi: ERC20_ABI, functionName, args })
14
-
15
22
  export const fmt = (wei) => formatEther(wei)
16
23
  export const fmtTok = (units) => Number(formatEther(units)).toLocaleString('en-US', { maximumFractionDigits: 2 })
17
- export { parseEther, encodeFunctionData }
24
+ export { parseEther, encodeFunctionData, zeroAddress }
18
25
 
19
- export async function tokenInfo(token, me) {
20
- const [c, name, symbol, locked, grad, cap] = await Promise.all([
21
- pad('curves', [token]), erc(token, 'name'), erc(token, 'symbol'), erc(token, 'locked'),
22
- pad('GRADUATION_BNB'), pad('MAX_PER_WALLET'),
23
- ])
24
- const [creator, realBnb, sold, graduated, pair] = c
25
- if (creator === '0x0000000000000000000000000000000000000000') {
26
+ const erc = (address, functionName, args = []) => client.readContract({ address, abi: ERC20_ABI, functionName, args })
27
+ const read = (address, abi, functionName, args = []) => client.readContract({ address, abi, functionName, args })
28
+
29
+ export function assertCreateTarget(addr = CONFIG.launchpad) {
30
+ const a = String(addr || '').toLowerCase()
31
+ if (a !== ADDR.v2.toLowerCase()) {
26
32
  throw new Error(
27
- `not a Gang token: ${token} (launchpad ${CONFIG.launchpad}). ` +
28
- `Upgrade: npm i -g @gangfun/cli@0.1.3 --registry=https://npm.pkg.github.com OR ` +
29
- `export GANG_LAUNCHPAD=0x315Ba2e8952D06094CC636b2B84229FA32fb23A7`
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.`,
30
36
  )
31
37
  }
32
- // curve end price (1e-7 BNB) for graduated tokens; live spot otherwise
33
- const price = graduated ? 100000000000n : await pad('spotPrice', [token])
38
+ }
39
+
40
+ const PADS = [
41
+ { id: 'v2', address: ADDR.v2, kind: 'v2', abi: V2_ABI },
42
+ { id: 'soft', address: ADDR.soft, kind: 'soft', abi: SOFT_ABI },
43
+ { id: 'u', address: ADDR.u, kind: 'v2', abi: V2_ABI },
44
+ { id: 's', address: ADDR.s, kind: 'v2', abi: V2_ABI },
45
+ ]
46
+
47
+ /** Resolve which pad hosts `token` by probing curves(token).creator != 0. */
48
+ export async function resolvePad(token) {
49
+ for (const pad of PADS) {
50
+ try {
51
+ const c = await read(pad.address, pad.abi, 'curves', [token])
52
+ const creator = Array.isArray(c) ? c[0] : c.creator
53
+ if (creator && creator !== zeroAddress) {
54
+ return { ...pad, curve: c }
55
+ }
56
+ } catch { /* wrong ABI shape or empty */ }
57
+ }
58
+ throw new Error(
59
+ `not a Gang token on known pads (V2/Soft/U/S): ${token}. ` +
60
+ `Upgrade: npm i -g @gangfun/cli@0.2.1 --registry=https://npm.pkg.github.com OR npm i -g github:gangfun/cli`,
61
+ )
62
+ }
63
+
64
+ function softInfoFromCurve(c) {
65
+ const [creator, realBnb, sold, graduated, pair] = c
66
+ return { kind: 'soft', creator, raised: realBnb, sold, graduated, pair, quote: ADDR.wbnb, gradAmt: null, taxed: false, virtualQuote: null, refPair: null }
67
+ }
68
+
69
+ function v2InfoFromCurve(c) {
70
+ const [creator, quote, realQuote, sold, gradAmt, virtualQuote, graduated, taxed, pair, refPair] = c
71
+ return { kind: 'v2', creator, raised: realQuote, sold, graduated, pair, quote, gradAmt, taxed, virtualQuote, refPair }
72
+ }
73
+
74
+ export async function tokenInfo(token, me) {
75
+ const pad = await resolvePad(token)
76
+ const base = pad.kind === 'soft' ? softInfoFromCurve(pad.curve) : v2InfoFromCurve(pad.curve)
77
+ const [name, symbol] = await Promise.all([erc(token, 'name'), erc(token, 'symbol')])
78
+ let locked = null
79
+ try { locked = await erc(token, 'locked') } catch { /* */ }
80
+ let cap = null
81
+ try { cap = await read(pad.address, pad.abi, 'MAX_PER_WALLET') } catch { /* */ }
82
+
83
+ let price = null
84
+ if (pad.kind === 'soft') {
85
+ try { price = base.graduated ? 100000000000n : await read(pad.address, pad.abi, 'spotPrice', [token]) } catch { /* */ }
86
+ } else if (base.virtualQuote && base.virtualQuote > 0n) {
87
+ // spot ≈ virtualQuote / (VIRTUAL_TOKEN - sold) * 1e18-ish; expose raised/grad instead
88
+ const vq = BigInt(base.virtualQuote)
89
+ const sold = BigInt(base.sold)
90
+ const VIRTUAL_TOKEN = 1066666666666666666666666667n
91
+ const tReserve = VIRTUAL_TOKEN - sold
92
+ if (tReserve > 0n) price = (vq * 10n ** 18n) / tReserve
93
+ }
94
+
95
+ const grad = base.gradAmt != null ? BigInt(base.gradAmt) : (pad.kind === 'soft' ? await read(pad.address, pad.abi, 'GRADUATION_BNB') : 15n * 10n ** 18n)
96
+ const raised = BigInt(base.raised)
34
97
  const info = {
35
- token, name, symbol, creator, graduated, pair: graduated ? pair : null, locked,
36
- priceBnb: Number(price) / 1e18,
37
- fdvBnb: (Number(price) / 1e18) * 1e9,
38
- raisedBnb: fmt(realBnb), graduationBnb: fmt(grad),
39
- progressPct: Number((realBnb * 10000n) / grad) / 100,
40
- soldTokens: fmtTok(sold), walletCapTokens: fmtTok(cap),
98
+ token, name, symbol, creator: base.creator, graduated: base.graduated,
99
+ pair: base.graduated ? base.pair : null, locked,
100
+ pad: pad.id, padAddress: pad.address, kind: base.kind,
101
+ quote: base.quote, taxed: !!base.taxed,
102
+ priceBnb: price != null ? Number(price) / 1e18 : null,
103
+ fdvBnb: price != null ? (Number(price) / 1e18) * 1e9 : null,
104
+ raisedBnb: fmt(raised), graduationBnb: fmt(grad),
105
+ progressPct: grad > 0n ? Number((raised * 10000n) / grad) / 100 : 0,
106
+ soldTokens: fmtTok(base.sold), walletCapTokens: cap != null ? fmtTok(cap) : null,
107
+ logoURI: null,
108
+ }
109
+ if (pad.kind === 'v2') {
110
+ try { info.logoURI = await read(pad.address, pad.abi, 'logoURIOf', [token]) } catch { /* U/S may lack */ }
41
111
  }
42
112
  if (me) {
43
- const [bal, room] = await Promise.all([erc(token, 'balanceOf', [me]), pad('walletRoom', [token, me])])
44
- info.myBalance = fmtTok(bal); info.myBalanceRaw = bal.toString(); info.myRoomTokens = fmtTok(room); info.myRoomRaw = room.toString()
113
+ const bal = await erc(token, 'balanceOf', [me])
114
+ info.myBalance = fmtTok(bal); info.myBalanceRaw = bal.toString()
115
+ try {
116
+ const room = await read(pad.address, pad.abi, 'walletRoom', [token, me])
117
+ info.myRoomTokens = fmtTok(room); info.myRoomRaw = room.toString()
118
+ } catch { /* */ }
45
119
  }
46
120
  return info
47
121
  }
48
122
 
49
123
  export async function listTokens(me) {
50
- const n = await pad('tokenCount')
51
- const addrs = await Promise.all(Array.from({ length: Number(n) }, (_, i) => pad('allTokens', [BigInt(i)])))
52
- return Promise.all(addrs.map((a) => tokenInfo(a, me)))
124
+ const out = []
125
+ for (const pad of PADS) {
126
+ try {
127
+ const n = await read(pad.address, pad.abi, 'tokenCount')
128
+ const addrs = await Promise.all(Array.from({ length: Number(n) }, (_, i) => read(pad.address, pad.abi, 'allTokens', [BigInt(i)])))
129
+ for (const a of addrs) {
130
+ try { out.push(await tokenInfo(a, me)) } catch { /* skip */ }
131
+ }
132
+ } catch { /* pad down */ }
133
+ }
134
+ return out
53
135
  }
54
136
 
55
- export async function quoteBuy(token, buyer, bnb) {
56
- const gross = parseEther(bnb)
57
- const [tokensOut, bnbIn, fee, refund] = await pad('quoteBuy', [token, buyer, gross])
58
- return { bnbGross: bnb, tokensOut, bnbIn, fee, refund, tokensOutFmt: fmtTok(tokensOut), bnbInFmt: fmt(bnbIn), feeFmt: fmt(fee), refundFmt: fmt(refund) }
137
+ /** LaunchpadV2 curve constants (LaunchpadV2.sol) — unique off-chain path = on-chain math. */
138
+ const V2_VIRTUAL_TOKEN = 1066666666666666666666666667n
139
+ const V2_BPS = 10000n
140
+ const V2_PROTOCOL_FEE_BPS = 100n // 1%
141
+
142
+ const ceilDiv = (a, b) => (b === 0n ? 0n : (a + b - 1n) / b)
143
+
144
+ /** Contract-equivalent buy quote (_prepBuy + _tokensOut / _quoteInFor). No Soft/U/S fallback. */
145
+ function v2QuoteBuyFromCurve(base, quoteIn, room) {
146
+ const vq = BigInt(base.virtualQuote)
147
+ const sold = BigInt(base.sold)
148
+ const raised = BigInt(base.raised)
149
+ const gradAmt = BigInt(base.gradAmt)
150
+ const tokensOutOf = (net) => {
151
+ const k = vq * V2_VIRTUAL_TOKEN
152
+ const qReserve = vq + raised
153
+ const tReserve = V2_VIRTUAL_TOKEN - sold
154
+ const newT = ceilDiv(k, qReserve + net)
155
+ return tReserve - newT
156
+ }
157
+ const quoteInFor = (tokensOut) => {
158
+ const k = vq * V2_VIRTUAL_TOKEN
159
+ const qReserve = vq + raised
160
+ const tReserve = V2_VIRTUAL_TOKEN - sold
161
+ return ceilDiv(k, tReserve - tokensOut) - qReserve
162
+ }
163
+ const recomputeFee = (gross, netIn) => {
164
+ let spentGross = ceilDiv(netIn * V2_BPS, V2_BPS - V2_PROTOCOL_FEE_BPS)
165
+ if (spentGross > gross) spentGross = gross
166
+ const taxPlat = (spentGross * V2_PROTOCOL_FEE_BPS) / V2_BPS
167
+ const net = spentGross - taxPlat
168
+ const refundGross = gross - spentGross
169
+ return { taxPlat, net, refundGross, spentGross }
170
+ }
171
+
172
+ let taxPlat = (quoteIn * V2_PROTOCOL_FEE_BPS) / V2_BPS
173
+ let net = quoteIn - taxPlat
174
+ let refundGross = 0n
175
+ const remaining = gradAmt > raised ? gradAmt - raised : 0n
176
+ if (net > remaining) {
177
+ net = remaining
178
+ ;({ taxPlat, net, refundGross } = recomputeFee(quoteIn, net))
179
+ if (net > remaining) net = remaining
180
+ }
181
+ let tokensOut = tokensOutOf(net)
182
+ if (room != null && tokensOut > room) {
183
+ tokensOut = room
184
+ net = quoteInFor(room)
185
+ ;({ taxPlat, net, refundGross } = recomputeFee(quoteIn, net))
186
+ }
187
+ return { tokensOut, spent: quoteIn - refundGross, fee: taxPlat, refund: refundGross }
188
+ }
189
+
190
+ /** Contract-equivalent sell quote (_quoteOut + 1% platform fee). */
191
+ function v2QuoteSellFromCurve(base, tokensIn) {
192
+ const vq = BigInt(base.virtualQuote)
193
+ const sold = BigInt(base.sold)
194
+ const raised = BigInt(base.raised)
195
+ const k = vq * V2_VIRTUAL_TOKEN
196
+ const qReserve = vq + raised
197
+ const tReserve = V2_VIRTUAL_TOKEN - sold
198
+ const newQ = ceilDiv(k, tReserve + tokensIn)
199
+ const quoteOut = qReserve - newQ
200
+ const fee = (quoteOut * V2_PROTOCOL_FEE_BPS) / V2_BPS
201
+ return { out: quoteOut - fee, fee }
202
+ }
203
+
204
+ export async function quoteBuy(token, buyer, amountHuman) {
205
+ const pad = await resolvePad(token)
206
+ const gross = parseEther(amountHuman)
207
+ if (pad.kind === 'soft') {
208
+ const [tokensOut, bnbIn, fee, refund] = await read(pad.address, pad.abi, 'quoteBuy', [token, buyer, gross])
209
+ return { kind: 'soft', pad: pad.id, padAddress: pad.address, amountHuman, tokensOut, spent: bnbIn, fee, refund,
210
+ tokensOutFmt: fmtTok(tokensOut), spentFmt: fmt(bnbIn), feeFmt: fmt(fee), refundFmt: fmt(refund), unit: 'BNB',
211
+ quotePath: 'onchain-quoteBuy' }
212
+ }
213
+ // Unique path: LaunchpadV2.sol _prepBuy / _tokensOut / _quoteInFor (no on-chain quoteBuy view).
214
+ const base = v2InfoFromCurve(pad.curve)
215
+ let room = null
216
+ try { room = await read(pad.address, pad.abi, 'walletRoom', [token, buyer]) } catch { /* */ }
217
+ const q = v2QuoteBuyFromCurve(base, gross, room)
218
+ return { kind: 'v2', pad: pad.id, padAddress: pad.address, quote: base.quote, amountHuman,
219
+ tokensOut: q.tokensOut, spent: q.spent, fee: q.fee, refund: q.refund,
220
+ tokensOutFmt: fmtTok(q.tokensOut), spentFmt: fmt(q.spent), feeFmt: fmt(q.fee), refundFmt: fmt(q.refund),
221
+ unit: 'quote', quotePath: 'LaunchpadV2.sol#_prepBuy', deterministic: true }
59
222
  }
60
223
 
61
224
  export async function quoteSell(token, tokensIn) {
62
- const [bnbToUser, fee] = await pad('quoteSell', [token, tokensIn])
63
- return { tokensIn, bnbToUser, fee, tokensInFmt: fmtTok(tokensIn), bnbToUserFmt: fmt(bnbToUser), feeFmt: fmt(fee) }
225
+ const pad = await resolvePad(token)
226
+ if (pad.kind === 'soft') {
227
+ const [bnbToUser, fee] = await read(pad.address, pad.abi, 'quoteSell', [token, tokensIn])
228
+ return { kind: 'soft', pad: pad.id, padAddress: pad.address, tokensIn, out: bnbToUser, fee,
229
+ tokensInFmt: fmtTok(tokensIn), outFmt: fmt(bnbToUser), feeFmt: fmt(fee), unit: 'BNB',
230
+ quotePath: 'onchain-quoteSell' }
231
+ }
232
+ const base = v2InfoFromCurve(pad.curve)
233
+ const q = v2QuoteSellFromCurve(base, tokensIn)
234
+ return { kind: 'v2', pad: pad.id, padAddress: pad.address, quote: base.quote, tokensIn, out: q.out, fee: q.fee,
235
+ tokensInFmt: fmtTok(tokensIn), outFmt: fmt(q.out), feeFmt: fmt(q.fee), unit: 'quote',
236
+ quotePath: 'LaunchpadV2.sol#_quoteOut', deterministic: true }
237
+ }
238
+
239
+ export async function isAgent(account) {
240
+ // Prefer V2 gate (create path); fall back Soft.
241
+ try { return await read(ADDR.v2, V2_ABI, 'isAgent', [account]) } catch { /* */ }
242
+ try { return await read(ADDR.soft, SOFT_ABI, 'isAgent', [account]) } catch { /* */ }
243
+ return false
64
244
  }
65
245
 
66
- export const isAgent = (a) => pad('isAgent', [a])
67
246
  export const balanceOf = (token, a) => erc(token, 'balanceOf', [a])
68
- export const allowance = (token, a) => erc(token, 'allowance', [a, CONFIG.launchpad])
247
+ export async function allowance(token, owner, spender) {
248
+ return erc(token, 'allowance', [owner, spender])
249
+ }
69
250
 
70
251
  export const calldata = {
71
- buy: (token, minTokensOut) => encodeFunctionData({ abi: LAUNCHPAD_ABI, functionName: 'buy', args: [token, minTokensOut] }),
72
- sell: (token, tokensIn, minBnbOut) => encodeFunctionData({ abi: LAUNCHPAD_ABI, functionName: 'sell', args: [token, tokensIn, minBnbOut] }),
73
- approve: (amount) => encodeFunctionData({ abi: ERC20_ABI, functionName: 'approve', args: [CONFIG.launchpad, amount] }),
74
- create: (name, symbol) => encodeFunctionData({ abi: LAUNCHPAD_ABI, functionName: 'create', args: [name, symbol] }),
252
+ softBuy: (token, minTokensOut) => encodeFunctionData({ abi: SOFT_ABI, functionName: 'buy', args: [token, minTokensOut] }),
253
+ softSell: (token, tokensIn, minBnbOut) => encodeFunctionData({ abi: SOFT_ABI, functionName: 'sell', args: [token, tokensIn, minBnbOut] }),
254
+ v2Buy: (token, quoteIn, minTokensOut) => encodeFunctionData({ abi: V2_ABI, functionName: 'buy', args: [token, quoteIn, minTokensOut] }),
255
+ v2Sell: (token, tokensIn, minQuoteOut) => encodeFunctionData({ abi: V2_ABI, functionName: 'sell', args: [token, tokensIn, minQuoteOut] }),
256
+ approve: (spender, amount) => encodeFunctionData({ abi: ERC20_ABI, functionName: 'approve', args: [spender, amount] }),
257
+ createV2: (name, symbol, logoURI, taxed, pairOrQuote) =>
258
+ encodeFunctionData({ abi: V2_ABI, functionName: 'create', args: [name, symbol, logoURI, taxed, pairOrQuote] }),
75
259
  }
76
260
 
77
261
  export async function decodeTx(hash) {
78
262
  const r = await client.waitForTransactionReceipt({ hash, timeout: 120_000 })
79
263
  const events = []
264
+ const known = new Map(PADS.map((p) => [p.address.toLowerCase(), p]))
80
265
  for (const log of r.logs) {
81
- if (log.address.toLowerCase() !== CONFIG.launchpad.toLowerCase()) continue
266
+ const pad = known.get(log.address.toLowerCase())
267
+ if (!pad) continue
82
268
  try {
83
- const ev = decodeEventLog({ abi: LAUNCHPAD_ABI, data: log.data, topics: log.topics })
84
- const a = Object.fromEntries(Object.entries(ev.args).map(([k, v]) => [k, typeof v === 'bigint' ? (k.toLowerCase().includes('token') && k !== 'token' ? fmtTok(v) : fmt(v)) : v]))
85
- events.push({ event: ev.eventName, ...a })
86
- } catch { /* not ours */ }
269
+ const ev = decodeEventLog({ abi: pad.abi, data: log.data, topics: log.topics })
270
+ const a = Object.fromEntries(Object.entries(ev.args).map(([k, v]) => {
271
+ if (typeof v !== 'bigint') return [k, v]
272
+ const lk = k.toLowerCase()
273
+ if (lk.includes('token') && k !== 'token' && !lk.includes('amount')) return [k, fmtTok(v)]
274
+ return [k, fmt(v)]
275
+ }))
276
+ events.push({ event: ev.eventName, pad: pad.id, ...a })
277
+ } catch { /* */ }
87
278
  }
88
279
  return { hash, status: r.status, block: Number(r.blockNumber), events }
89
280
  }
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { CONFIG, listTokens, tokenInfo, quoteBuy, quoteSell, isAgent, balanceOf, allowance, calldata, decodeTx, parseEther, fmt, fmtTok } from './chain.js'
2
+ import {
3
+ CONFIG, ADDR, listTokens, tokenInfo, quoteBuy, quoteSell, isAgent, balanceOf, allowance,
4
+ calldata, decodeTx, parseEther, fmt, fmtTok, resolvePad, assertCreateTarget, zeroAddress,
5
+ } from './chain.js'
3
6
  import { bawAddress, bawSettings, bawStatus, bawPreview, bawExecute } from './baw.js'
7
+ import { uploadLogoFile, LOGO_UI } from './logo.js'
4
8
 
5
9
  const args = process.argv.slice(2)
6
10
  const cmd = args[0]
@@ -15,19 +19,22 @@ const parseSlippage = (raw) => {
15
19
  return s
16
20
  }
17
21
 
18
- const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet)
22
+ const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet) · LaunchpadV2 create
19
23
 
20
24
  gang preflight check sign-in, Developer Mode, tradeAllTokens, Agent ID
21
- gang list all tokens with price, progress, my balance
22
- gang token --token <addr> one token in detail
23
- gang quote --token <addr> --bnb <n> buy quote for my wallet (fee, cap, refund)
24
- gang buy --token <addr> --bnb <n> [--slippage 1] preview a buy -> requestId
25
- gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] preview approve or sell -> requestId
26
- gang create --name <n> --symbol <s> preview token creation -> requestId
25
+ gang list tokens across V2 / Soft / U / S
26
+ gang token --token <addr> one token (auto-resolves pad)
27
+ gang quote --token <addr> --bnb <n> Soft: BNB quote; V2/U/S: quote-token amount (same flag)
28
+ gang buy --token <addr> --bnb <n> [--slippage 1] Soft: payable BNB; V2: approve quote then buy
29
+ gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1]
30
+ gang upload-logo --file <path> POST image → https://gang.fun/api/logos → logoUrl
31
+ gang create --name <n> --symbol <s> [--logo-uri URL | --logo <file>] [--taxed] [--pair-or-quote 0x…]
32
+ LaunchpadV2 ONLY (${ADDR.v2}); Soft/U/S create refused
27
33
  gang execute --request-id <id> broadcast a previewed call
28
34
  gang tx --hash <0x..> wait for receipt and decode Gang events
29
35
 
30
- env: GANG_LAUNCHPAD (default ${CONFIG.launchpad}), GANG_RPC_URL`
36
+ Logo UI (browser): ${LOGO_UI}
37
+ env: GANG_LAUNCHPAD (default V2 ${ADDR.v2}), GANG_RPC_URL, GANG_LOGO_API`
31
38
 
32
39
  function summarizePreview(p, extra = {}) {
33
40
  if (!p.success) return { success: false, step: extra.step, error: p.error }
@@ -43,6 +50,18 @@ function summarizePreview(p, extra = {}) {
43
50
  }
44
51
  }
45
52
 
53
+ async function resolveLogoUri() {
54
+ const uri = opt('logo-uri')
55
+ const file = opt('logo') || opt('logo-file') || opt('file-logo')
56
+ if (uri && file) fail('use either --logo-uri or --logo/--logo-file, not both')
57
+ if (uri) return { logoURI: uri, uploaded: false }
58
+ if (file) {
59
+ const up = await uploadLogoFile(file)
60
+ return { logoURI: up.logoUrl, uploaded: true, upload: up }
61
+ }
62
+ return { logoURI: '', uploaded: false }
63
+ }
64
+
46
65
  async function main() {
47
66
  switch (cmd) {
48
67
  case 'preflight': {
@@ -55,13 +74,18 @@ async function main() {
55
74
  if (!settings.devMode?.enabled) issues.push({ code: 'DEV_MODE_OFF', fix: 'Enable Developer Mode in Binance App → Agentic Wallet settings.' })
56
75
  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).' })
57
76
  agent = await isAgent(me)
58
- 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.' })
77
+ 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.' })
59
78
  }
60
- return out({ success: issues.length === 0, status, address: me, launchpad: CONFIG.launchpad, isAgent: agent, devMode: settings?.devMode?.enabled ?? null, tradeAllTokens: settings?.tradeAllTokens ?? null, issues })
79
+ return out({
80
+ success: issues.length === 0, status, address: me,
81
+ launchpad: CONFIG.launchpad, createPad: ADDR.v2, isAgent: agent,
82
+ devMode: settings?.devMode?.enabled ?? null, tradeAllTokens: settings?.tradeAllTokens ?? null, issues,
83
+ note: 'New create → LaunchpadV2 only (~15 BNB graduation). Soft/U/S: existing buy/sell/claim only.',
84
+ })
61
85
  }
62
86
  case 'list': {
63
87
  const me = bawStatus() === 'CONNECTED' ? bawAddress() : undefined
64
- return out({ success: true, launchpad: CONFIG.launchpad, tokens: await listTokens(me) })
88
+ return out({ success: true, createPad: ADDR.v2, tokens: await listTokens(me) })
65
89
  }
66
90
  case 'token': {
67
91
  const token = opt('token'); if (!token) fail('--token required')
@@ -69,10 +93,17 @@ async function main() {
69
93
  return out({ success: true, ...(await tokenInfo(token, me)) })
70
94
  }
71
95
  case 'quote': {
72
- const token = opt('token'), bnb = opt('bnb'); if (!token || !bnb) fail('--token and --bnb required')
96
+ const token = opt('token'), bnb = opt('bnb'); if (!token || !bnb) fail('--token and --bnb required (Soft archive: BNB; V2: quote ERC-20 amount)')
73
97
  const me = bawAddress()
74
98
  const q = await quoteBuy(token, me, bnb)
75
- return out({ success: true, token, buyer: me, tokensOut: q.tokensOutFmt, bnbSpent: q.bnbInFmt, fee: q.feeFmt, refund: q.refundFmt, note: q.refund > 0n ? 'Part of the BNB would be refunded (wallet 1% cap or graduation cap).' : undefined })
99
+ return out({
100
+ success: true, token, buyer: me, pad: q.pad, kind: q.kind, quote: q.quote,
101
+ quotePath: q.quotePath, deterministic: q.deterministic || false,
102
+ tokensOut: q.tokensOutFmt, spent: q.spentFmt, fee: q.feeFmt, refund: q.refundFmt, unit: q.unit,
103
+ note: q.refund > 0n
104
+ ? 'Part of the input would be refunded (wallet 1% cap or graduation cap).'
105
+ : (q.deterministic ? 'V2 quote = LaunchpadV2.sol curve math (unique path; no Soft/U/S fallback).' : undefined),
106
+ })
76
107
  }
77
108
  case 'buy': {
78
109
  const token = opt('token'), bnb = opt('bnb'); if (!token || !bnb) fail('--token and --bnb required')
@@ -81,12 +112,38 @@ async function main() {
81
112
  const info = await tokenInfo(token, me)
82
113
  if (info.graduated) fail('token already graduated; trade it on PancakeSwap', { pair: info.pair })
83
114
  if (!(await isAgent(me))) fail('this wallet holds no Agent ID (.ai); buying is gated', { address: me })
115
+ const pad = await resolvePad(token)
84
116
  const q = await quoteBuy(token, me, bnb)
85
- if (q.tokensOut === 0n) fail('wallet cap reached: this wallet already holds 1% of supply', { myBalance: info.myBalance })
117
+ if (q.tokensOut === 0n) fail('wallet cap reached or zero out', { myBalance: info.myBalance })
86
118
  const minOut = (q.tokensOut * BigInt(Math.round((100 - slippage) * 100))) / 10000n
87
- const data = calldata.buy(token, minOut)
88
- const p = bawPreview({ from: me, to: CONFIG.launchpad, value: parseEther(bnb), data })
89
- return out(summarizePreview(p, { step: 'buy', token, symbol: info.symbol, bnbSent: bnb, expectTokens: q.tokensOutFmt, expectBnbSpent: q.bnbInFmt, fee: q.feeFmt, expectRefund: q.refundFmt, minTokensOut: fmtTok(minOut) }))
119
+
120
+ if (pad.kind === 'soft') {
121
+ const data = calldata.softBuy(token, minOut)
122
+ const p = bawPreview({ from: me, to: pad.address, value: parseEther(bnb), data })
123
+ return out(summarizePreview(p, {
124
+ step: 'buy', kind: 'soft', token, symbol: info.symbol, pad: pad.id,
125
+ bnbSent: bnb, expectTokens: q.tokensOutFmt, expectSpent: q.spentFmt, fee: q.feeFmt,
126
+ expectRefund: q.refundFmt, minTokensOut: fmtTok(minOut),
127
+ }))
128
+ }
129
+
130
+ // V2 / U / S: quote ERC-20 approve then buy(token, quoteIn, minTokensOut)
131
+ const quoteIn = parseEther(bnb)
132
+ const allow = await allowance(info.quote, me, pad.address)
133
+ if (allow < quoteIn) {
134
+ const p = bawPreview({ from: me, to: info.quote, data: calldata.approve(pad.address, quoteIn) })
135
+ return out(summarizePreview(p, {
136
+ step: 'approve-quote', kind: 'v2', token, symbol: info.symbol, pad: pad.id, quote: info.quote,
137
+ amount: bnb, note: 'Approve quote ERC-20 first. After it confirms, re-run the same `gang buy` to preview the buy.',
138
+ }))
139
+ }
140
+ const data = calldata.v2Buy(token, quoteIn, minOut)
141
+ const p = bawPreview({ from: me, to: pad.address, data })
142
+ return out(summarizePreview(p, {
143
+ step: 'buy', kind: 'v2', token, symbol: info.symbol, pad: pad.id, quote: info.quote,
144
+ quoteIn: bnb, expectTokens: q.tokensOutFmt, expectSpent: q.spentFmt, fee: q.feeFmt,
145
+ expectRefund: q.refundFmt, minTokensOut: fmtTok(minOut), quotePath: q.quotePath, deterministic: q.deterministic || false,
146
+ }))
90
147
  }
91
148
  case 'sell': {
92
149
  const token = opt('token'); if (!token) fail('--token required')
@@ -99,22 +156,56 @@ async function main() {
99
156
  if (flag('all') && opt('amount')) fail('use either --all or --amount, not both')
100
157
  const amount = flag('all') ? bal : parseEther(opt('amount'))
101
158
  if (amount > bal) fail('amount exceeds balance', { balance: fmtTok(bal) })
102
- const allow = await allowance(token, me)
159
+ const pad = await resolvePad(token)
160
+ const allow = await allowance(token, me, pad.address)
103
161
  if (allow < amount) {
104
- const p = bawPreview({ from: me, to: token, data: calldata.approve(amount) })
105
- return out(summarizePreview(p, { step: 'approve', token, symbol: info.symbol, amount: fmtTok(amount), note: 'Approve first. After it confirms on-chain, run the same `gang sell` again to preview the sell.' }))
162
+ const p = bawPreview({ from: me, to: token, data: calldata.approve(pad.address, amount) })
163
+ return out(summarizePreview(p, {
164
+ step: 'approve', token, symbol: info.symbol, pad: pad.id, amount: fmtTok(amount),
165
+ note: 'Approve first. After it confirms on-chain, run the same `gang sell` again to preview the sell.',
166
+ }))
106
167
  }
107
168
  const slippage = parseSlippage(opt('slippage', '1'))
108
169
  const q = await quoteSell(token, amount)
109
- const minOut = (q.bnbToUser * BigInt(Math.round((100 - slippage) * 100))) / 10000n
110
- const p = bawPreview({ from: me, to: CONFIG.launchpad, data: calldata.sell(token, amount, minOut) })
111
- return out(summarizePreview(p, { step: 'sell', token, symbol: info.symbol, amount: fmtTok(amount), expectBnb: q.bnbToUserFmt, fee: q.feeFmt, minBnbOut: fmt(minOut) }))
170
+ const minOut = (q.out * BigInt(Math.round((100 - slippage) * 100))) / 10000n
171
+ if (pad.kind === 'soft') {
172
+ const p = bawPreview({ from: me, to: pad.address, data: calldata.softSell(token, amount, minOut) })
173
+ return out(summarizePreview(p, {
174
+ step: 'sell', kind: 'soft', token, symbol: info.symbol, pad: pad.id,
175
+ amount: fmtTok(amount), expectOut: q.outFmt, fee: q.feeFmt, minOut: fmt(minOut), unit: 'BNB',
176
+ }))
177
+ }
178
+ const p = bawPreview({ from: me, to: pad.address, data: calldata.v2Sell(token, amount, minOut) })
179
+ return out(summarizePreview(p, {
180
+ step: 'sell', kind: 'v2', token, symbol: info.symbol, pad: pad.id, quote: info.quote,
181
+ amount: fmtTok(amount), expectOut: q.outFmt, fee: q.feeFmt, minOut: fmt(minOut), unit: 'quote', quotePath: q.quotePath, deterministic: q.deterministic || false,
182
+ }))
183
+ }
184
+ case 'upload-logo': {
185
+ const file = opt('file') || opt('path') || opt('logo')
186
+ if (!file) fail('--file <path> required (png/jpeg/webp/gif ≤512KB)')
187
+ const up = await uploadLogoFile(file)
188
+ return out({ success: true, ...up, ui: LOGO_UI, next: `gang create --name … --symbol … --logo-uri ${up.logoUrl}` })
112
189
  }
113
190
  case 'create': {
114
191
  const name = opt('name'), symbol = opt('symbol'); if (!name || !symbol) fail('--name and --symbol required')
192
+ try { assertCreateTarget(CONFIG.launchpad) } catch (e) { fail(e.message) }
115
193
  const me = bawAddress()
116
- const p = bawPreview({ from: me, to: CONFIG.launchpad, data: calldata.create(name, symbol) })
117
- return out(summarizePreview(p, { step: 'create', name, symbol, note: 'No creation fee. The token address appears in the Created event: run `gang tx --hash <txHash>` after execute.' }))
194
+ if (!(await isAgent(me))) fail('this wallet holds no Agent ID (.ai); create is gated', { address: me })
195
+ const logo = await resolveLogoUri()
196
+ const taxed = flag('taxed')
197
+ const pairOrQuote = opt('pair-or-quote') || opt('pair') || zeroAddress
198
+ if (taxed && (!pairOrQuote || pairOrQuote === zeroAddress)) {
199
+ fail('--taxed requires --pair-or-quote <pair or quote CA>')
200
+ }
201
+ const data = calldata.createV2(name, symbol, logo.logoURI, taxed, pairOrQuote)
202
+ const p = bawPreview({ from: me, to: ADDR.v2, data })
203
+ return out(summarizePreview(p, {
204
+ step: 'create', pad: 'v2', launchpad: ADDR.v2, name, symbol,
205
+ logoURI: logo.logoURI || '(empty)', uploaded: logo.uploaded, upload: logo.upload,
206
+ taxed, pairOrQuote,
207
+ note: 'LaunchpadV2 only. Graduation ≈15 BNB (PoolCheck). No creation fee. Soft/U/S create archived. After execute: `gang tx --hash …` for Created.',
208
+ }))
118
209
  }
119
210
  case 'execute': {
120
211
  const id = opt('request-id'); if (!id) fail('--request-id required')
@@ -122,7 +213,12 @@ async function main() {
122
213
  if (!r.success) return out({ success: false, error: r.error })
123
214
  const d = r.data
124
215
  const pending = d.status === 'PENDING_CONFIRMATION'
125
- return out({ success: true, status: d.status, txHash: d.txHash, orderId: d.orderId, pendingConfirmation: pending, next: pending ? 'Ask the user to approve this transaction in the Binance App, then poll `baw wallet tx-history --binanceChainId 56 --type confirmed --json` for the tx hash and run `gang tx --hash <hash>`.' : (d.txHash ? `gang tx --hash ${d.txHash}` : undefined) })
216
+ return out({
217
+ success: true, status: d.status, txHash: d.txHash, orderId: d.orderId, pendingConfirmation: pending,
218
+ next: pending
219
+ ? 'Ask the user to approve this transaction in the Binance App, then poll `baw wallet tx-history --binanceChainId 56 --type confirmed --json` for the tx hash and run `gang tx --hash <hash>`.'
220
+ : (d.txHash ? `gang tx --hash ${d.txHash}` : undefined),
221
+ })
126
222
  }
127
223
  case 'tx': {
128
224
  const hash = opt('hash'); if (!hash) fail('--hash required')
package/src/logo.js ADDED
@@ -0,0 +1,40 @@
1
+ import { readFileSync, statSync } from 'node:fs'
2
+ import { extname, resolve } from 'node:path'
3
+
4
+ const MAX_BYTES = 512 * 1024
5
+ const MIME = {
6
+ '.png': 'image/png',
7
+ '.jpg': 'image/jpeg',
8
+ '.jpeg': 'image/jpeg',
9
+ '.webp': 'image/webp',
10
+ '.gif': 'image/gif',
11
+ }
12
+
13
+ export const LOGO_API = process.env.GANG_LOGO_API || 'https://gang.fun/api/logos'
14
+ export const LOGO_UI = 'https://gang.fun/dev/logo'
15
+
16
+ /** Read a local image → data URL → POST /api/logos → { logoUrl, id, bytes }. */
17
+ export async function uploadLogoFile(filePath) {
18
+ const abs = resolve(filePath)
19
+ const st = statSync(abs)
20
+ if (!st.isFile()) throw new Error(`not a file: ${abs}`)
21
+ if (st.size <= 0) throw new Error(`empty file: ${abs}`)
22
+ if (st.size > MAX_BYTES) throw new Error(`logo too large: ${st.size} bytes (max ${MAX_BYTES})`)
23
+ const ext = extname(abs).toLowerCase()
24
+ const mime = MIME[ext]
25
+ if (!mime) throw new Error(`unsupported image type ${ext || '(none)'}; use png/jpeg/webp/gif`)
26
+ const b64 = readFileSync(abs).toString('base64')
27
+ const image = `data:${mime};base64,${b64}`
28
+ const res = await fetch(LOGO_API, {
29
+ method: 'POST',
30
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
31
+ body: JSON.stringify({ image }),
32
+ })
33
+ const text = await res.text()
34
+ let json
35
+ try { json = JSON.parse(text) } catch { throw new Error(`logo upload non-JSON (${res.status}): ${text.slice(0, 200)}`) }
36
+ if (!res.ok || !json?.ok || !json?.logoUrl) {
37
+ throw new Error(json?.error || `logo upload failed (${res.status}): ${text.slice(0, 200)}`)
38
+ }
39
+ return { logoUrl: json.logoUrl, id: json.id, bytes: json.bytes ?? st.size, api: LOGO_API }
40
+ }