UNPKG

@wuwei-labs/srsly

Version:
146 lines 6.55 kB
/** * @purpose Reservation bid math helpers * * Pure-math + on-chain-fetch helpers for computing the contest minimum-bid * floor that a challenger must clear to take an existing reservation. * Mirrors the on-chain `ConfigState::bid_min_stardust` formula so client UIs * (and on-chain-aware tooling) can preview the floor without hitting an RPC * for every keystroke. */ import { fetchConfig } from '../accounts/config'; import { fetchContract } from '../accounts/contract'; import { fetchRental } from '../accounts/rental'; import { deriveActiveRental, deriveQueuedRental } from '../pda/srsly'; import { getRpcUrl } from './config'; import { createRpc } from './rpc'; const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111'; /** * Pure math: contest minimum-bid floor in stardust. * * Mirror of `ConfigState::bid_min_stardust` on-chain. * * floor = max(rate, defenderBid) × ramp * ramp = max(captureRate, 1 + (maxMult - 1) × closeness) * * `closenessFraction` is in `[0, 1]` — the fraction of the active rental's * remaining window that has elapsed since the defender's reservation. 0 = * defender just placed; 1 = active rental about to end (snipe). * * `contestedMultiplierMaxBps` is on the protocol's custom 100=1× scale: * 100 disables the ramp, 10_000 = 100×. */ export function computeMinimumBid(args) { const closeness = Math.max(0, Math.min(1, args.closenessFraction)); const closenessBps = BigInt(Math.round(closeness * 10_000)); const maxBps = BigInt(Math.max(100, args.contestedMultiplierMaxBps)); const extraBps = maxBps - 100n; // ramped_bps in 4-decimal precision: 10_000 = 1×, 46_500 = 4.65×. const rampedBps = 10000n + (extraBps * closenessBps) / 100n; const capBps = args.captureRateBps > 10000n ? args.captureRateBps : 10000n; const effBps = rampedBps > capBps ? rampedBps : capBps; const defenderBid = args.defenderBidStardust ?? 0n; const base = args.rateStardust > defenderBid ? args.rateStardust : defenderBid; return (base * effBps) / 10000n; } /** * High-level helper: compute the contest minimum bid for a contract right * now (or at `now + delaySeconds`). * * Fetches `ConfigState`, `ContractState`, and the active/queued `RentalState` * accounts, derives `closeness` from the active rental window, and returns * the floor a challenger must clear plus enough context to render a UI. * * @example * ```ts * const floor = await getMinimumBid({ contractAddress: 'Abc...' }); * console.log('Bid at least', floor.minBidStardust, 'stardust'); * * // Preview the floor 4 hours from now (snipe-window pricing): * const later = await getMinimumBid({ * contractAddress: 'Abc...', * delaySeconds: 4 * 60 * 60, * }); * ``` * * If the contract has no active rental yet, closeness = 0 and the floor * collapses to the rate-bump floor (`rate × captureRate`). */ export async function getMinimumBid(args) { const rpcUrl = args.rpcUrl ?? getRpcUrl(); const rpc = createRpc(rpcUrl); const cfgPromise = fetchConfig(rpcUrl); const contractPromise = fetchContract(args.contractAddress, rpcUrl); const [cfg, contract] = await Promise.all([cfgPromise, contractPromise]); const cfgData = cfg.data; const contractData = contract.data; const rateStardust = BigInt(contractData.rate); const atlasPerPoint = args.atlasPerPoint ?? BigInt(cfgData.atlasPerPoint); const stardustPerAtlas = BigInt(cfgData.stardustToAtlas); const activeAddr = await deriveActiveRental(contract.address); const queuedAddr = await deriveQueuedRental(contract.address); // Active rental — only meaningful if the contract field points at it. let activeStart = null; let activeEnd = null; if (contractData.activeRental && contractData.activeRental.toString() !== SYSTEM_PROGRAM_ID) { try { const active = await fetchRental(activeAddr, rpcUrl); const a = active.data; activeStart = Number(a.startTime); activeEnd = Number(a.endTime); } catch { // Account missing despite contract pointer — treat as no active rental. } } // Queued rental — defender bid lives here if a reservation exists. let defenderBidStardust = 0n; if (contractData.queuedRental && contractData.queuedRental.toString() !== SYSTEM_PROGRAM_ID) { try { const queued = await fetchRental(queuedAddr, rpcUrl); const q = queued.data; const bidAtlas = BigInt(q.bidAtlas); const bidPoints = BigInt(q.bidPoints); // Points are 8-decimal dust; converting to stardust requires the same // 8-decimal precision: stardust = points_dust * atlasPerPoint * // stardustPerAtlas / 1e8. With both at 1e8 precision this collapses // to points_dust * atlasPerPoint. const pointsAsStardust = bidPoints > 0n ? (bidPoints * atlasPerPoint * stardustPerAtlas) / 100000000n : 0n; defenderBidStardust = bidAtlas + pointsAsStardust; } catch { // Missing queued account — leave defenderBidStardust = 0. } } const evaluatedAtSeconds = args.nowSeconds !== undefined ? Math.floor(args.nowSeconds) : Math.floor(Date.now() / 1000) + Math.max(0, args.delaySeconds ?? 0); let closeness = 0; if (activeStart !== null && activeEnd !== null && activeEnd > activeStart) { const span = activeEnd - activeStart; const elapsed = evaluatedAtSeconds - activeStart; closeness = Math.max(0, Math.min(1, elapsed / span)); } const minBidStardust = computeMinimumBid({ rateStardust, defenderBidStardust, contestedMultiplierMaxBps: cfgData.contestedMultiplierMaxBps, captureRateBps: BigInt(cfgData.captureRateBps), closenessFraction: closeness, }); const baseStardust = rateStardust > defenderBidStardust ? rateStardust : defenderBidStardust; const rampMultiplier = baseStardust > 0n ? Number(minBidStardust) / Number(baseStardust) : 1; // RPC reference kept to match the codebase pattern even when unused; // future refinements (e.g. fetching chain time) can plug in here. void rpc; return { minBidStardust, defenderBidStardust, rateStardust, closenessFraction: closeness, rampMultiplier, evaluatedAtSeconds, activeEndSeconds: activeEnd, activeStartSeconds: activeStart, }; } //# sourceMappingURL=bidMath.js.map