@kestrelfi/lyc-sdk
v1.0.3
Published
Client for the long_yield_carry Anchor program: PDA derivation, account decoding, instruction builders, and high-level transaction plans.
Readme
Long Yield Carry TypeScript SDK
Client for the long_yield_carry Anchor program: PDA derivation, account decoding, instruction builders, and high-level transaction plans. The main entry point is LongYieldCarryClient (the “LYC client”).
Installation and import
In this monorepo, depend on the workspace package and use the long-yield-carry path alias (see root tsconfig.json):
import { LongYieldCarryClient } from "@kestrelfi/lyc-sdk";
import { BN } from "@anchor-lang/core";
import { address, type Address } from "@solana/kit";External consumers install @kestrelfi/lyc-sdk from npm. Two entries:
@kestrelfi/lyc-sdk— the full SDK. Requires the optional Kamino peer deps (@kamino-finance/klend-sdk,farms-sdk,scope-sdk) to be installed.@kestrelfi/lyc-sdk/core— browser-safe read + mint/burn surface (account fetchers, PDA derivation,MintTokenBuilder/BurnTokenBuilder, constants, IDL, and the send pipeline). No Kamino/Jupiter graph; this is what frontends should import. Seesrc/core.tsfor a usage sketch.
Building and publishing
The manifest is split between two consumers:
- Inside the monorepo (backend Lambdas, tests),
main/typespoint atsrc/index.ts— esbuild/tsx bundle the TypeScript source through the pnpm workspace links, no build step required. - On npm,
publishConfigrewritesmain/module/types/exportstodist/at pack time, andprepackruns tsup to produce it. tsup bundles the unpublished workspace packages (common,lending-platforms,oracle,perena-helpers,swap-aggregator,jupiter-helpers) intodist/— a consumer installing from npm cannot resolve theirfile:links, so they must never appear in the publisheddependencies(they live indevDependencies). Everything declared independencies/peerDependenciesstays external.
Always release with pnpm publish (or inspect with pnpm pack) from this
directory — never npm publish. npm ignores the publishConfig entry-point
overrides and would ship a package whose main points at monorepo-only
TypeScript source. Sanity-check a release by unpacking the pnpm pack tarball
and confirming package.json has dist/ entry points and no file: entries
outside devDependencies.
Constructing the client
You need an Anchor AnchorProvider (connection + wallet). The second argument selects the deployment environment and thus the program ID and RPC URL used by the SDK:
import { AnchorProvider } from "@anchor-lang/core";
import { LongYieldCarryClient } from "@kestrelfi/lyc-sdk";
const client = new LongYieldCarryClient(provider, "local");
// or "test" | "prod"Optional third argument: LongYieldCarryClientOptions — e.g. swapClient or a lendingPlatformClients override for tests or custom routing.
What hangs off the client
| Property | Role |
| ------------------------------------------------ | -------------------------------------------------------------------- |
| client.pda | Derive Token and YieldingBank PDAs |
| client.account | Fetch and cache on-chain accounts (LYCToken, LYCYieldingBank) |
| client.ix | Build single Kit Instructions |
| client.tx | Build composed transaction plans (ATAs, wraps, refreshes, CPI pools) |
| client.rpc | Shared RPC used by send helpers |
| client.swap | Aggregate swap client (used by manager flows and some planners) |
| client.getLendingPlatformClient(...) | Resolve the lending client for an on-chain lending position |
| client.getLendingPlatformClientByPlatform(...) | Resolve a lending client when only the platform enum is known |
| client.sendTransaction(...) | Sign, send, confirm, and apply plan cache invalidations |
Minting yTokens (mint_token)
Recommended: client.tx.mintToken.getTx(...)
Use this for end-user flows. It prepends, when needed:
- SOL collateral: create the user’s wSOL ATA (if missing), transfer lamports from the signer,
sync_native. - yToken: idempotent ATA creation for the user’s receipt account.
Then it appends mint_token. The signer must be the wallet that owns the collateral and will receive minted yTokens.
import { BN } from "@anchor-lang/core";
import { address } from "@solana/kit";
const signer = address(walletAddress);
const tokenPda = address("…"); // Token PDA
const mintPlan = await client.tx.mintToken.getTx({
signer,
token: tokenPda,
params: {
depositAmount: new BN(1_000_000_000), // base units, e.g. lamports
},
});
await client.sendTransaction(walletSigner, mintPlan);Burning yTokens (burn_token)
Recommended: client.tx.burnToken.getTx(...)
burn_token redeems from unlent collateral when balances suffice at execution time.
Without asyncFallback, the SDK sends BurnTokenParams.sync; insufficient unlent
balance surfaces as InsufficientUnlentCollateral. Pass asyncFallback (epoch/request
PDAs + BurnTokenParams.async) when the same transaction should create or extend an
async redemption queue instead.
const burnPlan = await client.tx.burnToken.getTx({
signer,
token: tokenPda,
params: {
burnAmount: new BN(500_000_000),
},
});
await client.sendTransaction(walletSigner, burnPlan);When collateral is native SOL (WSOL), the plan appends a close-account after
burn_token so proceeds settle as SOL.
const plainBurn = await client.tx.burnToken.getTx({
signer,
token: tokenPda,
params: {
burnAmount: new BN(500_000_000),
// Optional: include a fallback so the same burn_token instruction creates
// an async request if reserves are short by the time the tx lands.
asyncFallback: {
epochId: new BN(7),
// Omit requestSequence to let the builder read the next epoch sequence.
requestSequence: new BN(0),
},
},
});Async Burns
Async burns group one or more FIFO requests into a redemption epoch. Users still call burnToken; there is no separate user-facing create/request instruction:
await client.tx.burnToken.getTx({
signer,
token: tokenPda,
params: {
burnAmount: new BN(500_000_000),
asyncFallback: {
epochId: new BN(7),
requestSequence: new BN(0),
},
},
});Managers fund closed epochs by running decreaseCarryPosition with
redemptionEpoch when DCP loss should be charged to that epoch, then
topUpUnlentReserves to withdraw freed collateral. processAsyncBurn processes
requests in exact sequence order and rejects out-of-order requests.
Reading on-chain state
Token accounts → LYCToken
const token = await client.account.fetchToken(tokenPda);
// token.address — Token PDA
// token.data — raw IDL shape
// token.mint, token.collateralMint, token.decimals
// token.price, token.totalSupply, token.tvlUsd — UI helpersBatch and discovery:
client.account.fetchTokens([pda1, pda2])— multiple PDAs.client.account.fetchAllTokens()— all Token accounts owned by the program.
Use { fresh: true } to bypass the short-lived in-memory cache when you must see writes immediately:
await client.account.fetchToken(tokenPda, { fresh: true });Yielding bank accounts → LYCYieldingBank
const bank = await client.account.fetchYieldingBank(yieldingBankPda);
// bank.address, bank.data
// bank.baseMint, bank.defaultRedemptionMint, bank.id, bank.decimals // share decimals, canonical 8
// bank.sharePrice, bank.minSharePrice, bank.totalShares — UI helpersAlso: fetchYieldingBanks([...]), fetchAllYieldingBanks().
User holdings → UserHolding[]
Fetch every LYC yToken a wallet holds, with balances and optional USD notionals when you pass a collateral USD priceMap.
const holdings = await client.account.fetchUserHoldings(walletAddress, {
priceMap: { [collateralMint]: usdPrice },
});
// holdings[i].token — LYCToken model (price, label, decimals, …)
// holdings[i].balance — bigint, base units of the yToken
// holdings[i].uiBalance — decimal-adjusted number
// holdings[i].valueUsd — uiBalance × token.price × priceMap[collateralMint], if priced
// holdings[i].tokenAccount — user ATA for the yToken mintWithout a usable entry in priceMap for the collateral mint, valueUsd is omitted (no inference from stale on-chain TVL). Zero-balance holdings are filtered out by default; pass { includeEmpty: true } to include them.
PDAs (when you know mint + id)
const [tokenPda] = await client.pda.deriveTokenPda(yTokenMint, tokenId);
const [bankPda] = await client.pda.deriveYieldingBankPda(baseMint, yieldingBankId);Seeds match the program: TOKEN + mint + id; YIELDING_BANK + base mint + id.
Sending transactions
client.sendTransaction(payer, planOrInstructions, options?) accepts either a LongYieldCarryTransactionPlan (instructions, lookupTables, optional postSuccessCacheInvalidations) or a bare Instruction[]. When you pass a plan, post-success hooks clear SDK caches for affected Tokens / YieldingBanks plus any Kamino-related invalidations. Use the payer’s Kit TransactionSigner expected by common’s signSendAndConfirmTransaction.
Anchor error codes
The bundled IDL still lists InvalidTvlBiasBps: TVL bias configuration was removed on-chain, but the Anchor error variant is retained so numeric error discriminants do not shift versus older program builds. Match errors using IDL-derived names and regenerate TypeScript bindings when you upgrade the deployed program — do not rely on hard-coded Anchor error integers.
