@wuwei-labs/srsly
Version:
TypeScript SDK for SRSLY
245 lines • 11.9 kB
JavaScript
"use strict";
/**
* @purpose Simplified acceptRental instruction wrapper
*
* Thin convenience wrapper around Codama-generated acceptRental instruction.
* Accepts a rental contract and begins the rental period.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.acceptRental = acceptRental;
const kit_1 = require("@solana/kit");
const instructions_1 = require("../generated/codama/instructions");
const config_1 = require("../utils/config");
const signer_1 = require("../utils/signer");
const amount_1 = require("../params/amount");
const duration_1 = require("../params/duration");
const fleet_1 = require("../accounts/fleet");
const contract_1 = require("../accounts/contract");
const config_2 = require("../accounts/config");
const rental_1 = require("../accounts/rental");
const borrower_1 = require("../accounts/borrower");
const sage_1 = require("../pda/sage");
const srsly_1 = require("../pda/srsly");
const token_1 = require("../pda/token");
const thread_1 = require("../pda/thread");
const instructions_2 = require("../utils/instructions");
const types_1 = require("../generated/codama/types");
const discountAuth_1 = require("../utils/discountAuth");
/**
* Activate a rental: transfer fleet control to the borrower for the
* chosen duration and pay the first period's rate.
*
* @param params - Rental acceptance parameters
* @param config - Optional SDK configuration overrides
* @returns Array of instructions (Kit format or web3.js if PublicKey in config)
*
* @example
* ```typescript
* const ixs = await acceptRental({
* borrower: wallet,
* borrowerProfile: "BORROWER_PROFILE_ADDRESS",
* contract: "CONTRACT_ADDRESS",
* duration: { days: 7 },
* computeUnits: 400_000
* });
* ```
*/
async function acceptRental(params, config) {
// Merge config
const finalConfig = (0, config_1.mergeConfig)(config);
// Validate required params
if (!params.borrower) {
throw new Error('borrower is required');
}
if (!params.contract) {
throw new Error('contract is required');
}
if (!params.duration) {
throw new Error('duration is required');
}
if (!params.borrowerProfile) {
throw new Error('borrowerProfile is required');
}
// acceptRental issues several CPIs (SAGE rental_open, SLYvault deposit,
// optional discount verify, optional thread + queued-rental cleanup),
// so the 200k default budget reliably runs out. Force the caller to
// size it explicitly.
if (!params.computeUnits || params.computeUnits < 400_000) {
throw new Error(`computeUnits must be at least 400,000 for acceptRental (got ${params.computeUnits ?? 'undefined'}).`);
}
// Convert borrower to transaction signer
const borrowerSigner = (0, signer_1.toTransactionSigner)(params.borrower);
// Fetch config
const configAccount = await (0, config_2.fetchConfig)(finalConfig.rpcUrl);
// Fetch contract to get fleet, gameId
const contractAccount = await (0, contract_1.fetchContract)(params.contract, finalConfig.rpcUrl);
const fleet = contractAccount.data.fleet;
const gameId = contractAccount.data.gameId;
// Client-side rate safety check
if (params.rate != null) {
const expectedRate = (0, amount_1.convertAmount)(params.rate);
const contractRate = BigInt(contractAccount.data.rate);
if (expectedRate !== contractRate) {
throw new Error(`Contract rate mismatch: expected ${expectedRate} stardust but contract has ${contractRate} stardust. The rate may have changed since you last fetched it.`);
}
}
// Fetch fleet data for faction (needed to derive SAGE accounts)
const fleetState = await (0, fleet_1.fetchFleet)(fleet, finalConfig.rpcUrl);
const faction = fleetState.faction;
const borrowerProfile = (0, kit_1.address)(params.borrowerProfile);
// Derive all SAGE game accounts (profile faction, starbase, starbase player)
const gameAccounts = await (0, sage_1.deriveGameAccounts)(borrowerProfile, faction, gameId);
// Derive active rental PDA (no more slot logic)
const rentalState = await (0, srsly_1.deriveActiveRental)(params.contract);
// Resolve expiredBorrowerState: use param if provided, otherwise auto-detect
// When the same borrower re-rents their own expired contract, skip passing
// expiredBorrowerState to avoid Anchor's duplicate mutable account error.
const borrowerStatePda = await (0, srsly_1.deriveBorrowerState)(borrowerSigner);
let expiredBorrowerState;
let activeExpired = false;
let expiredReferrer;
let expiredDiscountBps = 0;
let expiredBorrowerManagedTA;
if (params.expiredBorrowerState) {
const explicit = (0, kit_1.address)(params.expiredBorrowerState);
// Don't pass if it matches the current borrower's state (same-borrower re-rent)
if (explicit !== borrowerStatePda) {
expiredBorrowerState = explicit;
}
}
try {
const activeRental = await (0, rental_1.fetchRental)(rentalState, finalConfig.rpcUrl);
if (activeRental.data.status !== types_1.RentalStatus.Available) {
// Check if expired (end_time in the past)
const now = Math.floor(Date.now() / 1000);
if (now >= Number(activeRental.data.endTime)) {
activeExpired = true;
expiredReferrer = (0, kit_1.isOption)(activeRental.data.referrer)
? ((0, kit_1.unwrapOption)(activeRental.data.referrer) ?? undefined)
: undefined;
expiredDiscountBps = activeRental.data.discountBps;
// Fetch expired borrower's managed ATA for discount refund
if (expiredDiscountBps > 0 && expiredReferrer) {
try {
const expiredBorrowerAccount = await (0, borrower_1.fetchBorrower)(activeRental.data.borrowerState, finalConfig.rpcUrl);
expiredBorrowerManagedTA = expiredBorrowerAccount.data.managedTokenAccount;
}
catch {
// Can't fetch — skip discount refund account
}
}
}
// Auto-detect expiredBorrowerState if not explicitly provided
if (!params.expiredBorrowerState) {
if (activeRental.data.borrowerState !== borrowerStatePda) {
expiredBorrowerState = activeRental.data.borrowerState;
}
}
}
}
catch {
// Rental state doesn't exist or can't be fetched — no expired rental to replace
}
// Derive rental authority PDA
const rentalAuthority = await (0, srsly_1.deriveRentalAuthority)();
// Derive contract thread and close_rental fiber (fiber index 1) — only if thread configured
const thread = (0, kit_1.address)(contractAccount.data.thread);
const hasThread = thread !== '11111111111111111111111111111111';
const contractThread = hasThread
? await (0, thread_1.deriveContractThread)(params.contract, rentalAuthority)
: undefined;
const closeRentalFiber = hasThread && contractThread ? await (0, thread_1.deriveFiber)(contractThread, 1) : undefined;
// Convert duration parameter
const durationSeconds = (0, duration_1.convertDuration)(params.duration);
// Get network-specific addresses (atlasMint sourced from on-chain config)
const atlasMint = configAccount.data.atlasMint;
const addresses = (0, config_1.getAddresses)(config);
const sageProgram = (0, kit_1.address)(addresses.sage);
const programAddress = (0, kit_1.address)(addresses.srsly);
const referrer = params.referrer
? (0, kit_1.address)(params.referrer)
: undefined;
// Derive contract token account (ATA of contract PDA)
const contractAddress = (0, kit_1.address)(params.contract);
const contractTokenAccount = await (0, token_1.deriveAssociatedTokenAccount)(contractAddress, atlasMint);
// Derive vault accounts for fee flush
const vaultAddress = (0, kit_1.address)(configAccount.data.slyvault);
const vaultTokenAccount = await (0, token_1.deriveAssociatedTokenAccount)(vaultAddress, atlasMint);
// Derive borrower token account (ATA)
const borrowerTokenAccount = await (0, token_1.deriveAssociatedTokenAccount)(borrowerSigner.address, atlasMint);
// Resolve queued rental accounts (named optional accounts)
const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
const queuedRentalKey = contractAccount.data.queuedRental;
let queuedBorrowerProfile;
let queuedBorrowerProfileFaction;
if (queuedRentalKey !== SYSTEM_PROGRAM_ID) {
const queuedRentalPda = await (0, srsly_1.deriveQueuedRental)(params.contract);
try {
const queuedRentalState = await (0, rental_1.fetchRental)(queuedRentalPda, finalConfig.rpcUrl);
if (queuedRentalState.data.status === types_1.RentalStatus.Queued) {
queuedBorrowerProfile = queuedRentalState.data.borrowerProfile;
queuedBorrowerProfileFaction = await (0, sage_1.deriveProfileFaction)(queuedBorrowerProfile);
}
}
catch {
// Queued rental can't be fetched — skip
}
}
// Call Codama-generated acceptRental instruction
const acceptRentalIx = await (0, instructions_1.getAcceptRentalInstructionAsync)({
borrower: borrowerSigner,
borrowerTokenAccount,
fleet: (0, kit_1.address)(fleet),
expiredBorrowerState,
contract: contractAddress,
contractTokenAccount,
vault: vaultAddress,
vaultTokenAccount,
...(contractThread && { contractThread }),
...(closeRentalFiber && { closeRentalFiber }),
referrer,
// Named optional accounts for expired fee flush
...(activeExpired && expiredReferrer && { expiredReferrer }),
...(activeExpired &&
expiredDiscountBps > 0 &&
expiredBorrowerManagedTA && {
expiredBorrowerTokenAccount: expiredBorrowerManagedTA,
}),
// Named optional accounts for queued activation
...(queuedBorrowerProfile && { queuedBorrowerProfile }),
...(queuedBorrowerProfileFaction && { queuedBorrowerProfileFaction }),
sageProgram,
duration: BigInt(durationSeconds),
...(params.discountAuth
? (() => {
const d = (0, discountAuth_1.deserializeDiscountAuth)(params.discountAuth);
return {
discountSignature: d.discountSignature,
discountMemberNonce: d.discountMemberNonce,
discountBps: d.discountBps,
discountExpiresAt: d.discountExpiresAt,
};
})()
: {
discountSignature: null,
discountMemberNonce: null,
discountBps: null,
discountExpiresAt: null,
}),
}, { programAddress });
// Append SAGE base accounts as remaining accounts [0..4]
const sageRemainingAccounts = [
{ address: borrowerProfile, role: kit_1.AccountRole.READONLY },
{ address: gameAccounts.profileFaction, role: kit_1.AccountRole.READONLY },
{ address: (0, kit_1.address)(gameId), role: kit_1.AccountRole.READONLY },
{ address: gameAccounts.starbase, role: kit_1.AccountRole.READONLY },
{ address: gameAccounts.starbasePlayer, role: kit_1.AccountRole.WRITABLE },
];
acceptRentalIx.accounts.push(...sageRemainingAccounts);
// Prepare instruction with optional compute budget
return (0, instructions_2.prepareInstructions)(acceptRentalIx, {
computeUnits: params.computeUnits,
PublicKey: finalConfig.PublicKey,
});
}
//# sourceMappingURL=acceptRental.js.map