apps/keeper/src/jobs/buyback.ts

Buys $SOLARY and burns it. Shown whole, as it was in the repository when this site was built. Line numbers link: add #L12 to the address.

apps/keeper/src/jobs/buyback.ts118 lines
1// buyback: spend the buyback slice (Σ coins.buyback_lamports − Σ buybacks) on $SOLARY through Jupiter (pump.fun2// curve before graduation, PumpSwap after) and burn what was bought. Runs when the slice reaches3// buyback.min_lamports (0.1 SOL), at most buyback.max_lamports (5 SOL) per run. Guards: price impact under4// buyback.max_impact_pct (3%), Jupiter's min-out at buyback.slippage_bps (1%), a quote no worse than 20% under the5// market price the market job last read, and a simulation showing what the swap would take from the vault.6// Two intents per buyback: 'buyback' (the swap; the buybacks row is written when it confirms) and 'burn'.7import { and, eq, inArray, isNotNull, isNull } from 'drizzle-orm';8import { buybacks, coins, intents } from '@solary/db';9import { formatCompact } from '@solary/core';10import { event, lamportsToCents, latestSolUsd, sol, type JobCtx, type Stats } from '../ctx';11import { createIntent, holdReason, intentState, openIntents, setIntent } from '../intents';12import { words } from '../ledger/words';13import { settingBig, settingNum, vaultObligations } from './common';14 15const DECIMALS = 6; // pump.fun coins16 17async function recordBuy(ctx: JobCtx, intentId: number, sig: string, mint: string): Promise<boolean> {18  const out = await ctx.sol.swapOutcome(sig, mint);19  if (!out) return false;20  const price = await latestSolUsd(ctx.db, 3_600_000);21  const inserted = await ctx.db.transaction(async (t) => {22    const [seen] = await t.select({ id: buybacks.id }).from(buybacks).where(eq(buybacks.buyTx, sig)).limit(1);23    await t.update(intents).set({ status: 'confirmed', updatedAt: new Date() }).where(eq(intents.id, intentId));24    if (seen) return false;25    await t.insert(buybacks).values({ lamports: out.lamports, tokens: out.tokens, buyTx: sig, usdCents: price ? lamportsToCents(out.lamports, price.usd) : 0 });26    return true;27  });28  if (inserted) await event(ctx.db, { action: 'buyback', message: words.bought(out.tokens, DECIMALS, out.lamports), amounts: { lamports: out.lamports.toString(), tokens: out.tokens.toString() }, sig });29  return inserted;30}31 32async function recordBurn(ctx: JobCtx, intentId: number, sig: string, rows: number[], tokens: bigint): Promise<void> {33  await ctx.db.transaction(async (t) => {34    if (rows.length) await t.update(buybacks).set({ burnTx: sig }).where(and(inArray(buybacks.id, rows), isNull(buybacks.burnTx)));35    await t.update(intents).set({ status: 'confirmed', updatedAt: new Date() }).where(eq(intents.id, intentId));36  });37  await event(ctx.db, { action: 'burn', message: words.burned(tokens, DECIMALS), amounts: { tokens: tokens.toString() }, sig });38}39 40/** Burn everything the vault holds of $SOLARY; rows waiting for a burn get its signature. */41async function burn(ctx: JobCtx, mint: string, hold: string | null): Promise<string> {42  const waiting = await ctx.db43    .select({ id: buybacks.id })44    .from(buybacks)45    .where(and(isNotNull(buybacks.buyTx), isNull(buybacks.burnTx)));46  const mine = waiting.map((w) => w.id);47  const priority = await settingBig(ctx, 'tx.priority_micro_lamports', 50_000n);48  const prepared = await ctx.sol.prepareBurn(mint, priority);49  if (!prepared.ok) return prepared.error === 'nothing to burn' ? 'nothing to burn' : `burn simulation failed: ${prepared.error}`;50  if (hold) return `${hold}: would burn ${prepared.tokens} base units`;51  const intentId = await createIntent(ctx, 'burn', { mint, tokens: prepared.tokens, rows: mine });52  const res = await ctx.sol.send(prepared, (sig, lvbh) => setIntent(ctx, intentId, 'sent', { sig, payload: { lastValidBlockHeight: lvbh } }));53  if (res.status === 'confirmed') {54    await recordBurn(ctx, intentId, res.signature, mine, prepared.tokens ?? 0n);55    return 'burned';56  }57  if (res.status !== 'pending') await setIntent(ctx, intentId, res.status, { payload: { error: res.error } });58  return `burn ${res.status}`;59}60 61export async function buybackJob(ctx: JobCtx): Promise<Stats> {62  // $SOLARY: the env mint in real mode, the official demo coin in fake mode63  let mint = ctx.sol.mode === 'real' ? ctx.env.solaryMint : undefined;64  const [official] = await ctx.db.select({ mint: coins.mint, priceUsd: coins.priceUsd }).from(coins).where(eq(coins.official, true)).limit(1);65  if (ctx.sol.mode === 'fake') mint = official?.mint.startsWith('FAKE') ? official.mint : undefined;66  if (!mint) return { skipped: ctx.sol.mode === 'real' ? 'NEXT_PUBLIC_SOLARY_MINT not set' : 'no official demo coin' };67 68  let reconciled = 0;69  for (const i of await openIntents(ctx, 'buyback')) {70    const st = await intentState(ctx, i);71    if (st === 'confirmed') reconciled += Number(await recordBuy(ctx, i.id, i.sig, mint));72    else if (st !== 'unknown') await setIntent(ctx, i.id, st);73  }74  for (const i of await openIntents(ctx, 'burn')) {75    const st = await intentState(ctx, i);76    if (st === 'confirmed') await recordBurn(ctx, i.id, i.sig, (i.payload.rows as number[] | undefined) ?? [], BigInt(String(i.payload.tokens ?? '0')));77    else if (st !== 'unknown') await setIntent(ctx, i.id, st);78  }79 80  const hold = holdReason(ctx);81  const burned = await burn(ctx, mint, hold);82 83  const min = await settingBig(ctx, 'buyback.min_lamports', 100_000_000n);84  const max = await settingBig(ctx, 'buyback.max_lamports', 5_000_000_000n);85  const reserve = await settingBig(ctx, 'vault.reserve_lamports', 10_000_000n);86  const owed = await vaultObligations(ctx);87  if (owed.buyback < min) return { reconciled, burn: burned, buyback_lamports: owed.buyback.toString(), skipped: 'under the buyback minimum' };88  const balance = await ctx.sol.vaultBalance();89  const free = balance - owed.holders - owed.platform - reserve;90  let amount = owed.buyback < max ? owed.buyback : max;91  if (free < amount) amount = free;92  if (amount < min) return { reconciled, burn: burned, skipped: 'vault holds too little above what it owes' };93 94  const slippage = await settingNum(ctx, 'buyback.slippage_bps', 100);95  const maxImpact = await settingNum(ctx, 'buyback.max_impact_pct', 3);96  const quote = await ctx.sol.quoteBuyback(mint, amount, slippage);97  if (quote.priceImpactPct > maxImpact) return { reconciled, burn: burned, skipped: `price impact ${quote.priceImpactPct.toFixed(2)}% over ${maxImpact}%` };98  const solUsd = await latestSolUsd(ctx.db);99  if (official?.priceUsd && official.priceUsd > 0 && solUsd) {100    const expected = ((Number(amount) / 1e9) * Number(solUsd.usd)) / official.priceUsd;101    const got = Number(quote.outTokens) / 10 ** DECIMALS;102    if (got < expected * 0.8) return { reconciled, burn: burned, skipped: `quote ${got.toFixed(0)} tokens is over 20% under the market (${expected.toFixed(0)})` };103  }104  const prepared = await ctx.sol.prepareBuyback(mint, quote, amount + 10_000_000n);105  if (!prepared.ok) return { reconciled, burn: burned, failed: prepared.error ?? 'simulation failed' };106  if (hold) {107    ctx.log(`${hold}: would buy about ${formatCompact(Number(quote.outTokens) / 10 ** DECIMALS, 2)} $SOLARY for ${sol(amount)} via ${quote.route}`);108    return { reconciled, burn: burned, would_buy: amount.toString() };109  }110  const intentId = await createIntent(ctx, 'buyback', { mint, lamports: amount, min_out: quote.minOut, route: quote.route });111  const res = await ctx.sol.send(prepared, (sig, lvbh) => setIntent(ctx, intentId, 'sent', { sig, payload: { lastValidBlockHeight: lvbh } }));112  if (res.status === 'confirmed') {113    await recordBuy(ctx, intentId, res.signature, mint);114    return { reconciled, bought: amount.toString(), burn: await burn(ctx, mint, hold) };115  }116  if (res.status !== 'pending') await setIntent(ctx, intentId, res.status, { payload: { error: res.error } });117  return { reconciled, burn: burned, status: res.status };118}