@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 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/api/logos/<id>.png
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/api/logos/<id>.png", ... }
94
- gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/logos/<id>.png
93
+ # → { "logoUrl": "ipfs://<cid>", "url": "https://cdn.gang.fun/ipfs/<cid>", ... }
94
+ gang create --name "My Agent Coin" --symbol MAC --logo-uri ipfs://<cid>
95
95
  gang execute --request-id …
96
96
  ```
97
97
 
@@ -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
- (a gang.fun logo URL is only checked to exist);
107
- temporary hosts such as litter.catbox.moe delete files within days. `--keep-logo-uri` puts a permanent URL (e.g. `ipfs://…`) on-chain
108
- as is; temporary hosts are refused.
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.0` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
212
+ `0.10.1` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
package/README.zh-CN.md CHANGED
@@ -80,7 +80,8 @@ gang execute --request-id …
80
80
 
81
81
  # 或者先上传,再发币
82
82
  gang upload-logo --file ./avatar.png
83
- gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/logos/<id>.png
83
+ # → { "logoUrl": "ipfs://<cid>", "url": "https://cdn.gang.fun/ipfs/<cid>", ... }
84
+ gang create --name "My Agent Coin" --symbol MAC --logo-uri ipfs://<cid>
84
85
  gang execute --request-id …
85
86
  ```
86
87
 
@@ -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),上链的是转存后的地址(gang.fun 的地址只检查图片存在);litter.catbox.moe 这类临时图床几天内就会删图。
94
- 确定地址是永久的(比如 `ipfs://…`),可以加 `--keep-logo-uri` 原样上链;临时图床会被拒绝。
94
+ 转存到 gang.fun(png / jpeg / webp / gif,≤512 KB),上链的是转存后的 `ipfs://<cid>`(已经在 gang.fun 的文件——`ipfs://<cid>` 或 `https://cdn.gang.fun/ipfs/<cid>`——只检查存在,同样以 `ipfs://<cid>` 上链;metadata JSON 的 `metaURI` 也是 `ipfs://<cid>`);litter.catbox.moe 这类临时图床几天内就会删图。
95
+ 确定地址是永久的(比如 `ipfs://…`),可以加 `--keep-logo-uri` 原样上链(gang.fun 上的文件加了也照样以 `ipfs://<cid>` 上链);临时图床会被拒绝。
95
96
 
