diff --git a/scripts/assets/CARS/CARS.json b/scripts/assets/CARS/CARS.json new file mode 100644 index 00000000..b282750e --- /dev/null +++ b/scripts/assets/CARS/CARS.json @@ -0,0 +1,6 @@ +{ + "name": "Rip Cars", + "symbol": "CARS", + "description": "The world’s first Hot Wheels gacha platform", + "image": "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/CARS/CARS.png" +} \ No newline at end of file diff --git a/scripts/assets/CARS/CARS.png b/scripts/assets/CARS/CARS.png new file mode 100644 index 00000000..b9c9ddc5 Binary files /dev/null and b/scripts/assets/CARS/CARS.png differ diff --git a/scripts/v0.7/rip-cars/allocation/.env.example b/scripts/v0.7/rip-cars/allocation/.env.example new file mode 100644 index 00000000..76b4a53f --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/.env.example @@ -0,0 +1,20 @@ +# Solana RPC. Staging surfpool, or http://127.0.0.1:8899 for a local surfpool. +RPC_URL= + +# Indexer Postgres (read replica) for fund events + ownership scores. Read-only. +FUTARCHY_PG_URL= + +# Launch authority key (base58 or JSON byte array). Only needed for --execute. +RIPCARS_AUTHORITY_KEY= + +# Nash congestion game (defaults match nash_equilibrium_sim.html): +OWNERSHIP_SPLIT=0.5 +NASH_EPSILON=1 +NASH_REACT=0.40 +NASH_START=rand +NASH_SEED=20260723 +SCORE_COLUMN=ownership_points +BOOST_MULTIPLIER=10 +BOOST_FILL_CEILING=3 +BOOST_LOOK_AHEAD_HOURS=1 +PRIORITY_FEE_MICRO_LAMPORTS=10000 diff --git a/scripts/v0.7/rip-cars/allocation/.gitignore b/scripts/v0.7/rip-cars/allocation/.gitignore new file mode 100644 index 00000000..bacc1777 --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +allocation.out.json diff --git a/scripts/v0.7/rip-cars/allocation/README.md b/scripts/v0.7/rip-cars/allocation/README.md new file mode 100644 index 00000000..f1198a2c --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/README.md @@ -0,0 +1,71 @@ +# Rip Cars allocation + +Standalone one-off (same CLI shape as laso / credible). Allocates the Rip Cars +launch via a **congestion-game Nash equilibrium** (ownership road vs accumulator +road — same model as `nash_equilibrium_sim.ts` / `nash_equilibrium_live.ts`), +then optionally approves every funding record on-chain. + +Dry-run by default. With `--execute` it **confirms each on-chain step at the +terminal**: closes the launch if it's live+expired (freezing the funder set +first), then approves every funder and verifies the total. + +Stops after setting the allocation — does **not** `completeLaunch`. The launch +is left `Closed`; complete it manually afterward. + +## Layout + +- `ripcars.ts` — entry point: config, close → allocate → approve → verify +- `allocation.ts` — pure Nash solver + accumulator weights +- `ac/fundingApproval.ts` — batched `setFundingRecordApproval` + launch reads +- `db.ts` — fund events + ownership scores (read-only Postgres) +- `utils.ts` — USDC formatting, table printer, `allocation.out.json`, prompts + +## Setup + +```bash +bun install +cp .env.example .env # then fill it in +``` + +`.env`: + +- `RPC_URL` — Solana RPC +- `FUTARCHY_PG_URL` — indexer Postgres (read-only; fund events + scores) +- `RIPCARS_AUTHORITY_KEY` — launch authority (base58 or JSON byte array). Only for `--execute`. + +Optional Nash knobs (HTML-sim defaults): + +| Env | Default | Meaning | +|-----|---------|---------| +| `OWNERSHIP_SPLIT` | `0.5` | Fraction of pool on the ownership road | +| `NASH_EPSILON` | `1` | Min $ gain before an agent switches | +| `NASH_REACT` | `0.40` | Flip probability for unhappy agents each round | +| `NASH_START` | `rand` | Initial roads: `rand` \| `acc` \| `own` | +| `NASH_SEED` | `20260723` | RNG seed (start + stochastic flips) | +| `SCORE_COLUMN` | `ownership_points` | Score column for the ownership road | + +Config at the top of `ripcars.ts` pulls `LAUNCH_ADDRESS` / pool size from +`../constants.ts` — triple-check before `--execute`. + +## Run + +```bash +bun ripcars.ts # dry run — prints CLI table + writes allocation.out.json +bun ripcars.ts --execute # approve on-chain (requires RIPCARS_AUTHORITY_KEY) +``` + +Each run writes `allocation.out.json` — same core shape as credible/laso +(`funder`, `kind`, `committed`, `approved` atoms). `kind` is the Nash road: +`ownership` | `accumulator`. + +## How the allocation works + +1. **Accumulator weights** — run the accelerated-cranker over the full pool; + each funder's approved amount becomes its weight on the accumulator road. +2. **Two roads** — ownership road water-fills `OWNERSHIP_SPLIT` of the pool by + score; accumulator road splits the rest by weight. Both capped at committed. +3. **Best response** — same as the HTML "Solve to Nash": unhappy agents flip + simultaneously with probability `NASH_REACT` each round until ε-Nash. +4. **Fill** — unused budget from commit-caps is redistributed by headroom so + `Σ approved === TOTAL_ALLOCATION`, then converted to atoms. +5. `--execute` batches `setFundingRecordApproval` for those amounts. diff --git a/scripts/v0.7/rip-cars/allocation/ac/constants.ts b/scripts/v0.7/rip-cars/allocation/ac/constants.ts new file mode 100644 index 00000000..d729e5d2 --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/ac/constants.ts @@ -0,0 +1,26 @@ +/** Number of decimal places for USDC on Solana. */ +export const USDC_DECIMALS = 6; + +/** Multiplier to convert whole USDC units to on-chain lamport amounts. */ +export const USDC_SCALAR = 10 ** USDC_DECIMALS; + +/** + * Buffer (in seconds) added to time-based checks before sending + * on-chain instructions. Accounts for clock drift. + */ +export const CLOCK_DRIFT_BUFFER_SECONDS = 10; + +/** + * Max funding record approvals per transaction batch. + * Each ix is small (~16k CUs), but each adds 2 unique accounts. + */ +export const APPROVAL_BATCH_SIZE = 10; + +/** + * Priority fee in microlamports per compute unit. + * Configurable via PRIORITY_FEE_MICRO_LAMPORTS env var. + */ +export const PRIORITY_FEE_MICRO_LAMPORTS = parseInt( + process.env.PRIORITY_FEE_MICRO_LAMPORTS ?? "10000", + 10, +); diff --git a/scripts/v0.7/rip-cars/allocation/ac/fundingApproval.ts b/scripts/v0.7/rip-cars/allocation/ac/fundingApproval.ts new file mode 100644 index 00000000..ea68d0b3 --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/ac/fundingApproval.ts @@ -0,0 +1,218 @@ +/** + * Funding record approval: batch sending of setFundingRecordApproval ixs. + * + * After a launch moves to "Closed" state (minimum raise met), the cranker + * must approve each funder's allocation before calling completeLaunch. + * + * Copied from laso/allocation (accelerated-cranker). Re-approval is safe — + * the Rust program uses delta accounting on total_approved_amount. + */ +import { + type Launch, + type LaunchpadClient, +} from "@metadaoproject/programs/launchpad/v0.7"; +import { + ComputeBudgetProgram, + type Connection, + type PublicKey, + Transaction, + TransactionExpiredBlockheightExceededError, + TransactionExpiredTimeoutError, +} from "@solana/web3.js"; +import type * as anchor from "@coral-xyz/anchor"; +import BN from "bn.js"; + +import { log } from "../logger"; +import { APPROVAL_BATCH_SIZE, PRIORITY_FEE_MICRO_LAMPORTS } from "./constants"; + +const logger = log.child({ module: "fundingApproval" }); + +const BATCH_SEND_RETRIES = 3; +const INTER_BATCH_DELAY_MS = 200; + +/** A single funder's approved allocation. */ +export interface FundingApproval { + funder: PublicKey; + /** Amount approved in token atoms (USDC lamports). Always <= committedAmount. */ + approvedAmount: BN; +} + +/** + * Send setFundingRecordApproval instructions in batches. + * + * @returns the maximum slot any batch confirmed at — pin post-approval + * verification reads with `minContextSlot`. 0 when there are no approvals. + */ +export async function approveFundingRecords( + launchpad: LaunchpadClient, + connection: Connection, + payer: anchor.Wallet, + launchAddr: PublicKey, + approvals: FundingApproval[], +): Promise<{ maxConfirmedSlot: number }> { + const addr = launchAddr.toBase58(); + let maxConfirmedSlot = 0; + let runningTotal = new BN(0); + + for (let i = 0; i < approvals.length; i += APPROVAL_BATCH_SIZE) { + const batch = approvals.slice(i, i + APPROVAL_BATCH_SIZE); + const batchNum = Math.floor(i / APPROVAL_BATCH_SIZE) + 1; + const totalBatches = Math.ceil(approvals.length / APPROVAL_BATCH_SIZE); + + const tx = new Transaction(); + tx.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: batch.length * 16_000, + }), + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: PRIORITY_FEE_MICRO_LAMPORTS, + }), + ); + + for (const approval of batch) { + const ix = await launchpad + .setFundingRecordApprovalIx({ + launch: launchAddr, + funder: approval.funder, + approvedAmount: approval.approvedAmount, + }) + .instruction(); + tx.add(ix); + } + + const { txHash, confirmedSlot } = await sendBatchWithRetry( + connection, + payer, + tx, + batchNum, + totalBatches, + ); + maxConfirmedSlot = Math.max(maxConfirmedSlot, confirmedSlot); + for (const approval of batch) + runningTotal = runningTotal.add(approval.approvedAmount); + + logger.info( + { + launchAddr: addr, + batch: batchNum, + totalBatches, + approvals: batch.length, + tx: txHash, + confirmedSlot, + runningTotal: runningTotal.toString(), + }, + "Approved funding record batch", + ); + + if (i + APPROVAL_BATCH_SIZE < approvals.length) { + await new Promise((r) => setTimeout(r, INTER_BATCH_DELAY_MS)); + } + } + + return { maxConfirmedSlot }; +} + +async function sendBatchWithRetry( + connection: Connection, + payer: anchor.Wallet, + tx: Transaction, + batchNum: number, + totalBatches: number, +): Promise<{ txHash: string; confirmedSlot: number }> { + for (let attempt = 1; attempt <= BATCH_SEND_RETRIES; attempt++) { + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + tx.feePayer = payer.publicKey; + tx.signatures = []; + tx.sign(payer.payer); + + const txHash = await connection.sendRawTransaction(tx.serialize(), { + skipPreflight: true, + }); + + try { + const confirmation = await connection.confirmTransaction( + { signature: txHash, blockhash, lastValidBlockHeight }, + "confirmed", + ); + if (confirmation.value.err) { + throw new Error( + `Approval batch ${batchNum}/${totalBatches} failed on-chain: ${JSON.stringify(confirmation.value.err)} (tx: ${txHash})`, + ); + } + return { txHash, confirmedSlot: confirmation.context.slot }; + } catch (err) { + const isExpiry = + err instanceof TransactionExpiredBlockheightExceededError || + err instanceof TransactionExpiredTimeoutError; + if (isExpiry && attempt < BATCH_SEND_RETRIES) { + logger.warn( + { + batch: batchNum, + totalBatches, + attempt, + maxAttempts: BATCH_SEND_RETRIES, + }, + "Approval batch expired — retrying with fresh blockhash", + ); + continue; + } + throw err; + } + } + throw new Error("unreachable"); +} + +const SLOT_PINNED_READ_ATTEMPTS = 5; + +function isMinContextSlotError(err: unknown): boolean { + if (!err || typeof err !== "object") return false; + const code = (err as { code?: unknown }).code; + if (code === -32016) return true; + const message = (err as { message?: unknown }).message; + return ( + typeof message === "string" && message.includes("Minimum context slot") + ); +} + +/** + * Read the launch account pinned to a minimum slot — read-your-writes safe. + */ +export async function fetchLaunchAtSlot( + connection: Connection, + launchpad: LaunchpadClient, + launchAddr: PublicKey, + minContextSlot: number, + opts?: { maxAttempts?: number; sleepMs?: (attempt: number) => number }, +): Promise { + const maxAttempts = opts?.maxAttempts ?? SLOT_PINNED_READ_ATTEMPTS; + const sleepMs = opts?.sleepMs ?? ((attempt: number) => 1000 * attempt); + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const info = await connection.getAccountInfo(launchAddr, { + commitment: "confirmed", + ...(minContextSlot > 0 ? { minContextSlot } : {}), + }); + if (!info) + throw new Error(`Launch account not found: ${launchAddr.toBase58()}`); + return await launchpad.deserializeLaunch(info); + } catch (err) { + if (isMinContextSlotError(err) && attempt < maxAttempts) { + logger.warn( + { + launchAddr: launchAddr.toBase58(), + minContextSlot, + attempt, + maxAttempts, + }, + "Replica behind minContextSlot — backing off before re-reading launch", + ); + await new Promise((r) => setTimeout(r, sleepMs(attempt))); + continue; + } + throw err; + } + } + throw new Error("unreachable"); +} diff --git a/scripts/v0.7/rip-cars/allocation/allocate.ts b/scripts/v0.7/rip-cars/allocation/allocate.ts new file mode 100644 index 00000000..f341d05c --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/allocate.ts @@ -0,0 +1,25 @@ +/** + * @deprecated Use `bun ripcars.ts` instead. + * + * The Rip Cars allocation CLI (Nash congestion game → allocation.out.json → + * --execute setFundingRecordApproval) lives in this folder. + * + * bun install + * bun ripcars.ts # dry-run: CLI table + allocation.out.json + * bun ripcars.ts --execute # approve funding records on-chain + * + * Interactive explorer: `bun nash_equilibrium_live.ts` + * See README.md. + */ +console.error( + [ + "allocate.ts has been replaced by the Rip Cars Nash allocation CLI.", + "", + " bun install", + " bun ripcars.ts # dry-run: CLI table + allocation.out.json", + " bun ripcars.ts --execute # approve funding records on-chain", + "", + "See scripts/v0.7/rip-cars/allocation/README.md", + ].join("\n"), +); +process.exit(1); diff --git a/scripts/v0.7/rip-cars/allocation/allocation.ts b/scripts/v0.7/rip-cars/allocation/allocation.ts new file mode 100644 index 00000000..0fc9ae81 --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/allocation.ts @@ -0,0 +1,911 @@ +/** + * Rip Cars allocation — congestion-game Nash equilibrium. + * + * Two roads share a fixed pool (congestion): + * - Ownership road: water-fills `ownershipSplit` of the pool by ownership SCORE + * among whoever picks it (capped at committed). + * - Accumulator road: splits the rest by accelerated-cranker ACCUMULATOR WEIGHT + * among whoever picks it (capped at committed). + * + * Each scored funder myopically best-responds to current congestion; unscored + * funders can only ride the accumulator road. Sequential best-response converges + * to an ε-Nash (same game as nash_equilibrium_sim.ts / nash_equilibrium_live.ts). + * + * Accumulator weights are the full-pool accelerated-cranker approved amounts + * (ratios only matter for the split among accumulator riders). + */ +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +import type { FundingApproval } from "./ac/fundingApproval.js"; + +const USDC_SCALAR = 1_000_000n; + +// ── types ─────────────────────────────────────────────────────────────────── + +/** On-chain funding record fields needed by the allocator. */ +export interface RipCarsFundingRecord { + funder: PublicKey; + committedAmount: BN; + committedAmountAccumulator: BN; + lastAccumulatorUpdate: BN; + /** Ownership score (0 if unscored). */ + score: number; +} + +export interface FundEvent { + funderAddr: string; + amount: BN; + timestamp: BN; +} + +export interface BoostConfig { + multiplier: number; + fillCeiling: number; + lookAheadSeconds: number; +} + +/** Which road the funder rides at equilibrium. */ +export type RoadKind = "ownership" | "accumulator"; + +export interface AllocationLine { + funder: PublicKey; + committedAmount: BN; + approvedAmount: BN; + kind: RoadKind; + score: number; + /** Accumulator weight used in the congestion game (atoms). */ + accumulatorWeight: BN; + lastAccumulatorUpdate: BN; + firstFundTime: BN; +} + +export interface AllocationResult { + lines: AllocationLine[]; + approvals: FundingApproval[]; + /** Funders capped when computing accumulator weights. */ + cappedCount: number; + ownershipCount: number; + /** Best-response rounds until ε-Nash (or max). */ + rounds: number; + atNash: boolean; + maxGain: number; +} + +export type NashStartMode = "acc" | "own" | "rand"; + +export interface AllocationConfig { + totalAllocation: BN; + /** Fraction of the pool budgeted to the ownership road (HTML "Ownership split"). */ + ownershipSplit: number; + /** Switch only if alternate road pays more than current by this many USDC dollars. */ + epsilon: number; + /** + * Per-round flip probability for unhappy agents (HTML "Reactivity"). + * Simultaneous stochastic BR — same as nash_equilibrium_sim.ts Solve. + */ + reactivity: number; + /** Initial road assignment before best-response. HTML default: rand. */ + startMode: NashStartMode; + /** RNG seed (HTML default 20260723). Used for rand start + stochastic flips. */ + seed: number; + boost: BoostConfig; + launchStartTime: BN; + secondsForLaunch: BN; + accumulatorActivationDelaySeconds: BN; +} + +// ── bigint helpers ────────────────────────────────────────────────────────── + +const bmax = (a: bigint, b: bigint): bigint => (a > b ? a : b); +const bmin = (a: bigint, b: bigint): bigint => (a < b ? a : b); +const bnToBig = (n: BN): bigint => BigInt(n.toString()); +const bigToBn = (n: bigint): BN => new BN(n.toString()); + +interface InternalRecord { + address: string; + funder: PublicKey; + committedAtoms: bigint; + accumulator: bigint; + lastUpdate: bigint; + score: number; +} + +interface InternalFundEvent { + funderAddr: string; + amount: bigint; + timestamp: bigint; +} + +// ── ACCUMULATOR WEIGHTS (faithful BigInt port) ────────────────────────────── + +function finalizeAccumulator( + r: InternalRecord, + closeTime: bigint, + activationTime: bigint, +): bigint { + if (r.lastUpdate === 0n) return r.accumulator; + if (closeTime <= activationTime) return r.accumulator; + const periodStart = bmax(r.lastUpdate, activationTime); + if (closeTime <= periodStart) return r.accumulator; + return r.accumulator + r.committedAtoms * (closeTime - periodStart); +} + +interface TimelineEntry { + time: bigint; + cumulative: bigint; +} + +function cumulativeAtTime(timeline: TimelineEntry[], time: bigint): bigint { + let cum = 0n; + for (const e of timeline) { + if (e.time <= time) cum = e.cumulative; + else break; + } + return cum; +} + +function computeBoostFactor( + delayedCum: bigint, + fillTarget: bigint, + multiplier: number, +): number { + if (delayedCum >= fillTarget) return 1; + const fillRatio = Number(delayedCum) / Number(fillTarget); + return 1 + (multiplier - 1) * (1 - fillRatio); +} + +function buildTimeline( + records: InternalRecord[], + fundEvents: InternalFundEvent[], +): { + timeline: TimelineEntry[]; + validatedMulti: Map; +} { + const sorted = records + .filter((r) => r.lastUpdate !== 0n) + .sort((a, b) => Number(a.lastUpdate - b.lastUpdate)); + const chain: TimelineEntry[] = []; + let cc = 0n; + for (const r of sorted) { + cc += r.committedAtoms; + chain.push({ time: r.lastUpdate, cumulative: cc }); + } + + const byFunder = new Map(); + for (const e of fundEvents) + ( + byFunder.get(e.funderAddr) ?? + byFunder.set(e.funderAddr, []).get(e.funderAddr)! + ).push(e); + const validatedMulti = new Map(); + for (const [addr, evs] of byFunder) { + if (evs.length <= 1) continue; + const rec = sorted.find((r) => r.address === addr); + if (!rec) continue; + const sum = evs.reduce((s, e) => s + e.amount, 0n); + if (sum !== rec.committedAtoms) continue; + if (evs.some((e) => e.timestamp > rec.lastUpdate)) continue; + validatedMulti.set( + addr, + [...evs].sort((a, b) => Number(a.timestamp - b.timestamp)), + ); + } + if (validatedMulti.size === 0) return { timeline: chain, validatedMulti }; + + const expandedEntries: Array<{ time: bigint; amount: bigint }> = []; + for (const r of sorted) { + const m = validatedMulti.get(r.address); + if (m) + for (const e of m) + expandedEntries.push({ time: e.timestamp, amount: e.amount }); + else expandedEntries.push({ time: r.lastUpdate, amount: r.committedAtoms }); + } + expandedEntries.sort((a, b) => Number(a.time - b.time)); + const expanded: TimelineEntry[] = []; + let ec = 0n; + for (const e of expandedEntries) { + ec += e.amount; + expanded.push({ time: e.time, cumulative: ec }); + } + for (const cp of chain) + if (cumulativeAtTime(expanded, cp.time) < cp.cumulative) + return { timeline: chain, validatedMulti: new Map() }; + if ( + expanded[expanded.length - 1].cumulative !== + chain[chain.length - 1].cumulative + ) { + return { timeline: chain, validatedMulti: new Map() }; + } + return { timeline: expanded, validatedMulti }; +} + +interface Finalized { + address: string; + committedAtoms: bigint; + finalAccumulator: bigint; +} + +function applyBoost( + records: InternalRecord[], + finalized: Finalized[], + target: bigint, + closeTime: bigint, + activationTime: bigint, + fundEvents: InternalFundEvent[], + boost: BoostConfig, +): bigint { + const DENOM = 10_000n; + const fillTarget = + (target * BigInt(Math.round(boost.fillCeiling * 100))) / 100n; + const lookAhead = BigInt(boost.lookAheadSeconds); + const { timeline, validatedMulti } = fundEvents.length + ? buildTimeline(records, fundEvents) + : { + timeline: [] as TimelineEntry[], + validatedMulti: new Map(), + }; + const recByAddr = new Map(records.map((r) => [r.address, r])); + let total = 0n; + for (const f of finalized) { + const multi = validatedMulti.get(f.address); + if (multi) { + let boosted = 0n; + for (const ev of multi) { + const periodStart = bmax(ev.timestamp, activationTime); + if (closeTime <= periodStart) continue; + const eventAccum = ev.amount * (closeTime - periodStart); + const lookupTime = bmin(ev.timestamp + lookAhead, closeTime); + const boostFloat = computeBoostFactor( + cumulativeAtTime(timeline, lookupTime), + fillTarget, + boost.multiplier, + ); + boosted += + (eventAccum * BigInt(Math.round(boostFloat * Number(DENOM)))) / DENOM; + } + f.finalAccumulator = boosted; + } else { + const rec = recByAddr.get(f.address); + const fundTime = rec ? rec.lastUpdate : closeTime; + const lookupTime = bmin(fundTime + lookAhead, closeTime); + const boostFloat = computeBoostFactor( + cumulativeAtTime(timeline, lookupTime), + fillTarget, + boost.multiplier, + ); + f.finalAccumulator = + (f.finalAccumulator * BigInt(Math.round(boostFloat * Number(DENOM)))) / + DENOM; + } + total += f.finalAccumulator; + } + return total; +} + +/** Full-pool accumulator approval — used as each funder's congestion-game weight. */ +function accumulatorApproved( + records: InternalRecord[], + target: bigint, + startTime: bigint, + duration: bigint, + activationDelay: bigint, + fundEvents: InternalFundEvent[], + boost: BoostConfig, +): { approved: Map; cappedCount: number } { + const closeTime = startTime + duration; + const activationTime = startTime + activationDelay; + const finalized: Finalized[] = []; + let totalCommitted = 0n; + let totalAcc = 0n; + for (const r of records) { + const fa = finalizeAccumulator(r, closeTime, activationTime); + finalized.push({ + address: r.address, + committedAtoms: r.committedAtoms, + finalAccumulator: fa, + }); + totalCommitted += r.committedAtoms; + totalAcc += fa; + } + if (target > totalCommitted) + throw new Error(`target ${target} > totalCommitted ${totalCommitted}`); + if (totalAcc === 0n) throw new Error("totalAccumulator is zero"); + + if (boost.multiplier > 1) { + const boosted = applyBoost( + records, + finalized, + target, + closeTime, + activationTime, + fundEvents, + boost, + ); + if (boosted >= totalAcc) totalAcc = boosted; + } + + const rows = finalized.map((f) => { + const uncapped = (f.finalAccumulator * target) / totalAcc; + const capped = uncapped > f.committedAtoms; + return { + address: f.address, + committed: f.committedAtoms, + approved: capped ? f.committedAtoms : uncapped, + capped, + }; + }); + let surplus = target - rows.reduce((s, r) => s + r.approved, 0n); + if (surplus > 0n) { + const unfilledTotal = rows + .filter((r) => !r.capped) + .reduce((s, r) => s + (r.committed - r.approved), 0n); + if (unfilledTotal > 0n) + for (const r of rows) + if (!r.capped) + r.approved += (surplus * (r.committed - r.approved)) / unfilledTotal; + } + let dust = target - rows.reduce((s, r) => s + r.approved, 0n); + let pass = 0; + while (dust > 0n && pass < 3) { + pass++; + let did = false; + for (let i = 0; dust > 0n && i < rows.length; i++) + if (rows[i].approved < rows[i].committed) { + rows[i].approved += 1n; + dust -= 1n; + did = true; + } + if (!did) + throw new Error(`dust distribution failed: ${dust} atoms with no room`); + } + const finalSum = rows.reduce((s, r) => s + r.approved, 0n); + if (finalSum !== target) + throw new Error(`INVARIANT: sum approved ${finalSum} != target ${target}`); + for (const r of rows) + if (r.approved > r.committed) + throw new Error(`INVARIANT: ${r.address} approved > committed`); + return { + approved: new Map(rows.map((r) => [r.address, r.approved])), + cappedCount: rows.filter((r) => r.capped).length, + }; +} + +// ── NASH CONGESTION GAME (port of nash_equilibrium_sim.ts) ─────────────────── + +interface Agent { + address: string; + score: number; + committed: number; + weight: number; +} + +/** Mulberry-compatible LCG matching the HTML sim seed. */ +function makeRng(seed: number): () => number { + let s = seed >>> 0; + return () => { + s = (Math.imul(s, 1664525) + 1013904223) >>> 0; + return s / 4294967296; + }; +} + +/** Water-fill ownership budget among ownership riders by score, capped at committed. */ +function ownAlloc( + agents: Agent[], + onOwn: boolean[], + ownBudget: number, +): { alloc: number[]; lam: number } { + const idx: number[] = []; + for (let i = 0; i < agents.length; i++) + if (onOwn[i] && agents[i].score > 0) idx.push(i); + idx.sort( + (x, y) => + agents[x].committed / agents[x].score - + agents[y].committed / agents[y].score, + ); + let rem = ownBudget; + let act = 0; + for (const i of idx) act += agents[i].score; + const alloc = new Array(agents.length).fill(0); + let lam = 0; + let from = idx.length; + for (let k = 0; k < idx.length; k++) { + const i = idx[k]; + if (act <= 0) break; + const thr = agents[i].committed / agents[i].score; + const cand = rem / act; + if (cand <= thr) { + lam = cand; + from = k; + break; + } + alloc[i] = agents[i].committed; + rem -= agents[i].committed; + act -= agents[i].score; + } + for (let k = from; k < idx.length; k++) { + const i = idx[k]; + alloc[i] = agents[i].score * lam; + } + return { alloc, lam }; +} + +/** Split accumulator budget among accumulator riders by weight, capped at committed. */ +function accAlloc( + agents: Agent[], + onOwn: boolean[], + accBudget: number, +): { alloc: number[]; rate: number } { + let sum = 0; + for (let i = 0; i < agents.length; i++) + if (!onOwn[i]) sum += agents[i].weight; + const alloc = new Array(agents.length).fill(0); + for (let i = 0; i < agents.length; i++) { + if (!onOwn[i]) + alloc[i] = + sum > 0 + ? Math.min(agents[i].committed, (agents[i].weight / sum) * accBudget) + : 0; + } + return { alloc, rate: sum > 0 ? accBudget / sum : 0 }; +} + +function maxSwitchGain( + agents: Agent[], + onOwn: boolean[], + ownBudget: number, + accBudget: number, + eps: number, +): { + unhappy: number; + maxGain: number; + best: number; // agent index with largest myopic gain (-1 if none) +} { + const { alloc: oa, lam } = ownAlloc(agents, onOwn, ownBudget); + const { alloc: aa, rate } = accAlloc(agents, onOwn, accBudget); + let unhappy = 0; + let maxGain = 0; + let best = -1; + for (let i = 0; i < agents.length; i++) { + if (agents[i].score <= 0) continue; + const cur = onOwn[i] ? oa[i] : aa[i]; + const alt = onOwn[i] + ? Math.min(agents[i].committed, agents[i].weight * rate) + : Math.min(agents[i].committed, agents[i].score * lam); + const gain = alt - cur; + if (gain > eps) unhappy++; + if (gain > maxGain) { + maxGain = gain; + best = i; + } + } + return { unhappy, maxGain, best }; +} + +/** Pack road assignment for cycle detection during polish. */ +function roadKey(onOwn: boolean[]): string { + // Compact bitstring — 581 funders fits fine as a string of 0/1. + let s = ""; + for (let i = 0; i < onOwn.length; i++) s += onOwn[i] ? "1" : "0"; + return s; +} + +/** + * Simultaneous stochastic myopic best-response — exact port of + * nash_equilibrium_sim.ts `step` + `solve`, then a greedy single-flip polish + * so we actually reach ε-Nash (stochastic alone can chatter near equilibrium). + */ +function solveNash( + agents: Agent[], + pool: number, + ownFrac: number, + eps: number, + reactivity: number, + startMode: NashStartMode, + seed: number, + maxRounds = 2000, +): { + onOwn: boolean[]; + payouts: number[]; + rounds: number; + atNash: boolean; + maxGain: number; +} { + const ownBudget = ownFrac * pool; + const accBudget = pool - ownBudget; + const rnd = makeRng(seed); + const onOwn = agents.map((a) => { + if (a.score <= 0) return false; + if (startMode === "own") return true; + if (startMode === "acc") return false; + return rnd() < 0.5; + }); + + for (let i = 0; i < agents.length; i++) + if (agents[i].score <= 0) onOwn[i] = false; + + // Phase 1: HTML-identical simultaneous stochastic BR. + let rounds = 0; + for (; rounds < maxRounds; rounds++) { + const { alloc: oa, lam } = ownAlloc(agents, onOwn, ownBudget); + const { alloc: aa, rate } = accAlloc(agents, onOwn, accBudget); + let unhappy = 0; + const flips: number[] = []; + for (let i = 0; i < agents.length; i++) { + if (agents[i].score <= 0) { + onOwn[i] = false; + continue; + } + const cur = onOwn[i] ? oa[i] : aa[i]; + const altOwn = Math.min(agents[i].committed, agents[i].score * lam); + const altAcc = Math.min(agents[i].committed, agents[i].weight * rate); + const alt = onOwn[i] ? altAcc : altOwn; + if (alt > cur + eps) { + unhappy++; + if (rnd() < reactivity) flips.push(i); + } + } + if (unhappy === 0) break; + for (const i of flips) onOwn[i] = !onOwn[i]; + } + + // Phase 2: greedy myopic polish — flip only the single most-unhappy agent + // each round. Breaks the chatter the HTML's stochastic Solve sometimes + // escapes by luck, and finishes when max gain is still tens of dollars. + const seen = new Set(); + const polishCap = agents.length * 4; + let cycled = false; + for (let p = 0; p < polishCap; p++) { + const { unhappy, maxGain, best } = maxSwitchGain( + agents, + onOwn, + ownBudget, + accBudget, + eps, + ); + if (unhappy === 0 || best < 0 || maxGain <= eps) break; + const key = roadKey(onOwn); + if (seen.has(key)) { + cycled = true; + break; + } + seen.add(key); + onOwn[best] = !onOwn[best]; + rounds++; + } + + // Phase 3: if greedy cycled, only keep flips that strictly reduce the + // unhappy count (hill-climb on the HTML's own settlement metric). + if ( + cycled || + maxSwitchGain(agents, onOwn, ownBudget, accBudget, eps).unhappy > 0 + ) { + const taboo = new Set(); + for (let p = 0; p < polishCap; p++) { + const before = maxSwitchGain(agents, onOwn, ownBudget, accBudget, eps); + if (before.unhappy === 0) break; + // Prefer the best non-taboo flip. + const { alloc: oa, lam } = ownAlloc(agents, onOwn, ownBudget); + const { alloc: aa, rate } = accAlloc(agents, onOwn, accBudget); + let pick = -1; + let pickGain = 0; + for (let i = 0; i < agents.length; i++) { + if (agents[i].score <= 0 || taboo.has(i)) continue; + const cur = onOwn[i] ? oa[i] : aa[i]; + const alt = onOwn[i] + ? Math.min(agents[i].committed, agents[i].weight * rate) + : Math.min(agents[i].committed, agents[i].score * lam); + const gain = alt - cur; + if (gain > eps && gain > pickGain) { + pickGain = gain; + pick = i; + } + } + if (pick < 0) break; + onOwn[pick] = !onOwn[pick]; + const after = maxSwitchGain(agents, onOwn, ownBudget, accBudget, eps); + if (after.unhappy >= before.unhappy) { + onOwn[pick] = !onOwn[pick]; // revert + taboo.add(pick); + } else { + taboo.clear(); + rounds++; + } + } + } + + // Final payouts include per-road dust so each road spends its full budget + // (commit caps permitting) — HTML sim leaves leftover on the table. + const payouts = allocateWithRoadDust(agents, onOwn, ownBudget, accBudget); + + const { unhappy, maxGain } = maxSwitchGain( + agents, + onOwn, + ownBudget, + accBudget, + eps, + ); + return { onOwn, payouts, rounds, atNash: unhappy === 0, maxGain }; +} + +/** + * Allocate each road's budget, then dust unused remainder onto riders on that + * same road who still have commit headroom (pro-rata). Mirrors the cranker's + * surplus + dust passes, scoped per road so ownership/accumulator budgets + * stay separate. + */ +function allocateWithRoadDust( + agents: Agent[], + onOwn: boolean[], + ownBudget: number, + accBudget: number, +): number[] { + const { alloc: oa } = ownAlloc(agents, onOwn, ownBudget); + const { alloc: aa } = accAlloc(agents, onOwn, accBudget); + const out = new Array(agents.length); + for (let i = 0; i < agents.length; i++) out[i] = onOwn[i] ? oa[i] : aa[i]; + + dustRoad(out, agents, onOwn, true, ownBudget); + dustRoad(out, agents, onOwn, false, accBudget); + return out; +} + +/** Redistribute a road's unspent budget among its riders by commit headroom. */ +function dustRoad( + alloc: number[], + agents: Agent[], + onOwn: boolean[], + ownershipRoad: boolean, + budget: number, +): void { + const riders: number[] = []; + for (let i = 0; i < agents.length; i++) { + if (onOwn[i] === ownershipRoad) riders.push(i); + } + if (riders.length === 0) return; + + for (let iter = 0; iter < 50; iter++) { + for (const i of riders) + alloc[i] = Math.min(agents[i].committed, Math.max(0, alloc[i])); + const spent = riders.reduce((s, i) => s + alloc[i], 0); + const diff = budget - spent; + if (Math.abs(diff) < 1e-9) break; + if (diff > 0) { + const head = riders.map((i) => + Math.max(0, agents[i].committed - alloc[i]), + ); + const ht = head.reduce((s, v) => s + v, 0); + if (ht <= 1e-9) break; // all capped — leftover handled cross-road below + for (let k = 0; k < riders.length; k++) + alloc[riders[k]] += (diff * head[k]) / ht; + } else { + const slack = riders.map((i) => Math.max(0, alloc[i])); + const st = slack.reduce((s, v) => s + v, 0); + if (st <= 1e-9) break; + for (let k = 0; k < riders.length; k++) + alloc[riders[k]] -= (-diff * slack[k]) / st; + } + } +} + +/** Round dollar amounts to atoms and dust-fix so Σ == target and approved <= committed. */ +function dollarsToAtomsExact( + dollars: Map, + committed: Map, + addrs: string[], + target: bigint, +): Map { + const atoms = new Map(); + for (const a of addrs) { + const c = committed.get(a) ?? 0n; + const raw = BigInt(Math.round((dollars.get(a) ?? 0) * Number(USDC_SCALAR))); + atoms.set(a, bmin(c, bmax(0n, raw))); + } + let sum = addrs.reduce((s, a) => s + atoms.get(a)!, 0n); + let diff = target - sum; + if (diff > 0n) { + let pass = 0; + while (diff > 0n && pass < 5) { + pass++; + let did = false; + for (const a of addrs) { + if (diff <= 0n) break; + const room = (committed.get(a) ?? 0n) - atoms.get(a)!; + if (room > 0n) { + atoms.set(a, atoms.get(a)! + 1n); + diff -= 1n; + did = true; + } + } + if (!did) + throw new Error(`atom dust under-fill: ${diff} atoms with no room`); + } + } else if (diff < 0n) { + let pass = 0; + while (diff < 0n && pass < 5) { + pass++; + let did = false; + for (const a of addrs) { + if (diff >= 0n) break; + if (atoms.get(a)! > 0n) { + atoms.set(a, atoms.get(a)! - 1n); + diff += 1n; + did = true; + } + } + if (!did) + throw new Error( + `atom dust over-fill: ${-diff} atoms with nothing to reclaim`, + ); + } + } + sum = addrs.reduce((s, a) => s + atoms.get(a)!, 0n); + if (sum !== target) + throw new Error(`INVARIANT: atom sum ${sum} != target ${target}`); + for (const a of addrs) { + if (atoms.get(a)! > (committed.get(a) ?? 0n)) + throw new Error(`INVARIANT: ${a} approved > committed`); + } + return atoms; +} + +/** + * Cross-road fill: if a road couldn't absorb its budget (everyone capped), + * move the leftover onto the other road's riders by headroom so Σ == pool + * before atom conversion. + */ +function fillToPool( + payouts: number[], + agents: Agent[], + pool: number, +): number[] { + const out = payouts.slice(); + for (let iter = 0; iter < 50; iter++) { + for (let i = 0; i < agents.length; i++) + out[i] = Math.min(agents[i].committed, Math.max(0, out[i])); + const total = out.reduce((s, v) => s + v, 0); + const diff = pool - total; + if (Math.abs(diff) < 1e-6) break; + if (diff > 0) { + const head = agents.map((a, i) => Math.max(0, a.committed - out[i])); + const ht = head.reduce((s, v) => s + v, 0); + if (ht <= 1e-9) break; + for (let i = 0; i < agents.length; i++) out[i] += (diff * head[i]) / ht; + } else { + const slack = out.map((v) => Math.max(0, v)); + const st = slack.reduce((s, v) => s + v, 0); + if (st <= 1e-9) break; + for (let i = 0; i < agents.length; i++) out[i] -= (-diff * slack[i]) / st; + } + } + return out; +} + +// ── public API ────────────────────────────────────────────────────────────── + +/** + * Compute Rip Cars approvals via congestion-game Nash equilibrium. + * + * @returns per-funder lines + approvals with Σ approved === totalAllocation + */ +export function computeAllocation( + records: RipCarsFundingRecord[], + fundEvents: FundEvent[], + config: AllocationConfig, +): AllocationResult { + if (records.length === 0) throw new Error("No funding records"); + if (config.ownershipSplit < 0 || config.ownershipSplit > 1) { + throw new Error( + `ownershipSplit must be in [0,1] (got ${config.ownershipSplit})`, + ); + } + if (config.epsilon < 0) + throw new Error(`epsilon must be >= 0 (got ${config.epsilon})`); + + const internal: InternalRecord[] = records.map((r) => ({ + address: r.funder.toBase58(), + funder: r.funder, + committedAtoms: bnToBig(r.committedAmount), + accumulator: bnToBig(r.committedAmountAccumulator), + lastUpdate: bnToBig(r.lastAccumulatorUpdate), + score: r.score, + })); + const events: InternalFundEvent[] = fundEvents.map((e) => ({ + funderAddr: e.funderAddr, + amount: bnToBig(e.amount), + timestamp: bnToBig(e.timestamp), + })); + + const target = bnToBig(config.totalAllocation); + const start = bnToBig(config.launchStartTime); + const duration = bnToBig(config.secondsForLaunch); + const activationDelay = bnToBig(config.accumulatorActivationDelaySeconds); + const poolUsdc = Number(target) / Number(USDC_SCALAR); + + // Accumulator weights = full-pool cranker approvals (ratio used on the acc road). + const { approved: weightAtoms, cappedCount } = accumulatorApproved( + internal, + target, + start, + duration, + activationDelay, + events, + config.boost, + ); + + // Keep full USDC precision (atoms / 1e6). Rounding to cents can leave up to + // ~5000 atoms of error per funder, which blows past the atom dust pass limit. + const agents: Agent[] = internal.map((r) => ({ + address: r.address, + score: r.score, + committed: Number(r.committedAtoms) / Number(USDC_SCALAR), + weight: Number(weightAtoms.get(r.address) ?? 0n) / Number(USDC_SCALAR), + })); + + if (config.reactivity <= 0 || config.reactivity > 1) { + throw new Error(`reactivity must be in (0,1] (got ${config.reactivity})`); + } + + const { onOwn, payouts, rounds, atNash, maxGain } = solveNash( + agents, + poolUsdc, + config.ownershipSplit, + config.epsilon, + config.reactivity, + config.startMode, + config.seed, + ); + const filled = fillToPool(payouts, agents, poolUsdc); + + const addrs = internal.map((r) => r.address); + const committedAtoms = new Map( + internal.map((r) => [r.address, r.committedAtoms]), + ); + const finalD = new Map(addrs.map((a, i) => [a, filled[i]])); + const finalAtoms = dollarsToAtomsExact(finalD, committedAtoms, addrs, target); + + const eventCountByFunder = new Map(); + const firstFundByFunder = new Map(); + for (const e of fundEvents) { + eventCountByFunder.set( + e.funderAddr, + (eventCountByFunder.get(e.funderAddr) ?? 0) + 1, + ); + const cur = firstFundByFunder.get(e.funderAddr); + if (!cur || e.timestamp.lt(cur)) + firstFundByFunder.set(e.funderAddr, e.timestamp); + } + + const lines: AllocationLine[] = internal.map((r, i) => { + const last = bigToBn(r.lastUpdate); + const first = + (eventCountByFunder.get(r.address) ?? 0) > 1 + ? (firstFundByFunder.get(r.address) ?? last) + : last; + return { + funder: r.funder, + committedAmount: bigToBn(r.committedAtoms), + approvedAmount: bigToBn(finalAtoms.get(r.address)!), + kind: onOwn[i] ? "ownership" : "accumulator", + score: r.score, + accumulatorWeight: bigToBn(weightAtoms.get(r.address) ?? 0n), + lastAccumulatorUpdate: last, + firstFundTime: first, + }; + }); + + const approvals: FundingApproval[] = lines.map((l) => ({ + funder: l.funder, + approvedAmount: l.approvedAmount, + })); + + return { + lines, + approvals, + cappedCount, + ownershipCount: lines.filter((l) => l.kind === "ownership").length, + rounds, + atNash, + maxGain, + }; +} diff --git a/scripts/v0.7/rip-cars/allocation/bun.lock b/scripts/v0.7/rip-cars/allocation/bun.lock new file mode 100644 index 00000000..242052e8 --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/bun.lock @@ -0,0 +1,373 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "ripcars-allocation", + "dependencies": { + "@coral-xyz/anchor": "0.29.0", + "@metadaoproject/programs": "0.1.0-alpha.7", + "@solana/web3.js": "1.98.4", + "bn.js": "5.2.3", + "bs58": "6.0.0", + "dotenv": "16.6.1", + "pg": "8.18.0", + }, + "devDependencies": { + "@types/bn.js": "5.2.0", + "@types/bun": "1.3.14", + "@types/pg": "8.20.0", + "typescript": "5.9.3", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@coral-xyz/anchor": ["@coral-xyz/anchor@0.29.0", "", { "dependencies": { "@coral-xyz/borsh": "^0.29.0", "@noble/hashes": "^1.3.1", "@solana/web3.js": "^1.68.0", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.2", "camelcase": "^6.3.0", "cross-fetch": "^3.1.5", "crypto-hash": "^1.3.0", "eventemitter3": "^4.0.7", "pako": "^2.0.3", "snake-case": "^3.0.4", "superstruct": "^0.15.4", "toml": "^3.0.0" } }, "sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA=="], + + "@coral-xyz/borsh": ["@coral-xyz/borsh@0.29.0", "", { "dependencies": { "bn.js": "^5.1.2", "buffer-layout": "^1.2.0" }, "peerDependencies": { "@solana/web3.js": "^1.68.0" } }, "sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ=="], + + "@metadaoproject/programs": ["@metadaoproject/programs@0.1.0-alpha.7", "", { "dependencies": { "@coral-xyz/anchor": "0.29.0", "@noble/hashes": "1.8.0", "@solana/spl-token": "0.3.11", "@solana/web3.js": "1.98.4", "@sqds/multisig": "2.1.4", "bn.js": "5.2.2" } }, "sha512-yaWRuOfdWiwVFmO0l6fSPJIP5K7KD6frVk0WIFz6H7CIu1hnZQcmM634R0LxW+PAlD7cHu9dliz1XKnOSUCFIg=="], + + "@metaplex-foundation/beet": ["@metaplex-foundation/beet@0.7.1", "", { "dependencies": { "ansicolors": "^0.3.2", "bn.js": "^5.2.0", "debug": "^4.3.3" } }, "sha512-hNCEnS2WyCiYyko82rwuISsBY3KYpe828ubsd2ckeqZr7tl0WVLivGkoyA/qdiaaHEBGdGl71OpfWa2rqL3DiA=="], + + "@metaplex-foundation/beet-solana": ["@metaplex-foundation/beet-solana@0.4.0", "", { "dependencies": { "@metaplex-foundation/beet": ">=0.1.0", "@solana/web3.js": "^1.56.2", "bs58": "^5.0.0", "debug": "^4.3.4" } }, "sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ=="], + + "@metaplex-foundation/cusper": ["@metaplex-foundation/cusper@0.0.2", "", {}, "sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA=="], + + "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], + + "@solana/buffer-layout-utils": ["@solana/buffer-layout-utils@0.2.0", "", { "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/web3.js": "^1.32.0", "bigint-buffer": "^1.1.5", "bignumber.js": "^9.0.1" } }, "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g=="], + + "@solana/codecs": ["@solana/codecs@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-data-structures": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/codecs-strings": "2.0.0-rc.1", "@solana/options": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ=="], + + "@solana/codecs-core": ["@solana/codecs-core@2.3.0", "", { "dependencies": { "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw=="], + + "@solana/codecs-data-structures": ["@solana/codecs-data-structures@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog=="], + + "@solana/codecs-numbers": ["@solana/codecs-numbers@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg=="], + + "@solana/codecs-strings": ["@solana/codecs-strings@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": ">=5" } }, "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g=="], + + "@solana/errors": ["@solana/errors@2.3.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ=="], + + "@solana/options": ["@solana/options@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-data-structures": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/codecs-strings": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA=="], + + "@solana/spl-token": ["@solana/spl-token@0.3.11", "", { "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", "@solana/spl-token-metadata": "^0.1.2", "buffer": "^6.0.3" }, "peerDependencies": { "@solana/web3.js": "^1.88.0" } }, "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ=="], + + "@solana/spl-token-metadata": ["@solana/spl-token-metadata@0.1.6", "", { "dependencies": { "@solana/codecs": "2.0.0-rc.1" }, "peerDependencies": { "@solana/web3.js": "^1.95.3" } }, "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA=="], + + "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], + + "@sqds/multisig": ["@sqds/multisig@2.1.4", "", { "dependencies": { "@metaplex-foundation/beet": "0.7.1", "@metaplex-foundation/beet-solana": "0.4.0", "@metaplex-foundation/cusper": "^0.0.2", "@solana/spl-token": "^0.3.6", "@solana/web3.js": "^1.70.3", "@types/bn.js": "^5.1.1", "assert": "^2.0.0", "bn.js": "^5.2.1", "buffer": "6.0.3", "invariant": "2.2.4" } }, "sha512-5w+NmwHOzl96nI50R/fjSD6uFydRLNUquhoEmmWbGepS4D9DnQyF2TKcUBfTyxV3sgJt00ypBt7SXB3y8WOzUQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + + "@types/bn.js": ["@types/bn.js@5.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + + "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "ansicolors": ["ansicolors@0.3.2", "", {}, "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="], + + "assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bigint-buffer": ["bigint-buffer@1.1.5", "", { "dependencies": { "bindings": "^1.3.0" } }, "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="], + + "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + + "bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "buffer-layout": ["buffer-layout@1.2.2", "", {}, "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA=="], + + "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], + + "crypto-hash": ["crypto-hash@1.3.0", "", {}, "sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], + + "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], + + "fastestsmallesttextencoderdecoder": ["fastestsmallesttextencoderdecoder@1.0.22", "", {}, "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + + "jayson": ["jayson@4.3.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "pako": ["pako@2.2.0", "", {}, "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w=="], + + "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="], + + "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], + + "pg-connection-string": ["pg-connection-string@2.14.0", "", {}, "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], + + "pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "rpc-websockets": ["rpc-websockets@9.3.10", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^6.0.0" } }, "sha512-QT5PQ6LiWhA5RCS93oWwgxU4XzQltkYm8C3aTmmKEgj0HolGRo3VbdzELw7CEV35l9T7Amha8Vnr4rCfSjVP+w=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], + + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + + "superstruct": ["superstruct@0.15.5", "", {}, "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ=="], + + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], + + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "utf-8-validate": ["utf-8-validate@6.0.6", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA=="], + + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], + + "ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "@coral-xyz/anchor/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "@metadaoproject/programs/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], + + "@metaplex-foundation/beet-solana/bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="], + + "@solana/codecs/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], + + "@solana/codecs/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], + + "@solana/codecs-data-structures/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], + + "@solana/codecs-data-structures/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], + + "@solana/codecs-data-structures/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], + + "@solana/codecs-strings/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], + + "@solana/codecs-strings/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], + + "@solana/codecs-strings/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], + + "@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "@solana/options/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], + + "@solana/options/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], + + "@solana/options/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], + + "@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "@solana/web3.js/superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], + + "borsh/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "rpc-websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "rpc-websockets/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "rpc-websockets/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + + "@coral-xyz/anchor/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "@metaplex-foundation/beet-solana/bs58/base-x": ["base-x@4.0.1", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="], + + "@solana/codecs-data-structures/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "@solana/codecs-strings/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "@solana/codecs/@solana/codecs-core/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], + + "@solana/codecs/@solana/codecs-numbers/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], + + "@solana/options/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "@solana/web3.js/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "borsh/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "@solana/codecs/@solana/codecs-core/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "@solana/codecs/@solana/codecs-numbers/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + } +} diff --git a/scripts/v0.7/rip-cars/allocation/db.ts b/scripts/v0.7/rip-cars/allocation/db.ts new file mode 100644 index 00000000..eee2e44f --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/db.ts @@ -0,0 +1,83 @@ +/** + * Indexer Postgres reads for Rip Cars allocation: + * - fund events (boost timeline) + * - ownership scores (floor track) + */ +import { Client } from "pg"; +import BN from "bn.js"; + +import type { FundEvent } from "./allocation"; +import { log } from "./logger"; + +const logger = log.child({ module: "db" }); + +interface FundRow { + funderAddr: string; + quoteAmount: string; + timestamp: Date; +} + +/** + * Fetch all fund events for a v0.7 launch, oldest first. + */ +export async function fetchFundEvents( + pgUrl: string, + launchAddr: string, +): Promise { + const client = new Client({ connectionString: pgUrl }); + await client.connect(); + try { + const result = await client.query( + `SELECT funder_addr AS "funderAddr", + quote_amount::text AS "quoteAmount", + timestamp + FROM v0_7_funds + WHERE launch_addr = $1 + ORDER BY timestamp ASC`, + [launchAddr], + ); + logger.info( + { launchAddr, rows: result.rows.length }, + "Fetched fund events", + ); + return result.rows.map((r) => ({ + funderAddr: r.funderAddr, + amount: new BN(r.quoteAmount), + timestamp: new BN(Math.floor(r.timestamp.getTime() / 1000)), + })); + } finally { + await client.end(); + } +} + +/** + * Fetch ownership scores keyed by wallet address. + * @param scoreColumn — `ownership_points` or `total_usd_value_days` + */ +export async function fetchOwnershipScores( + pgUrl: string, + scoreColumn: "ownership_points" | "total_usd_value_days", +): Promise> { + if ( + scoreColumn !== "ownership_points" && + scoreColumn !== "total_usd_value_days" + ) { + throw new Error( + `SCORE_COLUMN must be ownership_points or total_usd_value_days (got '${scoreColumn}')`, + ); + } + const client = new Client({ connectionString: pgUrl }); + await client.connect(); + try { + const result = await client.query<{ owner: string; score: number }>( + `SELECT owner, COALESCE(${scoreColumn}, 0)::float8 AS score + FROM futarchy.ownership_scores`, + ); + const out = new Map(); + for (const row of result.rows) out.set(row.owner, row.score); + logger.info({ scores: out.size, scoreColumn }, "Fetched ownership scores"); + return out; + } finally { + await client.end(); + } +} diff --git a/scripts/v0.7/rip-cars/allocation/logger.ts b/scripts/v0.7/rip-cars/allocation/logger.ts new file mode 100644 index 00000000..2b91dd3e --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/logger.ts @@ -0,0 +1,39 @@ +/** + * Minimal console logger with a pino-compatible surface. + */ +type Fields = Record; + +interface Logger { + info(objOrMsg: Fields | string, msg?: string): void; + warn(objOrMsg: Fields | string, msg?: string): void; + error(objOrMsg: Fields | string, msg?: string): void; + child(bindings: Fields): Logger; +} + +function emit( + level: "info" | "warn" | "error", + bindings: Fields, + objOrMsg: Fields | string, + msg?: string, +): void { + const message = typeof objOrMsg === "string" ? objOrMsg : (msg ?? ""); + const fields = + typeof objOrMsg === "string" ? bindings : { ...bindings, ...objOrMsg }; + const suffix = + Object.keys(fields).length > 0 ? ` ${JSON.stringify(fields)}` : ""; + const line = `[${level}] ${message}${suffix}`; + if (level === "error") console.error(line); + else if (level === "warn") console.warn(line); + else console.log(line); +} + +function make(bindings: Fields): Logger { + return { + info: (objOrMsg, msg) => emit("info", bindings, objOrMsg, msg), + warn: (objOrMsg, msg) => emit("warn", bindings, objOrMsg, msg), + error: (objOrMsg, msg) => emit("error", bindings, objOrMsg, msg), + child: (childBindings) => make({ ...bindings, ...childBindings }), + }; +} + +export const log: Logger = make({}); diff --git a/scripts/v0.7/rip-cars/allocation/package.json b/scripts/v0.7/rip-cars/allocation/package.json new file mode 100644 index 00000000..525f3a9d --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/package.json @@ -0,0 +1,31 @@ +{ + "name": "ripcars-allocation", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Rip Cars allocation CLI: accumulator + ownership-floor, then setFundingRecordApproval batches.", + "module": "ripcars.ts", + "scripts": { + "start": "bun ripcars.ts", + "execute": "bun ripcars.ts --execute", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@coral-xyz/anchor": "0.29.0", + "@metadaoproject/programs": "0.1.0-alpha.7", + "@solana/web3.js": "1.98.4", + "bn.js": "5.2.3", + "bs58": "6.0.0", + "dotenv": "16.6.1", + "pg": "8.18.0" + }, + "devDependencies": { + "@types/bn.js": "5.2.0", + "@types/bun": "1.3.14", + "@types/pg": "8.20.0", + "typescript": "5.9.3" + }, + "engines": { + "bun": ">=1.0.0" + } +} diff --git a/scripts/v0.7/rip-cars/allocation/ripcars.ts b/scripts/v0.7/rip-cars/allocation/ripcars.ts new file mode 100644 index 00000000..ffeb767a --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/ripcars.ts @@ -0,0 +1,346 @@ +/** + * ripcars — allocate the Rip Cars launch via congestion-game Nash equilibrium, + * then optionally approve funding records on-chain. + * + * Flow (mirrors laso / credible allocation CLI): + * 1. Fetch the launch. + * 2. --execute: if the launch is still live + expired, confirm and closeLaunch + * FIRST — freezes the funder set so a late fund can't invalidate allocation. + * 3. Fetch funding records (chain) + fund events + ownership scores (DB). + * 4. Allocate via Nash congestion game (ownership road vs accumulator road). + * 5. Dry-run stops here. --execute confirms + sends setFundingRecordApproval + * batches, then verifies total_approved == TOTAL_ALLOCATION on-chain. + * + * Stops after setting the allocation — does NOT completeLaunch. Launch is left + * Closed; completeLaunch + performance package are done manually afterward. + * + * Run: + * bun install + * bun ripcars.ts # dry run (default) + * bun ripcars.ts --execute # set allocation (requires RIPCARS_AUTHORITY_KEY) + */ +import * as anchor from "@coral-xyz/anchor"; +import { LaunchpadClient } from "@metadaoproject/programs/launchpad/v0.7"; +import { + ComputeBudgetProgram, + Connection, + Keypair, + PublicKey, +} from "@solana/web3.js"; +import BN from "bn.js"; +import { config as loadDotenv } from "dotenv"; + +import { + CLOCK_DRIFT_BUFFER_SECONDS, + PRIORITY_FEE_MICRO_LAMPORTS, +} from "./ac/constants"; +import { approveFundingRecords, fetchLaunchAtSlot } from "./ac/fundingApproval"; +import { + computeAllocation, + type BoostConfig, + type NashStartMode, + type RipCarsFundingRecord, +} from "./allocation"; +import { fetchFundEvents, fetchOwnershipScores } from "./db"; +import { log } from "./logger"; +import { + confirmYes, + fmtUsdc, + getSysvarClockTime, + loadKeypair, + printAllocationTable, + usdc, + writeAllocationJson, +} from "./utils"; +import { + LAUNCH_ADDRESS as DEFAULT_LAUNCH, + TOTAL_ALLOCATION as DEFAULT_POOL, +} from "../constants"; + +/** Audit file: the exact per-funder allocation this run computed. */ +const ALLOCATION_OUT_FILE = `${import.meta.dir}/allocation.out.json`; + +loadDotenv({ path: `${import.meta.dir}/.env`, override: true }); + +/************************************************* + * *********************************************** + * If you see the stars, it means... + ***********************************************/ +/** The launch to allocate (base58). Live (expired), Closed, or Complete. */ +/****************************************************** */ +const LAUNCH_ADDRESS = DEFAULT_LAUNCH.toBase58(); // Rip Cars ***** TRIPLE CHECK ***** +const TOTAL_ALLOCATION = usdc(DEFAULT_POOL); //************** KOLLAN CHANGE ME ***********/ + +// Congestion game (same knobs as nash_equilibrium_sim.html defaults). +const OWNERSHIP_SPLIT = Number( + process.env.OWNERSHIP_SPLIT ?? process.env.OWNERSHIP_FLOOR_FRACTION ?? "0.5", +); +const NASH_EPSILON = Number(process.env.NASH_EPSILON ?? "1"); +const NASH_REACT = Number(process.env.NASH_REACT ?? "0.40"); // HTML "Reactivity" +const NASH_START = (process.env.NASH_START ?? "rand") as NashStartMode; // HTML default +const NASH_SEED = Number(process.env.NASH_SEED ?? "20260723"); +const SCORE_COLUMN = (process.env.SCORE_COLUMN ?? "ownership_points") as + | "ownership_points" + | "total_usd_value_days"; + +// ── Accumulator boost (weights for the accumulator road) ── +const BOOST_MULTIPLIER = Number(process.env.BOOST_MULTIPLIER ?? "10"); +const BOOST_FILL_CEILING = Number(process.env.BOOST_FILL_CEILING ?? "3"); +const BOOST_LOOK_AHEAD_HOURS = Number( + process.env.BOOST_LOOK_AHEAD_HOURS ?? "1", +); + +// ───────────────────────────────────────────────────────────────────────────── + +const logger = log.child({ module: "ripcars" }); + +const boostConfig: BoostConfig = { + multiplier: BOOST_MULTIPLIER, + fillCeiling: BOOST_FILL_CEILING, + lookAheadSeconds: BOOST_LOOK_AHEAD_HOURS * 3600, +}; + +type LaunchAccount = Awaited< + ReturnType +> & { + accumulatorActivationDelaySeconds: number; +}; + +type OnChainFundingRecord = Awaited< + ReturnType +>[number] & { + account: { + committedAmountAccumulator: BN; + lastAccumulatorUpdate: BN; + }; +}; + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`${name} is required (.env)`); + return value; +} + +function loadPayer(): anchor.Wallet { + if (!EXECUTE) return new anchor.Wallet(Keypair.generate()); + return new anchor.Wallet(loadKeypair(requireEnv("RIPCARS_AUTHORITY_KEY"))); +} + +function launchState(account: LaunchAccount): string { + return Object.keys(account.state as Record)[0] ?? "unknown"; +} + +async function fetchLaunch(): Promise { + return (await launchpad.launchpad.account.launch.fetch( + launchAddr, + )) as LaunchAccount; +} + +async function closeExpiredLaunch(account: LaunchAccount): Promise { + const now = await getSysvarClockTime(connection); + const closeTime = + account.unixTimestampStarted!.toNumber() + account.secondsForLaunch; + if (now < closeTime + CLOCK_DRIFT_BUFFER_SECONDS) { + throw new Error( + `Launch not yet expired (clock ${now} < close ${closeTime}) — timetravel surfpool past close first`, + ); + } + const tx = await launchpad + .closeLaunchIx({ launch: launchAddr }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: PRIORITY_FEE_MICRO_LAMPORTS, + }), + ]) + .rpc(); + logger.info({ tx }, "Closed launch"); +} + +function abort(): void { + logger.info({}, "Aborted — no further transactions sent."); +} + +const EXECUTE = process.argv.includes("--execute"); + +if (TOTAL_ALLOCATION.isZero()) + throw new Error("Set TOTAL_ALLOCATION at the top of this file"); +if ( + SCORE_COLUMN !== "ownership_points" && + SCORE_COLUMN !== "total_usd_value_days" +) { + throw new Error( + `SCORE_COLUMN must be ownership_points or total_usd_value_days (got '${SCORE_COLUMN}')`, + ); +} + +const RPC_URL = requireEnv("RPC_URL"); +const PG_URL = requireEnv("FUTARCHY_PG_URL"); +const launchAddr = new PublicKey(LAUNCH_ADDRESS); +const payer = loadPayer(); +const connection = new Connection(RPC_URL, "confirmed"); +const provider = new anchor.AnchorProvider(connection, payer, { + commitment: "confirmed", +}); +const launchpad = LaunchpadClient.createClient({ provider }); + +async function main(): Promise { + if (NASH_START !== "acc" && NASH_START !== "own" && NASH_START !== "rand") { + throw new Error(`NASH_START must be acc|own|rand (got '${NASH_START}')`); + } + + logger.info( + { + mode: EXECUTE ? "EXECUTE" : "DRY-RUN", + wallet: payer.publicKey.toBase58(), + rpc: RPC_URL, + launch: LAUNCH_ADDRESS, + ownershipSplit: OWNERSHIP_SPLIT, + epsilon: NASH_EPSILON, + reactivity: NASH_REACT, + startMode: NASH_START, + seed: NASH_SEED, + scoreColumn: SCORE_COLUMN, + boost: boostConfig, + }, + "ripcars starting", + ); + + let account = await fetchLaunch(); + if (!account.unixTimestampStarted) + throw new Error("Launch has no start timestamp"); + logger.info( + { + state: launchState(account), + minimumRaiseAmount: account.minimumRaiseAmount.toString(), + totalApprovedAmount: account.totalApprovedAmount.toString(), + totalAllocation: TOTAL_ALLOCATION.toString(), + }, + "Launch fetched", + ); + + // DB reads before any on-chain write so a DB failure aborts cleanly. + const [fundEvents, scores] = await Promise.all([ + fetchFundEvents(PG_URL, LAUNCH_ADDRESS), + fetchOwnershipScores(PG_URL, SCORE_COLUMN), + ]); + + if (EXECUTE && launchState(account) === "live") { + if ( + !confirmYes( + "Launch is not closed. Close it now? (freezes the funder set)", + ) + ) + return abort(); + await closeExpiredLaunch(account); + account = await fetchLaunch(); + logger.info({ state: launchState(account) }, "State after close"); + } + + const allRecords = + (await launchpad.launchpad.account.fundingRecord.all()) as OnChainFundingRecord[]; + const records = allRecords.filter((r) => r.account.launch.equals(launchAddr)); + if (records.length === 0) + throw new Error("No funding records found for this launch"); + logger.info({ records: records.length }, "Funding records fetched"); + + const funderRecords: RipCarsFundingRecord[] = records.map((r) => ({ + funder: r.account.funder, + committedAmount: r.account.committedAmount, + committedAmountAccumulator: r.account.committedAmountAccumulator, + lastAccumulatorUpdate: r.account.lastAccumulatorUpdate, + score: scores.get(r.account.funder.toBase58()) ?? 0, + })); + const scored = funderRecords.filter((r) => r.score > 0).length; + logger.info( + { scored, unscored: funderRecords.length - scored }, + "Ownership scores joined", + ); + + const result = computeAllocation(funderRecords, fundEvents, { + totalAllocation: TOTAL_ALLOCATION, + ownershipSplit: OWNERSHIP_SPLIT, + epsilon: NASH_EPSILON, + reactivity: NASH_REACT, + startMode: NASH_START, + seed: NASH_SEED, + boost: boostConfig, + launchStartTime: account.unixTimestampStarted!, + secondsForLaunch: new BN(account.secondsForLaunch), + accumulatorActivationDelaySeconds: new BN( + account.accumulatorActivationDelaySeconds, + ), + }); + + printAllocationTable(result, TOTAL_ALLOCATION, account.unixTimestampStarted!); + writeAllocationJson(ALLOCATION_OUT_FILE, result); + logger.info( + { + file: "allocation.out.json", + funders: result.lines.length, + ownership: result.ownershipCount, + atNash: result.atNash, + rounds: result.rounds, + }, + "Wrote allocation audit file", + ); + + if (!EXECUTE) { + logger.info( + {}, + "Dry-run complete — no transactions sent. Re-run with --execute to approve on-chain.", + ); + return; + } + + const state = launchState(account); + if (state !== "closed") { + throw new Error( + `Launch is "${state}", not "closed" (min raise not met?) — cannot approve.`, + ); + } + + if ( + !confirmYes( + `Approve ${result.approvals.length} funders for ${fmtUsdc(TOTAL_ALLOCATION)}?`, + ) + ) + return abort(); + logger.info( + { approvals: result.approvals.length }, + "Sending approval batches", + ); + const { maxConfirmedSlot } = await approveFundingRecords( + launchpad, + connection, + payer, + launchAddr, + result.approvals, + ); + + let refreshed = await fetchLaunchAtSlot(connection, launchpad, launchAddr, 0); + if (refreshed.totalApprovedAmount.lt(TOTAL_ALLOCATION)) { + refreshed = await fetchLaunchAtSlot( + connection, + launchpad, + launchAddr, + maxConfirmedSlot, + ); + } + if (!refreshed.totalApprovedAmount.eq(TOTAL_ALLOCATION)) { + throw new Error( + `total_approved_amount (${refreshed.totalApprovedAmount.toString()}) != TOTAL_ALLOCATION (${TOTAL_ALLOCATION.toString()}) — allocation did not land exactly`, + ); + } + logger.info( + { totalApproved: refreshed.totalApprovedAmount.toString() }, + "Allocation set — launch left Closed for manual completeLaunch + performance package", + ); +} + +main().catch((err) => { + logger.error( + { err: err instanceof Error ? err.message : String(err) }, + "ripcars failed", + ); + process.exit(1); +}); diff --git a/scripts/v0.7/rip-cars/allocation/tsconfig.json b/scripts/v0.7/rip-cars/allocation/tsconfig.json new file mode 100644 index 00000000..40a86dfa --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["bun"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["*.ts", "ac/*.ts", "test/*.ts"] +} diff --git a/scripts/v0.7/rip-cars/allocation/utils.ts b/scripts/v0.7/rip-cars/allocation/utils.ts new file mode 100644 index 00000000..88088315 --- /dev/null +++ b/scripts/v0.7/rip-cars/allocation/utils.ts @@ -0,0 +1,151 @@ +/** + * Helpers for ripcars.ts — USDC formatting, key/clock loading, table printer, + * confirmation prompt, and allocation.out.json writer (same shape as credible/laso). + */ +import { writeFileSync } from "node:fs"; + +import { type Connection, Keypair, SYSVAR_CLOCK_PUBKEY } from "@solana/web3.js"; +import BN from "bn.js"; +import bs58 from "bs58"; + +import type { AllocationResult } from "./allocation"; + +export const USDC_DECIMALS = 6; + +/** Whole USDC → atoms. */ +export const usdc = (whole: number): BN => + new BN(whole).mul(new BN(10).pow(new BN(USDC_DECIMALS))); + +/** Format USDC atoms as a human dollar string. */ +export function fmtUsdc(atoms: BN): string { + const divisor = new BN(10).pow(new BN(USDC_DECIMALS)); + const whole = atoms.div(divisor).toString(); + const frac = atoms + .mod(divisor) + .toString() + .padStart(USDC_DECIMALS, "0") + .slice(0, 2); + return `$${Number(whole).toLocaleString("en-US")}.${frac}`; +} + +/** An allocation row in the audit JSON (compatible with credible/laso shape). */ +export interface AllocationRow { + funder: string; + kind: "ownership" | "accumulator"; + committed: string; + approved: string; + score?: number; + accumulatorWeight?: string; +} + +/** Write the allocation to a JSON audit file — exact per-funder amounts. */ +export function writeAllocationJson( + filePath: string, + result: AllocationResult, +): void { + const rows: AllocationRow[] = result.lines.map((l) => ({ + funder: l.funder.toBase58(), + kind: l.kind, + committed: l.committedAmount.toString(), + approved: l.approvedAmount.toString(), + score: l.score, + accumulatorWeight: l.accumulatorWeight.toString(), + })); + writeFileSync(filePath, JSON.stringify(rows, null, 2)); +} + +/** Blocking terminal confirmation. Returns true only on an explicit yes/y. */ +export function confirmYes(message: string): boolean { + const answer = prompt(`${message} [yes/no]`); + const normalized = answer?.trim().toLowerCase(); + return normalized === "yes" || normalized === "y"; +} + +/** + * Parse a private key. Supports a JSON byte array ([1,2,...], 64 bytes) or a + * base58 string. + */ +export function loadKeypair(raw: string): Keypair { + const trimmed = raw.trim(); + if (trimmed.startsWith("[")) { + const bytes = JSON.parse(trimmed) as number[]; + if (!Array.isArray(bytes) || bytes.length !== 64) { + throw new Error( + "RIPCARS_AUTHORITY_KEY JSON array must contain exactly 64 bytes", + ); + } + return Keypair.fromSecretKey(new Uint8Array(bytes)); + } + return Keypair.fromSecretKey(bs58.decode(trimmed)); +} + +/** Read on-chain unix time from the Clock sysvar. */ +export async function getSysvarClockTime( + connection: Connection, +): Promise { + const info = await connection.getAccountInfo(SYSVAR_CLOCK_PUBKEY); + if (!info || !info.data || info.data.length < 40) { + throw new Error("Failed to read Clock sysvar"); + } + return Number(info.data.readBigInt64LE(32)); +} + +/** + * Print the allocation as a readable CLI table, then assert Σ approved == target. + * Ownership-road funders first, then accumulator — each group time-sorted earliest fund first. + */ +export function printAllocationTable( + result: AllocationResult, + totalAllocation: BN, + launchStartTime: BN, +): void { + const sorted = [...result.lines].sort((a, b) => { + if (a.kind !== b.kind) return a.kind === "ownership" ? -1 : 1; + return a.lastAccumulatorUpdate.cmp(b.lastAccumulatorUpdate); + }); + + const hrsOf = (t: BN): string => + (t.sub(launchStartTime).toNumber() / 3600).toFixed(1); + + console.log( + "\n road funder score committed approved % commit hrs+start (first→last)", + ); + console.log(" ".padEnd(140, "─")); + let approvedTotal = new BN(0); + let committedTotal = new BN(0); + for (const l of sorted) { + approvedTotal = approvedTotal.add(l.approvedAmount); + committedTotal = committedTotal.add(l.committedAmount); + const pct = l.committedAmount.isZero() + ? "—" + : `${((l.approvedAmount.toNumber() / l.committedAmount.toNumber()) * 100).toFixed(1)}%`; + const hrs = l.lastAccumulatorUpdate.isZero() + ? "—" + : l.firstFundTime.eq(l.lastAccumulatorUpdate) + ? hrsOf(l.lastAccumulatorUpdate) + : `${hrsOf(l.firstFundTime)}→${hrsOf(l.lastAccumulatorUpdate)}`; + const scoreStr = + l.score > 0 ? Math.round(l.score).toLocaleString("en-US") : "—"; + console.log( + ` ${l.kind.padEnd(12)} ${l.funder.toBase58().padEnd(44)} ${scoreStr.padStart(10)} ${fmtUsdc(l.committedAmount).padStart(15)} ${fmtUsdc(l.approvedAmount).padStart(15)} ${pct.padStart(8)} ${hrs.padEnd(14)}`, + ); + } + console.log(" ".padEnd(140, "─")); + console.log( + ` Funders: ${result.lines.length} (${result.ownershipCount} ownership road, ${result.lines.length - result.ownershipCount} accumulator) | ` + + `Committed: ${fmtUsdc(committedTotal)} | Weight-capped: ${result.cappedCount}`, + ); + console.log( + ` Nash: ${result.atNash ? "✓ ε-equilibrium" : "✗ not settled"} after ${result.rounds} BR rounds` + + ` (max switch gain $${result.maxGain.toFixed(2)})`, + ); + console.log( + ` Σ approved: ${fmtUsdc(approvedTotal)} (target ${fmtUsdc(totalAllocation)}) ${approvedTotal.eq(totalAllocation) ? "✓ matches" : "✗ MISMATCH"}\n`, + ); + + if (!approvedTotal.eq(totalAllocation)) { + throw new Error( + `Allocation invariant failed: Σ approved (${approvedTotal.toString()}) !== TOTAL_ALLOCATION (${totalAllocation.toString()})`, + ); + } +} diff --git a/scripts/v0.7/rip-cars/claimAll.ts b/scripts/v0.7/rip-cars/claimAll.ts new file mode 100644 index 00000000..9b502a01 --- /dev/null +++ b/scripts/v0.7/rip-cars/claimAll.ts @@ -0,0 +1,270 @@ +/// Claims tokens for every funder of the completed credible launch, and +/// refunds any unapproved USDC (committed minus approved) along the way. +/// Claims and refunds are both permissionless; the local wallet just pays fees +/// and ATA rent. +/// +/// Transactions are fired without per-batch confirmation (a compute-unit price +/// helps them land under congestion). Correctness comes from verification +/// passes over the funding records themselves: each pass re-fetches the +/// records and re-sends only what hasn't landed, which is safe because claims +/// and refunds are idempotent via the records' flags. + +import * as anchor from "@coral-xyz/anchor"; +import { + LaunchpadClient, + getLaunchAddr, +} from "@metadaoproject/programs/launchpad/v0.7"; +import { + ComputeBudgetProgram, + PublicKey, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { + TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import BN from "bn.js"; +import { LAUNCH_AUTHORITY, TOKEN_SEED } from "./constants.js"; + +// Batches are homogeneous — claims-only or refunds-only — so 5 records fit the +// 1232-byte transaction limit (a mixed claim+refund batch only fits 3), and a +// failing refund can never revert co-batched claims. +const BATCH_SIZE = 5; + +// Matches the accelerated-cranker default; raise via env under congestion. +const PRIORITY_FEE_MICRO_LAMPORTS = parseInt( + process.env.PRIORITY_FEE_MICRO_LAMPORTS ?? "10000", + 10, +); + +// Fire passes until nothing is pending; each pass re-sends only what hasn't +// landed. Two passes suffice when everything lands first try. +const MAX_PASSES = 3; + +// Grace for fired transactions to land before the next verification pass. +const LAND_WAIT_MS = 30_000; + +// Static compute budgets per instruction, measured on a surfpool fork of the +// real launch (claim ~35k, refund ~31k, ATA create ~25k / ~5k when it already +// exists) and rounded up. Over-requesting only inflates the priority fee, +// which is priced per requested CU. +const CLAIM_CU = 45_000; +const REFUND_CU = 40_000; +const ATA_CREATE_CU = 30_000; + +const provider = anchor.AnchorProvider.env(); +const payer = ( + provider.wallet as anchor.Wallet & { payer: anchor.web3.Keypair } +).payer; + +const launchpad: LaunchpadClient = LaunchpadClient.createClient({ provider }); + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// Sign and fire one batch without waiting for confirmation — the next pass +// verifies against the records. Preflight stays on so a deterministic failure +// (e.g. a bad account) rejects at send time with logs instead of surfacing +// only as a still-pending record after the passes. +const sendBatch = async ( + batchIxs: TransactionInstruction[], + computeUnits: number, + label: string, +) => { + const { blockhash } = await provider.connection.getLatestBlockhash(); + + const tx = new Transaction(); + tx.add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: PRIORITY_FEE_MICRO_LAMPORTS, + }), + ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits }), + ...batchIxs, + ); + tx.recentBlockhash = blockhash; + tx.feePayer = payer.publicKey; + tx.partialSign(payer); + + const signature = await provider.connection.sendRawTransaction( + tx.serialize(), + ); + console.log(`${label}: ${signature}`); +}; + +/** + * One pass: fetch the records, work out what still needs a claim or refund, + * and fire the batches for it without waiting for confirmations. Returns the + * number of records that still needed work (0 means everything has landed). + * With countOnly, just reports that number without sending. + */ +const processPass = async ( + launch: PublicKey, + pass: number, + countOnly = false, +): Promise => { + const launchAccount = await launchpad.fetchLaunch(launch); + if (launchAccount === null) { + throw new Error("Launch account not found"); + } + + const state = Object.keys(launchAccount.state)[0]; + if (state !== "complete") { + throw new Error( + `Launch state is "${state}", claims require Complete (run complete.ts first)`, + ); + } + + const allFundingRecords = + await launchpad.launchpad.account.fundingRecord.all(); + const launchFundingRecords = allFundingRecords.filter((record) => + record.account.launch.equals(launch), + ); + console.log( + `Pass ${pass}: found ${launchFundingRecords.length} funding records`, + ); + + // On a fork, records cloned after the launch/vault froze can carry committed + // amounts the quote vault never received; skip refunds when the vault can't + // cover them (on a real cluster the vault always can). + const totalRefundable = launchFundingRecords.reduce( + (acc, record) => + record.account.isUsdcRefunded + ? acc + : acc.add( + record.account.committedAmount.sub(record.account.approvedAmount), + ), + new BN(0), + ); + const quoteVaultBalance = new BN( + ( + await provider.connection.getTokenAccountBalance( + launchAccount.launchQuoteVault, + ) + ).value.amount, + ); + const skipRefunds = totalRefundable.gt(quoteVaultBalance); + if (skipRefunds) { + console.log( + `Skipping refunds: refundable ${totalRefundable.toString()} exceeds quote vault balance ${quoteVaultBalance.toString()} (fork drift)`, + ); + } + + const needsRefund = (record: (typeof launchFundingRecords)[number]) => + !skipRefunds && + !record.account.isUsdcRefunded && + record.account.committedAmount.sub(record.account.approvedAmount).gtn(0); + + const claimRecords = launchFundingRecords.filter( + (record) => !record.account.isTokensClaimed, + ); + const refundRecords = launchFundingRecords.filter(needsRefund); + + const pendingRecords = launchFundingRecords.filter( + (record) => !record.account.isTokensClaimed || needsRefund(record), + ).length; + if (pendingRecords === 0 || countOnly) { + return pendingRecords; + } + + let batches = 0; + + // Phase 1: claims (claimIx prepends the funder's token ATA creation itself) + for (let i = 0; i < claimRecords.length; i += BATCH_SIZE) { + const batch = claimRecords.slice(i, i + BATCH_SIZE); + + const batchIxs: TransactionInstruction[] = []; + for (const record of batch) { + batchIxs.push( + ...( + await launchpad + .claimIx(launch, launchAccount.baseMint, record.account.funder) + .transaction() + ).instructions, + ); + } + + sendBatch( + batchIxs, + batch.length * (ATA_CREATE_CU + CLAIM_CU), + `Pass ${pass} claim batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(claimRecords.length / BATCH_SIZE)}`, + ); + batches++; + } + + // Phase 2: refunds. The refund requires the funder's USDC ATA to exist; + // unlike claimIx, the SDK's refundIx does not create it (funders may have + // funded from a non-ATA account or closed their ATA since) + for (let i = 0; i < refundRecords.length; i += BATCH_SIZE) { + const batch = refundRecords.slice(i, i + BATCH_SIZE); + + const batchIxs: TransactionInstruction[] = []; + for (const record of batch) { + batchIxs.push( + createAssociatedTokenAccountIdempotentInstruction( + payer.publicKey, + getAssociatedTokenAddressSync( + launchAccount.quoteMint, + record.account.funder, + true, + ), + record.account.funder, + launchAccount.quoteMint, + ), + ...( + await launchpad + .refundIx({ + launch, + funder: record.account.funder, + quoteMint: launchAccount.quoteMint, + }) + .transaction() + ).instructions, + ); + } + + sendBatch( + batchIxs, + batch.length * (ATA_CREATE_CU + REFUND_CU), + `Pass ${pass} refund batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(refundRecords.length / BATCH_SIZE)}`, + ); + batches++; + } + + console.log( + `Pass ${pass}: fired ${batches} transactions (${claimRecords.length} claims, ${refundRecords.length} refunds)`, + ); + return pendingRecords; +}; + +const main = async () => { + const tokenMint = await PublicKey.createWithSeed( + LAUNCH_AUTHORITY, + TOKEN_SEED, + TOKEN_PROGRAM_ID, + ); + const [launch] = getLaunchAddr(undefined, tokenMint); + console.log("Launch address:", launch.toBase58()); + + for (let pass = 1; pass <= MAX_PASSES; pass++) { + const pendingRecords = await processPass(launch, pass); + if (pendingRecords === 0) { + console.log("All records claimed and refunded!"); + return; + } + await sleep(LAND_WAIT_MS); + } + + const stillPending = await processPass(launch, MAX_PASSES + 1, true); + if (stillPending > 0) { + throw new Error( + `${stillPending} records still pending after ${MAX_PASSES} passes — rerun the script (safe: processed records are skipped)`, + ); + } + console.log("All records claimed and refunded!"); +}; + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/scripts/v0.7/rip-cars/complete.ts b/scripts/v0.7/rip-cars/complete.ts new file mode 100644 index 00000000..14a31994 --- /dev/null +++ b/scripts/v0.7/rip-cars/complete.ts @@ -0,0 +1,138 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + LaunchpadClient, + getLaunchAddr, +} from "@metadaoproject/programs/launchpad/v0.7"; +import { + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { ComputeBudgetProgram } from "@solana/web3.js"; +import { token } from "@coral-xyz/anchor/dist/cjs/utils/index.js"; +import { TOKEN_SEED, LUT_ADDRESS } from "./constants.js"; + +const provider = anchor.AnchorProvider.env(); +const payer = ( + provider.wallet as anchor.Wallet & { payer: anchor.web3.Keypair } +).payer; + +const launchpad: LaunchpadClient = LaunchpadClient.createClient({ provider }); + +export const completeLaunch = async () => { + // TODO: BEFORE RUNNING YOU NEED TO APPROVE THE FUNDING RECORDS + // SCRIPT MUST BE CREATED + const TOKEN = await PublicKey.createWithSeed( + payer.publicKey, + TOKEN_SEED, + token.TOKEN_PROGRAM_ID, + ); + console.log("Token address:", TOKEN.toBase58()); + + const [launch] = getLaunchAddr(undefined, TOKEN); + + if (launch === undefined) { + throw new Error( + "LAUNCH_TO_COMPLETE is not set. Please set it in the script.", + ); + } + + let launchAccount = await launchpad.fetchLaunch(launch); + + if (launchAccount === null) { + throw new Error("Launch account not found"); + } + + // const tx = await launchpad + // .completeLaunchIx({ + // launch: launch, + // baseMint: launchAccount.baseMint, + // launchAuthority: payer.publicKey, + // }) + // .preInstructions([ + // ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100000 }), + // ]) + // .transaction(); + + // const LUT = await provider.connection.getAddressLookupTable(LUT_ADDRESS); + // if (LUT === null || LUT.value === null) { + // throw new Error("LUT not found"); + // } + // const blockhash = (await provider.connection.getLatestBlockhash()).blockhash; + + // const message = new TransactionMessage({ + // payerKey: payer.publicKey, + // recentBlockhash: blockhash, + // instructions: tx.instructions, + // }).compileToV0Message([LUT.value]); + + // const vtx = new VersionedTransaction(message); + // vtx.sign([payer]); + + // // TODO: We want to run pre-checks before we fire this off, pre-checks should be done 5 minutes before expected + // // completion time. This should be one of the pre-checks... + // // TODO: Other pre-checks: + // // - SOL balance relative to the funding records + // // - Funding records approved + // // - Launch is not already complete + // // - Sum of funding records + // // - Launch account state (eg total approved) + // // const simulation = await provider.connection.simulateTransaction(vtx, { sigVerify: false }); + // // if (simulation.value.err) { + // // console.error("Transaction simulation failed:", simulation.value.err); + // // throw new Error( + // // `Simulation failed: ${JSON.stringify(simulation.value.err)}`, + // // ); + // // } + // // console.log("Simulation:", simulation); + // // console.log("Simulation:", simulation.value); + // // console.log("Simulation:", simulation.value.unitsConsumed); + // // return; + + // const completeTxHash = await provider.connection.sendTransaction(vtx); + + // console.log(`Complete launch transaction sent: ${completeTxHash}`); + + // const isDone = await provider.connection.confirmTransaction( + // completeTxHash, + // "confirmed", + // ); + + // if (isDone.value.err) { + // throw new Error( + // `Launch completion failed ${JSON.stringify(isDone.value.err)}`, + // ); + // } + + // console.log("Launch completion confirmed:", isDone); + + console.log("Launch completed successfully!"); + + console.log("Setting up performance package..."); + + // Refresh launch account to get the updated base mint + launchAccount = await launchpad.fetchLaunch(launch); + + if (launchAccount === null) { + throw new Error("Launch account not found"); + } + + // TODO: Ideally we don't wrap this into this call given I don't want ANYTHING about it + // to stall a complete launch call... + // TODO: Review this as we will want to do this manually.. + const initializePerformancePackageTxHash = await launchpad + .initializePerformancePackageIx({ + launch: launch, + baseMint: launchAccount.baseMint, + payer: payer.publicKey, + }) + .rpc(); + + console.log( + `Initialize performance package transaction sent: ${initializePerformancePackageTxHash}`, + ); + + console.log("Performance package set up successfully!"); +}; + +completeLaunch().catch(console.error); diff --git a/scripts/v0.7/rip-cars/constants.ts b/scripts/v0.7/rip-cars/constants.ts new file mode 100644 index 00000000..a678124e --- /dev/null +++ b/scripts/v0.7/rip-cars/constants.ts @@ -0,0 +1,48 @@ +import { PublicKey } from "@solana/web3.js"; + +// Token Details +export const TOKEN_SEED = "TVOzl2TKhXCRVn9U"; +export const TOKEN_ADDRESS = new PublicKey( + "CARSsxWPkpQWvfyRBwfGMGvysJBHdHGfE46X5MNgmeta", +); + +export const LAUNCH_AUTHORITY = new PublicKey( + "LncRyJVBbek7EFhnd6QNZRTLT9mReKWPGCpsfu5bqJS", +); + +// Team Config Details +export const TEAM_ADDRESS = new PublicKey( + "CnTDWPAEsN5RAgNapTJerDAc45TWavRQ1m3ACMpukYPd", +); // Rip Cars team squads address + +export const LAUNCH_ADDRESS = new PublicKey( + "8uMemVUT1ToSuda2jBVhcaPmJWq5C3bKe7DhrM5wkqZP", +); + +export const LUT_ADDRESS = new PublicKey( + "5MK8EEbyf8Fi3tgkX1dhAnE57swXw1b3dcBTg1RswSva", +); + +export const SPENDING_MEMBERS = [ + new PublicKey("CqK6aBSSycQU3igvptxvhf9YNC1v5EodCuCxuVLPPohh"), + new PublicKey("4Huto5Lv8z59tW4EezrYNSvBJhCc9U5bgmR5yH5csFDc"), +]; +// Even without a performance package, defaults need to be set +export const PERFORMANCE_PACKAGE_GRANTEE = new PublicKey( + "7iiE6ncVh5uJuBKw7JcwCjhUT8o2VT2JMeQZ8Tsi5Ckf", +); + +// Amount Details +export const MIN_GOAL = 250_000; // 250k USDC +export const SPENDING_LIMIT = 40_000; // 40k USDC +export const PERFORMANCE_PACKAGE_TOKEN_AMOUNT = 12_900_000; // 12.9M CARS +export const TOTAL_ALLOCATION = 250_000; // 250k USDC +export const PERFORMANCE_PACKAGE_UNLOCK_MONTHS = 18; // 18 months +export const ADDITIONAL_CARVEOUT = null; // 0 CARS +export const ADDITIONAL_CARVEOUT_RECIPIENT = undefined; +export const LAUNCH_DAYS = 4; + +export const TOKEN_NAME = "Rip Cars"; +export const TOKEN_SYMBOL = "CARS"; +export const TOKEN_URI = + "https://raw.githubusercontent.com/metaDAOproject/programs/refs/heads/develop/scripts/assets/CARS/CARS.json"; diff --git a/scripts/v0.7/rip-cars/end.ts b/scripts/v0.7/rip-cars/end.ts new file mode 100644 index 00000000..3f2b1b54 --- /dev/null +++ b/scripts/v0.7/rip-cars/end.ts @@ -0,0 +1,83 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + LaunchpadClient, + getLaunchAddr, +} from "@metadaoproject/programs/launchpad/v0.7"; +import { PublicKey, Transaction } from "@solana/web3.js"; +import * as token from "@solana/spl-token"; +import { TOKEN_SEED } from "./constants.js"; +import { createLookupTableForTransaction } from "../../utils/utils.js"; + +const provider = anchor.AnchorProvider.env(); +const payer = ( + provider.wallet as anchor.Wallet & { payer: anchor.web3.Keypair } +).payer; + +const launchpad: LaunchpadClient = LaunchpadClient.createClient({ provider }); + +export const end = async () => { + // Get token address + const TOKEN = await PublicKey.createWithSeed( + payer.publicKey, + TOKEN_SEED, + token.TOKEN_PROGRAM_ID, + ); + console.log("Token address:", TOKEN.toBase58()); + + const [launch] = getLaunchAddr(undefined, TOKEN); + + const closeLaunchIx = await launchpad + .closeLaunchIx({ + launch, + }) + .instruction(); + + // Build transaction without compute budget first + const tx = new Transaction().add(closeLaunchIx); + + const { blockhash } = await provider.connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + tx.feePayer = payer.publicKey; + + // Simulate transaction to get compute units used + tx.sign(payer); + + const txHash = await provider.connection.sendRawTransaction(tx.serialize()); + await provider.connection.confirmTransaction(txHash, "confirmed"); + + console.log("Launch closed", txHash); + + // TODO: If complete fails, we need to handle that no need to run ALT... + console.log("Creating ALT for complete"); + let launchAccount = await launchpad.fetchLaunch(launch); + if (!launchAccount) { + throw new Error("Launch account not found"); + } + const completeTx = await launchpad + .completeLaunchIx({ + launch: launch, + baseMint: launchAccount.baseMint, + launchAuthority: payer.publicKey, + }) + .transaction(); + + // TODO: consider this as potential for new script... or something that we run post close... + // NOTE This is ONLY the transaction to create the ALT, we don't complete it here... + // Create ALT for complete + const LUT = await createLookupTableForTransaction( + completeTx, + payer, + provider.connection, + ); + + console.log("LUT", LUT); + console.log("LUT", LUT.key.toBase58()); + const fetchedLUT = await provider.connection.getAddressLookupTable( + new PublicKey(LUT.key.toBase58()), + ); + console.log("fetchedLUT", fetchedLUT); + console.log("fetchedLUT", fetchedLUT.value?.key.toBase58()); + console.log("Add LUT to constants.ts"); +}; + +end().catch(console.error); diff --git a/scripts/v0.7/rip-cars/initialize.ts b/scripts/v0.7/rip-cars/initialize.ts new file mode 100644 index 00000000..a66ec9bc --- /dev/null +++ b/scripts/v0.7/rip-cars/initialize.ts @@ -0,0 +1,172 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + LaunchpadClient, + getLaunchAddr, + getLaunchSignerAddr, +} from "@metadaoproject/programs/launchpad/v0.7"; +import { + ComputeBudgetProgram, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import BN from "bn.js"; +import * as token from "@solana/spl-token"; +import { + TOKEN_SEED, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_URI, + MIN_GOAL, + SPENDING_LIMIT, + SPENDING_MEMBERS, + PERFORMANCE_PACKAGE_GRANTEE, + PERFORMANCE_PACKAGE_TOKEN_AMOUNT, + PERFORMANCE_PACKAGE_UNLOCK_MONTHS, + ADDITIONAL_CARVEOUT, + ADDITIONAL_CARVEOUT_RECIPIENT, + TEAM_ADDRESS, + TOKEN_ADDRESS, + LAUNCH_DAYS, +} from "./constants.js"; +import { secondsPerDay } from "../../utils/constants.js"; + +const provider = anchor.AnchorProvider.env(); +const payer = ( + provider.wallet as anchor.Wallet & { payer: anchor.web3.Keypair } +).payer; + +const LAUNCH_AUTHORITY = payer.publicKey; // NOTE: We're duplicating this.. but we should validate I'd assume + +const launchDurationSeconds = secondsPerDay * LAUNCH_DAYS; // 4 days + +const launchpad: LaunchpadClient = LaunchpadClient.createClient({ provider }); + +export const launch = async () => { + // Check balance of the payer + console.log("Checking balance of the payer"); + console.log("Payer address:", payer.publicKey.toBase58()); + const balance = await provider.connection.getBalance(payer.publicKey); + console.log("Balance:", balance / LAMPORTS_PER_SOL, "SOL"); + if (balance < LAMPORTS_PER_SOL * 0.001) { + throw new Error( + "Insufficient balance. Please fund the address with at least 0.001 SOL", + ); + } + + // Notify user of the balance for estimate of launch cost and with number of users with accounts + // TODO: Implement ^ + + // Get minimum balance for rent exemption + const lamports = await provider.connection.getMinimumBalanceForRentExemption( + token.MINT_SIZE, + ); + + const TOKEN = await PublicKey.createWithSeed( + payer.publicKey, + TOKEN_SEED, + token.TOKEN_PROGRAM_ID, + ); + console.log("Token address:", TOKEN.toBase58()); + if (TOKEN.toBase58() !== TOKEN_ADDRESS.toBase58()) { + throw new Error("Token address does not match the constants"); + } + // TODO: Confirm the token address from the constants + + const [launch] = getLaunchAddr(undefined, TOKEN); + const [launchSigner] = getLaunchSignerAddr(undefined, launch); + + console.log("Launch address:", launch.toBase58()); // Note: this is used in the constants for later + + const createTokenAccountIx = SystemProgram.createAccountWithSeed({ + fromPubkey: payer.publicKey, + newAccountPubkey: TOKEN, + basePubkey: payer.publicKey, + seed: TOKEN_SEED, + lamports: lamports, + space: token.MINT_SIZE, + programId: token.TOKEN_PROGRAM_ID, + }); + + const initializeMintIx = token.createInitializeMint2Instruction( + TOKEN, + 6, + launchSigner, + null, + ); + + // TODO: We should confirm the launch settings are expected results / build a test to make sure nothing is going to violate + // before we do all of this.. Now simulation will catch it. + const launchIx = await launchpad + .initializeLaunchIx({ + tokenName: TOKEN_NAME, + tokenSymbol: TOKEN_SYMBOL, + tokenUri: TOKEN_URI, + minimumRaiseAmount: new BN(MIN_GOAL * 10 ** 6), + baseMint: TOKEN, + monthlySpendingLimitAmount: new BN(SPENDING_LIMIT * 10 ** 6), + monthlySpendingLimitMembers: SPENDING_MEMBERS, + performancePackageGrantee: PERFORMANCE_PACKAGE_GRANTEE, + performancePackageTokenAmount: new BN( + PERFORMANCE_PACKAGE_TOKEN_AMOUNT * 10 ** 6, + ), + monthsUntilInsidersCanUnlock: PERFORMANCE_PACKAGE_UNLOCK_MONTHS, + secondsForLaunch: launchDurationSeconds, + teamAddress: TEAM_ADDRESS, + additionalTokensAmount: ADDITIONAL_CARVEOUT + ? new BN(ADDITIONAL_CARVEOUT * 10 ** 6) + : undefined, + additionalTokensRecipient: ADDITIONAL_CARVEOUT_RECIPIENT, + launchAuthority: LAUNCH_AUTHORITY, + hasBidWall: false, + }) + .instruction(); + + const ixs = [createTokenAccountIx, initializeMintIx, launchIx]; + + const { blockhash } = await provider.connection.getLatestBlockhash(); + + // TODO: Break this out into a helper as we use it a lot... + // Simulate without compute budget to get units consumed + const messageV0 = new TransactionMessage({ + instructions: ixs, + payerKey: payer.publicKey, + recentBlockhash: blockhash, + }).compileToV0Message(); + const simulationTx = new VersionedTransaction(messageV0); + const simulation = await provider.connection.simulateTransaction( + simulationTx, + { sigVerify: false }, + ); + + const computeUnitsUsed = simulation.value.unitsConsumed || 200_000; + // Add 20% buffer to the compute units + const computeUnitsWithBuffer = Math.floor(computeUnitsUsed * 1.2); + + console.log(`Simulated compute units: ${computeUnitsUsed}`); + console.log(`Setting compute unit limit: ${computeUnitsWithBuffer}`); + + const finalMessageV0 = new TransactionMessage({ + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: computeUnitsWithBuffer, + }), + ...ixs, + ], + payerKey: payer.publicKey, + recentBlockhash: blockhash, + }).compileToV0Message(); + const finalTx = new VersionedTransaction(finalMessageV0); + finalTx.sign([payer]); + + const txHash = await provider.connection.sendRawTransaction( + finalTx.serialize(), + ); + await provider.connection.confirmTransaction(txHash, "confirmed"); + + console.log("Launch initialized", txHash); +}; + +launch().catch(console.error); diff --git a/scripts/v0.7/rip-cars/start.ts b/scripts/v0.7/rip-cars/start.ts new file mode 100644 index 00000000..505cc4b9 --- /dev/null +++ b/scripts/v0.7/rip-cars/start.ts @@ -0,0 +1,52 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + LaunchpadClient, + getLaunchAddr, +} from "@metadaoproject/programs/launchpad/v0.7"; +import { PublicKey, Transaction } from "@solana/web3.js"; +import * as token from "@solana/spl-token"; +import { TOKEN_SEED } from "./constants.js"; + +const provider = anchor.AnchorProvider.env(); +const payer = ( + provider.wallet as anchor.Wallet & { payer: anchor.web3.Keypair } +).payer; + +const LAUNCH_AUTHORITY = payer.publicKey; + +const launchpad: LaunchpadClient = LaunchpadClient.createClient({ provider }); + +export const start = async () => { + const TOKEN = await PublicKey.createWithSeed( + payer.publicKey, + TOKEN_SEED, + token.TOKEN_PROGRAM_ID, + ); + console.log("Token address:", TOKEN.toBase58()); + + const [launch] = getLaunchAddr(undefined, TOKEN); + + const startLaunchIx = await launchpad + .startLaunchIx({ + launch, + launchAuthority: LAUNCH_AUTHORITY, + }) + .instruction(); + + // Build transaction without compute budget first + const tx = new Transaction().add(startLaunchIx); + + const { blockhash } = await provider.connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + tx.feePayer = payer.publicKey; + + // Simulate transaction to get compute units used + tx.sign(payer); + + const txHash = await provider.connection.sendRawTransaction(tx.serialize()); + await provider.connection.confirmTransaction(txHash, "confirmed"); + + console.log("Launch initialized", txHash); +}; + +start().catch(console.error);