@gangfun/cli 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/README.zh-CN.md +6 -4
- package/package.json +1 -1
- package/src/index.js +17 -7
- package/src/logo.js +43 -9
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ gang quote --token <addr> --bnb <n> # Soft archive: BNB; V2: quote ERC-
|
|
|
53
53
|
gang buy --token <addr> --bnb <n> [--slippage 1] [--follow <note tx>] [--note "<why>" [--conf 0-100] [--reveal-after 1-72h]]
|
|
54
54
|
gang follow <note tx> --bnb <n> [--slippage 1] [--note …] # buy the token a note is about, as a follow of it
|
|
55
55
|
gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
|
|
56
|
-
gang upload-logo (--file ./avatar.png | --url <image URL>) # → https://gang.fun/
|
|
56
|
+
gang upload-logo (--file ./avatar.png | --url <image URL>) # → ipfs://<cid> (view at https://cdn.gang.fun/ipfs/<cid>)
|
|
57
57
|
gang create … # = gang v3 create (GangLaunchpad); untaxed without --buy-tax / --sell-tax
|
|
58
58
|
# 打新 (fair mint) and V3 share one pad (GangLaunchpad). Mainnet by default (address built in); --testnet = BSC testnet set 10
|
|
59
59
|
gang fair create --name <n> --symbol <s> [--raise 15] [--cap 0.5] [--hold 60] [--mint-fee 1] [--exit-fee 1] [--shares 1] [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--description "…"] [--testnet]
|
|
@@ -90,8 +90,8 @@ gang execute --request-id …
|
|
|
90
90
|
|
|
91
91
|
# Or upload first, then create
|
|
92
92
|
gang upload-logo --file ./avatar.png
|
|
93
|
-
# → { "logoUrl": "https://gang.fun/
|
|
94
|
-
gang create --name "My Agent Coin" --symbol MAC --logo-uri
|
|
93
|
+
# → { "logoUrl": "ipfs://<cid>", "url": "https://cdn.gang.fun/ipfs/<cid>", ... }
|
|
94
|
+
gang create --name "My Agent Coin" --symbol MAC --logo-uri ipfs://<cid>
|
|
95
95
|
gang execute --request-id …
|
|
96
96
|
```
|
|
97
97
|
|
|
@@ -103,9 +103,10 @@ the launch. The preview shows `metaURI` and the uploaded `metadata`.
|
|
|
103
103
|
|
|
104
104
|
`logoURI` is written on-chain and can't be changed, so the logo must stay online for good. `--logo-uri` with an image hosted
|
|
105
105
|
anywhere other than gang.fun is downloaded and copied to gang.fun first (png / jpeg / webp / gif, ≤512 KB), and the copy goes on-chain
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
as
|
|
106
|
+
as `ipfs://<cid>`. Files already on gang.fun (`ipfs://<cid>`, `https://cdn.gang.fun/ipfs/<cid>`) are only checked to exist
|
|
107
|
+
and go on-chain as `ipfs://<cid>`. The metadata JSON (`metaURI`) is `ipfs://<cid>` too.
|
|
108
|
+
Temporary hosts such as litter.catbox.moe delete files within days. `--keep-logo-uri` puts a permanent URL (e.g. `ipfs://…`) on-chain
|
|
109
|
+
as is (a file on gang.fun still goes on as `ipfs://<cid>`); temporary hosts are refused.
|
|
109
110
|
`--logo-uri ""` / omit logo → empty `logoURI` on-chain (allowed). Default quote is WBNB. A non-WBNB quote needs `--quote <ERC-20> --ref-pool <Pancake V2 pair / V3 pool holding it>` (mainnet USDT defaults to the USDT/WBNB pool).
|
|
110
111
|
The CLI dry-runs `create` before any upload and names the contract error (`RefPoolRequired`, `ThinReferencePool`, …) if it would revert.
|
|
111
112
|
`--taxed` (LaunchpadV2's tax track) is refused: use `--buy-tax` / `--sell-tax` (`--buy-tax 1 --sell-tax 1 --dividend 100` is closest to the old 1% track).
|
|
@@ -197,6 +198,7 @@ gang agents --window 30d --sort follow
|
|
|
197
198
|
|---|---|
|
|
198
199
|
| `GANG_RPC_URL` | BSC HTTP RPC |
|
|
199
200
|
| `GANG_LOGO_API` | Logo upload endpoint (default `https://gang.fun/api/logos`) |
|
|
201
|
+
| `GANG_CDN_BASE` | CDN serving uploads as `<base>/ipfs/<cid>` (default `https://cdn.gang.fun`) |
|
|
200
202
|
| `GANG_API` | Indexer API for discovery and note publishing (default `https://gang.fun/api`) |
|
|
201
203
|
| `GANG_NOTES_DIR` | Local note queue, one file per note: salt and send status, kept 24h (default `~/.gang/notes`); `previews/` in it records what each trade preview carried (note, follow) for `gang execute`, also kept 24h |
|
|
202
204
|
| `GANG_UPDATE_CHECK` | `0` turns off the once-a-day check for a newer CLI on npm (cached in `~/.gang/update.json`) |
|
|
@@ -207,4 +209,4 @@ gang agents --window 30d --sort follow
|
|
|
207
209
|
|
|
208
210
|
## Version
|
|
209
211
|
|
|
210
|
-
`0.10.
|
|
212
|
+
`0.10.1` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
|
package/README.zh-CN.md
CHANGED
|
@@ -80,7 +80,8 @@ gang execute --request-id …
|
|
|
80
80
|
|
|
81
81
|
# 或者先上传,再发币
|
|
82
82
|
gang upload-logo --file ./avatar.png
|
|
83
|
-
|
|
83
|
+
# → { "logoUrl": "ipfs://<cid>", "url": "https://cdn.gang.fun/ipfs/<cid>", ... }
|
|
84
|
+
gang create --name "My Agent Coin" --symbol MAC --logo-uri ipfs://<cid>
|
|
84
85
|
gang execute --request-id …
|
|
85
86
|
```
|
|
86
87
|
|
|
@@ -90,8 +91,8 @@ x.com / twitter.com 链接(主页、帖子、社区),统一存成 `https:/
|
|
|
90
91
|
地址作为 `contractURI()` / `metaURI()` 固定在链上;和 `logoURI` 一样,发币后不能再改。preview 会显示 `metaURI` 和上传的 `metadata`。
|
|
91
92
|
|
|
92
93
|
`logoURI` 写在链上,发币后改不了,所以 logo 必须长期能打开。`--logo-uri` 填的不是 gang.fun 的图片时,CLI 会先下载这张图,
|
|
93
|
-
转存到 gang.fun(png / jpeg / webp / gif,≤512 KB
|
|
94
|
-
确定地址是永久的(比如 `ipfs://…`),可以加 `--keep-logo-uri`
|
|
94
|
+
转存到 gang.fun(png / jpeg / webp / gif,≤512 KB),上链的是转存后的 `ipfs://<cid>`(已经在 gang.fun 的文件——`ipfs://<cid>` 或 `https://cdn.gang.fun/ipfs/<cid>`——只检查存在,同样以 `ipfs://<cid>` 上链;metadata JSON 的 `metaURI` 也是 `ipfs://<cid>`);litter.catbox.moe 这类临时图床几天内就会删图。
|
|
95
|
+
确定地址是永久的(比如 `ipfs://…`),可以加 `--keep-logo-uri` 原样上链(gang.fun 上的文件加了也照样以 `ipfs://<cid>` 上链);临时图床会被拒绝。
|
|
95
96
|
|
|
96
97
|
默认计价是 WBNB。用其他代币计价时,加 `--quote <ERC-20> --ref-pool <持有该代币的 Pancake V2 pair / V3 池>`(主网 USDT 默认用 USDT/WBNB 池)。
|
|
97
98
|
上传任何东西之前 CLI 会先模拟一次 `create`,如果会回滚,就直接报出合约的错误名(`RefPoolRequired`、`ThinReferencePool` 等)。
|
|
@@ -170,6 +171,7 @@ gang agents --window 30d --sort follow
|
|
|
170
171
|
|---|---|
|
|
171
172
|
| `GANG_RPC_URL` | BSC RPC |
|
|
172
173
|
| `GANG_LOGO_API` | Logo 上传 API(默认 `https://gang.fun/api/logos`) |
|
|
174
|
+
| `GANG_CDN_BASE` | 上传文件的 CDN,地址为 `<base>/ipfs/<cid>`(默认 `https://cdn.gang.fun`) |
|
|
173
175
|
| `GANG_API` | indexer API,发现命令和公开附言都用它(默认 `https://gang.fun/api`) |
|
|
174
176
|
| `GANG_NOTES_DIR` | 附言的本地队列,每条一个文件,记着 salt 和提交状态,保留 24 小时(默认 `~/.gang/notes`);其中的 `previews/` 记着每次交易 preview 带了什么(附言、跟单),供 `gang execute` 使用,也保留 24 小时 |
|
|
175
177
|
| `GANG_UPDATE_CHECK` | 设为 `0` 关闭每天一次的新版本检查(查 npm,缓存在 `~/.gang/update.json`) |
|
|
@@ -180,4 +182,4 @@ gang agents --window 30d --sort follow
|
|
|
180
182
|
|
|
181
183
|
## 版本
|
|
182
184
|
|
|
183
|
-
`0.10.
|
|
185
|
+
`0.10.1` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gangfun/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Gang launchpad CLI for Binance Agentic Wallet agents: create on GangLaunchpad (untaxed or tax tokens), 打新 fair mint, PancakeSwap pool trades, LaunchpadV2 and legacy Soft/U/S trade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
calldata, decodeTx, parseEther, fmt, fmtTok, resolvePad, isAddress, client,
|
|
5
5
|
} from './chain.js'
|
|
6
6
|
import { bawAddress, bawSettings, bawStatus, bawPreview, bawExecute } from './baw.js'
|
|
7
|
-
import { uploadLogoFile, uploadMetadata, rehostLogoUrl, isGangLogo, checkGangLogo, tempHost, LOGO_UI } from './logo.js'
|
|
7
|
+
import { uploadLogoFile, uploadMetadata, rehostLogoUrl, isGangLogo, checkGangLogo, tempHost, ipfsCid, ipfsUri, isIpfsUri, cdnUrl, LOGO_UI } from './logo.js'
|
|
8
8
|
import { API, fetchMarket, rank, watch, parseDuration } from './discover.js'
|
|
9
9
|
import {
|
|
10
10
|
makeNote, withNote, queueNote, findByRequestId, findByHashes, hashesIn, publishNote, hasQueued, followSuffix, readFollowRef,
|
|
@@ -220,12 +220,13 @@ const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet
|
|
|
220
220
|
gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
|
|
221
221
|
graduated: sells into the pool through GangRouter ([--to-bnb] [--from] [--calldata])
|
|
222
222
|
gang upload-logo (--file <path> | --url <image URL>)
|
|
223
|
-
copy an image to
|
|
223
|
+
copy an image to gang.fun → logoUrl (ipfs://<cid>, for --logo-uri) and url (CDN)
|
|
224
224
|
gang create … = gang v3 create (below): new tokens go to GangLaunchpad. Untaxed unless
|
|
225
225
|
--buy-tax / --sell-tax are given. LaunchpadV2 / Soft / U / S take no new tokens
|
|
226
226
|
(their tokens still trade with gang buy / sell); --taxed (the old V2 track) is refused
|
|
227
227
|
--logo-uri images are copied to gang.fun first (logoURI is permanent on-chain;
|
|
228
|
-
other hosts expire). --keep-logo-uri puts a permanent URL (e.g. ipfs://) on-chain as is
|
|
228
|
+
other hosts expire). --keep-logo-uri puts a permanent URL (e.g. ipfs://) on-chain as is;
|
|
229
|
+
gang.fun files (ipfs://<cid>, cdn.gang.fun/ipfs/<cid>) always go on as ipfs://<cid>
|
|
229
230
|
gang fair create --name <n> --symbol <s> [--raise 15] [--cap 0.5] [--hold 60] [--mint-fee 1] [--exit-fee 1]
|
|
230
231
|
[--shares 1] [--quote 0x… --ref-pool 0x…] [--logo <file> | --logo-uri URL] [--website URL]
|
|
231
232
|
[--twitter @handle] [--description <text>]
|
|
@@ -408,9 +409,18 @@ async function resolveLogoUri() {
|
|
|
408
409
|
if (uri && file) fail('use either --logo-uri or --logo/--logo-file, not both')
|
|
409
410
|
if (flag('keep-logo-uri') && !uri) fail('--keep-logo-uri only applies together with --logo-uri <URL>')
|
|
410
411
|
if (uri) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
// a file on gang.fun's IPFS bucket (ipfs://<cid> or its CDN URL) goes on-chain as ipfs://<cid>, once the CDN serves it
|
|
413
|
+
const cid = ipfsCid(uri)
|
|
414
|
+
if (cid || isGangLogo(uri)) {
|
|
415
|
+
try {
|
|
416
|
+
await checkGangLogo(cid ? cdnUrl(cid) : uri)
|
|
417
|
+
} catch (e) {
|
|
418
|
+
// an ipfs:// file that is not on gang.fun may be pinned elsewhere: on-chain as is only when asked
|
|
419
|
+
const elsewhere = isIpfsUri(uri) && !e.retryable
|
|
420
|
+
if (elsewhere && flag('keep-logo-uri')) return { logoURI: uri, uploaded: false }
|
|
421
|
+
fail(e.message, elsewhere ? { hint: 'not on gang.fun: use --logo ./file.png, or --keep-logo-uri if this file stays pinned on IPFS' } : retryHint(e))
|
|
422
|
+
}
|
|
423
|
+
return { logoURI: cid ? ipfsUri(cid) : uri, uploaded: false }
|
|
414
424
|
}
|
|
415
425
|
const temp = tempHost(uri)
|
|
416
426
|
// logoURI can't be changed after create, so an image on another host is copied to gang.fun first
|
|
@@ -457,7 +467,7 @@ async function resolveMetaUri({ name, symbol, logoURI }) {
|
|
|
457
467
|
const meta = { name, symbol, description, image: logoURI, external_link: links?.website, twitter: links?.twitter }
|
|
458
468
|
try {
|
|
459
469
|
const up = await uploadMetadata(meta)
|
|
460
|
-
return { metaURI: up.
|
|
470
|
+
return { metaURI: up.uri, metadata: up.metadata }
|
|
461
471
|
} catch (e) {
|
|
462
472
|
return fail(e.message, retryHint(e))
|
|
463
473
|
}
|
package/src/logo.js
CHANGED
|
@@ -9,6 +9,11 @@ const UPLOAD_TIMEOUT_MS = 60_000
|
|
|
9
9
|
|
|
10
10
|
export const LOGO_API = (process.env.GANG_LOGO_API || 'https://gang.fun/api/logos').replace(/\/+$/, '')
|
|
11
11
|
export const LOGO_UI = 'https://gang.fun/dev/logo'
|
|
12
|
+
// the upload endpoint stores files in an IPFS bucket as ipfs/<cid>, served by this CDN
|
|
13
|
+
// (where --logo-uri ipfs://<cid> is checked; an upload's own url comes from the endpoint)
|
|
14
|
+
export const CDN_BASE = (process.env.GANG_CDN_BASE || 'https://cdn.gang.fun').replace(/\/+$/, '')
|
|
15
|
+
// CIDv1 (raw, sha2-256, base32) as the upload endpoint makes it
|
|
16
|
+
const CID_RE = /^b[a-z2-7]{58}$/
|
|
12
17
|
|
|
13
18
|
// file hosts that delete uploads after hours or days: a logoURI there turns into a broken image, and it can't be changed on-chain
|
|
14
19
|
const TEMP_HOSTS = ['litter.catbox.moe', 'tmpfiles.org', '0x0.st', 'uguu.se', 'file.io', 'transfer.sh']
|
|
@@ -32,7 +37,35 @@ function sniffMime(buf) {
|
|
|
32
37
|
return null
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
/**
|
|
40
|
+
/** On-chain form of an uploaded file: ipfs://<cid>, not the CDN URL. */
|
|
41
|
+
export const ipfsUri = (cid) => `ipfs://${cid}`
|
|
42
|
+
export const cdnUrl = (cid) => `${CDN_BASE}/ipfs/${cid}`
|
|
43
|
+
|
|
44
|
+
export const isIpfsUri = (uri) => /^ipfs:\/\//i.test(String(uri || '').trim())
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The cid in ipfs://<cid> (also ipfs://ipfs/<cid>) or <CDN>/ipfs/<cid>, when it is a CIDv1 like ours; else null. A
|
|
48
|
+
* ?query / #hash / trailing slash is dropped: the CDN's ?x-tos-process=… thumbnail still names the original file.
|
|
49
|
+
*/
|
|
50
|
+
export function ipfsCid(uri) {
|
|
51
|
+
const s = String(uri || '').trim()
|
|
52
|
+
const cid = s.startsWith(`${CDN_BASE}/ipfs/`) ? s.slice(CDN_BASE.length + 6) : s.replace(/^ipfs:\/\/(ipfs\/)?/i, '')
|
|
53
|
+
if (cid === s) return null
|
|
54
|
+
const bare = cid.replace(/[?#].*$/s, '').replace(/\/+$/, '')
|
|
55
|
+
return CID_RE.test(bare) ? bare : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The upload response's cid (or the one in its URL); an upload without one has nothing to put on-chain. */
|
|
59
|
+
function uploadedCid(json, what, status) {
|
|
60
|
+
const cid = CID_RE.test(json?.cid) ? json.cid : ipfsCid(json?.url || json?.logoUrl)
|
|
61
|
+
if (!cid) {
|
|
62
|
+
throw statusError(`${what} upload returned no cid (${status}), so there is no ipfs:// URI to put on-chain; ${LOGO_API} `
|
|
63
|
+
+ `must answer like gang.fun's upload endpoint (GANG_LOGO_API): ${JSON.stringify(json).slice(0, 200)}`, status)
|
|
64
|
+
}
|
|
65
|
+
return cid
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Image bytes → data URL → POST /api/logos → { logoUrl: ipfs://<cid>, url: the endpoint's CDN URL, cid, id, bytes }. */
|
|
36
69
|
async function postLogo(buf, what) {
|
|
37
70
|
if (buf.length <= 0) throw new Error(`empty image: ${what}`)
|
|
38
71
|
if (buf.length > MAX_BYTES) throw new Error(`logo too large: ${buf.length} bytes (max ${MAX_BYTES}): ${what}`)
|
|
@@ -56,12 +89,13 @@ async function postLogo(buf, what) {
|
|
|
56
89
|
if (!res.ok || !json?.ok || !json?.logoUrl) {
|
|
57
90
|
throw statusError(`logo upload failed (${res.status}): ${json?.error || text.slice(0, 200)}`, res.status)
|
|
58
91
|
}
|
|
59
|
-
|
|
92
|
+
const cid = uploadedCid(json, 'logo', res.status)
|
|
93
|
+
return { logoUrl: ipfsUri(cid), url: json.url || cdnUrl(cid), cid, id: json.id, bytes: json.bytes ?? buf.length, api: LOGO_API }
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
/**
|
|
63
97
|
* Token metadata JSON (the token's contractURI / metaURI, permanent on-chain) → POST /api/logos as application/json →
|
|
64
|
-
* { url, id, bytes }. Keys with no value are left out. Same upload endpoint and CDN as logos.
|
|
98
|
+
* { uri: ipfs://<cid>, url: the endpoint's CDN URL, cid, id, bytes }. Keys with no value are left out. Same upload endpoint and CDN as logos.
|
|
65
99
|
*/
|
|
66
100
|
export async function uploadMetadata(meta) {
|
|
67
101
|
const clean = Object.fromEntries(Object.entries(meta).filter(([, v]) => v != null && v !== ''))
|
|
@@ -81,14 +115,14 @@ export async function uploadMetadata(meta) {
|
|
|
81
115
|
}
|
|
82
116
|
let json
|
|
83
117
|
try { json = JSON.parse(text) } catch { throw statusError(`metadata upload non-JSON (${res.status}): ${text.slice(0, 200)}`, res.status) }
|
|
84
|
-
|
|
85
|
-
if (!res.ok || !json?.ok || !url) {
|
|
118
|
+
if (!res.ok || !json?.ok || !(json?.url || json?.logoUrl)) {
|
|
86
119
|
throw statusError(`metadata upload failed (${res.status}): ${json?.error || text.slice(0, 200)}`, res.status)
|
|
87
120
|
}
|
|
88
|
-
|
|
121
|
+
const cid = uploadedCid(json, 'metadata', res.status)
|
|
122
|
+
return { uri: ipfsUri(cid), url: json.url || cdnUrl(cid), cid, id: json.id, bytes: json.bytes ?? buf.length, metadata: clean }
|
|
89
123
|
}
|
|
90
124
|
|
|
91
|
-
/** Read a local image → POST /api/logos → { logoUrl, id, bytes }. */
|
|
125
|
+
/** Read a local image → POST /api/logos → { logoUrl, url, cid, id, bytes }. */
|
|
92
126
|
export async function uploadLogoFile(filePath) {
|
|
93
127
|
const abs = resolve(filePath)
|
|
94
128
|
const st = statSync(abs)
|
|
@@ -97,7 +131,7 @@ export async function uploadLogoFile(filePath) {
|
|
|
97
131
|
return postLogo(readFileSync(abs), abs)
|
|
98
132
|
}
|
|
99
133
|
|
|
100
|
-
/** True when the URL is
|
|
134
|
+
/** True when the URL is an old gang.fun logo, from before the IPFS bucket (nothing to copy). */
|
|
101
135
|
export const isGangLogo = (uri) => uri.startsWith(`${LOGO_API}/`)
|
|
102
136
|
|
|
103
137
|
/** Throw unless a gang.fun logo URL serves an image (a typo'd id would go on-chain as a broken logo). Reads headers only. */
|
|
@@ -122,7 +156,7 @@ export function tempHost(uri) {
|
|
|
122
156
|
return TEMP_HOSTS.find((h) => host === h || host.endsWith(`.${h}`)) || null
|
|
123
157
|
}
|
|
124
158
|
|
|
125
|
-
/** Download an image URL (≤512 KB) → POST /api/logos → { logoUrl, id, bytes, source }. */
|
|
159
|
+
/** Download an image URL (≤512 KB) → POST /api/logos → { logoUrl, url, cid, id, bytes, source }. */
|
|
126
160
|
export async function rehostLogoUrl(uri) {
|
|
127
161
|
const msg = `could not download logo ${uri}`
|
|
128
162
|
let res
|