@gangfun/cli 0.12.0 → 0.14.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/src/buyback.js ADDED
@@ -0,0 +1,205 @@
1
+ import { createPublicClient, http, parseAbi, encodeFunctionData, formatUnits, hexToString, isAddress, zeroAddress } from 'viem'
2
+ import { padNets, NOT_ON_MAINNET } from './gangpad.js'
3
+ import { walletFor } from './fair.js'
4
+ import { parseNum, revertOf as revertIn, step } from './steps.js'
5
+
6
+ /**
7
+ * `gang buyback …` and `gang create … --buyback <pct> [--buyback-to <addr>]` — GangBuybackSplitter (dev 税按比例回购
8
+ * 销毁): a V3 token's tax recipient that splits each payment between a buy-back in the token's PancakeSwap pool, sent to
9
+ * 0xdead, and the dev's payee (the dev unless it names another address; v2 splitters only, `gang buyback set-payee`).
10
+ * GangBuybackSplitterFactory clones one per token for the dev (`create(bps, salt, payee)`, at `predict(dev, salt)`) and has
11
+ * GangHandlerAdmin list it, so the dev names it as the recipient at create (launch.js does the three steps: clone →
12
+ * create → bind). A dev who doesn't
13
+ * want it leaves --buyback out: the tax goes to their own address as before.
14
+ * The ratio: a raise applies at once, a cut 3 days after it is asked for (then anyone applies it within 7 days, or it
15
+ * lapses). The keeper buys back in chunks (≤ 0.5% of the pool's quote side, never > 2% above the 10-minute average).
16
+ * Env: GANG_SPLITTER_FACTORY / GANG_TESTNET_SPLITTER_FACTORY override the built-in factory addresses.
17
+ */
18
+
19
+ // BSC 56, factory v2 (payee; docs/MAINNET-BUYBACK-SPLITTER-2026-10-28.md). v1 was 0x6bcd…8546 (2-argument create)
20
+ export const MAINNET_SPLITTER_FACTORY = '0x93467Cb38B9Cb7f6EF3b67d2226Eb8b77ca58F2f'
21
+ export const TESTNET_SPLITTER_FACTORY = '0x08b9E2Cf5F666b55F262239b19e25de25b8ba87D'
22
+ /** The factory's clones; v1 (mainnet's first factory, 0x6bcd…8546) pays the dev, v2 its payee. */
23
+ export const BUYBACK_TYPE = 'buyback-burn-v2'
24
+ const BUYBACK_TYPES = ['buyback-burn-v1', BUYBACK_TYPE]
25
+
26
+ export const FACTORY_ABI = parseAbi([
27
+ 'function create(uint16 buybackBps, bytes32 salt, address payee) returns (address splitter)',
28
+ 'function predict(address dev, bytes32 salt) view returns (address)',
29
+ 'function pad() view returns (address)',
30
+ 'error NotAgent()',
31
+ 'error NotFactory()',
32
+ 'error BadBps()',
33
+ ])
34
+
35
+ export const SPLITTER_ABI = parseAbi([
36
+ 'function moduleType() view returns (bytes32)',
37
+ 'function dev() view returns (address)',
38
+ 'function token() view returns (address)',
39
+ 'function quote() view returns (address)',
40
+ 'function buybackBps() view returns (uint16)',
41
+ 'function pendingCut() view returns (bool live, uint16 bps, uint64 at)',
42
+ 'function buybackLeft() view returns (uint256)',
43
+ 'function unsplit() view returns (uint256)',
44
+ 'function devOwed() view returns (uint256)',
45
+ 'function previewBurn() view returns (bool ready, uint256 quoteIn, uint256 tokensOut)',
46
+ 'function bind(address token)',
47
+ 'function setBuybackBps(uint16 newBps)',
48
+ 'function applyBuybackBps()',
49
+ 'function releaseDev() returns (uint256)',
50
+ 'function payee() view returns (address)',
51
+ 'function setPayee(address newPayee)',
52
+ 'error NotDev()',
53
+ 'error BadBps()',
54
+ 'error AlreadyBound()',
55
+ 'error NotCurve()',
56
+ 'error NotCreator()',
57
+ 'error NotRecipient()',
58
+ 'error NoProposal()',
59
+ 'error NotDue()',
60
+ 'error CutLapsed()',
61
+ 'error NothingOwed()',
62
+ ])
63
+
64
+ const PAD_ABI = parseAbi([
65
+ 'function taxConfigOf(address token) view returns (uint16 buyTaxBps, uint16 sellTaxBps, uint16 dividendBps, address recipient)',
66
+ ])
67
+
68
+ const HINTS = {
69
+ NotAgent: 'only an Agent can create a V3 token, so only an Agent gets a buy-back splitter',
70
+ NotFactory: 'this factory is not allowed on the platform\'s GangHandlerAdmin (any more): it cannot list a splitter',
71
+ NotDev: 'only the dev this splitter was made for can change its ratio',
72
+ NotCreator: 'the token was not created by this splitter\'s dev',
73
+ NotRecipient: 'the token does not pay its tax to this splitter',
74
+ AlreadyBound: 'the splitter is bound to a token already',
75
+ NotDue: 'the 3-day notice of this cut has not run yet',
76
+ CutLapsed: 'the cut was not applied within 7 days after its notice and has lapsed: ask for it again',
77
+ NoProposal: 'no cut asked for',
78
+ NothingOwed: 'nothing is owed to the dev',
79
+ }
80
+ const revertOf = (e) => revertIn(e, [SPLITTER_ABI, FACTORY_ABI], HINTS)
81
+
82
+ export function splitterFactoryAddress(testnet) {
83
+ return (testnet ? process.env.GANG_TESTNET_SPLITTER_FACTORY || TESTNET_SPLITTER_FACTORY
84
+ : process.env.GANG_SPLITTER_FACTORY || MAINNET_SPLITTER_FACTORY) || null
85
+ }
86
+
87
+ export const notDeployed = (testnet) =>
88
+ `GangBuybackSplitterFactory is not deployed on ${testnet ? 'the BSC testnet' : 'BSC mainnet'} in this CLI version: set ${testnet ? 'GANG_TESTNET_SPLITTER_FACTORY' : 'GANG_SPLITTER_FACTORY'} to its address`
89
+
90
+ /** --buyback <pct> (0–100, the share of the recipient's part bought back) in bps. */
91
+ export const buybackBpsOf = (fail, raw) => Math.round(parseNum(fail, 'buyback', raw, 0, 100) * 100)
92
+
93
+ const pct = (bps) => Number(bps) / 100
94
+ const iso = (s) => new Date(Number(s) * 1000).toISOString()
95
+
96
+ /** The splitter's state when `addr` is a GangBuybackSplitter; null when it is not (an EOA, a partner, another module). */
97
+ export async function splitterOf(read, addr) {
98
+ if (!addr || addr === zeroAddress) return null
99
+ let type
100
+ try { type = await read(addr, SPLITTER_ABI, 'moduleType') } catch { return null }
101
+ const moduleType = hexToString(type, { size: 32 }).replace(/\0+$/, '')
102
+ if (!BUYBACK_TYPES.includes(moduleType)) return null
103
+ const [dev, token, bps, cut, left, unsplit, owed, burn] = await Promise.all(
104
+ ['dev', 'token', 'buybackBps', 'pendingCut', 'buybackLeft', 'unsplit', 'devOwed', 'previewBurn'].map((f) => read(addr, SPLITTER_ABI, f)))
105
+ // v1 has no payee: it pays the dev
106
+ const hasPayee = moduleType !== 'buyback-burn-v1'
107
+ const payee = hasPayee ? await read(addr, SPLITTER_ABI, 'payee') : dev
108
+ const toDev = payee.toLowerCase() === dev.toLowerCase()
109
+ // the pad takes 18-decimal quotes only
110
+ const q = (x) => formatUnits(x, 18)
111
+ return {
112
+ splitter: addr, moduleType, dev, payee, payeeChangeable: hasPayee, token: token === zeroAddress ? null : token, buybackPct: pct(bps), buybackBps: Number(bps),
113
+ pendingCut: cut[0] ? { toPct: pct(cut[1]), appliesFrom: iso(cut[2]), lapsesAfter: iso(Number(cut[2]) + 7 * 86400) } : null,
114
+ buybackLeft: q(left), unsplit: q(unsplit), devOwed: q(owed),
115
+ nextChunk: burn[0] ? { quoteIn: q(burn[1]), tokensBurned: formatUnits(burn[2], 18) } : null,
116
+ note: `${pct(bps)}% of each tax payment to this recipient is bought back in the pool and burned (0xdead), the rest goes to ${toDev ? 'the dev' : `the payee ${payee}`}; a raise applies at once, a cut only 3 days after it is asked for${hasPayee ? '; the dev can change the payee at once (gang buyback set-payee)' : ''}`,
117
+ }
118
+ }
119
+
120
+ function buybackCtx(testnet) {
121
+ const net = padNets('v3')[testnet ? 'testnet' : 'mainnet']
122
+ if (!net.pad) throw new Error(NOT_ON_MAINNET)
123
+ const client = createPublicClient({ chain: net.chain, transport: http(net.rpc) })
124
+ return { ...net, client, read: (address, abi, functionName, args = []) => client.readContract({ address, abi, functionName, args }) }
125
+ }
126
+
127
+ /** Dry-run a call; { failed } with the revert when it would not pass. */
128
+ export async function simulateCall(client, from, address, abi, functionName, args = []) {
129
+ try {
130
+ const { result } = await client.simulateContract({ account: from, address, abi, functionName, args })
131
+ return { result }
132
+ } catch (e) {
133
+ const r = revertOf(e)
134
+ return { failed: r ? { reverted: true, ...r } : { reverted: false, error: e.shortMessage || e.message.split('\n')[0] } }
135
+ }
136
+ }
137
+
138
+ const SUBS = ['status', 'set', 'set-payee', 'apply', 'bind', 'release']
139
+ const USAGE = 'usage: gang buyback <status|set|set-payee|apply|bind|release> --token <addr> [--pct <0-100>] [--to <addr>] [--testnet] — see gang help'
140
+
141
+ /** `gang buyback <sub>`: the splitter of a V3 token (its tax recipient), found from --token. */
142
+ export async function runBuyback(sub, { opt, flag, out, fail, summarize }) {
143
+ if (!SUBS.includes(sub)) return fail(USAGE, { got: sub ?? null })
144
+ const testnet = flag('testnet')
145
+ const rawOnly = flag('calldata')
146
+ let ctx
147
+ try { ctx = buybackCtx(testnet) } catch (e) { return fail(e.message) }
148
+ const token = opt('token')
149
+ if (!isAddress(token ?? '')) fail('--token must be a V3 token address (0x…)', { got: token ?? null })
150
+ let recipient
151
+ try { recipient = (await ctx.read(ctx.pad, PAD_ABI, 'taxConfigOf', [token]))[3] } catch {
152
+ return fail('not a V3 token on this pad', { token, chainId: Number(ctx.chainId) })
153
+ }
154
+ const s = await splitterOf(ctx.read, recipient)
155
+ if (!s) return fail('this token\'s tax recipient is not a buy-back splitter', { token, recipient: recipient === zeroAddress ? null : recipient })
156
+ const chainId = Number(ctx.chainId)
157
+ const me = () => walletFor(ctx, opt('from'))
158
+ const send = async (functionName, args, extra) => {
159
+ const sim = await simulateCall(ctx.client, me(), s.splitter, SPLITTER_ABI, functionName, args)
160
+ if (sim.failed) return out({ success: false, step: functionName, ...sim.failed })
161
+ const data = encodeFunctionData({ abi: SPLITTER_ABI, functionName, args })
162
+ return out(step(ctx, { from: me(), to: s.splitter, data }, { chainId, token, splitter: s.splitter, ...extra }, rawOnly, summarize))
163
+ }
164
+
165
+ if (sub === 'status') return out({ success: true, chainId, ...s, ...(s.token === null && { bound: false }) })
166
+
167
+ if (sub === 'bind') {
168
+ if (s.token) return fail('already bound', { boundTo: s.token })
169
+ return send('bind', [token], { step: 'bind', note: 'anyone may bind; the token must be the dev\'s and pay its tax to this splitter' })
170
+ }
171
+
172
+ if (sub === 'set') {
173
+ if (me().toLowerCase() !== s.dev.toLowerCase()) return fail(HINTS.NotDev, { wallet: me(), dev: s.dev })
174
+ if (!flag('pct')) fail('--pct <0-100> required: the new buy-back share')
175
+ const bps = Math.round(parseNum(fail, 'pct', opt('pct'), 0, 100) * 100)
176
+ return send('setBuybackBps', [bps], bps >= s.buybackBps
177
+ ? { step: 'set-buyback', fromPct: s.buybackPct, toPct: bps / 100, note: `applies at once${s.pendingCut ? ' and cancels the pending cut' : ''}` }
178
+ : {
179
+ step: 'ask-cut', fromPct: s.buybackPct, toPct: bps / 100, appliesFromAbout: new Date(Date.now() + 3 * 86400_000).toISOString(),
180
+ note: `a cut waits 3 days (holders see it coming), then anyone runs gang buyback apply --token ${token}${testnet ? ' --testnet' : ''} within 7 days or it lapses${s.pendingCut ? '; this replaces the cut already asked for' : ''}`,
181
+ })
182
+ }
183
+
184
+ if (sub === 'set-payee') {
185
+ if (!s.payeeChangeable) return fail('this splitter (buyback-burn-v1) has no payee: it pays the dev; only splitters from the current factory (v2) can name another address')
186
+ if (me().toLowerCase() !== s.dev.toLowerCase()) return fail('only the dev this splitter was made for can change its payee', { wallet: me(), dev: s.dev })
187
+ const to = opt('to')
188
+ if (!isAddress(to ?? '') || to === zeroAddress) fail('--to <0x…> required (checksummed or all lowercase): the address the dev\'s part goes to from now on', { got: to ?? null })
189
+ if (to.toLowerCase() === s.payee.toLowerCase()) return fail('that is the payee already', { payee: s.payee })
190
+ return send('setPayee', [to], {
191
+ step: 'set-payee', fromPayee: s.payee, toPayee: to,
192
+ note: 'applies at once (the dev\'s own share; holders are not affected). Quote that arrived before is split to the current payee first; what is owed goes to the new one',
193
+ })
194
+ }
195
+
196
+ if (sub === 'apply') {
197
+ if (!s.pendingCut) return fail(HINTS.NoProposal, { buybackPct: s.buybackPct })
198
+ if (Date.parse(s.pendingCut.appliesFrom) > Date.now()) return fail(HINTS.NotDue, { appliesFrom: s.pendingCut.appliesFrom })
199
+ return send('applyBuybackBps', [], { step: 'apply-cut', fromPct: s.buybackPct, toPct: s.pendingCut.toPct, note: 'anyone may apply a cut once its notice ran' })
200
+ }
201
+
202
+ // release
203
+ if (!(Number(s.devOwed) > 0)) return fail(HINTS.NothingOwed)
204
+ return send('releaseDev', [], { step: 'release', amount: s.devOwed, payee: s.payee, note: 'anyone may; the payee gets what a failed push left owed' })
205
+ }
package/src/fair.js CHANGED
@@ -5,6 +5,7 @@ import { API } from './discover.js'
5
5
  import { UA } from './version.js'
