@gangfun/cli 0.12.0 → 0.13.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 +18 -1
- package/README.zh-CN.md +18 -1
- package/package.json +1 -1
- package/src/bond.js +564 -0
- package/src/buyback.js +183 -0
- package/src/fair.js +8 -1
- package/src/index.js +80 -1
- package/src/launch.js +284 -0
- package/src/pool.js +10 -1
- package/src/v3.js +11 -2
package/src/buyback.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
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>` — GangBuybackSplitter (dev 税按比例回购销毁): a V3 token's tax
|
|
8
|
+
* recipient that splits each payment between a buy-back in the token's PancakeSwap pool, sent to 0xdead, and the dev.
|
|
9
|
+
* GangBuybackSplitterFactory clones one per token for the dev (`create(bps, salt)`, at `predict(dev, salt)`) and has
|
|
10
|
+
* GangHandlerAdmin list it, so the dev names it as the recipient at create (launch.js does the three steps: clone →
|
|
11
|
+
* create → bind). A dev who doesn't
|
|
12
|
+
* want it leaves --buyback out: the tax goes to their own address as before.
|
|
13
|
+
* 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
|
|
14
|
+
* lapses). The keeper buys back in chunks (≤ 0.5% of the pool's quote side, never > 2% above the 10-minute average).
|
|
15
|
+
* Env: GANG_SPLITTER_FACTORY / GANG_TESTNET_SPLITTER_FACTORY override the built-in factory addresses.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// BSC 56, 2026-10-28 (docs/MAINNET-BUYBACK-SPLITTER-2026-10-28.md); lists clones once the Safe ran its batch
|
|
19
|
+
export const MAINNET_SPLITTER_FACTORY = '0x6bcd79206da85Aa0643Db2164Ab887b468618546'
|
|
20
|
+
export const TESTNET_SPLITTER_FACTORY = '0x0Ff5C8801BdeaDac374fC14fEd6e7B31977c9DA1'
|
|
21
|
+
export const BUYBACK_TYPE = 'buyback-burn-v1'
|
|
22
|
+
|
|
23
|
+
export const FACTORY_ABI = parseAbi([
|
|
24
|
+
'function create(uint16 buybackBps, bytes32 salt) returns (address splitter)',
|
|
25
|
+
'function predict(address dev, bytes32 salt) view returns (address)',
|
|
26
|
+
'function pad() view returns (address)',
|
|
27
|
+
'error NotAgent()',
|
|
28
|
+
'error NotFactory()',
|
|
29
|
+
'error BadBps()',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
export const SPLITTER_ABI = parseAbi([
|
|
33
|
+
'function moduleType() view returns (bytes32)',
|
|
34
|
+
'function dev() view returns (address)',
|
|
35
|
+
'function token() view returns (address)',
|
|
36
|
+
'function quote() view returns (address)',
|
|
37
|
+
'function buybackBps() view returns (uint16)',
|
|
38
|
+
'function pendingCut() view returns (bool live, uint16 bps, uint64 at)',
|
|
39
|
+
'function buybackLeft() view returns (uint256)',
|
|
40
|
+
'function unsplit() view returns (uint256)',
|
|
41
|
+
'function devOwed() view returns (uint256)',
|
|
42
|
+
'function previewBurn() view returns (bool ready, uint256 quoteIn, uint256 tokensOut)',
|
|
43
|
+
'function bind(address token)',
|
|
44
|
+
'function setBuybackBps(uint16 newBps)',
|
|
45
|
+
'function applyBuybackBps()',
|
|
46
|
+
'function releaseDev() returns (uint256)',
|
|
47
|
+
'error NotDev()',
|
|
48
|
+
'error BadBps()',
|
|
49
|
+
'error AlreadyBound()',
|
|
50
|
+
'error NotCurve()',
|
|
51
|
+
'error NotCreator()',
|
|
52
|
+
'error NotRecipient()',
|
|
53
|
+
'error NoProposal()',
|
|
54
|
+
'error NotDue()',
|
|
55
|
+
'error CutLapsed()',
|
|
56
|
+
'error NothingOwed()',
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
const PAD_ABI = parseAbi([
|
|
60
|
+
'function taxConfigOf(address token) view returns (uint16 buyTaxBps, uint16 sellTaxBps, uint16 dividendBps, address recipient)',
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const HINTS = {
|
|
64
|
+
NotAgent: 'only an Agent can create a V3 token, so only an Agent gets a buy-back splitter',
|
|
65
|
+
NotFactory: 'this factory is not allowed on the platform\'s GangHandlerAdmin (any more): it cannot list a splitter',
|
|
66
|
+
NotDev: 'only the dev this splitter was made for can change its ratio',
|
|
67
|
+
NotCreator: 'the token was not created by this splitter\'s dev',
|
|
68
|
+
NotRecipient: 'the token does not pay its tax to this splitter',
|
|
69
|
+
AlreadyBound: 'the splitter is bound to a token already',
|
|
70
|
+
NotDue: 'the 3-day notice of this cut has not run yet',
|
|
71
|
+
CutLapsed: 'the cut was not applied within 7 days after its notice and has lapsed: ask for it again',
|
|
72
|
+
NoProposal: 'no cut asked for',
|
|
73
|
+
NothingOwed: 'nothing is owed to the dev',
|
|
74
|
+
}
|
|
75
|
+
const revertOf = (e) => revertIn(e, [SPLITTER_ABI, FACTORY_ABI], HINTS)
|
|
76
|
+
|
|
77
|
+
export function splitterFactoryAddress(testnet) {
|
|
78
|
+
return (testnet ? process.env.GANG_TESTNET_SPLITTER_FACTORY || TESTNET_SPLITTER_FACTORY
|
|
79
|
+
: process.env.GANG_SPLITTER_FACTORY || MAINNET_SPLITTER_FACTORY) || null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const notDeployed = (testnet) =>
|
|
83
|
+
`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`
|
|
84
|
+
|
|
85
|
+
/** --buyback <pct> (0–100, the share of the recipient's part bought back) in bps. */
|
|
86
|
+
export const buybackBpsOf = (fail, raw) => Math.round(parseNum(fail, 'buyback', raw, 0, 100) * 100)
|
|
87
|
+
|
|
88
|
+
const pct = (bps) => Number(bps) / 100
|
|
89
|
+
const iso = (s) => new Date(Number(s) * 1000).toISOString()
|
|
90
|
+
|
|
91
|
+
/** The splitter's state when `addr` is a GangBuybackSplitter; null when it is not (an EOA, a partner, another module). */
|
|
92
|
+
export async function splitterOf(read, addr) {
|
|
93
|
+
if (!addr || addr === zeroAddress) return null
|
|
94
|
+
let type
|
|
95
|
+
try { type = await read(addr, SPLITTER_ABI, 'moduleType') } catch { return null }
|
|
96
|
+
if (hexToString(type, { size: 32 }) !== BUYBACK_TYPE) return null
|
|
97
|
+
const [dev, token, bps, cut, left, unsplit, owed, burn] = await Promise.all(
|
|
98
|
+
['dev', 'token', 'buybackBps', 'pendingCut', 'buybackLeft', 'unsplit', 'devOwed', 'previewBurn'].map((f) => read(addr, SPLITTER_ABI, f)))
|
|
99
|
+
// the pad takes 18-decimal quotes only
|
|
100
|
+
const q = (x) => formatUnits(x, 18)
|
|
101
|
+
return {
|
|
102
|
+
splitter: addr, moduleType: BUYBACK_TYPE, dev, token: token === zeroAddress ? null : token, buybackPct: pct(bps), buybackBps: Number(bps),
|
|
103
|
+
pendingCut: cut[0] ? { toPct: pct(cut[1]), appliesFrom: iso(cut[2]), lapsesAfter: iso(Number(cut[2]) + 7 * 86400) } : null,
|
|
104
|
+
buybackLeft: q(left), unsplit: q(unsplit), devOwed: q(owed),
|
|
105
|
+
nextChunk: burn[0] ? { quoteIn: q(burn[1]), tokensBurned: formatUnits(burn[2], 18) } : null,
|
|
106
|
+
note: `${pct(bps)}% of each tax payment to this recipient is bought back in the pool and burned (0xdead), the rest goes to the dev; a raise applies at once, a cut only 3 days after it is asked for`,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buybackCtx(testnet) {
|
|
111
|
+
const net = padNets('v3')[testnet ? 'testnet' : 'mainnet']
|
|
112
|
+
if (!net.pad) throw new Error(NOT_ON_MAINNET)
|
|
113
|
+
const client = createPublicClient({ chain: net.chain, transport: http(net.rpc) })
|
|
114
|
+
return { ...net, client, read: (address, abi, functionName, args = []) => client.readContract({ address, abi, functionName, args }) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Dry-run a call; { failed } with the revert when it would not pass. */
|
|
118
|
+
export async function simulateCall(client, from, address, abi, functionName, args = []) {
|
|
119
|
+
try {
|
|
120
|
+
const { result } = await client.simulateContract({ account: from, address, abi, functionName, args })
|
|
121
|
+
return { result }
|
|
122
|
+
} catch (e) {
|
|
123
|
+
const r = revertOf(e)
|
|
124
|
+
return { failed: r ? { reverted: true, ...r } : { reverted: false, error: e.shortMessage || e.message.split('\n')[0] } }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const SUBS = ['status', 'set', 'apply', 'bind', 'release']
|
|
129
|
+
const USAGE = 'usage: gang buyback <status|set|apply|bind|release> --token <addr> [--pct <0-100>] [--testnet] — see gang help'
|
|
130
|
+
|
|
131
|
+
/** `gang buyback <sub>`: the splitter of a V3 token (its tax recipient), found from --token. */
|
|
132
|
+
export async function runBuyback(sub, { opt, flag, out, fail, summarize }) {
|
|
133
|
+
if (!SUBS.includes(sub)) return fail(USAGE, { got: sub ?? null })
|
|
134
|
+
const testnet = flag('testnet')
|
|
135
|
+
const rawOnly = flag('calldata')
|
|
136
|
+
let ctx
|
|
137
|
+
try { ctx = buybackCtx(testnet) } catch (e) { return fail(e.message) }
|
|
138
|
+
const token = opt('token')
|
|
139
|
+
if (!isAddress(token ?? '')) fail('--token must be a V3 token address (0x…)', { got: token ?? null })
|
|
140
|
+
let recipient
|
|
141
|
+
try { recipient = (await ctx.read(ctx.pad, PAD_ABI, 'taxConfigOf', [token]))[3] } catch {
|
|
142
|
+
return fail('not a V3 token on this pad', { token, chainId: Number(ctx.chainId) })
|
|
143
|
+
}
|
|
144
|
+
const s = await splitterOf(ctx.read, recipient)
|
|
145
|
+
if (!s) return fail('this token\'s tax recipient is not a buy-back splitter', { token, recipient: recipient === zeroAddress ? null : recipient })
|
|
146
|
+
const chainId = Number(ctx.chainId)
|
|
147
|
+
const me = () => walletFor(ctx, opt('from'))
|
|
148
|
+
const send = async (functionName, args, extra) => {
|
|
149
|
+
const sim = await simulateCall(ctx.client, me(), s.splitter, SPLITTER_ABI, functionName, args)
|
|
150
|
+
if (sim.failed) return out({ success: false, step: functionName, ...sim.failed })
|
|
151
|
+
const data = encodeFunctionData({ abi: SPLITTER_ABI, functionName, args })
|
|
152
|
+
return out(step(ctx, { from: me(), to: s.splitter, data }, { chainId, token, splitter: s.splitter, ...extra }, rawOnly, summarize))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (sub === 'status') return out({ success: true, chainId, ...s, ...(s.token === null && { bound: false }) })
|
|
156
|
+
|
|
157
|
+
if (sub === 'bind') {
|
|
158
|
+
if (s.token) return fail('already bound', { boundTo: s.token })
|
|
159
|
+
return send('bind', [token], { step: 'bind', note: 'anyone may bind; the token must be the dev\'s and pay its tax to this splitter' })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (sub === 'set') {
|
|
163
|
+
if (me().toLowerCase() !== s.dev.toLowerCase()) return fail(HINTS.NotDev, { wallet: me(), dev: s.dev })
|
|
164
|
+
if (!flag('pct')) fail('--pct <0-100> required: the new buy-back share')
|
|
165
|
+
const bps = Math.round(parseNum(fail, 'pct', opt('pct'), 0, 100) * 100)
|
|
166
|
+
return send('setBuybackBps', [bps], bps >= s.buybackBps
|
|
167
|
+
? { step: 'set-buyback', fromPct: s.buybackPct, toPct: bps / 100, note: `applies at once${s.pendingCut ? ' and cancels the pending cut' : ''}` }
|
|
168
|
+
: {
|
|
169
|
+
step: 'ask-cut', fromPct: s.buybackPct, toPct: bps / 100, appliesFromAbout: new Date(Date.now() + 3 * 86400_000).toISOString(),
|
|
170
|
+
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' : ''}`,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (sub === 'apply') {
|
|
175
|
+
if (!s.pendingCut) return fail(HINTS.NoProposal, { buybackPct: s.buybackPct })
|
|
176
|
+
if (Date.parse(s.pendingCut.appliesFrom) > Date.now()) return fail(HINTS.NotDue, { appliesFrom: s.pendingCut.appliesFrom })
|
|
177
|
+
return send('applyBuybackBps', [], { step: 'apply-cut', fromPct: s.buybackPct, toPct: s.pendingCut.toPct, note: 'anyone may apply a cut once its notice ran' })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// release
|
|
181
|
+
if (!(Number(s.devOwed) > 0)) return fail(HINTS.NothingOwed)
|
|
182
|
+
return send('releaseDev', [], { step: 'release', amount: s.devOwed, note: 'anyone may; the dev gets what a failed push left owed' })
|
|
183
|
+
}
|
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,21 @@ 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> [--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. Steps: clone your buy-back splitter
|
|
290
|
+
(GangBuybackSplitterFactory; listed as a recipient in the same tx) → create with it as
|
|
291
|
+
the recipient → bind → the rest of the launch. Without --buyback the tax goes to you
|
|
292
|
+
gang create --fair … [--shares <n>] --bond <quote> [--bond-hold …] --bond-days <7-90>
|
|
293
|
+
打新: create (the dev's --shares are bought in it) → bond. Progress is read from the
|
|
294
|
+
chain (a token this wallet created with this symbol since the plan); a run that finds
|
|
295
|
+
none never creates again by itself (the first may still be landing): --restart does.
|
|
296
|
+
Done stays done: running it again only reports. --note goes on the create only;
|
|
297
|
+
--bond needs GangBond on the network (testnet only for now), checked before the create
|
|
280
298
|
NOTES: gang create / buy / sell, and gang mint / withdraw on a 打新 sale, take [--note "<why>" [--conf 0-100]
|
|
281
299
|
[--reveal-after 1-72h]] (a create's note is its launch manifesto); gang buy also
|
|
282
300
|
--follow <note tx>. Mainnet only; pool trades (graduated / launched) carry no note
|
|
@@ -295,6 +313,47 @@ graduated tokens only).
|
|
|
295
313
|
The router is checked first (a GangRouter with pool trades, not Pancake / an old one);
|
|
296
314
|
a reverted simulation with --calldata is doNotSend: true, no tx. V2 / Soft / U / S:
|
|
297
315
|
--from only on graduated tokens, mainnet only
|
|
316
|
+
A creator selling below its bonded X (gang bond) is refused unless --break-bond;
|
|
317
|
+
past the deadline, --settle-first settles the bond before the sell.
|
|
318
|
+
|
|
319
|
+
BOND (押金承诺, GangBond; any GangLaunchpad token, from create on; GANG_BOND / GANG_TESTNET_BOND = its address):
|
|
320
|
+
gang bond post --token <addr> --hold <tokens|N%> --days <7-90> (--amount <quote> | --cover <coverage %>) [--use-wbnb]
|
|
321
|
+
creator only, any time after create (V3: buy on the curve first): bond in the token's
|
|
322
|
+
quote (WBNB: paid in BNB, --use-wbnb pays WBNB; also on topup) promising the creator
|
|
323
|
+
keeps at least --hold tokens (N% = of the 1B supply) from now on. The --days clock
|
|
324
|
+
starts when the token graduates / launches (at once if it already has). --cover 100 =
|
|
325
|
+
exactly what dumping X fetches now (only once on the pair). No early withdrawal;
|
|
326
|
+
below X anyone reports it, on the curve too
|
|
327
|
+
gang bond topup --token <addr> (--amount <quote> | --cover <target %>) | gang bond raise-hold --token <addr> --hold <tokens|N%>
|
|
328
|
+
gang bond extend --token <addr> --days <n>
|
|
329
|
+
creator only: the bond only grows, X only rises, the commitment only gets longer (90
|
|
330
|
+
days in all before the pair; a deadline ≤ 90 days from now after it)
|
|
331
|
+
gang bond status --token <addr> state, phase (pending: waiting for the pair / running), X and the creator's balance /
|
|
332
|
+
headroom, coverage + tier once on the pair (≥100% full cover, 50–100% bond, <50% no
|
|
333
|
+
badge), toFullCover, time left, buy-back progress, the creator's record
|
|
334
|
+
gang bond start --token <addr> anyone, once the token graduated / launched: start the clock (deadline = now + days)
|
|
335
|
+
gang bond report --token <addr> anyone, when the creator holds less than X: 1% of the bond to you, the rest buys
|
|
336
|
+
back and burns the token (simulated first; nothing is sent when not violated)
|
|
337
|
+
gang bond settle --token <addr> anyone, after the deadline while X is held: the bond goes back to the creator
|
|
338
|
+
gang bond refund --token <addr> creator, posted before the pair and still no pair 30 days later: run it once to ask
|
|
339
|
+
(a 3-day notice: X still binds, graduation / launch voids it), again after it to
|
|
340
|
+
get the bond back
|
|
341
|
+
gang bond burn --token <addr> [--slippage 1]
|
|
342
|
+
anyone: one buy-back chunk of a slashed bond (≤ 0.5% of the pair's quote side; waits
|
|
343
|
+
for a 10-minute average and skips when the price is > 2% above it). Slashed before the
|
|
344
|
+
pair: waits for it; none 30 days after the post → the rest goes to 0xdead as quote
|
|
345
|
+
gang bond tx --hash <0x..> decode GangBond events. --testnet, --calldata, --from as in gang v3. A bonded
|
|
346
|
+
creator's gang v3 sell (curve or pool), gang fair sell and gang fair withdraw are
|
|
347
|
+
checked against X (--break-bond / --settle-first)
|
|
348
|
+
BUYBACK (a V3 token whose recipient is a buy-back splitter; GANG_SPLITTER_FACTORY / GANG_TESTNET_SPLITTER_FACTORY):
|
|
349
|
+
gang buyback status --token <addr> ratio, a cut asked for (applies from / lapses after), quote waiting to be bought
|
|
350
|
+
back, owed to the dev, the next chunk (the keeper buys it back every few minutes)
|
|
351
|
+
gang buyback set --token <addr> --pct <0-100>
|
|
352
|
+
dev only: a higher share applies at once; a lower one after a 3-day notice
|
|
353
|
+
gang buyback apply --token <addr> anyone: apply a cut whose notice ran (within 7 days, or it lapses)
|
|
354
|
+
gang buyback bind --token <addr> | gang buyback release --token <addr>
|
|
355
|
+
anyone: bind the splitter to its token (the launch does it) / push the dev what a
|
|
356
|
+
failed payment left owed. --testnet, --calldata, --from as in gang v3
|
|
298
357
|
gang execute --request-id <id> broadcast a previewed call
|
|
299
358
|
gang tx --hash <0x..> [--testnet] wait for receipt and decode Gang events: V2 / Soft / U / S, GangLaunchpad (V3 /
|
|
300
359
|
打新, tax router, vaults) (publishes a queued --note; mainnet). GangLaunchpad events
|
|
@@ -662,6 +721,8 @@ const createGateAgent = (me) =>
|
|
|
662
721
|
|
|
663
722
|
// what `gang v3` / `gang fair` (and `gang create`) get from here; each call adds its `notes` (padNotes)
|
|
664
723
|
const PAD_IO = { opt, flag, out, fail, summarize: summarizePreview, resolveLogo: resolveLogoUri, checkMeta: parseMeta, resolveMeta: resolveMetaUri }
|
|
724
|
+
/** `gang create … --dev-buy … [--bond …]` / `gang fair create … --bond …`: one command, one tx per run (launch.js). */
|
|
725
|
+
const LAUNCH_DEPS = { io: PAD_IO, runV3, runFair, runBond, padNotes: (kind, sub, pre) => padNotes(kind, sub, pre) }
|
|
665
726
|
|
|
666
727
|
// ---------------------------------------------------------------- one command per action, whatever the pad
|
|
667
728
|
// gang buy|sell|info|status|claim|mint|withdraw|release --token <addr> ask GangLaunchpad's kindOf what the token is and
|
|
@@ -780,16 +841,19 @@ function create() {
|
|
|
780
841
|
if (flag('fair')) {
|
|
781
842
|
const wrong = V3_CREATE_FLAGS.find(flag)
|
|
782
843
|
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`)
|
|
844
|
+
if (isLaunch('fair', flag)) return runLaunch('fair', LAUNCH_DEPS)
|
|
783
845
|
return runFair('create', { ...PAD_IO, notes: padNotes('fair', 'create') })
|
|
784
846
|
}
|
|
785
847
|
const wrong = FAIR_CREATE_FLAGS.find(flag)
|
|
786
848
|
if (wrong) fail(`--${wrong} is a 打新 (fair-mint) setting: add --fair to create a sale (gang create --fair …)`)
|
|
849
|
+
if (isLaunch('v3', flag)) return runLaunch('v3', LAUNCH_DEPS)
|
|
787
850
|
return runV3('create', { ...PAD_IO, notes: padNotes('v3', 'create') })
|
|
788
851
|
}
|
|
789
852
|
|
|
790
853
|
// commands whose JSON result carries `update`; `watch` streams NDJSON and help is plain text
|
|
791
854
|
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'
|
|
855
|
+
'buy', 'follow', 'sell', 'claim', 'mint', 'withdraw', 'release', 'upload-logo', 'create', 'execute', 'tx', 'fair', 'v3', 'bond',
|
|
856
|
+
'buyback'])
|
|
793
857
|
// gang tx decodes these beside the V2 / Soft / U / S pads' events (GangLaunchpad: both kinds; the tax router; the vaults)
|
|
794
858
|
const GANG_EVENT_ABIS = [...V3_EVENT_ABIS, ...FAIR_EVENT_ABIS]
|
|
795
859
|
|
|
@@ -803,11 +867,26 @@ async function main() {
|
|
|
803
867
|
if (flag('description') && !tokenCreate) {
|
|
804
868
|
fail('--description only applies when creating a token (`gang create`, `gang create --fair`; it goes into the token metadata)')
|
|
805
869
|
}
|
|
870
|
+
// --bond-hold / --bond-days / --restart only mean something in a chained launch (--dev-buy / --bond); never drop them silently
|
|
871
|
+
// gang fair create and gang create --fair are 打新 creates; the rest V3
|
|
872
|
+
const launchKind = cmd === 'fair' || (cmd === 'create' && flag('fair')) ? 'fair' : 'v3'
|
|
873
|
+
if (tokenCreate && !isLaunch(launchKind, flag)) {
|
|
874
|
+
// a 打新 dev buys with --shares in the create itself: say so here, not "add --bond" (which would only lead to that)
|
|
875
|
+
if (launchKind === 'fair' && flag('dev-buy')) fail(FAIR_DEV_BUY)
|
|
876
|
+
const lone = LAUNCH_FLAGS.find((f) => flag(f))
|
|
877
|
+
if (lone) fail(`--${lone} only applies to a launch in one command: add --bond <quote>${launchKind === 'fair' ? '' : ' (or --dev-buy <quote> / --buyback <pct>)'}`)
|
|
878
|
+
}
|
|
806
879
|
switch (cmd) {
|
|
807
880
|
case 'fair':
|
|
881
|
+
if (args[1] === 'create' && isLaunch('fair', flag)) return runLaunch('fair', LAUNCH_DEPS)
|
|
808
882
|
return runFair(args[1], alias('fair', args[1]))
|
|
809
883
|
case 'v3':
|
|
884
|
+
if (args[1] === 'create' && isLaunch('v3', flag)) return runLaunch('v3', LAUNCH_DEPS)
|
|
810
885
|
return runV3(args[1], alias('v3', args[1]))
|
|
886
|
+
case 'bond':
|
|
887
|
+
return runBond(args[1], { opt, flag, out, fail, summarize: summarizePreview })
|
|
888
|
+
case 'buyback':
|
|
889
|
+
return runBuyback(args[1], { opt, flag, out, fail, summarize: summarizePreview })
|
|
811
890
|
case 'preflight': {
|
|
812
891
|
const issues = []
|
|
813
892
|
const status = bawStatus()
|
package/src/launch.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { createPublicClient, http, parseAbi, formatUnits, encodeFunctionData, zeroAddress } from 'viem'
|
|
5
|
+
import { DIR } from './notes.js'
|
|
6
|
+
import { padNets, NOT_ON_MAINNET } from './gangpad.js'
|
|
7
|
+
import { bondAddress, BOND_ABI } from './bond.js'
|
|
8
|
+
import { walletFor } from './fair.js'
|
|
9
|
+
import { parseAmount, parseIntIn, step } from './steps.js'
|
|
10
|
+
import { buybackBpsOf, FACTORY_ABI, notDeployed, simulateCall, SPLITTER_ABI, splitterFactoryAddress, splitterOf } from './buyback.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One command for a launch: `gang create … [--buyback <pct>] [--dev-buy <quote> [--bond <quote> [--bond-hold <tokens|N%|all>]
|
|
14
|
+
* --bond-days <7-90>]]` (V3) and `gang fair create … [--shares <n>] --bond <quote> [--bond-hold …] --bond-days <7-90>` (打新: the dev buys its
|
|
15
|
+
* shares in the create itself). Each run builds the next tx, as every other multi-step command does (wrap / approve
|
|
16
|
+
* steps in between): run it, execute, run the same command again.
|
|
17
|
+
*
|
|
18
|
+
* Progress comes from the chain, not from what was executed: a plan file keeps the wallet, the name / symbol and the
|
|
19
|
+
* pad's token count before the create, and each run looks for a token this wallet created with that symbol since then
|
|
20
|
+
* (none yet → the create; found and the dev holds none → the V3 dev buy; holds some and no bond → the bond; else done).
|
|
21
|
+
* The plan is written only once the create preview succeeds, and a run that finds no token never builds a second
|
|
22
|
+
* create on its own (the first may still be landing): `--restart` does. The plan keeps the token once found (no rescan
|
|
23
|
+
* on later runs) and is marked done at the end: a done launch only reports, even if the dev's balance or the bond
|
|
24
|
+
* changed since (no second buy, bond or create) — for 7 days, then the plan expires.
|
|
25
|
+
* --buyback (V3, taxed): before the create, GangBuybackSplitterFactory clones the dev's splitter at
|
|
26
|
+
* predict(dev, salt), with a fresh salt per plan kept in it (so the address is known before the clone and never another
|
|
27
|
+
* launch's), the create names it as the recipient, and a bind step follows the create. A launch keeps the mode it began
|
|
28
|
+
* with: adding --buyback after the create (or dropping it before) is refused rather than starting over.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const LAUNCH_DIR = process.env.GANG_LAUNCH_DIR || join(dirname(DIR), 'launch')
|
|
32
|
+
const TTL_MS = 7 * 24 * 3600_000
|
|
33
|
+
const PAD_ABI = parseAbi([
|
|
34
|
+
'function tokenCount() view returns (uint256)',
|
|
35
|
+
'function allTokens(uint256) view returns (address)',
|
|
36
|
+
'function kindOf(address token) view returns (uint8)',
|
|
37
|
+
'function curves(address token) view returns (address creator, address quote, uint128 realQuote, uint128 sold, uint128 gradAmt, uint128 virtualQuote, bool graduated, uint16 buyTaxBps, uint16 sellTaxBps, uint16 dividendBps, address pair, address refPair)',
|
|
38
|
+
'function sales(address token) view returns (address creator, uint64 progressAt, uint16 capShares, uint16 holdBps, address quote, uint16 sold, bool launched, uint16 mintFeeBps, uint16 exitFeeBps, address gate, uint128 sharePrice, address pair, address refPool, uint16 markSold)',
|
|
39
|
+
])
|
|
40
|
+
const ERC20_ABI = parseAbi(['function balanceOf(address) view returns (uint256)', 'function symbol() view returns (string)'])
|
|
41
|
+
/** Flags only the chain reads; everything else goes to the step's own command. */
|
|
42
|
+
export const LAUNCH_FLAGS = ['dev-buy', 'bond', 'bond-hold', 'bond-days', 'buyback', 'restart']
|
|
43
|
+
const NOTE_FLAGS = ['note', 'conf', 'reveal-after']
|
|
44
|
+
/** `gang fair create --dev-buy`, with or without --bond (index.js stops the lone flag, runLaunch the chained one). */
|
|
45
|
+
export const FAIR_DEV_BUY = 'a 打新 dev buys its shares in the create itself: use --shares <n> (not --dev-buy)'
|
|
46
|
+
|
|
47
|
+
/** The run asks for a chained launch (`kind` 'v3' | 'fair'). */
|
|
48
|
+
export const isLaunch = (kind, flag) => flag('bond') || flag('buyback') || (kind === 'v3' && flag('dev-buy'))
|
|
49
|
+
|
|
50
|
+
const planPath = (key) => join(LAUNCH_DIR, `${createHash('sha256').update(key).digest('hex').slice(0, 32)}.json`)
|
|
51
|
+
|
|
52
|
+
function readPlan(key) {
|
|
53
|
+
try {
|
|
54
|
+
const p = JSON.parse(readFileSync(planPath(key), 'utf8'))
|
|
55
|
+
return p.createdAt > Date.now() - TTL_MS ? p : null
|
|
56
|
+
} catch { return null }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writePlan(key, plan) {
|
|
60
|
+
mkdirSync(LAUNCH_DIR, { recursive: true, mode: 0o700 })
|
|
61
|
+
for (const name of readdirSync(LAUNCH_DIR)) {
|
|
62
|
+
const f = join(LAUNCH_DIR, name)
|
|
63
|
+
try { if (Date.now() - statSync(f).mtimeMs > TTL_MS) unlinkSync(f) } catch { /* gone */ }
|
|
64
|
+
}
|
|
65
|
+
const p = planPath(key)
|
|
66
|
+
writeFileSync(`${p}.${process.pid}.tmp`, JSON.stringify({ ...plan, createdAt: Date.now() }), { mode: 0o600 })
|
|
67
|
+
renameSync(`${p}.${process.pid}.tmp`, p)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** An argv reader over `base` (the real one) with `over` replacing / adding flags; a value of undefined drops one. */
|
|
71
|
+
function argsWith(base, over, drop = []) {
|
|
72
|
+
const has = (n) => (n in over ? over[n] !== undefined : !drop.includes(n) && base.flag(n))
|
|
73
|
+
return {
|
|
74
|
+
flag: has,
|
|
75
|
+
opt: (n, def) => (n in over ? (over[n] === undefined ? def : String(over[n])) : drop.includes(n) ? def : base.opt(n, def)),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Entry. `kind` 'v3' | 'fair'. deps: { io (index.js PAD_IO: opt, flag, out, fail, …), runV3, runFair, runBond, padNotes }.
|
|
81
|
+
*/
|
|
82
|
+
export async function runLaunch(kind, { io, runV3, runFair, runBond, padNotes }) {
|
|
83
|
+
const { opt, flag, out, fail } = io
|
|
84
|
+
const testnet = flag('testnet')
|
|
85
|
+
const net = padNets(kind)[testnet ? 'testnet' : 'mainnet']
|
|
86
|
+
if (!net.pad) return fail(NOT_ON_MAINNET)
|
|
87
|
+
const name = opt('name'), symbol = opt('symbol')
|
|
88
|
+
if (!name || !symbol) fail('--name and --symbol are required')
|
|
89
|
+
if (flag('follow')) fail('--follow does not apply to a launch')
|
|
90
|
+
if (kind === 'fair' && flag('dev-buy')) fail(FAIR_DEV_BUY)
|
|
91
|
+
if (kind === 'fair' && flag('buyback')) fail('--buyback is for V3 tokens: it makes the tax recipient a buy-back splitter, and a 打新 token has no recipient')
|
|
92
|
+
|
|
93
|
+
// everything checked before the create, so a launch never stops half way on a bad flag
|
|
94
|
+
const devBuy = kind === 'v3' && flag('dev-buy') ? parseAmount(fail, 'dev-buy', opt('dev-buy')) : null
|
|
95
|
+
const bonding = flag('bond')
|
|
96
|
+
if (!bonding && (flag('bond-hold') || flag('bond-days'))) fail('--bond-hold / --bond-days go with --bond <quote>')
|
|
97
|
+
let bond = null
|
|
98
|
+
if (bonding) {
|
|
99
|
+
if (flag('cover')) fail('--cover needs the PancakeSwap pair: a launch bonds before it, so give --bond <quote>')
|
|
100
|
+
if (kind === 'v3' && !devBuy) fail('a V3 launch with --bond needs --dev-buy <quote>: the creator holds nothing until it buys on the curve')
|
|
101
|
+
if (!bondAddress(testnet)) fail(`GangBond is not deployed on ${testnet ? 'the BSC testnet' : 'BSC mainnet'} in this CLI version: drop --bond (and --bond-hold / --bond-days), or add --testnet`)
|
|
102
|
+
const hold = String(opt('bond-hold', 'all')).trim()
|
|
103
|
+
if (!(hold === 'all' || /^(\d+(\.\d*)?|\.\d+)%?$/.test(hold)) || /^0*(\.0*)?%?$/.test(hold)) {
|
|
104
|
+
fail('--bond-hold must be all (everything the dev holds), a token amount, or N% of the supply', { got: opt('bond-hold') })
|
|
105
|
+
}
|
|
106
|
+
bond = { amount: parseAmount(fail, 'bond', opt('bond')), hold, days: parseIntIn(fail, 'bond-days', opt('bond-days'), 7, 90) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --buyback: a taxed V3 token whose recipient (the part left after holders) is the dev's splitter
|
|
110
|
+
let buyback = null
|
|
111
|
+
if (flag('buyback')) {
|
|
112
|
+
const zero = (f) => /^0+(\.0+)?$/.test(opt(f) ?? '')
|
|
113
|
+
if (!flag('buy-tax') || !flag('sell-tax') || (zero('buy-tax') && zero('sell-tax'))) {
|
|
114
|
+
fail('--buyback needs a taxed token: add --buy-tax and --sell-tax (each 1–5, in %); the buy-back is paid from the tax')
|
|
115
|
+
}
|
|
116
|
+
if (flag('recipient')) fail('--buyback makes the recipient your buy-back splitter: leave --recipient out')
|
|
117
|
+
if (/^0*100(\.0+)?$/.test(opt('dividend', '0'))) fail('--dividend 100 leaves nothing for the recipient, so nothing to buy back: lower it or drop --buyback')
|
|
118
|
+
const factory = splitterFactoryAddress(testnet)
|
|
119
|
+
if (!factory) fail(notDeployed(testnet))
|
|
120
|
+
buyback = { bps: buybackBpsOf(fail, opt('buyback')), factory }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const client = createPublicClient({ chain: net.chain, transport: http(net.rpc) })
|
|
124
|
+
const read = (address, abi, functionName, args = []) => client.readContract({ address, abi, functionName, args })
|
|
125
|
+
const me = walletFor({ chainId: net.chainId }, opt('from'))
|
|
126
|
+
const key = [net.chainId, net.pad, me.toLowerCase(), kind, name, symbol].join('|')
|
|
127
|
+
const steps = [...(buyback ? ['buyback-splitter'] : []), kind === 'v3' ? 'create' : 'create (dev shares included)', ...(buyback ? ['bind'] : []),
|
|
128
|
+
...(devBuy ? ['dev-buy'] : []), ...(bond ? ['bond'] : [])]
|
|
129
|
+
const rerun = 're-run this same command after executing it'
|
|
130
|
+
const tag = (stage, extra = {}) => ({ launch: { stage, steps, ...extra } })
|
|
131
|
+
const withTag = (stage, extra) => (o) => out({ ...o, ...tag(stage, extra) })
|
|
132
|
+
|
|
133
|
+
let plan = flag('restart') ? null : readPlan(key)
|
|
134
|
+
const base = { kind, name, symbol, me, chainId: net.chainId, pad: net.pad }
|
|
135
|
+
|
|
136
|
+
// ---- a launch keeps the mode it began with (the create, once built, is never redone by a changed flag)
|
|
137
|
+
const created = plan?.startCount !== undefined
|
|
138
|
+
if (buyback && created && !plan.splitter) {
|
|
139
|
+
return fail('this launch was created without --buyback (its recipient is fixed): run it again without --buyback, or add --restart for a new token')
|
|
140
|
+
}
|
|
141
|
+
if (!buyback && !created && plan?.salt) {
|
|
142
|
+
return fail('this launch began with --buyback (its splitter is cloned or on the way): run it again with --buyback, or add --restart without it')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- buy-back splitter: cloned for the dev before the create, at predict(dev, plan.salt)
|
|
146
|
+
if (buyback && !created) {
|
|
147
|
+
if (!plan?.salt) {
|
|
148
|
+
const salt = `0x${randomBytes(32).toString('hex')}`
|
|
149
|
+
const at = await read(buyback.factory, FACTORY_ABI, 'predict', [me, salt])
|
|
150
|
+
const sim = await simulateCall(client, me, buyback.factory, FACTORY_ABI, 'create', [buyback.bps, salt])
|
|
151
|
+
if (sim.failed) return out({ success: false, step: 'buyback-splitter', ...tag('buyback-splitter'), ...sim.failed })
|
|
152
|
+
writePlan(key, { ...base, salt, splitter: at, factory: buyback.factory })
|
|
153
|
+
const data = encodeFunctionData({ abi: FACTORY_ABI, functionName: 'create', args: [buyback.bps, salt] })
|
|
154
|
+
return out({
|
|
155
|
+
...step({ chainId: net.chainId }, { from: me, to: buyback.factory, data }, {
|
|
156
|
+
step: 'buyback-splitter', chainId: Number(net.chainId), factory: buyback.factory, buybackPct: buyback.bps / 100,
|
|
157
|
+
splitter: at,
|
|
158
|
+
note: `clones your buy-back splitter (listed as a tax recipient in the same tx): ${buyback.bps / 100}% of the recipient's part is bought back and burned, the rest goes to you. A raise applies at once later, a cut only after a 3-day notice`,
|
|
159
|
+
}, flag('calldata'), io.summarize),
|
|
160
|
+
...tag('buyback-splitter', { splitter: at, next: `${rerun}: it then builds the create with the splitter as the recipient` }),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
if ((await client.getCode({ address: plan.splitter }) ?? '0x') === '0x') {
|
|
164
|
+
return out({
|
|
165
|
+
success: false, ...tag('buyback-splitter', { splitter: plan.splitter }), error: 'the buy-back splitter is not on chain yet',
|
|
166
|
+
hint: `if the clone was executed, wait for it to land and ${rerun.replace(' after executing it', '')}; if it was not, add --restart to build it again`,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
const s = await splitterOf(read, plan.splitter)
|
|
170
|
+
if (!s || s.dev.toLowerCase() !== me.toLowerCase() || s.token) {
|
|
171
|
+
return fail('the splitter at this launch\'s address is not an unbound buy-back splitter of this wallet: add --restart', { splitter: plan.splitter })
|
|
172
|
+
}
|
|
173
|
+
if (s.buybackBps !== buyback.bps) {
|
|
174
|
+
return fail(`this launch's splitter buys back ${s.buybackPct}%, not ${buyback.bps / 100}%: run it again with --buyback ${s.buybackPct}, or add --restart for a new splitter (gang buyback set changes it after the create)`, { splitter: plan.splitter })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- create
|
|
179
|
+
if (!created) {
|
|
180
|
+
const startCount = (await read(net.pad, PAD_ABI, 'tokenCount')).toString()
|
|
181
|
+
const createIo = {
|
|
182
|
+
...io,
|
|
183
|
+
...argsWith(io, plan?.splitter ? { recipient: plan.splitter } : {}, LAUNCH_FLAGS),
|
|
184
|
+
out: (o) => {
|
|
185
|
+
// the plan starts with the create tx itself: a wrap / approve step before it (or a failed preview) leaves none,
|
|
186
|
+
// so the next run builds the create again
|
|
187
|
+
const built = o.success && o.step === 'create'
|
|
188
|
+
if (built) writePlan(key, { ...base, ...(plan?.splitter && { salt: plan.salt, splitter: plan.splitter, factory: plan.factory }), startCount })
|
|
189
|
+
const after = steps[steps.indexOf(kind === 'v3' ? 'create' : 'create (dev shares included)') + 1]
|
|
190
|
+
return out({
|
|
191
|
+
...o,
|
|
192
|
+
...tag('create', built ? { next: `${rerun}: it finds the new token and continues with ${after ?? 'nothing'}` } : o.success ? { next: `${rerun}: it then builds the create` } : {}),
|
|
193
|
+
})
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
if (kind === 'v3') return runV3('create', { ...createIo, notes: padNotes('v3', 'create') })
|
|
197
|
+
return runFair('create', { ...createIo, notes: padNotes('fair', 'create') })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- the token this plan created: kept in the plan once found, else looked up from the pad's list since the plan
|
|
201
|
+
let token = plan.token ?? null
|
|
202
|
+
if (!token) {
|
|
203
|
+
const count = Number(await read(net.pad, PAD_ABI, 'tokenCount'))
|
|
204
|
+
for (let i = Number(plan.startCount); i < count && !token; i++) {
|
|
205
|
+
const t = await read(net.pad, PAD_ABI, 'allTokens', [BigInt(i)])
|
|
206
|
+
const k = Number(await read(net.pad, PAD_ABI, 'kindOf', [t]))
|
|
207
|
+
const creator = k === 1 ? (await read(net.pad, PAD_ABI, 'curves', [t]))[0] : k === 2 ? (await read(net.pad, PAD_ABI, 'sales', [t]))[0] : zeroAddress
|
|
208
|
+
if (creator.toLowerCase() !== me.toLowerCase()) continue
|
|
209
|
+
if ((await read(t, ERC20_ABI, 'symbol')) === symbol) token = t
|
|
210
|
+
}
|
|
211
|
+
if (token) {
|
|
212
|
+
plan = { ...plan, token }
|
|
213
|
+
writePlan(key, plan)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!token) {
|
|
217
|
+
return out({
|
|
218
|
+
success: false, ...tag('create'), error: 'no token from this launch yet',
|
|
219
|
+
hint: `if the create was executed, wait for it to land and ${rerun.replace(' after executing it', '')}; if it was not (or its preview failed), add --restart to build the create again`,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// later steps carry no note (it went on the create) and take the token from the plan
|
|
224
|
+
const stepIo = (over) => ({ ...io, ...argsWith(io, { ...over, token }, [...LAUNCH_FLAGS, ...NOTE_FLAGS, 'name', 'symbol']) })
|
|
225
|
+
const bal = await read(token, ERC20_ABI, 'balanceOf', [me])
|
|
226
|
+
const buybackState = () => (plan.splitter ? splitterOf(read, plan.splitter) : null)
|
|
227
|
+
|
|
228
|
+
// ---- done: the report only, whatever happened to the balance or the bond since
|
|
229
|
+
const report = async () => {
|
|
230
|
+
const done = {
|
|
231
|
+
success: true, ...tag('done', { token, again: 'this launch is done; running the command again only reports it. Another token with the same name / symbol: add --restart' }),
|
|
232
|
+
token, name, symbol, devBalance: formatUnits(bal, 18), chainId: Number(net.chainId),
|
|
233
|
+
}
|
|
234
|
+
const bb = await buybackState()
|
|
235
|
+
if (bb) done.buyback = bb
|
|
236
|
+
if (!bond) return out({ ...done, note: `launched: gang ${kind} info --token ${token}${testnet ? ' --testnet' : ''}` })
|
|
237
|
+
return runBond('status', { ...stepIo({}), out: (o) => out({ ...done, bondStatus: o, note: `launched and bonded: gang bond status --token ${token}${testnet ? ' --testnet' : ''}` }) })
|
|
238
|
+
}
|
|
239
|
+
if (plan.done) return report()
|
|
240
|
+
|
|
241
|
+
// ---- bind the splitter to the token (anyone may; the launch does it right after the create)
|
|
242
|
+
if (plan.splitter) {
|
|
243
|
+
const bound = await read(plan.splitter, SPLITTER_ABI, 'token')
|
|
244
|
+
if (bound === zeroAddress) {
|
|
245
|
+
const sim = await simulateCall(client, me, plan.splitter, SPLITTER_ABI, 'bind', [token])
|
|
246
|
+
if (sim.failed) return out({ success: false, step: 'bind', ...tag('bind', { token }), ...sim.failed })
|
|
247
|
+
const data = encodeFunctionData({ abi: SPLITTER_ABI, functionName: 'bind', args: [token] })
|
|
248
|
+
return out({
|
|
249
|
+
...step({ chainId: net.chainId }, { from: me, to: plan.splitter, data }, {
|
|
250
|
+
step: 'bind', chainId: Number(net.chainId), token, splitter: plan.splitter,
|
|
251
|
+
note: 'ties the splitter to this token for good (what it buys back and burns)',
|
|
252
|
+
}, flag('calldata'), io.summarize),
|
|
253
|
+
...tag('bind', { token, next: `${rerun}${devBuy ? ': it continues with the dev buy' : bond ? ': it continues with the bond' : ': the launch is then done'}` }),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
if (bound.toLowerCase() !== token.toLowerCase()) {
|
|
257
|
+
return out({ success: false, ...tag('bind', { token }), error: 'the splitter is bound to another token', splitter: plan.splitter, boundTo: bound })
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- V3 dev buy (a 打新 dev already holds its shares' tokens)
|
|
262
|
+
if (devBuy && bal === 0n) {
|
|
263
|
+
return runV3('buy', {
|
|
264
|
+
...stepIo({ amount: devBuy }), out: withTag('dev-buy', { token, next: `${rerun}${bond ? ': it continues with the bond' : ': the launch is then done'}` }),
|
|
265
|
+
notes: padNotes('v3', 'buy', { note: null }),
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---- bond
|
|
270
|
+
if (bond) {
|
|
271
|
+
const b = await read(bondAddress(testnet), BOND_ABI, 'bondOf', [token])
|
|
272
|
+
if (Number(b.round) === 0) {
|
|
273
|
+
const hold = bond.hold === 'all' ? formatUnits(bal, 18) : bond.hold
|
|
274
|
+
return runBond('post', {
|
|
275
|
+
...stepIo({ amount: bond.amount, hold, days: bond.days, cover: undefined }),
|
|
276
|
+
out: withTag('bond', { token, next: `${rerun}: it reports the launch` }),
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---- every step done: mark the plan, so later runs only report (never a second buy, bond or create)
|
|
282
|
+
writePlan(key, { ...plan, done: true })
|
|
283
|
+
return report()
|
|
284
|
+
}
|