@gangfun/cli 0.1.4 → 0.3.0
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 +59 -32
- package/README.zh-CN.md +50 -47
- package/package.json +2 -2
- package/src/abi.js +45 -3
- package/src/chain.js +238 -47
- package/src/discover.js +266 -0
- package/src/index.js +228 -28
- package/src/logo.js +40 -0
package/src/index.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
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'
|
|
8
|
+
import { API, fetchMarket, rank, watch, parseDuration } from './discover.js'
|
|
4
9
|
|
|
5
10
|
const args = process.argv.slice(2)
|
|
6
11
|
const cmd = args[0]
|
|
7
12
|
const opt = (name, def) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : def }
|
|
8
13
|
const flag = (name) => args.includes(`--${name}`)
|
|
9
14
|
const out = (o) => console.log(JSON.stringify(o, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2))
|
|
10
|
-
|
|
15
|
+
// `gang watch` speaks NDJSON, so its failures are one line too (with event/fatal for stream consumers)
|
|
16
|
+
const fail = (msg, extra = {}) => {
|
|
17
|
+
if (cmd === 'watch') console.log(JSON.stringify({ event: 'error', fatal: true, success: false, error: msg, ...extra }))
|
|
18
|
+
else out({ success: false, error: msg, ...extra })
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
11
21
|
|
|
12
22
|
const parseSlippage = (raw) => {
|
|
13
23
|
const s = Number(raw)
|
|
@@ -15,19 +25,41 @@ const parseSlippage = (raw) => {
|
|
|
15
25
|
return s
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
const
|
|
28
|
+
const parseCount = (name, raw, max) => {
|
|
29
|
+
const n = Number(raw)
|
|
30
|
+
if (!Number.isInteger(n) || n < 1 || n > max) fail(`--${name} must be an integer between 1 and ${max}`, { got: raw })
|
|
31
|
+
return n
|
|
32
|
+
}
|
|
33
|
+
const parsePct = (raw) => {
|
|
34
|
+
const n = Number(raw)
|
|
35
|
+
if (!Number.isFinite(n) || n < 0 || n > 100) fail('--min-pct must be a number between 0 and 100', { got: raw })
|
|
36
|
+
return n
|
|
37
|
+
}
|
|
38
|
+
const WATCH_EVENTS = ['trade', 'progress', 'graduated', 'new_token', 'near_grad']
|
|
39
|
+
|
|
40
|
+
const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet) · LaunchpadV2 create
|
|
19
41
|
|
|
20
42
|
gang preflight check sign-in, Developer Mode, tradeAllTokens, Agent ID
|
|
21
|
-
gang list
|
|
22
|
-
gang
|
|
23
|
-
|
|
24
|
-
gang
|
|
25
|
-
gang
|
|
26
|
-
gang
|
|
43
|
+
gang list tokens across V2 / Soft / U / S
|
|
44
|
+
gang hot [--limit 10] [--track all|v2|soft] [--include-graduated]
|
|
45
|
+
curve tokens ranked by 24h USD volume
|
|
46
|
+
gang new [--limit 10] [--max-age 2h] [--track …] newest curve tokens first
|
|
47
|
+
gang near-grad [--min-pct 50] [--limit 10] [--track …] closest to graduation
|
|
48
|
+
gang watch [<addr>] [--interval 10] [--min-pct 50] [--events a,b] [--max-events N] [--timeout 30m] [--webhook URL]
|
|
49
|
+
NDJSON stream (min interval: 3s token, 10s market). <addr>: trade / progress / graduated.
|
|
50
|
+
no addr: new_token / near_grad / graduated across the market
|
|
51
|
+
gang token --token <addr> one token (auto-resolves pad)
|
|
52
|
+
gang quote --token <addr> --bnb <n> Soft: BNB quote; V2/U/S: quote-token amount (same flag)
|
|
53
|
+
gang buy --token <addr> --bnb <n> [--slippage 1] Soft: payable BNB; V2: approve quote then buy
|
|
54
|
+
gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1]
|
|
55
|
+
gang upload-logo --file <path> POST image → https://gang.fun/api/logos → logoUrl
|
|
56
|
+
gang create --name <n> --symbol <s> [--logo-uri URL | --logo <file>] [--taxed] [--pair-or-quote 0x…]
|
|
57
|
+
LaunchpadV2 ONLY (${ADDR.v2}); Soft/U/S create refused
|
|
27
58
|
gang execute --request-id <id> broadcast a previewed call
|
|
28
59
|
gang tx --hash <0x..> wait for receipt and decode Gang events
|
|
29
60
|
|
|
30
|
-
|
|
61
|
+
Logo UI (browser): ${LOGO_UI}
|
|
62
|
+
env: GANG_LAUNCHPAD (default V2 ${ADDR.v2}), GANG_RPC_URL, GANG_LOGO_API, GANG_API (default ${API})`
|
|
31
63
|
|
|
32
64
|
function summarizePreview(p, extra = {}) {
|
|
33
65
|
if (!p.success) return { success: false, step: extra.step, error: p.error }
|
|
@@ -43,6 +75,18 @@ function summarizePreview(p, extra = {}) {
|
|
|
43
75
|
}
|
|
44
76
|
}
|
|
45
77
|
|
|
78
|
+
async function resolveLogoUri() {
|
|
79
|
+
const uri = opt('logo-uri')
|
|
80
|
+
const file = opt('logo') || opt('logo-file') || opt('file-logo')
|
|
81
|
+
if (uri && file) fail('use either --logo-uri or --logo/--logo-file, not both')
|
|
82
|
+
if (uri) return { logoURI: uri, uploaded: false }
|
|
83
|
+
if (file) {
|
|
84
|
+
const up = await uploadLogoFile(file)
|
|
85
|
+
return { logoURI: up.logoUrl, uploaded: true, upload: up }
|
|
86
|
+
}
|
|
87
|
+
return { logoURI: '', uploaded: false }
|
|
88
|
+
}
|
|
89
|
+
|
|
46
90
|
async function main() {
|
|
47
91
|
switch (cmd) {
|
|
48
92
|
case 'preflight': {
|
|
@@ -55,13 +99,97 @@ async function main() {
|
|
|
55
99
|
if (!settings.devMode?.enabled) issues.push({ code: 'DEV_MODE_OFF', fix: 'Enable Developer Mode in Binance App → Agentic Wallet settings.' })
|
|
56
100
|
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
101
|
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.' })
|
|
102
|
+
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
103
|
}
|
|
60
|
-
return out({
|
|
104
|
+
return out({
|
|
105
|
+
success: issues.length === 0, status, address: me,
|
|
106
|
+
launchpad: CONFIG.launchpad, createPad: ADDR.v2, isAgent: agent,
|
|
107
|
+
devMode: settings?.devMode?.enabled ?? null, tradeAllTokens: settings?.tradeAllTokens ?? null, issues,
|
|
108
|
+
note: 'New create → LaunchpadV2 only (~15 BNB graduation). Soft/U/S: existing buy/sell/claim only.',
|
|
109
|
+
})
|
|
61
110
|
}
|
|
62
111
|
case 'list': {
|
|
63
112
|
const me = bawStatus() === 'CONNECTED' ? bawAddress() : undefined
|
|
64
|
-
return out({ success: true,
|
|
113
|
+
return out({ success: true, createPad: ADDR.v2, tokens: await listTokens(me) })
|
|
114
|
+
}
|
|
115
|
+
case 'hot':
|
|
116
|
+
case 'new':
|
|
117
|
+
case 'near-grad': {
|
|
118
|
+
const limit = parseCount('limit', opt('limit', '10'), 100)
|
|
119
|
+
const minPct = parsePct(opt('min-pct', '50'))
|
|
120
|
+
const maxAgeSec = opt('max-age') ? parseDuration(opt('max-age'), 'max-age') : null
|
|
121
|
+
const track = opt('track', 'all')
|
|
122
|
+
const includeGraduated = flag('include-graduated')
|
|
123
|
+
const market = await fetchMarket(track)
|
|
124
|
+
const tokens = rank(market.tokens, cmd, { limit, minPct, maxAgeSec, includeGraduated })
|
|
125
|
+
const empty = {
|
|
126
|
+
hot: includeGraduated ? 'No token traded in the last 24h.' : 'No curve token traded in the last 24h.',
|
|
127
|
+
new: maxAgeSec != null ? `No curve token created in the last ${opt('max-age')}.` : 'No curve tokens.',
|
|
128
|
+
'near-grad': `No curve token at ≥${minPct}% of graduation; lower --min-pct to see the closest ones.`,
|
|
129
|
+
}[cmd]
|
|
130
|
+
return out({
|
|
131
|
+
success: true, source: API, sort: cmd, track, count: tokens.length,
|
|
132
|
+
...(cmd === 'near-grad' ? { minPct } : {}), ...(maxAgeSec != null ? { maxAgeSec } : {}),
|
|
133
|
+
...(market.truncated ? { truncated: true } : {}),
|
|
134
|
+
tokens,
|
|
135
|
+
note: tokens.length ? 'Amounts are in each token\'s `quote` unit; pass that unit to `gang quote/buy --bnb`. Data, not advice.' : empty,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
case 'watch': {
|
|
139
|
+
const positional = args[1] && !args[1].startsWith('--') ? args[1] : null
|
|
140
|
+
if (positional && flag('token')) fail('give the token either as <addr> or via --token, not both')
|
|
141
|
+
if (!positional && flag('token') && !opt('token')) fail('--token needs an address (or omit it to watch the whole market)')
|
|
142
|
+
const token = positional || opt('token')
|
|
143
|
+
if (token && !/^0x[0-9a-fA-F]{40}$/.test(token)) fail('watch: token must be a 0x… address', { got: token })
|
|
144
|
+
// market mode pulls the full token list every tick, so it gets a higher floor than a single token
|
|
145
|
+
const intervalSec = Math.max(token ? 3 : 10, parseDuration(opt('interval', '10'), 'interval'))
|
|
146
|
+
const maxEvents = opt('max-events') ? parseCount('max-events', opt('max-events'), 1e6) : Infinity
|
|
147
|
+
const deadline = opt('timeout') ? Date.now() + parseDuration(opt('timeout'), 'timeout') * 1000 : Infinity
|
|
148
|
+
const minPct = parsePct(opt('min-pct', '50'))
|
|
149
|
+
const webhook = opt('webhook')
|
|
150
|
+
if (webhook && !/^https?:\/\//.test(webhook)) fail('--webhook must be an http(s) URL', { got: webhook })
|
|
151
|
+
let events = null
|
|
152
|
+
if (opt('events')) {
|
|
153
|
+
events = new Set(opt('events').split(',').map((s) => s.trim().replace(/-/g, '_')).filter(Boolean))
|
|
154
|
+
const bad = [...events].filter((e) => !WATCH_EVENTS.includes(e))
|
|
155
|
+
if (bad.length) fail(`unknown --events: ${bad.join(', ')}`, { allowed: WATCH_EVENTS })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let count = 0
|
|
159
|
+
const hooks = new Set()
|
|
160
|
+
const line = (o) => console.log(JSON.stringify({ ...o, emittedAt: Math.floor(Date.now() / 1000) }))
|
|
161
|
+
// meta events (watching / error / ended / timeout) go to stdout only and do not count toward --max-events
|
|
162
|
+
const emit = (ev, { meta = false } = {}) => {
|
|
163
|
+
line(ev)
|
|
164
|
+
if (meta) return
|
|
165
|
+
count++
|
|
166
|
+
if (webhook) {
|
|
167
|
+
const p = fetch(webhook, {
|
|
168
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
169
|
+
body: JSON.stringify(ev), signal: AbortSignal.timeout(10_000),
|
|
170
|
+
}).then((r) => { if (!r.ok) line({ event: 'error', error: `webhook ${r.status}` }) })
|
|
171
|
+
.catch((e) => line({ event: 'error', error: `webhook: ${e.message}` }))
|
|
172
|
+
.finally(() => hooks.delete(p))
|
|
173
|
+
hooks.add(p)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// aborts in-flight API requests at the deadline so --timeout isn't stretched by a slow API.
|
|
177
|
+
// setTimeout caps at 2^31-1 ms (~24.8d) and fires at once beyond it; longer timeouts rely on sleep's check.
|
|
178
|
+
const stopper = new AbortController()
|
|
179
|
+
const delay = deadline - Date.now()
|
|
180
|
+
const deadlineTimer = delay <= 2 ** 31 - 1 ? setTimeout(() => stopper.abort(), delay) : null
|
|
181
|
+
// the timer runs on the monotonic clock and may beat Date.now() by a hair; treat its firing as the deadline
|
|
182
|
+
const shouldStop = () => count >= maxEvents || stopper.signal.aborted || Date.now() >= deadline
|
|
183
|
+
try {
|
|
184
|
+
await watch({ token, intervalSec, minPct, track: opt('track', 'all'), events, emit, shouldStop, stop: stopper.signal })
|
|
185
|
+
} catch (e) {
|
|
186
|
+
if (!shouldStop()) throw e // startup failure → fail() prints it as one NDJSON line
|
|
187
|
+
} finally {
|
|
188
|
+
clearTimeout(deadlineTimer)
|
|
189
|
+
}
|
|
190
|
+
if (count < maxEvents && (stopper.signal.aborted || Date.now() >= deadline)) line({ event: 'timeout', events: count })
|
|
191
|
+
await Promise.allSettled([...hooks])
|
|
192
|
+
return
|
|
65
193
|
}
|
|
66
194
|
case 'token': {
|
|
67
195
|
const token = opt('token'); if (!token) fail('--token required')
|
|
@@ -69,10 +197,17 @@ async function main() {
|
|
|
69
197
|
return out({ success: true, ...(await tokenInfo(token, me)) })
|
|
70
198
|
}
|
|
71
199
|
case 'quote': {
|
|
72
|
-
const token = opt('token'), bnb = opt('bnb'); if (!token || !bnb) fail('--token and --bnb required')
|
|
200
|
+
const token = opt('token'), bnb = opt('bnb'); if (!token || !bnb) fail('--token and --bnb required (Soft archive: BNB; V2: quote ERC-20 amount)')
|
|
73
201
|
const me = bawAddress()
|
|
74
202
|
const q = await quoteBuy(token, me, bnb)
|
|
75
|
-
return out({
|
|
203
|
+
return out({
|
|
204
|
+
success: true, token, buyer: me, pad: q.pad, kind: q.kind, quote: q.quote,
|
|
205
|
+
quotePath: q.quotePath, deterministic: q.deterministic || false,
|
|
206
|
+
tokensOut: q.tokensOutFmt, spent: q.spentFmt, fee: q.feeFmt, refund: q.refundFmt, unit: q.unit,
|
|
207
|
+
note: q.refund > 0n
|
|
208
|
+
? 'Part of the input would be refunded (wallet 1% cap or graduation cap).'
|
|
209
|
+
: (q.deterministic ? 'V2 quote = LaunchpadV2.sol curve math (unique path; no Soft/U/S fallback).' : undefined),
|
|
210
|
+
})
|
|
76
211
|
}
|
|
77
212
|
case 'buy': {
|
|
78
213
|
const token = opt('token'), bnb = opt('bnb'); if (!token || !bnb) fail('--token and --bnb required')
|
|
@@ -81,12 +216,38 @@ async function main() {
|
|
|
81
216
|
const info = await tokenInfo(token, me)
|
|
82
217
|
if (info.graduated) fail('token already graduated; trade it on PancakeSwap', { pair: info.pair })
|
|
83
218
|
if (!(await isAgent(me))) fail('this wallet holds no Agent ID (.ai); buying is gated', { address: me })
|
|
219
|
+
const pad = await resolvePad(token)
|
|
84
220
|
const q = await quoteBuy(token, me, bnb)
|
|
85
|
-
if (q.tokensOut === 0n) fail('wallet cap reached
|
|
221
|
+
if (q.tokensOut === 0n) fail('wallet cap reached or zero out', { myBalance: info.myBalance })
|
|
86
222
|
const minOut = (q.tokensOut * BigInt(Math.round((100 - slippage) * 100))) / 10000n
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
223
|
+
|
|
224
|
+
if (pad.kind === 'soft') {
|
|
225
|
+
const data = calldata.softBuy(token, minOut)
|
|
226
|
+
const p = bawPreview({ from: me, to: pad.address, value: parseEther(bnb), data })
|
|
227
|
+
return out(summarizePreview(p, {
|
|
228
|
+
step: 'buy', kind: 'soft', token, symbol: info.symbol, pad: pad.id,
|
|
229
|
+
bnbSent: bnb, expectTokens: q.tokensOutFmt, expectSpent: q.spentFmt, fee: q.feeFmt,
|
|
230
|
+
expectRefund: q.refundFmt, minTokensOut: fmtTok(minOut),
|
|
231
|
+
}))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// V2 / U / S: quote ERC-20 approve then buy(token, quoteIn, minTokensOut)
|
|
235
|
+
const quoteIn = parseEther(bnb)
|
|
236
|
+
const allow = await allowance(info.quote, me, pad.address)
|
|
237
|
+
if (allow < quoteIn) {
|
|
238
|
+
const p = bawPreview({ from: me, to: info.quote, data: calldata.approve(pad.address, quoteIn) })
|
|
239
|
+
return out(summarizePreview(p, {
|
|
240
|
+
step: 'approve-quote', kind: 'v2', token, symbol: info.symbol, pad: pad.id, quote: info.quote,
|
|
241
|
+
amount: bnb, note: 'Approve quote ERC-20 first. After it confirms, re-run the same `gang buy` to preview the buy.',
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
const data = calldata.v2Buy(token, quoteIn, minOut)
|
|
245
|
+
const p = bawPreview({ from: me, to: pad.address, data })
|
|
246
|
+
return out(summarizePreview(p, {
|
|
247
|
+
step: 'buy', kind: 'v2', token, symbol: info.symbol, pad: pad.id, quote: info.quote,
|
|
248
|
+
quoteIn: bnb, expectTokens: q.tokensOutFmt, expectSpent: q.spentFmt, fee: q.feeFmt,
|
|
249
|
+
expectRefund: q.refundFmt, minTokensOut: fmtTok(minOut), quotePath: q.quotePath, deterministic: q.deterministic || false,
|
|
250
|
+
}))
|
|
90
251
|
}
|
|
91
252
|
case 'sell': {
|
|
92
253
|
const token = opt('token'); if (!token) fail('--token required')
|
|
@@ -99,22 +260,56 @@ async function main() {
|
|
|
99
260
|
if (flag('all') && opt('amount')) fail('use either --all or --amount, not both')
|
|
100
261
|
const amount = flag('all') ? bal : parseEther(opt('amount'))
|
|
101
262
|
if (amount > bal) fail('amount exceeds balance', { balance: fmtTok(bal) })
|
|
102
|
-
const
|
|
263
|
+
const pad = await resolvePad(token)
|
|
264
|
+
const allow = await allowance(token, me, pad.address)
|
|
103
265
|
if (allow < amount) {
|
|
104
|
-
const p = bawPreview({ from: me, to: token, data: calldata.approve(amount) })
|
|
105
|
-
return out(summarizePreview(p, {
|
|
266
|
+
const p = bawPreview({ from: me, to: token, data: calldata.approve(pad.address, amount) })
|
|
267
|
+
return out(summarizePreview(p, {
|
|
268
|
+
step: 'approve', token, symbol: info.symbol, pad: pad.id, amount: fmtTok(amount),
|
|
269
|
+
note: 'Approve first. After it confirms on-chain, run the same `gang sell` again to preview the sell.',
|
|
270
|
+
}))
|
|
106
271
|
}
|
|
107
272
|
const slippage = parseSlippage(opt('slippage', '1'))
|
|
108
273
|
const q = await quoteSell(token, amount)
|
|
109
|
-
const minOut = (q.
|
|
110
|
-
|
|
111
|
-
|
|
274
|
+
const minOut = (q.out * BigInt(Math.round((100 - slippage) * 100))) / 10000n
|
|
275
|
+
if (pad.kind === 'soft') {
|
|
276
|
+
const p = bawPreview({ from: me, to: pad.address, data: calldata.softSell(token, amount, minOut) })
|
|
277
|
+
return out(summarizePreview(p, {
|
|
278
|
+
step: 'sell', kind: 'soft', token, symbol: info.symbol, pad: pad.id,
|
|
279
|
+
amount: fmtTok(amount), expectOut: q.outFmt, fee: q.feeFmt, minOut: fmt(minOut), unit: 'BNB',
|
|
280
|
+
}))
|
|
281
|
+
}
|
|
282
|
+
const p = bawPreview({ from: me, to: pad.address, data: calldata.v2Sell(token, amount, minOut) })
|
|
283
|
+
return out(summarizePreview(p, {
|
|
284
|
+
step: 'sell', kind: 'v2', token, symbol: info.symbol, pad: pad.id, quote: info.quote,
|
|
285
|
+
amount: fmtTok(amount), expectOut: q.outFmt, fee: q.feeFmt, minOut: fmt(minOut), unit: 'quote', quotePath: q.quotePath, deterministic: q.deterministic || false,
|
|
286
|
+
}))
|
|
287
|
+
}
|
|
288
|
+
case 'upload-logo': {
|
|
289
|
+
const file = opt('file') || opt('path') || opt('logo')
|
|
290
|
+
if (!file) fail('--file <path> required (png/jpeg/webp/gif ≤512KB)')
|
|
291
|
+
const up = await uploadLogoFile(file)
|
|
292
|
+
return out({ success: true, ...up, ui: LOGO_UI, next: `gang create --name … --symbol … --logo-uri ${up.logoUrl}` })
|
|
112
293
|
}
|
|
113
294
|
case 'create': {
|
|
114
295
|
const name = opt('name'), symbol = opt('symbol'); if (!name || !symbol) fail('--name and --symbol required')
|
|
296
|
+
try { assertCreateTarget(CONFIG.launchpad) } catch (e) { fail(e.message) }
|
|
115
297
|
const me = bawAddress()
|
|
116
|
-
|
|
117
|
-
|
|
298
|
+
if (!(await isAgent(me))) fail('this wallet holds no Agent ID (.ai); create is gated', { address: me })
|
|
299
|
+
const logo = await resolveLogoUri()
|
|
300
|
+
const taxed = flag('taxed')
|
|
301
|
+
const pairOrQuote = opt('pair-or-quote') || opt('pair') || zeroAddress
|
|
302
|
+
if (taxed && (!pairOrQuote || pairOrQuote === zeroAddress)) {
|
|
303
|
+
fail('--taxed requires --pair-or-quote <pair or quote CA>')
|
|
304
|
+
}
|
|
305
|
+
const data = calldata.createV2(name, symbol, logo.logoURI, taxed, pairOrQuote)
|
|
306
|
+
const p = bawPreview({ from: me, to: ADDR.v2, data })
|
|
307
|
+
return out(summarizePreview(p, {
|
|
308
|
+
step: 'create', pad: 'v2', launchpad: ADDR.v2, name, symbol,
|
|
309
|
+
logoURI: logo.logoURI || '(empty)', uploaded: logo.uploaded, upload: logo.upload,
|
|
310
|
+
taxed, pairOrQuote,
|
|
311
|
+
note: 'LaunchpadV2 only. Graduation ≈15 BNB (PoolCheck). No creation fee. Soft/U/S create archived. After execute: `gang tx --hash …` for Created.',
|
|
312
|
+
}))
|
|
118
313
|
}
|
|
119
314
|
case 'execute': {
|
|
120
315
|
const id = opt('request-id'); if (!id) fail('--request-id required')
|
|
@@ -122,7 +317,12 @@ async function main() {
|
|
|
122
317
|
if (!r.success) return out({ success: false, error: r.error })
|
|
123
318
|
const d = r.data
|
|
124
319
|
const pending = d.status === 'PENDING_CONFIRMATION'
|
|
125
|
-
return out({
|
|
320
|
+
return out({
|
|
321
|
+
success: true, status: d.status, txHash: d.txHash, orderId: d.orderId, pendingConfirmation: pending,
|
|
322
|
+
next: pending
|
|
323
|
+
? '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>`.'
|
|
324
|
+
: (d.txHash ? `gang tx --hash ${d.txHash}` : undefined),
|
|
325
|
+
})
|
|
126
326
|
}
|
|
127
327
|
case 'tx': {
|
|
128
328
|
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
|
+
}
|