@gangfun/cli 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/pool.js CHANGED
@@ -3,7 +3,7 @@ import { decimalsOf, isMissingFunction, poolSellQuote, ROUTER_ABI, routerDeadlin
3
3
 
4
4
  /**
5
5
  * PancakeSwap (V2 pool) buys and sells of a graduated / launched token through GangRouter — V3 and 打新 tokens
6
- * (`gang v3 buy|sell`, `gang fair buy|sell`) and V2 / Soft / U / S tokens (`gang buy|sell`), all the same way:
6
+ * (V3 / 打新, also as `gang v3|fair buy|sell`) and V2 / Soft / U / S tokens (`gang buy|sell`), all the same way:
7
7
  * poolBuyBNB(token, quote, minTokensOut, deadline) payable path [WBNB, token] or [WBNB, quote, token]
8
8
  * poolBuy(token, quote, quoteIn, minTokensOut, deadline) path [quote, token]; approve quote → router first
9
9
  * poolSell(token, quote, amountIn, minQuoteOut, deadline) path [token, quote]; approve token → router first
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 v3 sell` / `gang fair sell` build this sell through GangRouter (poolSell / --to-bnb poolSellForBNB, minOut as here); '
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
@@ -3,7 +3,7 @@ import { decimalsOf, defaultRefPool, dividendOf, VAULT_ABI, fundingStep as fund,
3
3
  import { API } from './discover.js'
4
4
  import { UA } from './version.js'
5
5
  import { walletFor } from './fair.js'
6
- import { MERGED_ERRORS_ABI, MERGED_HINTS, NOT_ON_MAINNET, noteFlagError, notThisKind, padNets } from './gangpad.js'
6
+ import { MERGED_ERRORS_ABI, MERGED_HINTS, NOT_ON_MAINNET, notThisKind, padNets } from './gangpad.js'
7
7
  import { poolPayBnb, poolTrade } from './pool.js'
8
8
 
9
9
  /**
@@ -69,7 +69,7 @@ const HINTS = {
69
69
  BadTaxConfig: 'a tax setting is out of bounds, or the recipient is neither you nor on the whitelist: see reason',
70
70
  NotAgent: 'this wallet holds no Agent ID; V3 create and curve trades are Agent-only',
71
71
  WalletCapReached: 'this wallet already holds the per-wallet cap (1% of supply)',
72
- AlreadyGraduated: 'the token graduated: re-run gang v3 buy / sell, which now trade it in the PancakeSwap pool',
72
+ AlreadyGraduated: 'the token graduated: re-run gang buy / gang sell, which now trade it in the PancakeSwap pool',
73
73
  Slippage: 'the price moved past --slippage; re-run to re-quote',
74
74
  RefPoolRequired: 'a non-WBNB --quote needs --ref-pool: a Pancake V2 pair or V3 pool that holds the quote token',
75
75
  QuoteNotInPool: '--ref-pool does not contain the --quote token',
@@ -90,6 +90,9 @@ const HINTS = {
90
90
  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
91
  }
92
92
 
93
+ /** What `gang tx` decodes a V3 trade's logs with (the pad, the tax router, the dividend vault). */
94
+ export const V3_EVENT_ABIS = [PAD_ABI, TAX_ROUTER_ABI, VAULT_ABI]
95
+
93
96
  export function v3Ctx(testnet) {
94
97
  const net = V3_NETS[testnet ? 'testnet' : 'mainnet']
95
98
  if (!net.pad) throw new Error(NOT_ON_MAINNET)
@@ -100,6 +103,7 @@ export function v3Ctx(testnet) {
100
103
  const symbolOf = async (ctx, token) => { try { return await ctx.read(token, ERC20_ABI, 'symbol') } catch { return null } }
101
104
  const pct = (bps) => Number(bps) / 100
102
105
  const VIRTUAL_TOKEN = 1_066_666_666_666_666_666_666_666_667n
106
+ const ceilDiv = (a, b) => (a + b - 1n) / b
103
107
 
104
108
  /** Revert name (+ BadTaxConfig reason) of a failed call, or null when it was the RPC. */
105
109
  const revertOf = (e) => revertIn(e, [PAD_ABI, TAX_ROUTER_ABI, GANG_ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS })
@@ -178,10 +182,22 @@ export async function curveOf(ctx, token) {
178
182
  toGraduation: await units(ctx, quote, remaining),
179
183
  progressPct: graduated ? 100 : gradAmt > 0n ? Number((realQuote * 10_000n) / gradAmt) / 100 : 0,
180
184
  priceQuote: await units(ctx, quote, price), priceFrom: graduated ? 'pool' : 'curve', priceRaw: price,
181
- owedToRecipient: await units(ctx, quote, owed), owedRaw: owed, reserveTokenRaw,
185
+ owedToRecipient: await units(ctx, quote, owed), owedRaw: owed, reserveTokenRaw, curveRaw: { realQuote, sold, virtualQuote },
182
186
  }
183
187
  }
184
188
 
189
+ /**
190
+ * What a curve buy of `quoteIn` that gets `tokensOut` spends, the fee included (CurveLogic _quoteInFor + _recomputeFee):
191
+ * less than quoteIn when the wallet cap or the graduation cut trimmed it and the rest is refunded.
192
+ */
193
+ async function spentFor(ctx, c, quoteIn, tokensOut) {
194
+ const { realQuote, sold, virtualQuote } = c.curveRaw
195
+ const net = ceilDiv(virtualQuote * VIRTUAL_TOKEN, VIRTUAL_TOKEN - sold - tokensOut) - (virtualQuote + realQuote)
196
+ const feeBps = BigInt(Math.round((await feesOf(ctx)).curveFeePct * 100))
197
+ const gross = ceilDiv(net * 10_000n, 10_000n - feeBps)
198
+ return gross < quoteIn ? gross : quoteIn
199
+ }
200
+
185
201
  /** `token` funding for `need` toward the pad (wrap BNB, then approve): a step object, or null when ready. */
186
202
  const fundingStep = (ctx, me, token, need, sym, approveStep, rawOnly, summarize) =>
187
203
  fund(ctx, { me, token, need, spender: ctx.pad, sym, approveStep }, rawOnly, summarize)
@@ -205,14 +221,11 @@ const SUBS = ['list', 'info', 'status', 'create', 'buy', 'sell', 'claim', 'incom
205
221
  const USAGE = 'usage: gang v3 <list|info|status|create|buy|sell|claim|income|release|tx> [--testnet] — see gang help'
206
222
 
207
223
  /**
208
- * Entry for `gang v3 <sub>`. io: { opt, flag, out, fail, summarize, resolveLogo, checkMeta, resolveMeta } from index.js
209
- * (summarize = summarizePreview, so previews look like every other command's).
224
+ * Entry for `gang v3 <sub>`. io: { opt, flag, out, fail, summarize, resolveLogo, checkMeta, resolveMeta, notes } from
225
+ * index.js (summarize = summarizePreview, so previews look like every other command's; notes = padNotes: --note / --follow).
210
226
  */
211
- export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo, checkMeta, resolveMeta }) {
227
+ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo, checkMeta, resolveMeta, notes }) {
212
228
  if (!SUBS.includes(sub)) return fail(USAGE, { got: sub ?? null })
213
- // agent notes are matched to the server's V2 / Soft trades; V3 txs are not matched there yet
214
- const noteError = noteFlagError(flag, 'GangLaunchpad tokens (gang create / gang v3)', 'a GangLaunchpad tx')
215
- if (noteError) return fail(noteError)
216
229
  const testnet = flag('testnet')
217
230
  const net = testnet ? ' --testnet' : ''
218
231
  const rawOnly = flag('calldata')
@@ -224,7 +237,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
224
237
  const path = recipient ? `/v3/recipients/${recipient.toLowerCase()}?limit=20` : `/v3/tokens?status=${status}&limit=30`
225
238
  const r = await fetch(`${API}${path}`, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(10_000) })
