UNPKG

@wuwei-labs/srsly

Version:
171 lines 7.92 kB
/** * @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