@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/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/chain.js
CHANGED
|
@@ -32,8 +32,8 @@ const PADS = [
|
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
const GANG_POINTER = {
|
|
35
|
-
v3: (t) => `a GangLaunchpad V3 (bonding-curve) token:
|
|
36
|
-
fair: (t) => `a GangLaunchpad 打新 (fair-mint) token:
|
|
35
|
+
v3: (t) => `a GangLaunchpad V3 (bonding-curve) token: gang info|buy|sell|status --token ${t} trade it (re-run if this was an RPC hiccup)`,
|
|
36
|
+
fair: (t) => `a GangLaunchpad 打新 (fair-mint) token: gang info|status --token ${t}; before launch gang mint / withdraw, after it gang buy / sell (re-run if this was an RPC hiccup)`,
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** Resolve which pad hosts `token` by probing curves(token).creator != 0; a GangLaunchpad token is pointed at its command. */
|
|
@@ -261,13 +261,36 @@ export const calldata = {
|
|
|
261
261
|
wrap: () => encodeFunctionData({ abi: WBNB_ABI, functionName: 'deposit' }),
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Events of the logs that decode with one of `abis`, each with the emitting contract: GangLaunchpad logs, which `gang tx`
|
|
266
|
+
* reads beside the V2 / Soft / U / S pads'. Integers stay as on chain (strings), marked `units: 'raw'`: amounts are in wei
|
|
267
|
+
* (18 decimals), bps / shares / counts as they are; the V2 / Soft events are formatted (no `units`).
|
|
268
|
+
*/
|
|
269
|
+
export function decodeLogs(logs, abis) {
|
|
270
|
+
const events = []
|
|
271
|
+
for (const log of logs) {
|
|
272
|
+
for (const abi of abis) {
|
|
273
|
+
try {
|
|
274
|
+
const ev = decodeEventLog({ abi, data: log.data, topics: log.topics })
|
|
275
|
+
events.push({ event: ev.eventName, contract: log.address, units: 'raw', ...Object.fromEntries(Object.entries(ev.args).map(([k, v]) => [k, typeof v === 'bigint' ? v.toString() : v])) })
|
|
276
|
+
break
|
|
277
|
+
} catch { /* other contract / event */ }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return events
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** `extraAbis`: decoders for logs not from the V2 / Soft / U / S pads (see decodeLogs). */
|
|
284
|
+
export async function decodeTx(hash, extraAbis = []) {
|
|
265
285
|
const r = await client.waitForTransactionReceipt({ hash, timeout: 120_000 })
|
|
266
286
|
const events = []
|
|
267
287
|
const known = new Map(PADS.map((p) => [p.address.toLowerCase(), p]))
|
|
268
288
|
for (const log of r.logs) {
|
|
269
289
|
const pad = known.get(log.address.toLowerCase())
|
|
270
|
-
if (!pad)
|
|
290
|
+
if (!pad) {
|
|
291
|
+
events.push(...decodeLogs([log], extraAbis))
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
271
294
|
try {
|
|
272
295
|
const ev = decodeEventLog({ abi: pad.abi, data: log.data, topics: log.topics })
|
|
273
296
|
const a = Object.fromEntries(Object.entries(ev.args).map(([k, v]) => {
|
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);
|
|
@@ -81,7 +82,7 @@ const STATUS = ['none', 'minting', 'launched']
|
|
|
81
82
|
const HINTS = {
|
|
82
83
|
BadConfig: 'a rule is out of bounds: see reason',
|
|
83
84
|
NotAgent: 'this wallet holds no Agent ID; fair mint is Agent-only',
|
|
84
|
-
NotMinting: 'the sale already launched: trade the token in the pool with gang
|
|
85
|
+
NotMinting: 'the sale already launched: trade the token in the pool with gang buy / gang sell',
|
|
85
86
|
WalletCapReached: 'this wallet already holds the per-wallet cap of shares',
|
|
86
87
|
Slippage: 'fewer shares are left than --min-shares (someone filled first)',
|
|
87
88
|
BadAmount: '--shares is more than this wallet holds in the sale',
|
|
@@ -104,6 +105,9 @@ const HINTS = {
|
|
|
104
105
|
VaultFloorTooHigh: 'the platform\'s dividend vault requires a larger holding than fair-mint rewards allow, so no sale can be created right now; ask gang to fix the vault setting',
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/** What `gang tx` decodes a 打新 trade's logs with (the pad, the holder-reward vault). */
|
|
109
|
+
export const FAIR_EVENT_ABIS = [PAD_ABI, VAULT_ABI]
|
|
110
|
+
|
|
107
111
|
export function fairCtx(testnet) {
|
|
108
112
|
const net = FAIR_NETS[testnet ? 'testnet' : 'mainnet']
|
|
109
113
|
if (!net.pad) throw new Error(NOT_ON_MAINNET)
|
|
@@ -244,7 +248,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
244
248
|
if (!['minting', 'launched', 'all'].includes(status)) fail('--status must be minting, launched or all')
|
|
245
249
|
const r = await fetch(`${API}/fair/sales?status=${status}&limit=30`, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(10_000) })
|
|
246
250
|
.then((x) => (x.ok ? x.json() : { error: x.status })).catch((e) => ({ error: e.message }))
|
|
247
|
-
if (r.error) return out({ success: false, error: `gang.fun fair list unavailable (${r.error})`, hint: `use gang
|
|
251
|
+
if (r.error) return out({ success: false, error: `gang.fun fair list unavailable (${r.error})`, hint: `use gang info --token <addr>${testnet ? ' --testnet' : ''} for one sale` })
|
|
248
252
|
return out({ success: true, status, items: (r.items || []).map((x) => ({
|
|
249
253
|
token: x.address, symbol: x.symbol, status: x.status, sold: x.sold, participants: x.participants,
|
|
250
254
|
sharePrice: x.sharePrice && formatUnits(BigInt(x.sharePrice), 18), quote: x.quoteSymbol, chainId: x.chainId,
|
|
@@ -336,7 +340,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
336
340
|
const sale = await saleOf(ctx, token)
|
|
337
341
|
if (!sale) return notFair(token)
|
|
338
342
|
if (sale.status !== 'minting') {
|
|
339
|
-
return fail(`sale is ${sale.status}, not minting`, { hint: `it launched: buy in the pool with gang
|
|
343
|
+
return fail(`sale is ${sale.status}, not minting`, { hint: `it launched: buy in the pool with gang buy --token ${token} --amount <quote>${testnet ? ' --testnet' : ''}` })
|
|
340
344
|
}
|
|
341
345
|
const me = walletFor(ctx, opt('from'))
|
|
342
346
|
if (sale.gate !== zeroAddress && (await ctx.read(sale.gate, ERC721_ABI, 'balanceOf', [me])) === 0n) {
|
|
@@ -382,7 +386,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
382
386
|
if (sale.status !== 'minting') {
|
|
383
387
|
return fail(`sale is ${sale.status}: withdraw only while minting`, sale.launched ? {
|
|
384
388
|
pair: sale.pair,
|
|
385
|
-
hint: `it launched: sell in the pool with \`gang
|
|
389
|
+
hint: `it launched: sell in the pool with \`gang sell --token ${token} (--amount <tokens> | --all)${testnet ? ' --testnet' : ''}${opt('from') ? ` --from ${opt('from')}` : ''} [--slippage 1] [--to-bnb]\``
|
|
386
390
|
+ ' (through GangRouter; minOut from the tax handler\'s previewSellNextBlock)',
|
|
387
391
|
} : {})
|
|
388
392
|
}
|
|
@@ -390,6 +394,11 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
390
394
|
const held = await ctx.read(ctx.pad, PAD_ABI, 'positions', [token, me])
|
|
391
395
|
if (!held) return fail('this wallet has no shares in this sale')
|
|
392
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 })
|
|
393
402
|
const [payout, fee] = await ctx.read(ctx.pad, PAD_ABI, 'previewWithdraw', [token, me, BigInt(shares)])
|
|
394
403
|
// the price is fixed, so only the exit fee can move the payout: a mint landing first can restart the 7-day clock
|
|
395
404
|
// and bring a 1–2% fee back. 0.1% below the preview refuses that and nothing else.
|
|
@@ -401,7 +410,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
401
410
|
step: 'withdraw', chainId: Number(ctx.chainId), token, shares,
|
|
402
411
|
receive: await units(ctx, sale.quote, payout), minReceive: await units(ctx, sale.quote, minPayout),
|
|
403
412
|
exitFee: await units(ctx, sale.quote, fee), quote: sale.quoteSymbol,
|
|
404
|
-
exitFeePct: sale.exitFeeNowPct,
|
|
413
|
+
exitFeePct: sale.exitFeeNowPct, ...guard?.info,
|
|
405
414
|
note: sale.stale
|
|
406
415
|
? `no progress for 7 days: no exit fee, the full mint price back; the ${sale.mintFeePct}% paid at mint is not returned`
|
|
407
416
|
: `mint price − ${sale.exitFeeNowPct}% exit fee (none after 7 days without 10 new shares); the ${sale.mintFeePct}% paid at mint is not returned`,
|
|
@@ -414,8 +423,8 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
414
423
|
const sale = await saleOf(ctx, token)
|
|
415
424
|
if (!sale) return notFair(token)
|
|
416
425
|
if (!sale.launched || !sale.pair) {
|
|
417
|
-
return fail(`sale is ${sale.status}: gang
|
|
418
|
-
hint: sub === 'buy' ?
|
|
426
|
+
return fail(`sale is ${sale.status}: gang ${sub} trades the token in its pool after launch`, {
|
|
427
|
+
hint: sub === 'buy' ? `while minting, buy shares with gang mint --token ${token} --shares <n>` : `while minting, sell shares back with gang withdraw --token ${token} (--shares <n> | --all)`,
|
|
419
428
|
})
|
|
420
429
|
}
|
|
421
430
|
const slippage = parseNum(fail, 'slippage', opt('slippage', '1'), 0, 50)
|
|
@@ -459,6 +468,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
459
468
|
amount: sub === 'sell' && flag('all') ? null : parseAmount(fail, 'amount', opt('amount')), all: sub === 'sell' && flag('all'),
|
|
460
469
|
swapBackGas: Number(sellTaxBps) > 0, payBnb: sub === 'buy' && poolPayBnb(ctx, sale.quote, flag), toBnb: flag('to-bnb'),
|
|
461
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') } }),
|
|
462
472
|
}, { rawOnly, summarize, revertOf }))
|
|
463
473
|
}
|
|
464
474
|
|
|
@@ -473,7 +483,7 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
|
|
|
473
483
|
if (r.claimableRaw === 0n) {
|
|
474
484
|
return fail('nothing to claim yet', {
|
|
475
485
|
...(unsold && { creditedUnsoldEstimate: r.creditedUnsoldEstimate, note: r.note }),
|
|
476
|
-
hint: `gang
|
|
486
|
+
hint: `gang status --token ${token}${testnet ? ' --testnet' : ''}`,
|
|
477
487
|
})
|
|
478
488
|
}
|
|
479
489
|
const data = encodeFunctionData({ abi: VAULT_ABI, functionName: 'claim', args: [token] })
|
package/src/gangpad.js
CHANGED
|
@@ -69,7 +69,7 @@ export const MERGED_ERRORS_ABI = parseAbi([
|
|
|
69
69
|
])
|
|
70
70
|
export const MERGED_HINTS = {
|
|
71
71
|
Untaxed: 'this token is untaxed (0% / 0%): it never gets a pool tax, a tax route or dividends; nothing to release or claim on it',
|
|
72
|
-
NotReclaimable: 'only a 打新 (fair-mint) token can be sold back to the pad; a V3 token is sold on its curve (gang
|
|
72
|
+
NotReclaimable: 'only a 打新 (fair-mint) token can be sold back to the pad; a V3 token is sold on its curve (gang sell)',
|
|
73
73
|
NotLocked: 'the sale already launched (the token is unlocked): sell-back is over, trade the token on PancakeSwap',
|
|
74
74
|
WrongKind: 'this token is the other kind on the merged pad: V3 tokens use `gang v3 …`, 打新 tokens use `gang fair …`',
|
|
75
75
|
BadKind: 'the tax handler refused the token kind (a pad / handler wiring problem): report it to gang',
|
|
@@ -99,10 +99,10 @@ export async function notThisKind(ctx, token, want, net = '') {
|
|
|
99
99
|
const kind = await kindOf(ctx, token)
|
|
100
100
|
const ask = want === 'v3' ? 'a LaunchpadV3 (tax) token' : 'a 打新 (fair-mint) sale'
|
|
101
101
|
if (kind === 'fair' && want === 'v3') {
|
|
102
|
-
return { error: `this is a 打新 (fair-mint) token, not a V3 token:
|
|
102
|
+
return { error: `this is a 打新 (fair-mint) token, not a V3 token: gang info / buy / sell / mint / withdraw --token ${token}${net} pick the right pad code`, kind }
|
|
103
103
|
}
|
|
104
104
|
if (kind === 'v3' && want === 'fair') {
|
|
105
|
-
return { error: `this is a V3 (bonding-curve) token, not a 打新 sale:
|
|
105
|
+
return { error: `this is a V3 (bonding-curve) token, not a 打新 sale: gang info / buy / sell --token ${token}${net} pick the right pad code`, kind }
|
|
106
106
|
}
|
|
107
107
|
return { error: `not ${ask} on this pad`, kind }
|
|
108
108
|
}
|