226
239
  .then((x) => (x.ok ? x.json() : { error: x.status })).catch((e) => ({ error: e.message }))
227
- if (r.error) return out({ success: false, error: `gang.fun V3 data unavailable (${r.error})`, hint: `use gang v3 info --token <addr>${net} for one token (reads the chain)` })
240
+ 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)` })
228
241
  // the quote's own decimals from gang.fun (18 when an older server does not send them)
229
242
  const f = (x, d) => (x == null ? null : formatUnits(BigInt(x), d ?? 18))
230
243
  if (recipient) {
@@ -254,7 +267,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
254
267
  const token = addrArg(opt, fail, 'token', 'a V3 token address')
255
268
  const c = await curveOf(ctx, token)
256
269
  if (!c) return notV3(token)
257
- const { owedRaw: _raw, priceRaw: _p, reserveTokenRaw: rT, ...pub } = c
270
+ const { owedRaw: _raw, priceRaw: _p, reserveTokenRaw: rT, curveRaw: _cv, ...pub } = c
258
271
  // in the pool the tax is held as tokens and sold in small chunks under a per-block budget
259
272
  const swapBack = c.graduated && c.pair ? await swapBackState(ctx, c.taxHandler, token, rT) : null
260
273
  const fees = await feesOf(ctx)
@@ -298,7 +311,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
298
311
  if (d.claimableRaw === 0n) {
299
312
  return fail('nothing to claim yet', {
300
313
  ...(d.creditedUnsoldTokens && Number(d.creditedUnsoldTokens) > 0 && { creditedUnsoldEstimate: d.creditedUnsoldEstimate, note: d.note }),
301
- hint: `gang v3 status --token ${token}${testnet ? ' --testnet' : ''}`,
314
+ hint: `gang status --token ${token}${testnet ? ' --testnet' : ''}`,
302
315
  })
303
316
  }
304
317
  const sim = await simulate(ctx, me, vault, VAULT_ABI, 'claim', [token])
@@ -367,7 +380,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
367
380
  }
368
381
  const rest = 100 - dividend
369
382
  const fees = await feesOf(ctx)
370
- return out(step(ctx, { from: me, to: ctx.pad, data: createData(logo.logoURI, meta.metaURI) }, {
383
+ return out(notes.step(ctx, { from: me, to: ctx.pad, data: createData(logo.logoURI, meta.metaURI) }, {
371
384
  step: 'create', chainId: Number(ctx.chainId), name, symbol, logoURI: logo.logoURI, ...(logo.uploaded && { uploaded: true }),
372
385
  metaURI: meta.metaURI, metadata: meta.metadata,
373
386
  quote: (await symbolOf(ctx, quote)) || quote, graduationAt: await units(ctx, quote, gradAmt),
@@ -378,8 +391,8 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
378
391
  ? `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`
379
392
  + (dividend > 0 ? '; if no holder has 10,000+ tokens when a tax is paid, the holders\' part goes to the recipient too' : '')
380
393
  : 'untaxed: no tax in the PancakeSwap pool after graduation, no dividend, no recipient',
381
- 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 v3 tx --hash …\`.`,
382
- }, rawOnly, summarize))
394
+ 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 …\`.`,
395
+ }, 'create'))
383
396
  }