6
6
  import { MERGED_ERRORS_ABI, MERGED_HINTS, NOT_ON_MAINNET, notThisKind, padNets } from './gangpad.js'
7
7
  import { poolPayBnb, poolTrade } from './pool.js'
8
+ import { bondAddress, bondGuard } from './bond.js'
8
9
 
9
10
  /**
10
11
  * `gang fair …` — 打新 (fair mint): one fixed price per share both ways (mint buys at it, withdraw sells back at it);
@@ -393,6 +394,11 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
393
394
  const held = await ctx.read(ctx.pad, PAD_ABI, 'positions', [token, me])
394
395
  if (!held) return fail('this wallet has no shares in this sale')
395
396
  const shares = flag('all') ? Number(held) : parseIntIn(fail, 'shares', opt('shares'), 1, Number(held))
397
+ // a bond posted during the sale already binds: selling shares back takes their tokens out of the creator's wallet
398
+ const guard = bondAddress(testnet) && await bondGuard(ctx, {
399
+ token, me, amount: BigInt(shares) * (await ctx.read(ctx.pad, PAD_ABI, 'TOKENS_PER_SHARE')), testnet, breakBond: flag('break-bond'), settleFirst: flag('settle-first'),
400
+ }, rawOnly, summarize)
401
+ if (guard?.refuse) return out({ success: false, step: 'withdraw', wallet: me, ...guard.refuse })
396
402
  const [payout, fee] = await ctx.read(ctx.pad, PAD_ABI, 'previewWithdraw', [token, me, BigInt(shares)])
397
403
  // the price is fixed, so only the exit fee can move the payout: a mint landing first can restart the 7-day clock
398
404
  // and bring a 1–2% fee back. 0.1% below the preview refuses that and nothing else.
@@ -404,7 +410,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
404
410
  step: 'withdraw', chainId: Number(ctx.chainId), token, shares,
405
411
  receive: await units(ctx, sale.quote, payout), minReceive: await units(ctx, sale.quote, minPayout),
406
412
  exitFee: await units(ctx, sale.quote, fee), quote: sale.quoteSymbol,
407
- exitFeePct: sale.exitFeeNowPct,
413
+ exitFeePct: sale.exitFeeNowPct, ...guard?.info,
408
414
  note: sale.stale
409
415
  ? `no progress for 7 days: no exit fee, the full mint price back; the ${sale.mintFeePct}% paid at mint is not returned`
410
416
  : `mint price − ${sale.exitFeeNowPct}% exit fee (none after 7 days without 10 new shares); the ${sale.mintFeePct}% paid at mint is not returned`,
@@ -462,6 +468,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
462
468
  amount: sub === 'sell' && flag('all') ? null : parseAmount(fail, 'amount', opt('amount')), all: sub === 'sell' && flag('all'),
463
469
  swapBackGas: Number(sellTaxBps) > 0, payBnb: sub === 'buy' && poolPayBnb(ctx, sale.quote, flag), toBnb: flag('to-bnb'),
464
470
  base: { kind: 'fair', pair: sale.pair, ...notes.pool, ...(outWarning && { ...outWarning, confirmedOut: true }) },
471
+ ...(sub === 'sell' && { bond: { testnet, breakBond: flag('break-bond'), settleFirst: flag('settle-first') } }),
465
472
  }, { rawOnly, summarize, revertOf }))
466
473
  }
467
474
 
package/src/index.js CHANGED
@@ -17,6 +17,9 @@ import { rememberPreview, previewFor } from './previews.js'
17
17
  import { UA, checkUpdate } from './version.js'
18
18
  import { runFair, FAIR_EVENT_ABIS } from './fair.js'
19
19
  import { runV3, v3Ctx, V3_EVENT_ABIS } from './v3.js'
20
+ import { runBond } from './bond.js'
21
+ import { runBuyback } from './buyback.js'
22
+ import { FAIR_DEV_BUY, isLaunch, LAUNCH_FLAGS, runLaunch } from './launch.js'
20
23
  import { poolPayBnb, poolTrade } from './pool.js'
21
24
  import { parseAmount, ROUTER_ABI, ROUTER_HINTS, revertOf as revertIn, step as writeStep } from './steps.js'
22
25
  import { MERGED_ERRORS_ABI, kindOf, padNets } from './gangpad.js'
@@ -277,6 +280,22 @@ graduated tokens only).
277
280
  gang.fun; the token's contractURI / metaURI, permanent). --logo-uri images are copied
278
281
  to gang.fun first (other hosts expire); --keep-logo-uri puts a permanent URL (e.g.
279
282
  ipfs://) on-chain as is; gang.fun files always go on as ipfs://<cid>
283
+ LAUNCH in one command (one tx per run: execute it, then run the very same command again until launch.stage = done):
284
+ gang create … --dev-buy <quote> [--bond <quote> [--bond-hold <tokens|N%|all>] --bond-days <7-90>]
285
+ V3: create → the dev buys on the curve (--dev-buy, as gang buy) → bonds what it
286
+ holds (--bond-hold, default all; gang bond post). Wrap / approve steps come in between
287
+ gang create … --buy-tax <1-5> --sell-tax <1-5> --buyback <0-100> [--buyback-to <addr>] [--dev-buy … [--bond …]]
288
+ V3, taxed: the recipient's part of the tax is split, --buyback % bought back in the
289
+ pool and burned (0xdead), the rest to you (or --buyback-to <addr>; changeable later
290
+ with gang buyback set-payee). Steps: clone your buy-back splitter
291
+ (GangBuybackSplitterFactory; listed as a recipient in the same tx) → create with it as
292
+ the recipient → bind → the rest of the launch. Without --buyback the tax goes to you
293
+ gang create --fair … [--shares <n>] --bond <quote> [--bond-hold …] --bond-days <7-90>
294
+ 打新: create (the dev's --shares are bought in it) → bond. Progress is read from the
295
+ chain (a token this wallet created with this symbol since the plan); a run that finds
296
+ none never creates again by itself (the first may still be landing): --restart does.
297
+ Done stays done: running it again only reports. --note goes on the create only;
298
+ --bond needs GangBond on the network (testnet only for now), checked before the create
280
299
  NOTES: gang create / buy / sell, and gang mint / withdraw on a 打新 sale, take [--note "<why>" [--conf 0-100]
281
300
  [--reveal-after 1-72h]] (a create's note is its launch manifesto); gang buy also
282
301
  --follow <note tx>. Mainnet only; pool trades (graduated / launched) carry no note
@@ -295,6 +314,51 @@ graduated tokens only).
295
314
  The router is checked first (a GangRouter with pool trades, not Pancake / an old one);
296
315
  a reverted simulation with --calldata is doNotSend: true, no tx. V2 / Soft / U / S:
297
316
  --from only on graduated tokens, mainnet only
317
+ A creator selling below its bonded X (gang bond) is refused unless --break-bond;
318
+ past the deadline, --settle-first settles the bond before the sell.
319
+
320
+ BOND (押金承诺, GangBond; any GangLaunchpad token, from create on; GANG_BOND / GANG_TESTNET_BOND = its address):
321
+ gang bond post --token <addr> --hold <tokens|N%> --days <7-90> (--amount <quote> | --cover <coverage %>) [--use-wbnb]
322
+ creator only, any time after create (V3: buy on the curve first): bond in the token's
323
+ quote (WBNB: paid in BNB, --use-wbnb pays WBNB; also on topup) promising the creator
324
+ keeps at least --hold tokens (N% = of the 1B supply) from now on. The --days clock
325
+ starts when the token graduates / launches (at once if it already has). --cover 100 =
326
+ exactly what dumping X fetches now (only once on the pair). No early withdrawal;
327
+ below X anyone reports it, on the curve too
328
+ gang bond topup --token <addr> (--amount <quote> | --cover <target %>) | gang bond raise-hold --token <addr> --hold <tokens|N%>
329
+ gang bond extend --token <addr> --days <n>
330
+ creator only: the bond only grows, X only rises, the commitment only gets longer (90
331
+ days in all before the pair; a deadline ≤ 90 days from now after it)
332
+ gang bond status --token <addr> state, phase (pending: waiting for the pair / running), X and the creator's balance /
333
+ headroom, coverage + tier once on the pair (≥100% full cover, 50–100% bond, <50% no
334
+ badge), toFullCover, time left, buy-back progress, the creator's record
335
+ gang bond start --token <addr> anyone, once the token graduated / launched: start the clock (deadline = now + days)
336
+ gang bond report --token <addr> anyone, when the creator holds less than X: 1% of the bond to you, the rest buys
337
+ back and burns the token (simulated first; nothing is sent when not violated)
338
+ gang bond settle --token <addr> anyone, after the deadline while X is held: the bond goes back to the creator
339
+ gang bond refund --token <addr> creator, posted before the pair and still no pair 30 days later: run it once to ask
340
+ (a 3-day notice: X still binds, graduation / launch voids it), again after it to
341
+ get the bond back
342
+ gang bond burn --token <addr> [--slippage 1]
343
+ anyone: one buy-back chunk of a slashed bond (≤ 0.5% of the pair's quote side; waits
344
+ for a 10-minute average and skips when the price is > 2% above it). Slashed before the
345
+ pair: waits for it; none 30 days after the post → the rest goes to 0xdead as quote
346
+ gang bond tx --hash <0x..> decode GangBond events. --testnet, --calldata, --from as in gang v3. A bonded
347
+ creator's gang v3 sell (curve or pool), gang fair sell and gang fair withdraw are
348
+ checked against X (--break-bond / --settle-first)
349
+ BUYBACK (a V3 token whose recipient is a buy-back splitter; GANG_SPLITTER_FACTORY / GANG_TESTNET_SPLITTER_FACTORY):
350
+ gang buyback status --token <addr> ratio, a cut asked for (applies from / lapses after), quote waiting to be bought
351
+ back, the payee and what is owed to it, the next chunk (the keeper buys it back
352
+ every few minutes)
353
+ gang buyback set --token <addr> --pct <0-100>
354
+ dev only: a higher share applies at once; a lower one after a 3-day notice
355
+ gang buyback set-payee --token <addr> --to <addr>
356
+ dev only: the part not bought back goes to --to from now on (at once; splitters
357
+ from the current factory; the first mainnet ones, buyback-burn-v1, pay the dev)
358
+ gang buyback apply --token <addr> anyone: apply a cut whose notice ran (within 7 days, or it lapses)
359
+ gang buyback bind --token <addr> | gang buyback release --token <addr>
360
+ anyone: bind the splitter to its token (the launch does it) / push the payee what a
361
+ failed payment left owed. --testnet, --calldata, --from as in gang v3
298
362
  gang execute --request-id <id> broadcast a previewed call
299
363
  gang tx --hash <0x..> [--testnet] wait for receipt and decode Gang events: V2 / Soft / U / S, GangLaunchpad (V3 /
300
364
  打新, tax router, vaults) (publishes a queued --note; mainnet). GangLaunchpad events
@@ -662,6 +726,8 @@ const createGateAgent = (me) =>
662
726
 
663
727
  // what `gang v3` / `gang fair` (and `gang create`) get from here; each call adds its `notes` (padNotes)
664
728
  const PAD_IO = { opt, flag, out, fail, summarize: summarizePreview, resolveLogo: resolveLogoUri, checkMeta: parseMeta, resolveMeta: resolveMetaUri }
729
+ /** `gang create … --dev-buy … [--bond …]` / `gang fair create … --bond …`: one command, one tx per run (launch.js). */
730
+ const LAUNCH_DEPS = { io: PAD_IO, runV3, runFair, runBond, padNotes: (kind, sub, pre) => padNotes(kind, sub, pre) }
665
731
 
