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}