On-chain queries
Query borrow positions, debt, token balances, staking shares, and immediate unstake value from Solana.
Use API responses to render fast previews, but use Solana accounts for balances, parameters, debt, and transaction decisions. This module queries all supported borrow markets from the deployment manifest without scanning unrelated program accounts.
Portfolio reader
Create read-nest-portfolio.ts beside nest-client.ts:
import { PublicKey } from "@solana/web3.js";
import { getAccount } from "@solana/spl-token";
import {
ata,
borrowVaultPda,
confirmedChainTime,
type NestClient,
} from "./nest-client";
function bn(value: { toString(): string }) {
return BigInt(value.toString());
}
async function tokenBalanceOrZero(
client: NestClient,
mint: PublicKey,
owner: PublicKey,
tokenProgram: PublicKey,
) {
const address = ata(mint, owner, tokenProgram);
const info = await client.connection.getAccountInfo(address, "confirmed");
if (!info) return { address, amount: 0n };
const account = await getAccount(
client.connection,
address,
"confirmed",
tokenProgram,
);
if (!account.owner.equals(owner) || !account.mint.equals(mint)) {
throw new Error(`Unexpected token account data at ${address}`);
}
return { address, amount: account.amount };
}
export async function readBorrowPositions(client: NestClient, owner: PublicKey) {
const coreProgram = new PublicKey(client.deployment.programs.nestCore);
return Promise.all(client.deployment.collateral.map(async (market) => {
const collateralConfig = new PublicKey(market.collateralConfig);
const address = borrowVaultPda(coreProgram, owner, collateralConfig);
const vault = await client.core.account.vault.fetchNullable(address);
if (!vault) return null;
if (
!vault.owner.equals(owner)
|| !vault.collateralConfig.equals(collateralConfig)
) throw new Error(`Borrow vault identity mismatch at ${address}`);
const principalDebtNusd6 = bn(vault.principalDebt);
const storedFeeNusd6 = bn(vault.accruedFee);
return {
symbol: market.symbol,
address,
collateralConfig,
collateralRaw: bn(vault.collateralRaw),
principalDebtNusd6,
storedFeeNusd6,
storedDebtNusd6: principalDebtNusd6 + storedFeeNusd6,
lastAccrualTs: bn(vault.lastAccrualTs),
};
})).then((positions) => positions.filter((position) => position !== null));
}
export async function readStakingPosition(client: NestClient, owner: PublicKey) {
const stakingStateAddress = new PublicKey(client.deployment.accounts.stakingState);
const protocolAddress = new PublicKey(client.deployment.accounts.protocol);
const snusdMint = new PublicKey(client.deployment.mints.snusd);
const snusdTokenProgram = new PublicKey(client.deployment.tokenPrograms.snusd);
const [state, protocol, snusd] = await Promise.all([
client.stake.account.stakingState.fetch(stakingStateAddress),
client.core.account.protocol.fetch(protocolAddress),
tokenBalanceOrZero(client, snusdMint, owner, snusdTokenProgram),
]);
const totalShares = bn(state.totalShares);
const stakingVaultNusd6 = bn(state.stakingVaultNusd);
const reservedPendingClaimsNusd6 = bn(state.reservedPendingClaims);
if (reservedPendingClaimsNusd6 > stakingVaultNusd6) {
throw new Error("Recorded pending claims exceed staking assets");
}
const redeemableNusd6 = stakingVaultNusd6 - reservedPendingClaimsNusd6;
const grossValueNusd6 = totalShares === 0n
? 0n
: snusd.amount * redeemableNusd6 / totalShares;
const targetAprBps = bn(protocol.stakerTargetAprBps);
const feeNumerator = grossValueNusd6 * targetAprBps * 86_400n;
const feeDenominator = 10_000n * 31_536_000n;
const unstakeFeeNusd6 = feeNumerator === 0n
? 0n
: (feeNumerator + feeDenominator - 1n) / feeDenominator;
const netValueNusd6 = grossValueNusd6 > unstakeFeeNusd6
? grossValueNusd6 - unstakeFeeNusd6
: 0n;
return {
stakingStateAddress,
snusdAccount: snusd.address,
walletSharesSnusd6: snusd.amount,
estimatedGrossValueNusd6: grossValueNusd6,
estimatedUnstakeFeeNusd6: unstakeFeeNusd6,
estimatedNetValueNusd6: netValueNusd6,
targetAprBps,
totalShares,
redeemableNusd6,
};
}
export async function readNestPortfolio(client: NestClient, owner: PublicKey) {
const protocolAddress = new PublicKey(client.deployment.accounts.protocol);
const nusdMint = new PublicKey(client.deployment.mints.nusd);
const usdcMint = new PublicKey(client.deployment.mints.usdc);
const nusdTokenProgram = new PublicKey(client.deployment.tokenPrograms.nusd);
const usdcTokenProgram = new PublicKey(client.deployment.tokenPrograms.usdc);
const [chainTime, protocol, borrowPositions, staking, nusd, usdc] =
await Promise.all([
confirmedChainTime(client.connection),
client.core.account.protocol.fetch(protocolAddress),
readBorrowPositions(client, owner),
readStakingPosition(client, owner),
tokenBalanceOrZero(client, nusdMint, owner, nusdTokenProgram),
tokenBalanceOrZero(client, usdcMint, owner, usdcTokenProgram),
]);
return {
owner,
chainTime,
protocol: {
address: protocolAddress,
paused: protocol.paused,
badDebtNusd6: bn(protocol.badDebtNusd),
totalDebtNusd6: bn(protocol.totalDebt),
stabilityFeeAprBps: bn(protocol.stabilityFeeAprBps),
psmSwapInFeeBps: bn(protocol.psmSwapInFeeBps),
psmSwapOutFeeBps: bn(protocol.psmSwapOutFeeBps),
},
balances: {
nusd6: nusd.amount,
snusd6: staking.walletSharesSnusd6,
usdc6: usdc.amount,
},
borrowPositions,
staking,
};
}Project current debt
The stored account does not update every second. Project the fee from
lastAccrualTs to the confirmed chain timestamp using the integer-exact helper
in Accrued stability fees. Do this independently for
each position because each vault has its own last-accrual time.
Read current market parameters
The manifest is discovery data. Before minting or withdrawing, fetch the collateral configuration account and use its current pause flags, caps, LTV, liquidation threshold, oracle limits, totals, and decimals:
export async function readCurrentMarket(client: NestClient, config: string) {
const address = new PublicKey(config);
const account = await client.core.account.collateralConfig.fetch(address);
if (!account.protocol.equals(new PublicKey(client.deployment.accounts.protocol))) {
throw new Error("Collateral config belongs to another protocol account");
}
return { address, account };
}Do not use JavaScript numbers for token accounting
Keep raw amounts, prices, timestamps, basis points, debt, and shares as
bigint or Anchor BN. Convert to decimal strings only at the UI boundary.