@wuwei-labs/srsly
Version:
TypeScript SDK for SRSLY
143 lines • 6.8 kB
JavaScript
import { describe, expect, it, vi } from 'vitest';
vi.mock('../accounts/contract', () => ({
fetchContract: vi.fn(),
}));
vi.mock('../accounts/config', () => ({
fetchConfig: vi.fn(),
}));
vi.mock('../utils/rpc', () => ({
createRpc: vi.fn(() => ({
getMinimumBalanceForRentExemption: (size) => ({
// Solana rent formula constants: ACCOUNT_STORAGE_OVERHEAD=128,
// lamports_per_byte_year=3_480, exemption_threshold=2 years.
send: async () => {
const bytes = Number(size) + 128;
return BigInt(bytes * 3_480 * 2);
},
}),
})),
}));
vi.mock('../utils/config', () => ({
getRpcUrl: () => 'http://localhost:8899',
}));
import { fetchContract } from '../accounts/contract';
import { fetchConfig } from '../accounts/config';
import { estimateOwnerSetupCost, estimateBorrowerSetupCost, estimateRentalCost, tickBudget, } from './estimate';
import { MINIMUM_THREAD_FUNDING, DEFAULT_EXECUTION_BUDGET, TX_BASE_FEE_LAMPORTS, PER_TICK_LAMPORTS, } from './constants';
import { CONTRACT_STATE_SIZE, RENTAL_STATE_SIZE, BORROWER_STATE_SIZE } from './sizes';
import { SPL_ATA_SIZE } from './constants';
// Rent formula used by our mock — matches Solana mainnet rent-exemption math.
const rentFor = (size) => BigInt((size + 128) * 3_480 * 2);
describe('estimateOwnerSetupCost', () => {
it('baseline matches COSTS.md breakdown', async () => {
const quote = await estimateOwnerSetupCost();
const contractStateRent = rentFor(CONTRACT_STATE_SIZE);
const rentalStateRent = rentFor(RENTAL_STATE_SIZE);
const ataRent = rentFor(SPL_ATA_SIZE);
expect(quote.breakdown.contractState).toBe(contractStateRent);
expect(quote.breakdown.rentalStateActive).toBe(rentalStateRent);
expect(quote.breakdown.rentalStateQueued).toBe(rentalStateRent);
expect(quote.breakdown.contractAta).toBe(ataRent);
expect(quote.breakdown.ownerAta).toBe(ataRent);
expect(quote.breakdown.threadInfraBase).toBe(MINIMUM_THREAD_FUNDING);
expect(quote.breakdown.executionBudget).toBe(DEFAULT_EXECUTION_BUDGET);
expect(quote.rentLamports).toBe(contractStateRent + rentalStateRent * 2n + ataRent + ataRent);
expect(quote.threadLamports).toBe(MINIMUM_THREAD_FUNDING + DEFAULT_EXECUTION_BUDGET);
expect(quote.txFees).toBe(TX_BASE_FEE_LAMPORTS * 2n);
expect(quote.total).toBe(quote.rentLamports + quote.threadLamports + quote.txFees);
});
it('ownerAtaExists skips owner ATA rent', async () => {
const with_ = await estimateOwnerSetupCost();
const without = await estimateOwnerSetupCost({ ownerAtaExists: true });
const ataRent = rentFor(SPL_ATA_SIZE);
expect(with_.total - without.total).toBe(ataRent);
expect(without.breakdown.ownerAta).toBe(0n);
});
it('custom executionBudget flows through to thread funding', async () => {
const quote = await estimateOwnerSetupCost({ executionBudget: { sol: 0.01 } });
expect(quote.breakdown.executionBudget).toBe(10000000n);
expect(quote.threadLamports).toBe(MINIMUM_THREAD_FUNDING + 10000000n);
});
});
describe('estimateBorrowerSetupCost', () => {
it('sums BorrowerState + managed ATA rent + tx fee', async () => {
const quote = await estimateBorrowerSetupCost();
const borrowerStateRent = rentFor(BORROWER_STATE_SIZE);
const ataRent = rentFor(SPL_ATA_SIZE);
expect(quote.breakdown.borrowerState).toBe(borrowerStateRent);
expect(quote.breakdown.managedAta).toBe(ataRent);
expect(quote.rentLamports).toBe(borrowerStateRent + ataRent);
expect(quote.txFees).toBe(TX_BASE_FEE_LAMPORTS);
expect(quote.total).toBe(borrowerStateRent + ataRent + TX_BASE_FEE_LAMPORTS);
});
});
describe('estimateRentalCost', () => {
const mockContract = (rate) => ({
data: { rate },
address: 'Contract11111111111111111111111111111111111',
});
const mockConfig = (overrides = {}) => ({
data: {
stardustToAtlas: 100000000n,
serviceFeeBps: 1000n,
baseBps: 10_000,
...overrides,
},
address: 'Config1111111111111111111111111111111111111',
});
it('1 ATLAS/day × 86400s yields 100M Stardust escrow, 10M service fee', async () => {
vi.mocked(fetchContract).mockResolvedValue(mockContract(1n));
vi.mocked(fetchConfig).mockResolvedValue(mockConfig());
const quote = await estimateRentalCost({
contract: 'AnyContract',
durationSeconds: 86400n,
});
expect(quote.atlasEscrow).toBe(100000000n);
expect(quote.atlasServiceFee).toBe(10000000n);
expect(quote.atlasBid).toBe(0n);
expect(quote.atlasTotal).toBe(100000000n);
expect(quote.serviceFeeBps).toBe(1_000);
expect(quote.solTxFees).toBe(TX_BASE_FEE_LAMPORTS);
});
it('rate in Stardust units (rate >= stardustToAtlas) is not rescaled', async () => {
// 5 ATLAS/day already in Stardust = 500M.
vi.mocked(fetchContract).mockResolvedValue(mockContract(500000000n));
vi.mocked(fetchConfig).mockResolvedValue(mockConfig());
const quote = await estimateRentalCost({
contract: 'AnyContract',
durationSeconds: 86400n * 3n, // 3 days
});
expect(quote.atlasEscrow).toBe(1500000000n);
});
it('pro-rates sub-day durations via integer division', async () => {
vi.mocked(fetchContract).mockResolvedValue(mockContract(1n));
vi.mocked(fetchConfig).mockResolvedValue(mockConfig());
// Half a day -> 50M Stardust.
const quote = await estimateRentalCost({
contract: 'AnyContract',
durationSeconds: 43200n,
});
expect(quote.atlasEscrow).toBe(50000000n);
expect(quote.atlasServiceFee).toBe(5000000n);
});
it('bidStardust flows into atlasTotal but not into atlasEscrow', async () => {
vi.mocked(fetchContract).mockResolvedValue(mockContract(1n));
vi.mocked(fetchConfig).mockResolvedValue(mockConfig());
const quote = await estimateRentalCost({
contract: 'AnyContract',
durationSeconds: 86400n,
bidStardust: 25000000n,
});
expect(quote.atlasEscrow).toBe(100000000n);
expect(quote.atlasBid).toBe(25000000n);
expect(quote.atlasTotal).toBe(125000000n);
});
});
describe('tickBudget', () => {
it('scales by PER_TICK_LAMPORTS', () => {
expect(tickBudget(365)).toBe(365n * PER_TICK_LAMPORTS);
expect(tickBudget(0)).toBe(0n);
expect(tickBudget(1000000n)).toBe(1000000n * PER_TICK_LAMPORTS);
});
});
//# sourceMappingURL=estimate.test.js.map