@wuwei-labs/srsly
Version:
TypeScript SDK for SRSLY
171 lines • 7.92 kB
JavaScript
/**
* @purpose Simplified reserveRental instruction wrapper
*
* Thin convenience wrapper around Codama-generated reserveRental instruction.
* Reserves a rental contract using ATLAS for a GBM-style auction.
*/
import { AccountRole, address } from '@solana/kit';
import { getReserveRentalInstructionAsync } from '../generated/codama/instructions';
import { mergeConfig, getAddresses } from '../utils/config';
import { toTransactionSigner } from '../utils/signer';
import { convertAmount } from '../params/amount';
import { convertDuration } from '../params/duration';
import { fetchContract } from '../accounts/contract';
import { fetchConfig } from '../accounts/config';
import { fetchFleet } from '../accounts/fleet';
import { fetchBorrower } from '../accounts/borrower';
import { fetchRental } from '../accounts/rental';
import { RentalStatus } from '../generated/codama/types';
import { deriveGameAccounts } from '../pda/sage';
import { deriveActiveRental, deriveQueuedRental, deriveRentalAuthority, deriveBorrowerState, } from '../pda/srsly';
import { deriveAssociatedTokenAccount } from '../pda/token';
import { deriveContractThread, deriveFiber } from '../pda/thread';
import { prepareInstructions } from '../utils/instructions';
import { getMinimumBid } from '../utils/bidMath';
/**
* Bid on a contract via the GBM-style reservation auction. The winner
* gets an exclusive window to accept once the current rental ends.
*
* @param params - Reservation parameters
* @param config - Optional SDK configuration overrides
* @returns Array of instructions (Kit format or web3.js if PublicKey in config)
*
* @example
* ```typescript
* // Reserve with 5 ATLAS for a 7-day rental
* const ixs = await reserveRental({
* challenger: wallet,
* contract: "CONTRACT_ADDRESS",
* amount: 5,
* duration: { days: 7 },
* computeUnits: 200_000
* });
* ```
*/
export async function reserveRental(params, config) {
// Merge config
const finalConfig = mergeConfig(config);
// Validate required params
if (!params.challenger) {
throw new Error('challenger is required');
}
if (!params.contract) {
throw new Error('contract is required');
}
if (!params.challengerProfile) {
throw new Error('challengerProfile is required for reserveRental');
}
if (!params.duration) {
throw new Error('duration is required');
}
const bidPoints = params.bidPoints ?? 0;
let bidAtlasValue = params.bidAtlas !== undefined ? convertAmount(params.bidAtlas) : 0n;
const userProvidedBid = params.bidPoints !== undefined || params.bidAtlas !== undefined;
if (bidPoints > 0 && bidAtlasValue > 0n) {
throw new Error('bidPoints and bidAtlas are mutually exclusive');
}
// Auto-fill the ATLAS bid when the caller omitted both bid params AND
// there is an existing defender to outbid. First reservations allow
// zero-bid, so we leave those untouched.
const autoMinBid = params.autoMinBid ?? true;
if (autoMinBid && !userProvidedBid) {
const floor = await getMinimumBid({
contractAddress: params.contract,
rpcUrl: config?.rpcUrl,
});
if (floor.defenderBidStardust > 0n) {
bidAtlasValue = floor.minBidStardust;
}
}
// Convert challenger to transaction signer
const challengerSigner = toTransactionSigner(params.challenger);
// Get network-specific addresses
const addresses = getAddresses(config);
const programAddress = address(addresses.srsly);
const sageProgram = address(addresses.sage);
// Fetch config for mint
const configAccount = await fetchConfig(finalConfig.rpcUrl);
const atlasMint = configAccount.data.atlasMint;
// Fetch contract to get contractTokenAccount
const contractAccount = await fetchContract(params.contract, finalConfig.rpcUrl);
// Derive rental PDAs
const activeRental = await deriveActiveRental(params.contract);
const rentalState = await deriveQueuedRental(params.contract);
// Derive rental authority
const rentalAuthority = await deriveRentalAuthority();
// Derive contract thread and close_rental fiber (fiber index 1)
const contractThread = await deriveContractThread(params.contract, rentalAuthority);
const closeRentalFiber = await deriveFiber(contractThread, 1);
// Derive contract token account (ATA of contract PDA)
const contractAddress = address(params.contract);
const contractTokenAccount = await deriveAssociatedTokenAccount(contractAddress, atlasMint);
// Derive challenger's token account
const challengerTokenAccount = await deriveAssociatedTokenAccount(challengerSigner.address, atlasMint);
// Convert duration to seconds
const durationSeconds = convertDuration(params.duration);
const referrer = params.referrer
? address(params.referrer)
: undefined;
// Build instruction input
const instructionInput = {
challenger: challengerSigner,
contract: contractAddress,
contractThread,
closeRentalFiber,
contractTokenAccount,
activeRental,
challengerTokenAccount,
rentalState,
referrer,
sageProgram,
bidPoints,
bidAtlas: bidAtlasValue,
duration: BigInt(durationSeconds),
};
// If there's a current queued reservation, provide defender accounts
// Fetch queued rental to check if there's an existing reservation holder
try {
const queuedRental = await fetchRental(rentalState, finalConfig.rpcUrl);
if (queuedRental.data.status === RentalStatus.Queued) {
const defenderBorrowerState = queuedRental.data.borrowerState;
const challengerBorrowerState = await deriveBorrowerState(challengerSigner);
if (defenderBorrowerState === challengerBorrowerState) {
// Self-contest: challenger is the current defender.
// Program reuses challenger_state, but we still need defender_managed_token_account
// for the ATLAS refund path.
const challengerAccount = await fetchBorrower(challengerBorrowerState, finalConfig.rpcUrl);
instructionInput.defenderManagedTokenAccount = challengerAccount.data.managedTokenAccount;
}
else {
instructionInput.defenderState = defenderBorrowerState;
const defenderAccount = await fetchBorrower(defenderBorrowerState, finalConfig.rpcUrl);
instructionInput.defenderManagedTokenAccount = defenderAccount.data.managedTokenAccount;
}
}
}
catch {
// Queued rental doesn't exist or is Available — no defender
}
// Derive SAGE accounts for remaining_accounts
const challengerProfileAddr = address(params.challengerProfile);
const fleetState = await fetchFleet(contractAccount.data.fleet, finalConfig.rpcUrl);
const gameAccounts = await deriveGameAccounts(challengerProfileAddr, fleetState.faction, contractAccount.data.gameId);
// Call Codama-generated instruction
const kitInstruction = await getReserveRentalInstructionAsync(instructionInput, {
programAddress,
});
// Append SAGE accounts as remaining_accounts
const sageRemainingAccounts = [
{ address: challengerProfileAddr, role: AccountRole.READONLY },
{ address: gameAccounts.profileFaction, role: AccountRole.READONLY },
{ address: gameAccounts.starbase, role: AccountRole.READONLY },
{ address: gameAccounts.starbasePlayer, role: AccountRole.READONLY },
];
kitInstruction.accounts.push(...sageRemainingAccounts);
// Prepare instructions with optional compute budget
return prepareInstructions(kitInstruction, {
computeUnits: params.computeUnits,
PublicKey: finalConfig.PublicKey,
});
}
//# sourceMappingURL=reserveRental.js.map