@gangfun/cli 0.1.3 → 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 +40 -27
- package/README.zh-CN.md +37 -47
- package/package.json +2 -2
- package/src/abi.js +45 -3
- package/src/chain.js +238 -47
- package/src/index.js +123 -27
- package/src/logo.js +40 -0
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
|
-
|
|
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
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
74
|
-
-
|
|
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` |
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
13
|
+
npm i -g @gangfun/cli@0.2.1 --registry=https://npm.pkg.github.com
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
或:`npm i -g github:gangfun/cli`
|
|
17
17
|
|
|
18
|
-
|
|
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.
|
|
51
|
-
2. 打开 **Developer Mode**
|
|
52
|
-
3. `baw auth signin
|
|
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
|
|
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
|
-
|
|
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
|
|
74
|
-
-
|
|
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` |
|
|
83
|
-
| `GANG_RPC_URL` | BSC
|
|
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
|
|
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
|
-
"description": "Gang launchpad CLI
|
|
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
|
-
|
|
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 {
|
|
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 ||
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
`
|
|
28
|
-
`
|
|
29
|
-
`
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
info.myBalance = fmtTok(bal); info.myBalanceRaw = bal.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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
63
|
-
|
|
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
|
|
247
|
+
export async function allowance(token, owner, spender) {
|
|
248
|
+
return erc(token, 'allowance', [owner, spender])
|
|
249
|
+
}
|
|
69
250
|
|
|
70
251
|
export const calldata = {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
266
|
+
const pad = known.get(log.address.toLowerCase())
|
|
267
|
+
if (!pad) continue
|
|
82
268
|
try {
|
|
83
|
-
const ev = decodeEventLog({ abi:
|
|
84
|
-
const a = Object.fromEntries(Object.entries(ev.args).map(([k, v]) =>
|
|
85
|
-
|
|
86
|
-
|
|
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 {
|
|
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
|
|
22
|
-
gang token --token <addr> one token
|
|
23
|
-
gang quote --token <addr> --bnb <n>
|
|
24
|
-
gang buy --token <addr> --bnb <n> [--slippage 1]
|
|
25
|
-
gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1]
|
|
26
|
-
gang
|
|
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
|
-
|
|
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({
|
|
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,
|
|
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({
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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, {
|
|
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.
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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({
|
|
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
|
+
}
|