666
732
  // ---------------------------------------------------------------- one command per action, whatever the pad
667
733
  // gang buy|sell|info|status|claim|mint|withdraw|release --token <addr> ask GangLaunchpad's kindOf what the token is and
@@ -780,16 +846,19 @@ function create() {
780
846
  if (flag('fair')) {
781
847
  const wrong = V3_CREATE_FLAGS.find(flag)
782
848
  if (wrong) fail(`--${wrong} is a V3 tax setting; a 打新 sale's pool tax is fixed (1%): leave it out, or drop --fair for a V3 token`)
849
+ if (isLaunch('fair', flag)) return runLaunch('fair', LAUNCH_DEPS)
783
850
  return runFair('create', { ...PAD_IO, notes: padNotes('fair', 'create') })
784
851
  }
785
852
  const wrong = FAIR_CREATE_FLAGS.find(flag)
786
853
  if (wrong) fail(`--${wrong} is a 打新 (fair-mint) setting: add --fair to create a sale (gang create --fair …)`)
854
+ if (isLaunch('v3', flag)) return runLaunch('v3', LAUNCH_DEPS)
787
855
  return runV3('create', { ...PAD_IO, notes: padNotes('v3', 'create') })
788
856
  }
789
857
 
790
858
  // commands whose JSON result carries `update`; `watch` streams NDJSON and help is plain text