384
397
 
385
398
  if (sub === 'buy') {
@@ -389,13 +402,14 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
389
402
  if (c.graduated) {
390
403
  // graduated: buy in the PancakeSwap pool through GangRouter (the buy tax applies; no Agent gate, no wallet cap)
391
404
  if (!c.pair) return fail('the pad marks this token graduated but has no PancakeSwap pair on record: nothing to trade it on', { token })
405
+ if (notes.following) fail('--follow only counts on curve buys before graduation; this token trades on PancakeSwap now', { pair: c.pair })
392
406
  if (!flag('amount') && !flag('bnb')) fail('--amount <quote (or BNB) to spend> required')
393
407
  const raw = parseAmount(fail, flag('amount') ? 'amount' : 'bnb', opt('amount') ?? opt('bnb'))
394
408
  return out(await poolTrade(ctx, {
395
409
  side: 'buy', token, quote: c.quote, me: walletFor(ctx, opt('from')), amount: raw,
396
410
  slippage: parseSlippage(fail, opt('slippage', '1')), buyTaxBps: Math.round(c.buyTaxPct * 100),
397
411
  sellTaxBps: Math.round(c.sellTaxPct * 100), handler: c.taxHandler, payBnb: poolPayBnb(ctx, c.quote, flag),
398
- base: { kind: 'v3', pair: c.pair },
412
+ base: { kind: 'v3', pair: c.pair, ...notes.pool },
399
413
  }, { rawOnly, summarize, revertOf }))
400
414
  }
401
415
  const me = walletFor(ctx, opt('from'))
@@ -405,6 +419,9 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
405
419
  const raw = parseAmount(fail, flag('amount') ? 'amount' : 'bnb', opt('amount') ?? opt('bnb'))
406
420
  const quoteIn = parseUnits(raw, await decimalsOf(ctx, c.quote))
407
421
  const slippage = parseSlippage(fail, opt('slippage', '1'))
422
+ // asked from gang.fun before any wrap / approve round, so a follow that can't count fails first
423
+ const follow = await notes.follow(token, me)
424
+ const priced = async (tokensOut) => follow && notes.priced(follow, token, await spentFor(ctx, c, quoteIn, tokensOut), await decimalsOf(ctx, c.quote))
408
425
  const extra = (tokensOut, minOut) => ({
409
426
  step: 'buy', chainId: Number(ctx.chainId), token, spend: raw, quote: c.quoteSymbol, expectTokens: formatUnits(tokensOut, 18),
410
427
  minTokensOut: formatUnits(minOut, 18), curveTaxPct: 0, ...(c.buyTaxPct > 0 && { poolBuyTaxPct: c.buyTaxPct }),
@@ -422,20 +439,20 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
422
439
  if (sim.failed) return out({ success: false, step: 'buy', via: 'router', ...sim.failed })
423
440
  const minOut = minOf(sim.result, slippage)
424
441
  const data = encodeFunctionData({ abi: GANG_ROUTER_ABI, functionName: 'v3BuyBNB', args: [token, minOut, routerDeadline()] })
425
- return out(step(ctx, { from: me, to: ctx.router, data, value: quoteIn }, {
442
+ return out(notes.step(ctx, { from: me, to: ctx.router, data, value: quoteIn }, {
426
443
  ...extra(sim.result, minOut), via: 'router', curveFeePct: (await feesOf(ctx)).curveFeePct,
427
444
  note: 'paid in BNB through GangRouter; spend includes the 1% curve fee (no tax on the curve; the buy tax applies only in the pool after graduation); at the 1% wallet cap or the graduation cut the excess comes back to this wallet as WBNB (not BNB)',
428
- }, rawOnly, summarize))
445
+ }, 'buy', await priced(sim.result)))
429
446
  }
430
447
  const pre = await fundingStep(ctx, me, c.quote, quoteIn, c.quoteSymbol, 'approve-quote', rawOnly, summarize)
431
- if (pre) return out(pre)
448
+ if (pre) return out(pre.success ? { ...pre, ...notes.skipped(follow) } : pre)
432
449
  const sim = await simulate(ctx, me, ctx.pad, PAD_ABI, 'buy', [token, quoteIn, 0n])
433
450
  if (sim.failed) return out({ success: false, step: 'buy', ...sim.failed })
434
451
  const minOut = minOf(sim.result, slippage)
435
- return out(step(ctx, { from: me, to: ctx.pad, data: encodeFunctionData({ abi: PAD_ABI, functionName: 'buy', args: [token, quoteIn, minOut] }) }, {
452
+ return out(notes.step(ctx, { from: me, to: ctx.pad, data: encodeFunctionData({ abi: PAD_ABI, functionName: 'buy', args: [token, quoteIn, minOut] }) }, {
436
453
  ...extra(sim.result, minOut), curveFeePct: (await feesOf(ctx)).curveFeePct,
437
454
  note: 'spend includes the 1% curve fee (no tax on the curve; the buy tax applies only in the pool after graduation); at the 1% wallet cap the excess is refunded',
438
- }, rawOnly, summarize))
455
+ }, 'buy', await priced(sim.result)))
439
456
  }
440
457
 
441
458
  if (sub === 'sell') {
@@ -454,7 +471,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
454
471
  return out(await poolTrade(ctx, {
455
472
  side: 'sell', token, quote: c.quote, me, amount: flag('all') ? null : parseAmount(fail, 'amount', opt('amount')), all: flag('all'),
456
473
  slippage, buyTaxBps: Math.round(c.buyTaxPct * 100), sellTaxBps: Math.round(c.sellTaxPct * 100), handler: c.taxHandler,
457
- swapBackGas: c.sellTaxPct > 0, toBnb: flag('to-bnb'), base: { kind: 'v3', pair: c.pair },
474
+ swapBackGas: c.sellTaxPct > 0, toBnb: flag('to-bnb'), base: { kind: 'v3', pair: c.pair, ...notes.pool },
458
475
  }, { rawOnly, summarize, revertOf }))
459
476
  }
460
477
  if (flag('all')) return fail('--all needs a wallet (or --from <addr>) whose balance can be read', { pair: c.pair })
@@ -477,16 +494,16 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
477
494
  const amount = flag('all') ? bal : parseUnits(parseAmount(fail, 'amount', opt('amount')), 18)
478
495
  if (amount > bal) fail('amount exceeds balance', { balance: formatUnits(bal, 18) })
479
496
  const pre = await fundingStep(ctx, me, token, amount, c.quoteSymbol, 'approve', rawOnly, summarize)
480
- if (pre) return out(pre)
497
+ if (pre) return out(pre.success ? { ...pre, ...notes.skipped() } : pre)
481
498
  const sim = await simulate(ctx, me, ctx.pad, PAD_ABI, 'sell', [token, amount, 0n])
482
499
  if (sim.failed) return out({ success: false, step: 'sell', ...sim.failed })
483
500
  const minOut = minOf(sim.result, slippage)
484
- return out(step(ctx, { from: me, to: ctx.pad, data: encodeFunctionData({ abi: PAD_ABI, functionName: 'sell', args: [token, amount, minOut] }) }, {
501
+ return out(notes.step(ctx, { from: me, to: ctx.pad, data: encodeFunctionData({ abi: PAD_ABI, functionName: 'sell', args: [token, amount, minOut] }) }, {
485
502
  step: 'sell', chainId: Number(ctx.chainId), token, amount: formatUnits(amount, 18), quote: c.quoteSymbol,
486
503
  expectOut: await units(ctx, c.quote, sim.result), minOut: await units(ctx, c.quote, minOut), curveTaxPct: 0,
487
504
  ...(c.sellTaxPct > 0 && { poolSellTaxPct: c.sellTaxPct }),
488
505
  curveFeePct: (await feesOf(ctx)).curveFeePct, note: 'expectOut is after the 1% curve fee only (no tax on the curve; the sell tax applies only in the pool after graduation)',
489
- }, rawOnly, summarize))
506
+ }, 'sell'))
490
507
  }
491
508
 
492
509
  if (sub === 'release') {