UNPKG

@wuwei-labs/srsly

Version:
199 lines 8.77 kB
/** * @purpose Simplified closeContract instruction wrapper * * Thin convenience wrapper around Codama-generated closeContract instruction. * Closes a contract with no active rentals and claims remaining payments. */ import { AccountRole, address } from '@solana/kit'; import { getCloseContractInstructionAsync, getCloseContractThreadInstructionAsync, } from '../generated/codama/instructions'; import { mergeConfig, getAddresses } from '../utils/config'; import { toTransactionSigner } from '../utils/signer'; import { fetchContract } from '../accounts/contract'; import { fetchConfig } from '../accounts/config'; import { fetchRental } from '../accounts/rental'; import { fetchThreadByAddress } from '../accounts/thread'; import { deriveAssociatedTokenAccount } from '../pda/token'; import { deriveContract, deriveBorrowerState, deriveRentalAuthority } from '../pda/srsly'; import { deriveContractThread, deriveFiber } from '../pda/thread'; import { prepareInstructions } from '../utils/instructions'; /** * Close a rental contract and refund rent to the owner. Idempotent — * recovers cleanly from partial prior closes. When no rental is active, * the contract's automation thread is closed in the same transaction. * * @param params - Contract closing parameters * @param config - Optional SDK configuration overrides * @returns Kit instruction (or web3.js if PublicKey in config) * * @example * ```typescript * // Option 1: Provide fleet only (RECOMMENDED - idempotent) * const ix = await closeContract({ * owner: wallet, * fleet: fleetAddress * }); * * // Option 2: Provide contract only (contract MUST exist) * const ix2 = await closeContract({ * owner: wallet, * contract: contractAddress * }); * * // Option 3: Provide both (most explicit, no fetching) * const ix3 = await closeContract({ * owner: wallet, * fleet: fleetAddress, * contract: contractAddress * }); * ``` */ export async function closeContract(params, config) { // Merge config const finalConfig = mergeConfig(config); // Validate required params if (!params.owner) { throw new Error('owner is required'); } if (!params.fleet && !params.contract) { throw new Error('Either fleet or contract must be provided'); } // Convert owner to transaction signer const ownerSigner = toTransactionSigner(params.owner); // Get atlasMint from on-chain config const configAccount = await fetchConfig(finalConfig.rpcUrl); const atlasMint = configAccount.data.atlasMint; const addresses = getAddresses(config); const programAddress = address(addresses.srsly); // Resolve fleet and contract addresses let fleetAddress; let contractAddress; if (params.fleet && params.contract) { // Both provided - use as-is fleetAddress = address(params.fleet); contractAddress = address(params.contract); } else if (params.fleet) { // Only fleet provided - derive contract from fleet PDA fleetAddress = address(params.fleet); contractAddress = await deriveContract(fleetAddress, programAddress); } else if (params.contract) { // Only contract provided - fetch contract to get fleet contractAddress = address(params.contract); } else { // Should never reach here due to validation above throw new Error('Either fleet or contract must be provided'); } // Always fetch contract — needed for fleet (if not provided) and thread check const contractAccount = await fetchContract(contractAddress, finalConfig.rpcUrl); if (!params.fleet) { fleetAddress = contractAccount.data.fleet; } // Get sageProgram from config if not provided const sageProgram = params.sageProgram ? address(params.sageProgram) : address(addresses.sage); // Derive contract token account (ATA of contract PDA for ATLAS) const contractTokenAccount = await deriveAssociatedTokenAccount(contractAddress, atlasMint); // Check contract state const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111'; const hasThread = contractAccount.data.thread !== SYSTEM_PROGRAM_ID; const hasActiveSlot = contractAccount.data.activeRental !== SYSTEM_PROGRAM_ID; const hasQueuedSlot = contractAccount.data.queuedRental !== SYSTEM_PROGRAM_ID; // Check if active rental is expired let activeExpired = false; if (hasActiveSlot) { const activeRental = await fetchRental(contractAccount.data.activeRental, finalConfig.rpcUrl); const now = Math.floor(Date.now() / 1000); if (activeRental.data.endTime <= BigInt(now)) { activeExpired = true; } } // Resolve queued borrower accounts when a queued rental exists let queuedBorrowerState; let queuedBorrowerTokenAccount; if (hasQueuedSlot) { let queuedBorrower; if (params.queuedBorrower) { queuedBorrower = address(params.queuedBorrower); } else { const queuedRental = await fetchRental(contractAccount.data.queuedRental, finalConfig.rpcUrl); queuedBorrower = queuedRental.data.borrower; } queuedBorrowerState = await deriveBorrowerState(queuedBorrower); queuedBorrowerTokenAccount = await deriveAssociatedTokenAccount(queuedBorrower, atlasMint); } // Helper: build a closeContractThread instruction const buildThreadCloseIx = async () => { const rentalAuthority = await deriveRentalAuthority(); const contractThread = await deriveContractThread(contractAddress, rentalAuthority); const threadAccount = await fetchThreadByAddress(contractThread, finalConfig.rpcUrl); const fiberIds = Array.from(threadAccount.data.fiberIds); const fiberAccounts = await Promise.all(fiberIds.map(async (id) => ({ address: await deriveFiber(contractThread, id), role: AccountRole.WRITABLE, }))); const ix = await getCloseContractThreadInstructionAsync({ owner: ownerSigner, contract: contractAddress, thread: contractThread }, { programAddress }); ix.accounts.push(...fiberAccounts); return ix; }; // Helper: build a closeContract instruction with optional queued borrower accounts const buildCloseContractIx = async (includeQueued) => { return getCloseContractInstructionAsync({ owner: ownerSigner, contract: contractAddress, mint: atlasMint, fleet: fleetAddress, contractTokenAccount: contractTokenAccount, sageProgram, ...(includeQueued && queuedBorrowerState && queuedBorrowerTokenAccount ? { queuedBorrowerState, queuedBorrowerTokenAccount } : {}), }, { programAddress }); }; // Compose instructions based on scenario const ixs = []; if (activeExpired && hasQueuedSlot) { // Expired active + queued: multi-instruction composition // 1. closeRental — settles expired active const { closeRental } = await import('./closeRental'); const closeRentalResult = await closeRental({ payer: params.owner, contract: contractAddress }, config); ixs.push(...closeRentalResult.instructions); // 2. closeContractThread (if thread exists) if (hasThread) { ixs.push(await buildThreadCloseIx()); } // 3. closeContract — force-closes queued, sets to_close ixs.push(await buildCloseContractIx(true)); // 4. closeContract — final close (no active, no queued, no thread) ixs.push(await buildCloseContractIx(false)); } else if (activeExpired && !hasQueuedSlot) { // Expired active only, no queued — settle then close const { closeRental } = await import('./closeRental'); const closeRentalResult = await closeRental({ payer: params.owner, contract: contractAddress }, config); ixs.push(...closeRentalResult.instructions); if (hasThread) { ixs.push(await buildThreadCloseIx()); } ixs.push(await buildCloseContractIx(false)); } else if (!activeExpired && hasQueuedSlot) { // Non-expired active + queued — deferred close with queued borrower accounts ixs.push(await buildCloseContractIx(true)); } else { // No active rental (or no rental at all) — simple close if (hasThread) { ixs.push(await buildThreadCloseIx()); } ixs.push(await buildCloseContractIx(false)); } return prepareInstructions(ixs, { computeUnits: params.computeUnits, PublicKey: finalConfig.PublicKey, }); } //# sourceMappingURL=closeContract.js.map