@kestrelfi/lyc-sdk
v1.0.16
Published
Client for the long_yield_carry Anchor program: PDA derivation, account decoding, instruction builders, and high-level transaction plans.
Readme
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 "bn.js";
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.
Emergency circuit-breaker CLI
From the monorepo root, disable a yielding bank's circuit breaker with its PDA:
pnpm run lyc:disable-yb-cb -- prod <YIELDING_BANK_PDA>Pass --keypair <PATH> to select an explicit admin keypair on test. On prod,
the bank admin must be a configured Squads vault; the command fails closed
otherwise. It creates a proposal that must be approved and executed before the
circuit breaker is disabled.
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 |
Conservative lending-position flow band
For tokens with both a regular carry position and a conservative (lowest configured target-utilization) position, ordinary collateral flows use a 10%–20% band around the 15% strategic allocation:
- New lent collateral fills the conservative position toward 20% of total post-deposit lent collateral, then routes the remainder to regular positions.
- Outflows use the conservative position first down to 10% of total post-withdrawal lent collateral, then use regular positions.
- Async redemptions preserve the 10% floor while other positions and existing unlent reserves are available, but may cross it as a final liveness fallback.
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 "bn.js";
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);Minting from any Jupiter-routable token
The input does not have to be the yToken's configured collateral. Set inputMint
to any mint for which Jupiter can find a route and the builder will create one
atomic transaction plan that:
- creates any required associated token accounts and wraps native SOL when needed;
- swaps the exact
depositAmountfrominputMintinto the configured collateral; and - deposits the received collateral and mints yTokens.
The Jupiter swap is inserted before mint_token, so the user does not need to
swap or hold the collateral in a separate transaction.
For example, to spend exactly 10 USDC to mint a SOL-collateralized yToken:
import BN from "bn.js";
import { address } from "@solana/kit";
const signer = address(walletAddress);
const tokenPda = address(lycTokenPda);
const usdcMint = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const mintPlan = await client.tx.mintToken.getTx({
signer,
token: tokenPda,
params: {
inputMint: usdcMint,
depositAmount: new BN(10_000_000), // 10 USDC in 6-decimal base units
slippageBps: 50, // optional; defaults to 50 (0.5%)
},
});
// Send the complete plan, not only mintPlan.instructions: Jupiter routes may
// require the address lookup tables included in mintPlan.lookupTables.
await client.sendTransaction(walletSigner, mintPlan);depositAmount is always denominated in base units of inputMint. You may pass
"all" to spend the wallet's entire input-token balance. When the input is native
SOL, "all" keeps 0.01 SOL in the wallet for transaction fees and rent. For other
input tokens, the SDK must know the mint's token program to resolve "all"; use an
explicit BN amount if the mint is not registered by the SDK.
The swap is exact-in. slippageBps controls the minimum collateral output accepted
by Jupiter, and the builder deposits that guaranteed output without consuming any
collateral the user already held. Any execution surplus remains in the user's
collateral account. Building the plan fails without sending a transaction if
Jupiter cannot find a route or the quote is otherwise invalid.
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.
