@gangfun/cli 0.11.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 +59 -43
- package/README.zh-CN.md +59 -43
- package/package.json +1 -1
- package/src/bond.js +564 -0
- package/src/buyback.js +183 -0
- package/src/chain.js +27 -4
- package/src/fair.js +18 -8
- package/src/gangpad.js +3 -3
- package/src/index.js +384 -158
- package/src/launch.js +284 -0
- package/src/pool.js +11 -2
- package/src/steps.js +1 -1
- package/src/v3.js +18 -6
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
|
+
}
|
package/src/pool.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { parseAbi, encodeFunctionData, formatUnits, parseUnits, zeroAddress } from 'viem'
|
|
2
2
|
import { decimalsOf, isMissingFunction, poolSellQuote, ROUTER_ABI, routerDeadline, step, units, GAS_RESERVE } from './steps.js'
|
|
3
|
+
import { bondGuard } from './bond.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* PancakeSwap (V2 pool) buys and sells of a graduated / launched token through GangRouter — V3 and 打新 tokens
|
|
6
|
-
* (
|
|
7
|
+
* (V3 / 打新, also as `gang v3|fair buy|sell`) and V2 / Soft / U / S tokens (`gang buy|sell`), all the same way:
|
|
7
8
|
* poolBuyBNB(token, quote, minTokensOut, deadline) payable path [WBNB, token] or [WBNB, quote, token]
|
|
8
9
|
* poolBuy(token, quote, quoteIn, minTokensOut, deadline) path [quote, token]; approve quote → router first
|
|
9
10
|
* poolSell(token, quote, amountIn, minQuoteOut, deadline) path [token, quote]; approve token → router first
|
|
@@ -12,6 +13,7 @@ import { decimalsOf, isMissingFunction, poolSellQuote, ROUTER_ABI, routerDeadlin
|
|
|
12
13
|
* Minimums: a buy = Pancake getAmountsOut less the token's buy tax, less --slippage; a sell = the tax handler's
|
|
13
14
|
* previewSellNextBlock (the sell tax and the chunk of held tax it sells first included, as of the next block), or
|
|
14
15
|
* getAmountsOut less the sell tax when there is no handler quote (V2 tokens, untaxed V3), less --slippage.
|
|
16
|
+
* A sell with `p.bond` ({ testnet, breakBond, settleFirst }) is checked against the seller's GangBond first (bondGuard).
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
// Pancake V2 routers: a GangRouter address must never be one of these (they would take an approve and swap for anyone).
|
|
@@ -185,7 +187,7 @@ async function build(ctx, p, { rawOnly, summarize, revertOf }) {
|
|
|
185
187
|
const quoteSymbol = (await symbolOf(ctx, quote)) || quote
|
|
186
188
|
const deadline = routerDeadline()
|
|
187
189
|
const taxPct = (bps) => (bps || 0) / 100
|
|
188
|
-
let call, probe, value = 0n, pull = null, fields
|
|
190
|
+
let call, probe, value = 0n, pull = null, fields, guard = null
|
|
189
191
|
try {
|
|
190
192
|
if (side === 'buy') {
|
|
191
193
|
const payBnb = !!p.payBnb
|
|
@@ -226,6 +228,12 @@ async function build(ctx, p, { rawOnly, summarize, revertOf }) {
|
|
|
226
228
|
const amount = p.all ? bal : parseUnits(p.amount, 18)
|
|
227
229
|
if (amount === 0n) throw new PoolError(`--amount ${p.amount} rounds to 0 tokens`)
|
|
228
230
|
if (amount > bal) return failOut({ step: 'pool-sell', error: 'amount exceeds balance', balance: formatUnits(bal, 18) })
|
|
231
|
+
// the creator's own bond: refuse a sell that breaks it (unless --break-bond), settle first past the deadline
|
|
232
|
+
if (p.bond) {
|
|
233
|
+
guard = await bondGuard(ctx, { token, me, amount, ...p.bond }, rawOnly, summarize)
|
|
234
|
+
if (guard?.refuse) return failOut({ step: 'pool-sell', wallet: me, ...guard.refuse })
|
|
235
|
+
if (guard?.settleStep) return guard.settleStep
|
|
236
|
+
}
|
|
229
237
|
const taxed = (p.sellTaxBps || 0) > 0
|
|
230
238
|
let quoteOut, quotedWith
|
|
231
239
|
if (taxed && p.handler) {
|
|
@@ -254,6 +262,7 @@ async function build(ctx, p, { rawOnly, summarize, revertOf }) {
|
|
|
254
262
|
deadline: Number(deadline), quotedWith,
|
|
255
263
|
...(toBnb && !quoteWbnb && { expectQuoteBeforeBnb: await units(ctx, quote, quoteOut) }),
|
|
256
264
|
gas: p.swapBackGas ? POOL_SELL_GAS_NOTE : 'let the wallet estimate gas',
|
|
265
|
+
...guard?.info,
|
|
257
266
|
note: quotedWith === 'getAmountsOut'
|
|
258
267
|
? `minOut = getAmountsOut of the amount less the ${taxPct(p.sellTaxBps)}% sell tax${toBnb && !quoteWbnb ? ' (then quote → WBNB)' : ''}, less --slippage (the sell reverts below it)`
|
|
259
268
|
: `minOut = the tax handler's ${quotedWith} (sell tax and the chunk of held tax sold first included, as of the next block)${toBnb && !quoteWbnb ? ', then getAmountsOut quote → WBNB' : ''}, less --slippage (the sell reverts below it)`,
|
package/src/steps.js
CHANGED
|
@@ -263,7 +263,7 @@ export async function swapBackState(ctx, handler, token, reserveToken) {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
/** Hint for selling a graduated / launched token on PancakeSwap with the minOut of a pool quote (poolSellQuote) as amountOutMin. */
|
|
266
|
-
export const POOL_SELL_HINT = '`gang
|
|
266
|
+
export const POOL_SELL_HINT = '`gang sell` builds this sell through GangRouter (poolSell / --to-bnb poolSellForBNB, minOut as here); '
|
|
267
267
|
+ 'or sell on PancakeSwap with swapExactTokensForTokensSupportingFeeOnTransferTokens (the token takes its sell tax in the transfer), passing minOut (poolQuote.minOut / poolSellAll.minOut, set by --slippage) as amountOutMin so the sell reverts rather than fills below it; '
|
|
268
268
|
+ 'the quote includes the small chunk of tax the handler sells right before a sell and assumes the tx lands in a new block (previewSellNextBlock). '
|
|
269
269
|
+ 'Let the wallet estimate gas, or give at least 750k: a sell that triggers a chunk uses about 660k (about 330k without), and too little gas reverts with SwapBackOutOfGas'
|
package/src/v3.js
CHANGED
|
@@ -5,6 +5,8 @@ import { UA } from './version.js'
|
|
|
5
5
|
import { walletFor } from './fair.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 { bondGuard } from './bond.js'
|
|
9
|
+
import { splitterOf } from './buyback.js'
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* `gang v3 …` — LaunchpadV3 (tax recipient): a token with its own buy / sell tax (1–5% each), a dividend share and a
|
|
@@ -69,7 +71,7 @@ const HINTS = {
|
|
|
69
71
|
BadTaxConfig: 'a tax setting is out of bounds, or the recipient is neither you nor on the whitelist: see reason',
|
|
70
72
|
NotAgent: 'this wallet holds no Agent ID; V3 create and curve trades are Agent-only',
|
|
71
73
|
WalletCapReached: 'this wallet already holds the per-wallet cap (1% of supply)',
|
|
72
|
-
AlreadyGraduated: 'the token graduated: re-run gang
|
|
74
|
+
AlreadyGraduated: 'the token graduated: re-run gang buy / gang sell, which now trade it in the PancakeSwap pool',
|
|
73
75
|
Slippage: 'the price moved past --slippage; re-run to re-quote',
|
|
74
76
|
RefPoolRequired: 'a non-WBNB --quote needs --ref-pool: a Pancake V2 pair or V3 pool that holds the quote token',
|
|
75
77
|
QuoteNotInPool: '--ref-pool does not contain the --quote token',
|
|
@@ -90,6 +92,9 @@ const HINTS = {
|
|
|
90
92
|
VaultFloorTooHigh: 'the platform\'s dividend vault requires a larger holding than V3 dividends allow (10,000 tokens), so a dividend token can\'t be created right now; --dividend 0 still works, or ask gang to fix the vault setting',
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/** What `gang tx` decodes a V3 trade's logs with (the pad, the tax router, the dividend vault). */
|
|
96
|
+
export const V3_EVENT_ABIS = [PAD_ABI, TAX_ROUTER_ABI, VAULT_ABI]
|
|
97
|
+
|
|
93
98
|
export function v3Ctx(testnet) {
|
|
94
99
|
const net = V3_NETS[testnet ? 'testnet' : 'mainnet']
|
|
95
100
|
if (!net.pad) throw new Error(NOT_ON_MAINNET)
|
|
@@ -234,7 +239,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
|
|
|
234
239
|
const path = recipient ? `/v3/recipients/${recipient.toLowerCase()}?limit=20` : `/v3/tokens?status=${status}&limit=30`
|
|
235
240
|
const r = await fetch(`${API}${path}`, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(10_000) })
|
|
236
241
|
.then((x) => (x.ok ? x.json() : { error: x.status })).catch((e) => ({ error: e.message }))
|
|
237
|
-
if (r.error) return out({ success: false, error: `gang.fun V3 data unavailable (${r.error})`, hint: `use gang
|
|
242
|
+
if (r.error) return out({ success: false, error: `gang.fun V3 data unavailable (${r.error})`, hint: `use gang info --token <addr>${net} for one token (reads the chain)` })
|
|
238
243
|
// the quote's own decimals from gang.fun (18 when an older server does not send them)
|
|
239
244
|
const f = (x, d) => (x == null ? null : formatUnits(BigInt(x), d ?? 18))
|
|
240
245
|
if (recipient) {
|
|
@@ -271,7 +276,9 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
|
|
|
271
276
|
const taxNote = c.buyTaxPct > 0 || c.sellTaxPct > 0
|
|
272
277
|
? `buy ${c.buyTaxPct}% / sell ${c.sellTaxPct}% tax only in the PancakeSwap pool after graduation (${fees.platformTaxPct}% of volume to the platform, then ${c.dividendPct}% of the rest to holders, the remainder to the recipient); curve trades pay only the ${fees.curveFeePct}% fee, no tax`
|
|
273
278
|
: `untaxed; curve trades pay the ${fees.curveFeePct}% fee`
|
|
274
|
-
|
|
279
|
+
// a recipient that is the dev's buy-back splitter: its ratio and what waits to be bought back
|
|
280
|
+
const buyback = c.recipient && !c.recipientIsCreator ? await splitterOf(ctx.read, c.recipient) : null
|
|
281
|
+
return out({ success: true, chainId: Number(ctx.chainId), ...fees, ...pub, taxNote, ...(swapBack && { swapBack }), ...(buyback && { buyback }) })
|
|
275
282
|
}
|
|
276
283
|
|
|
277
284
|
if (sub === 'status') {
|
|
@@ -308,7 +315,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
|
|
|
308
315
|
if (d.claimableRaw === 0n) {
|
|
309
316
|
return fail('nothing to claim yet', {
|
|
310
317
|
...(d.creditedUnsoldTokens && Number(d.creditedUnsoldTokens) > 0 && { creditedUnsoldEstimate: d.creditedUnsoldEstimate, note: d.note }),
|
|
311
|
-
hint: `gang
|
|
318
|
+
hint: `gang status --token ${token}${testnet ? ' --testnet' : ''}`,
|
|
312
319
|
})
|
|
313
320
|
}
|
|
314
321
|
const sim = await simulate(ctx, me, vault, VAULT_ABI, 'claim', [token])
|
|
@@ -388,7 +395,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
|
|
|
388
395
|
? `of each ${buyTax}% buy / ${sellTax}% sell tax in the PancakeSwap pool (after graduation; the curve charges no tax): ${fees.platformTaxPct}% of volume to the platform, then ${dividend}% of the rest to holders, ${rest}% to the recipient in the quote`
|
|
389
396
|
+ (dividend > 0 ? '; if no holder has 10,000+ tokens when a tax is paid, the holders\' part goes to the recipient too' : '')
|
|
390
397
|
: 'untaxed: no tax in the PancakeSwap pool after graduation, no dividend, no recipient',
|
|
391
|
-
note: `${taxed ? 'tax rates and recipient are' : 'untaxed is'} fixed at create. Curve trades pay only the ${fees.curveFeePct}% fee, no tax${taxed ? ': the tax starts in the pool after graduation' : ''}. tokenIfNext changes if another create lands first: the real address is the Created event from \`gang
|
|
398
|
+
note: `${taxed ? 'tax rates and recipient are' : 'untaxed is'} fixed at create. Curve trades pay only the ${fees.curveFeePct}% fee, no tax${taxed ? ': the tax starts in the pool after graduation' : ''}. tokenIfNext changes if another create lands first: the real address is the Created event from \`gang tx --hash …\`.`,
|
|
392
399
|
}, 'create'))
|
|
393
400
|
}
|
|
394
401
|
|
|
@@ -469,6 +476,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
|
|
|
469
476
|
side: 'sell', token, quote: c.quote, me, amount: flag('all') ? null : parseAmount(fail, 'amount', opt('amount')), all: flag('all'),
|
|
470
477
|
slippage, buyTaxBps: Math.round(c.buyTaxPct * 100), sellTaxBps: Math.round(c.sellTaxPct * 100), handler: c.taxHandler,
|
|
471
478
|
swapBackGas: c.sellTaxPct > 0, toBnb: flag('to-bnb'), base: { kind: 'v3', pair: c.pair, ...notes.pool },
|
|
479
|
+
bond: { testnet, breakBond: flag('break-bond'), settleFirst: flag('settle-first') },
|
|
472
480
|
}, { rawOnly, summarize, revertOf }))
|
|
473
481
|
}
|
|
474
482
|
if (flag('all')) return fail('--all needs a wallet (or --from <addr>) whose balance can be read', { pair: c.pair })
|
|
@@ -490,6 +498,9 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
|
|
|
490
498
|
if (bal === 0n) fail('no balance to sell')
|
|
491
499
|
const amount = flag('all') ? bal : parseUnits(parseAmount(fail, 'amount', opt('amount')), 18)
|
|
492
500
|
if (amount > bal) fail('amount exceeds balance', { balance: formatUnits(bal, 18) })
|
|
501
|
+
// a bond posted on the curve already binds: a creator's curve sell below X breaks it
|
|
502
|
+
const guard = await bondGuard(ctx, { token, me, amount, testnet, breakBond: flag('break-bond'), settleFirst: flag('settle-first') }, rawOnly, summarize)
|
|
503
|
+
if (guard?.refuse) return out({ success: false, step: 'sell', wallet: me, ...guard.refuse })
|
|
493
504
|
const pre = await fundingStep(ctx, me, token, amount, c.quoteSymbol, 'approve', rawOnly, summarize)
|
|
494
505
|
if (pre) return out(pre.success ? { ...pre, ...notes.skipped() } : pre)
|
|
495
506
|
const sim = await simulate(ctx, me, ctx.pad, PAD_ABI, 'sell', [token, amount, 0n])
|
|
@@ -499,7 +510,8 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
|
|
|
499
510
|
step: 'sell', chainId: Number(ctx.chainId), token, amount: formatUnits(amount, 18), quote: c.quoteSymbol,
|
|
500
511
|
expectOut: await units(ctx, c.quote, sim.result), minOut: await units(ctx, c.quote, minOut), curveTaxPct: 0,
|
|
501
512
|
...(c.sellTaxPct > 0 && { poolSellTaxPct: c.sellTaxPct }),
|
|
502
|
-
curveFeePct: (await feesOf(ctx)).curveFeePct,
|
|
513
|
+
curveFeePct: (await feesOf(ctx)).curveFeePct, ...guard?.info,
|
|
514
|
+
note: 'expectOut is after the 1% curve fee only (no tax on the curve; the sell tax applies only in the pool after graduation)',
|
|
503
515
|
}, 'sell'))
|
|
504
516
|
}
|
|
505
517
|
|