@wuwei-labs/srsly
Version:
TypeScript SDK for SRSLY
146 lines • 6.55 kB
JavaScript
/**
* @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