@wuwei-labs/srsly
Version:
TypeScript SDK for SRSLY
235 lines • 10.5 kB
JavaScript
;
/**
* @purpose Simplified closeContract instruction wrapper
*
* Thin convenience wrapper around Codama-generated closeContract instruction.
* Closes a contract with no active rentals and claims remaining payments.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.closeContract = closeContract;
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 contract_1 = require("../accounts/contract");
const config_2 = require("../accounts/config");
const rental_1 = require("../accounts/rental");
const thread_1 = require("../accounts/thread");
const token_1 = require("../pda/token");
const srsly_1 = require("../pda/srsly");
const thread_2 = require("../pda/thread");
const instructions_2 = require("../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
* });
* ```
*/
async function closeContract(params, config) {
// Merge config
const finalConfig = (0, config_1.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 = (0, signer_1.toTransactionSigner)(params.owner);
// Get atlasMint from on-chain config
const configAccount = await (0, config_2.fetchConfig)(finalConfig.rpcUrl);
const atlasMint = configAccount.data.atlasMint;
const addresses = (0, config_1.getAddresses)(config);
const programAddress = (0, kit_1.address)(addresses.srsly);
// Resolve fleet and contract addresses
let fleetAddress;
let contractAddress;
if (params.fleet && params.contract) {
// Both provided - use as-is
fleetAddress = (0, kit_1.address)(params.fleet);
contractAddress = (0, kit_1.address)(params.contract);
}
else if (params.fleet) {
// Only fleet provided - derive contract from fleet PDA
fleetAddress = (0, kit_1.address)(params.fleet);
contractAddress = await (0, srsly_1.deriveContract)(fleetAddress, programAddress);
}
else if (params.contract) {
// Only contract provided - fetch contract to get fleet
contractAddress = (0, kit_1.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 (0, contract_1.fetchContract)(contractAddress, finalConfig.rpcUrl);
if (!params.fleet) {
fleetAddress = contractAccount.data.fleet;
}
// Get sageProgram from config if not provided
const sageProgram = params.sageProgram
? (0, kit_1.address)(params.sageProgram)
: (0, kit_1.address)(addresses.sage);
// Derive contract token account (ATA of contract PDA for ATLAS)
const contractTokenAccount = await (0, token_1.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 (0, rental_1.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 = (0, kit_1.address)(params.queuedBorrower);
}
else {
const queuedRental = await (0, rental_1.fetchRental)(contractAccount.data.queuedRental, finalConfig.rpcUrl);
queuedBorrower = queuedRental.data.borrower;
}
queuedBorrowerState = await (0, srsly_1.deriveBorrowerState)(queuedBorrower);
queuedBorrowerTokenAccount = await (0, token_1.deriveAssociatedTokenAccount)(queuedBorrower, atlasMint);
}
// Helper: build a closeContractThread instruction
const buildThreadCloseIx = async () => {
const rentalAuthority = await (0, srsly_1.deriveRentalAuthority)();
const contractThread = await (0, thread_2.deriveContractThread)(contractAddress, rentalAuthority);
const threadAccount = await (0, thread_1.fetchThreadByAddress)(contractThread, finalConfig.rpcUrl);
const fiberIds = Array.from(threadAccount.data.fiberIds);
const fiberAccounts = await Promise.all(fiberIds.map(async (id) => ({
address: await (0, thread_2.deriveFiber)(contractThread, id),
role: kit_1.AccountRole.WRITABLE,
})));
const ix = await (0, instructions_1.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 (0, instructions_1.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 Promise.resolve().then(() => __importStar(require('./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 Promise.resolve().then(() => __importStar(require('./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 (0, instructions_2.prepareInstructions)(ixs, {
computeUnits: params.computeUnits,
PublicKey: finalConfig.PublicKey,
});
}
//# sourceMappingURL=closeContract.js.map