791
859
  const UPDATE_CHECKED = new Set(['preflight', 'list', 'hot', 'new', 'near-grad', 'notes', 'agents', 'token', 'info', 'status', 'quote',
792
- 'buy', 'follow', 'sell', 'claim', 'mint', 'withdraw', 'release', 'upload-logo', 'create', 'execute', 'tx', 'fair', 'v3'])
860
+ 'buy', 'follow', 'sell', 'claim', 'mint', 'withdraw', 'release', 'upload-logo', 'create', 'execute', 'tx', 'fair', 'v3', 'bond',
861
+ 'buyback'])
793
862
  // gang tx decodes these beside the V2 / Soft / U / S pads' events (GangLaunchpad: both kinds; the tax router; the vaults)
794
863
  const GANG_EVENT_ABIS = [...V3_EVENT_ABIS, ...FAIR_EVENT_ABIS]
795
864
 
@@ -803,11 +872,26 @@ async function main() {
803
872
  if (flag('description') && !tokenCreate) {
804
873
  fail('--description only applies when creating a token (`gang create`, `gang create --fair`; it goes into the token metadata)')
805
874
  }
875
+ // --bond-hold / --bond-days / --restart only mean something in a chained launch (--dev-buy / --bond); never drop them silently
876
+ // gang fair create and gang create --fair are 打新 creates; the rest V3
877
+ const launchKind = cmd === 'fair' || (cmd === 'create' && flag('fair')) ? 'fair' : 'v3'
878
+ if (tokenCreate && !isLaunch(launchKind, flag)) {
879
+ // a 打新 dev buys with --shares in the create itself: say so here, not "add --bond" (which would only lead to that)
880
+ if (launchKind === 'fair' && flag('dev-buy')) fail(FAIR_DEV_BUY)
881
+ const lone = LAUNCH_FLAGS.find((f) => flag(f))
882
+ if (lone) fail(`--${lone} only applies to a launch in one command: add --bond <quote>${launchKind === 'fair' ? '' : ' (or --dev-buy <quote> / --buyback <pct>)'}`)
883
+ }
806
884
  switch (cmd) {
807
885
  case 'fair':
886
+ if (args[1] === 'create' && isLaunch('fair', flag)) return runLaunch('fair', LAUNCH_DEPS)
808
887
  return runFair(args[1], alias('fair', args[1]))
809
888
  case 'v3':
889
+ if (args[1] === 'create' && isLaunch('v3', flag)) return runLaunch('v3', LAUNCH_DEPS)
810
890
  return runV3(args[1], alias('v3', args[1]))
891
+ case 'bond':
892
+ return runBond(args[1], { opt, flag, out, fail, summarize: summarizePreview })
893
+ case 'buyback':
894
+ return runBuyback(args[1], { opt, flag, out, fail, summarize: summarizePreview })
811
895
  case 'preflight': {
812
896
  const issues = []
813
897
  const status = bawStatus()