96
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.0` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
185
+ `0.10.1` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gangfun/cli",
3
- "version": "0.10.0",
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 https://gang.fun/api/logos → logoUrl
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
- if (isGangLogo(uri)) {
412
- try { await checkGangLogo(uri) } catch (e) { fail(e.message, retryHint(e)) }
413
- return { logoURI: uri, uploaded: false }
412
+ // a file on gang.fun's IPFS bucket (ipfs://<cid> or its CDN URL) goes on-chain as ipfs://<cid>, once the CDN serves it
413
+ const cid = ipfsCid(uri)
414
+ if (cid || isGangLogo(uri)) {
415
+ try {
416
+ await checkGangLogo(cid ? cdnUrl(cid) : uri)
417
+ } catch (e) {
418
+ // an ipfs:// file that is not on gang.fun may be pinned elsewhere: on-chain as is only when asked
419
+ const elsewhere = isIpfsUri(uri) && !e.retryable
420
+ if (elsewhere && flag('keep-logo-uri')) return { logoURI: uri, uploaded: false }
421
+ fail(e.message, elsewhere ? { hint: 'not on gang.fun: use --logo ./file.png, or --keep-logo-uri if this file stays pinned on IPFS' } : retryHint(e))
422
+ }
423
+ return { logoURI: cid ? ipfsUri(cid) : uri, uploaded: false }
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.url, metadata: up.metadata }
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
- /** Image bytes → data URL → POST /api/logos → { logoUrl, id, bytes }. */
40
+ /** On-chain form of an uploaded file: ipfs://<cid>, not the CDN URL. */
41
+ export const ipfsUri = (cid) => `ipfs://${cid}`
42
+ export const cdnUrl = (cid) => `${CDN_BASE}/ipfs/${cid}`
43
+
44
+ export const isIpfsUri = (uri) => /^ipfs:\/\//i.test(String(uri || '').trim())
45
+
46
+ /**
47
+ * The cid in ipfs://<cid> (also ipfs://ipfs/<cid>) or <CDN>/ipfs/<cid>, when it is a CIDv1 like ours; else null. A
48
+ * ?query / #hash / trailing slash is dropped: the CDN's ?x-tos-process=… thumbnail still names the original file.
49
+ */
50
+ export function ipfsCid(uri) {
51
+ const s = String(uri || '').trim()
52
+ const cid = s.startsWith(`${CDN_BASE}/ipfs/`) ? s.slice(CDN_BASE.length + 6) : s.replace(/^ipfs:\/\/(ipfs\/)?/i, '')
53
+ if (cid === s) return null
54
+ const bare = cid.replace(/[?#].*$/s, '').replace(/\/+$/, '')
55
+ return CID_RE.test(bare) ? bare : null
56
+ }
57
+
58
+ /** The upload response's cid (or the one in its URL); an upload without one has nothing to put on-chain. */
59
+ function uploadedCid(json, what, status) {
60
+ const cid = CID_RE.test(json?.cid) ? json.cid : ipfsCid(json?.url || json?.logoUrl)
61
+ if (!cid) {
62
+ throw statusError(`${what} upload returned no cid (${status}), so there is no ipfs:// URI to put on-chain; ${LOGO_API} `
63
+ + `must answer like gang.fun's upload endpoint (GANG_LOGO_API): ${JSON.stringify(json).slice(0, 200)}`, status)
64
+ }
65
+ return cid
66
+ }
67
+
68
+ /** Image bytes → data URL → POST /api/logos → { logoUrl: ipfs://<cid>, url: the endpoint's CDN URL, cid, id, bytes }. */
36
69
  async function postLogo(buf, what) {
37
70
  if (buf.length <= 0) throw new Error(`empty image: ${what}`)
38
71
  if (buf.length > MAX_BYTES) throw new Error(`logo too large: ${buf.length} bytes (max ${MAX_BYTES}): ${what}`)
@@ -56,12 +89,13 @@ async function postLogo(buf, what) {
56
89
  if (!res.ok || !json?.ok || !json?.logoUrl) {
57
90
  throw statusError(`logo upload failed (${res.status}): ${json?.error || text.slice(0, 200)}`, res.status)
58
91
  }
59
- return { logoUrl: json.logoUrl, id: json.id, bytes: json.bytes ?? buf.length, api: LOGO_API }
92
+ const cid = uploadedCid(json, 'logo', res.status)
93
+ return { logoUrl: ipfsUri(cid), url: json.url || cdnUrl(cid), cid, id: json.id, bytes: json.bytes ?? buf.length, api: LOGO_API }
60
94
  }
61
95
 
62
96
  /**
63
97
  * Token metadata JSON (the token's contractURI / metaURI, permanent on-chain) → POST /api/logos as application/json →
64
- * { url, id, bytes }. Keys with no value are left out. Same upload endpoint and CDN as logos.
98
+ * { uri: ipfs://<cid>, url: the endpoint's CDN URL, cid, id, bytes }. Keys with no value are left out. Same upload endpoint and CDN as logos.
65
99
  */
66
100
  export async function uploadMetadata(meta) {
67
101
  const clean = Object.fromEntries(Object.entries(meta).filter(([, v]) => v != null && v !== ''))
@@ -81,14 +115,14 @@ export async function uploadMetadata(meta) {
81
115
  }
82
116
  let json
83
117
  try { json = JSON.parse(text) } catch { throw statusError(`metadata upload non-JSON (${res.status}): ${text.slice(0, 200)}`, res.status) }
84
- const url = json?.url || json?.logoUrl
85
- if (!res.ok || !json?.ok || !url) {
118
+ if (!res.ok || !json?.ok || !(json?.url || json?.logoUrl)) {
86
119
  throw statusError(`metadata upload failed (${res.status}): ${json?.error || text.slice(0, 200)}`, res.status)
87
120
  }
88
- return { url, id: json.id, bytes: json.bytes ?? buf.length, metadata: clean }
121
+ const cid = uploadedCid(json, 'metadata', res.status)
122
+ return { uri: ipfsUri(cid), url: json.url || cdnUrl(cid), cid, id: json.id, bytes: json.bytes ?? buf.length, metadata: clean }
89
123
  }
90
124
 
91
- /** Read a local image → POST /api/logos → { logoUrl, id, bytes }. */
125
+ /** Read a local image → POST /api/logos → { logoUrl, url, cid, id, bytes }. */
92
126
  export async function uploadLogoFile(filePath) {
93
127
  const abs = resolve(filePath)
94
128
  const st = statSync(abs)
@@ -97,7 +131,7 @@ export async function uploadLogoFile(filePath) {
97
131
  return postLogo(readFileSync(abs), abs)
98
132
  }
99
133
 
100
- /** True when the URL is already a gang.fun logo (nothing to copy). */
134
+ /** True when the URL is an old gang.fun logo, from before the IPFS bucket (nothing to copy). */
101
135
  export const isGangLogo = (uri) => uri.startsWith(`${LOGO_API}/`)
102
136
 
103
137
  /** Throw unless a gang.fun logo URL serves an image (a typo'd id would go on-chain as a broken logo). Reads headers only. */
@@ -122,7 +156,7 @@ export function tempHost(uri) {
122
156
  return TEMP_HOSTS.find((h) => host === h || host.endsWith(`.${h}`)) || null
123
157
  }
124
158
 
125
- /** Download an image URL (≤512 KB) → POST /api/logos → { logoUrl, id, bytes, source }. */
159
+ /** Download an image URL (≤512 KB) → POST /api/logos → { logoUrl, url, cid, id, bytes, source }. */
126
160
  export async function rehostLogoUrl(uri) {
127
161
  const msg = `could not download logo ${uri}`
128
162
  let res