npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elmntl/jlpd-sdk

v2.0.0

Published

SDK for JLP.D (JLP Deconstructed) by Elemental — p-stv-core, jlpd-strategy, and elemental-lend clients

Readme

@elmntl/jlpd-sdk

TypeScript SDK for the Elemental Vaults on-chain stack. Wraps three Solana programs through one consistent surface so frontends, indexers, and back-end services don't need to hand-roll discriminators, PDA derivation, or account layouts.

LP surface vs admin/operator surface

An LP-facing frontend only ever needs the root export of pStvCore (and, read-only, elementalLend/jlpdStrategy for dashboards). Genesis/operator/ keeper-only builders are namespaced OUT of the root under .admin so they never show up in LP-facing autocomplete or bundles — this is a deliberate surface fence, not an oversight (round-6 hardening; breaking re-org, see CHANGELOG).

| Surface | What's there | Who calls it | | --- | --- | --- | | pStvCore.* (root) | createDepositIx, createRequestWithdrawIx, createClaimWithdrawIx, createInstantWithdrawIx, createProcessDelayedDepositIx (round-7 LOW-5 — permissionless, completes an LP's own delayed deposit); buildDepositContext, buildClaimWithdrawContext; checkDepositReadiness/checkClaimReadiness/checkRequestWithdrawReadiness; fetchVaultDashboard; PDA/account/event/error/price/LUT/send-tx helpers | LP frontends, wallets | | pStvCore.admin.* | createInitOrUpdateStvIx, createSeedStvIx, createAcceptVaultAdminIx, createCloseStvIx, createAddManagerIx/createRemoveManagerIx, createDepositToStrategyIx/createWithdrawFromStrategyIx, createOverrideClaimWithdrawIx, createProcessEpochIx, createTripCircuitBreakerIx, freeze/unfreeze WR + evX builders, migrate admin builders (createMigrateLendIx/createMigrateRequestIx/createMigrateExecuteIx/createMigrateCancelIx), buildProcessEpochContext/buildDepositToStrategyContext | Vault admin (cold/warm), managers, keepers — genesis, epoch cranking, sweep cranking, freeze/override circuit breakers. Never an LP flow. | | elementalLend.admin.* | Strategy init/close/reset, manager-role management, manager-signed kVault/Jupiter Lend sweep/unsweep builders, buildProtocolActionIxs (round-7 MED-3 — manager-signed protocol deposit/withdraw, derives a ManagerRole PDA) | Manager/keeper, invoked by p-stv-core's own admin flows — never an LP | | jlpdStrategy.admin.* | Strategy-state init/close, config init, manager-role management | Admin/keeper genesis only |

elemental-lend-v2 is experimental and undeployed — not exported from the SDK root at all (no elementalLendV2 namespace; the module stays on disk, unpublished).

@elmntl/jlpd-sdk
├── /common          shared types, buffer helpers, connection type, discriminators
├── /p-stv-core      vault management — deposits, withdraws, epochs, fees      (level N)
├── /elemental-lend  idle-base lending sweep into Kamino + Jupiter Lend Earn   (level N − 1)
├── /jlpd-strategy   JLP-Deconstructed yield strategy + lend-adapter wiring    (level N + 1 / N + 2)
└── /onyc-strategy   onyc/USDC strategy (STV 16) — error-code messages today

At a glance

| Module | Wraps program | What it gives you | | ---------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @elmntl/jlpd-sdk/p-stv-core | PSTVH77GiPqA3msXmpjAyUXdh3MytK37GCPbPzWu3Rc | STV / GlobalConfig / WithdrawRequest / ManagerRole accounts; all 14 instruction builders, 5 LP-facing at root (createDepositIx, createRequestWithdrawIx, createClaimWithdrawIx, createInstantWithdrawIx, createProcessDelayedDepositIx) + 9 admin/operator under .admin (createProcessEpochIx, createMigrateLendIx, …) — see "LP surface vs admin/operator surface" above; event decoders (18 event types); the sendSmartTx smart sender (auto CU sim, priority fee, LUT merge); wSOL wrap/unwrap helpers; USD price fetcher; fetchVaultDashboard (live NAV/pps/evX-position reader); resolveVaultError (on-chain VaultError → LP-facing message) | | @elmntl/jlpd-sdk/elemental-lend | EHaGVh7p6xSxZsq4CcEEZhnV5adDvWg1gS1Rc5rLiBpC | StrategyState / StvPosition / ManagerRole accounts; CPI + protocol instruction builders; Kamino kVault and Jupiter Lend Earn account-resolution helpers; auto-route / auto-unroute account builders for sweep flows | | @elmntl/jlpd-sdk/jlpd-strategy | GXqt4ZH2UUBsLWwMNJiZMXza3q7xEGChfW8XjVRjLxr5 | JlpdConfig / per-asset StrategyState / StvPosition; CPI + rebalance instruction builders (createSwapJlpIx, createSettleYieldIx, createDepositToAdapterIx, …); Jupiter swap quote / instruction fetchers; on-chain JLP price + custody readers | | @elmntl/jlpd-sdk/onyc-strategy | 8PGHppQRB8GSN7Mh1Mptv7J1WZ1JqZCghG7AawrNoJZ | resolveOnycStrategyError — the onyc/USDC strategy's OWN OnycStrategyError space mapped to LP-facing messages (separate error space from p-stv-core's VaultError — see that function's doc). Narrower today than the other modules: no PDA/account/instruction helpers yet, error messages only | | @elmntl/jlpd-sdk/common | (no program) | Buffer reads with bounds checks, ATA derivation, the canonical SolanaConnection structural type, the StrategyStateHeader / StvPosition interfaces shared across every strategy program, and the generic resolveProgramError/extractCustomProgramErrorCode custom-error parser every program's error map is built on |

Module layout

Every program-specific module follows the same file shape, so you only have to learn the pattern once:

<module>/
├── constants.ts           program ID, seeds, instruction discriminators, sizes, flags
├── pda.ts                 findXxxPda() helpers — pure synchronous PDA derivation
├── types.ts               account interfaces + event types
├── accounts.ts            deserializeXxx() + fetchXxx() readers + GPA queries
├── instructions.ts        createXxxIx() instruction builders
├── events.ts              event decoders (p-stv-core: 1-byte disc; Anchor: 8-byte)
└── (module-specific files)

Module-specific files:
  p-stv-core:       remaining-accounts.ts, send-tx.ts, sol-wrap.ts, prices.ts,
                     readiness.ts, withdraw-requests.ts, errors.ts, dashboard.ts
  elemental-lend:   kamino-vault.ts, jupiter-lend.ts, protocol-actions.ts
  jlpd-strategy:    swap-jlp.ts, settle-yield.ts, jlp-data.ts,
                    jlp-borrow.ts, jupusd-earn.ts, adapter.ts

onyc-strategy is an exception to the standard shape above: it wraps a strategy program p-stv-core CPIs into, not a program this SDK issues its own instructions against, so it currently only has errors.ts. Its own numeric Custom(n) error space is entirely separate from p-stv-core's — see resolveOnycStrategyError's doc for how the two relate.

Naming conventions hold across modules:

| Pattern | Returns | Side effects | | --------------- | ------------------------- | ------------ | | findXxxPda | [PublicKey, number] | none | | deserializeXxx| typed account | none | | fetchXxx | typed account | one RPC call | | createXxxIx | TransactionInstruction | none | | buildXxx* | helper objects | none |

build* is reserved for helpers that produce ephemeral data (remaining accounts, batched instruction lists). Anything that returns a wire-ready instruction is named createXxxIx.

Naming exceptions worth knowing

Each program has its own ManagerRole PDA scoped to a different anchor account. To prevent same-named imports from shadowing each other, the helpers carry a per-program prefix:

| Program | Function | PDA seeds | | --------------- | ------------------------- | ---------------------------------------- | | p-stv-core | findStvManagerRolePda | ["manager", stv, manager] | | elemental-lend | findLendManagerRolePda | ["manager", strategy_state, manager] | | jlpd-strategy | findJlpdManagerRolePda | ["manager", config, manager] |

Cross-cutting design

SolanaConnection (structural type)

Every helper that needs RPC accepts SolanaConnection, defined in common/connection.ts as a Pick<Connection, ...> of just the methods we use. This means callers can pass any version of @solana/web3.js without nominal type mismatches across SDK / consumer versions. Narrower call sites use a further Pick (e.g. ProtocolActionConnection in elemental-lend/protocol-actions.ts is Pick<SolanaConnection, "getAccountInfo" | "getMultipleAccountsInfo">).

Numeric convention (BN vs number)

Documented at the top of p-stv-core/types.ts:

  • u64 fields are deserialized as BN (bn.js)
  • u32 / u16 / u8 fields are deserialized as plain JavaScript number

Mixing the two does not auto-convert and silently truncates — when adding new fields to any account interface, follow the rule.

Ix builder u64 args accept BN | number | bigint. createDepositIx / createRequestWithdrawIx / createWithdrawFromStrategyIx / createInstantWithdrawIx / createSeedStvIx / InitOrUpdateStvArgs.vaultId / findStvPda / findEvMintPda (and every other u64-typed builder arg in p-stv-core/instructions.ts and pda.ts) accept a native bigint in addition to BN/numberfetchVaultDashboard returns bigint throughout (see that section below), so a displayed value flows straight into a deposit/withdraw amount with no new BN(x.toString()) bridge. All three input types serialize byte-identically for the same logical value (common/toBN, the single normalization point every u64 write funnels through); common/__tests__/buffer.test.ts and p-stv-core/__tests__/bigint-args.test.ts hex-compare BN vs number vs bigint encodings to lock this down.

Discriminators

  • p-stv-core (Pinocchio) uses 1-byte instruction discriminators (0x00–0x0C, 13 instructions). Account discriminators are still 8 bytes for Anchor compatibility.
  • elemental-lend and jlpd-strategy are Anchor 0.32.1 programs and use 8-byte instruction discriminators throughout.
  • Account discriminators that are truly shared across programs (ManagerRole, StrategyState, StvPosition) live once in common/constants.ts and are re-exported by each module.

Each constant is documented inline with the source — for example IX_ADD_MANAGER in elemental-lend has a // sha256("global:add_manager")[..8] comment so an auditor can re-derive it.

Buffer reads

common/buffer.ts provides bounds-checked readers (readPubkey, readU64, readU32, readU16, readU8) and the corresponding writers / optional-encoders. Each read helper validates offset + width <= data.length and throws a descriptive RangeError rather than silently reading garbage when the offset is wrong.

Send-tx layer

p-stv-core/send-tx.ts exposes sendSmartTx(connection, instructions, payer, signTransaction, options?):

  1. Pre-checks the legacy serialized size to decide if a versioned tx is needed (no double-sign).
  2. Fetches blockhash + every default LUT (lend / stv / jlpd) + Helius priority-fee in parallel.
  3. Simulates with an inflated CU limit, takes the actual unitsConsumed, applies a 5% buffer, and prepends the right ComputeBudgetProgram instructions.
  4. Falls back from legacy → versioned automatically when CU instructions push the size over MAX_LEGACY_SIZE.
  5. Confirms via lastValidBlockHeight.

The signTransaction callback is generic over Transaction | VersionedTransaction so wallets pass the same callback the SDK uses on either path. The optional rpcUrl field in SmartTxOptions is the only way the SDK reaches a Helius endpoint — it never reads private fields off the connection object.

Address Lookup Tables

p-stv-core/send-tx.ts exports DEFAULT_LUT_ADDRESSES:

{
  lend: PublicKey,   // Elemental Lend infrastructure
  stv:  PublicKey,   // p-STV Core vaults / evMints / vault ATAs
  jlpd: PublicKey,   // JLPD Strategy state / positions / oracles
}

sendSmartTx always fetches all three. Consumers building transactions outside sendSmartTx should import these and merge them into their own LUT list. Server-side jobs (jlpd-server/src/jobs/ltv-rebalance.ts) use this same export — the SDK is the single source of truth.

Installation

npm install @elmntl/jlpd-sdk
# or
yarn add @elmntl/jlpd-sdk
# or
pnpm add @elmntl/jlpd-sdk

@elmntl/jlpd-sdk is the only current package. @elemental-stv-core/sdk and @elemental-vaults/sdk are earlier names for this same package — @elemental-stv-core/sdk is deprecated on the npm registry (frozen at 0.13.4, missing the 0.14.0 breaking claim-redirect removal) and neither alias receives new releases. If you see either name in an older script or doc in this monorepo, treat it as historical and install @elmntl/jlpd-sdk instead — npm install @elemental-stv-core/sdk from a clean environment gets you a stale, unmaintained copy with a real ABI difference (see CHANGELOG.md's 0.14.0 entry).

Peer dependencies must be installed in the consumer:

npm install @solana/web3.js@^1.95.0 @solana/spl-token@^0.4.0

Quick examples

Deposit base into a vault from a frontend

Every deposit into a strategy-linked STV must carry a strategy NAV tail. Core CPIs the strategy's update_aum to price pre-deposit NAV, so remainingAccounts needs [strategyState(w), stvPosition(ro), strategyProgram(ro)] (plus a lend tail first, if the vault has lendProgram active) — and protocolAumCount/autoRouteCount must be wired from the SAME buildDepositContext call that built remainingAccounts. All three are optional params on createDepositIx that silently default to empty/zero — dropping any one of them compiles fine and builds an instruction that reverts on-chain (IncompleteStrategyAccounts) with no TypeScript error to warn you. The example below is copy-paste correct for vault 16 ("onyc", base USDC, strategy-linked, no lend) — spread the FULL ctx object into createDepositIx, never just one field off it.

import BN from "bn.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
  findConfigPda,
  findStvPda,
  findEvMintPda,
  fetchStv,
  buildDepositContext,
  createDepositIx,
  checkDepositReadiness,
  sendSmartTx,
} from "@elmntl/jlpd-sdk/p-stv-core";
import { findAta, createIdempotentAtaIx } from "@elmntl/jlpd-sdk/common";

// Inputs assumed from the caller's context:
//   connection: Connection — RPC connection
//   user: PublicKey        — the connected wallet's public key
//   signTransaction        — the connected wallet's sign callback

const VAULT_ID = 16; // onyc / USDC

const [config] = findConfigPda();
const [stv] = findStvPda(VAULT_ID);
const [evMint] = findEvMintPda(VAULT_ID);

// 1. Fetch the STV. Needed both to build the VaultInfo below (baseMint,
//    lendProgram, strategy) and to derive the ATAs that follow (baseMint,
//    feeReceiver).
const stvState = await fetchStv(connection, VAULT_ID);

// 2. Preflight the strategy-freshness gate BEFORE building a transaction — a
//    stale strategy reverts StaleStrategyData. This mirrors that on-chain
//    gate exactly, so the UI can show "vault pricing is refreshing, try again
//    shortly" instead of letting the user pay gas for a guaranteed revert.
//
//    EXHAUSTIVE switch, not a single `=== "stale"` check (round-7 MED-1) —
//    `DepositReadiness` has 4 members; blocking on only one and falling
//    through on everything else means a NON-"ok", NON-"stale" status (e.g.
//    `unknownFreshnessCapability`, which p-stv-core's own capability
//    registry can legitimately return for a strategy program this SDK
//    doesn't ship canonical support for) silently proceeds into a
//    deterministic on-chain revert instead of being surfaced to the LP.
const readiness = await checkDepositReadiness(connection, stvState);
switch (readiness.status) {
  case "ok":
    break; // proceed to build + send below
  case "stale":
    throw new Error(readiness.hint); // or: disable the deposit button and render readiness.hint
  case "unsupportedAllocator":
    throw new Error("this helper does not preflight allocator vaults — see checkDepositReadiness's own doc");
  case "unknownFreshnessCapability":
    // Every strategy this SDK ships canonical support for (onyc, JLPD) is
    // ALWAYS registered — see common/strategy-refresh.ts's own doc — so
    // reaching this branch for vault 16 specifically would itself be a bug
    // report, not a routine "try again" state. Still handled explicitly:
    // never silently treat an unrecognized status as safe to proceed past.
    throw new Error(
      `no freshness capability registered for strategy ${readiness.strategyProgramId.toBase58()}`,
    );
}

// 3. Build the full remaining-accounts tail (lend, if active — onyc/vault 16
//    has none — plus the strategy pair + strategy program).
const ctx = await buildDepositContext(connection, {
  address: stv,
  baseMint: stvState.baseMint,
  lendProgram: stvState.lendProgram,
  strategy: stvState.strategy,
  tokenProgram: TOKEN_PROGRAM_ID,
});

const vaultAta = findAta(stvState.baseMint, stv, TOKEN_PROGRAM_ID);
const userBaseAta = findAta(stvState.baseMint, user, TOKEN_PROGRAM_ID);
const userEvAta = findAta(evMint, user, TOKEN_PROGRAM_ID);
const feeReceiverEvAta = findAta(evMint, stvState.feeReceiver, TOKEN_PROGRAM_ID);

const amount = new BN(1_000_000_000); // 1,000 USDC (6 decimals)
const minShares = new BN(0); // set a real slippage floor in production

const ix = createDepositIx({
  user, config, stv, vaultAta, userBaseAta, userEvAta, evMint, feeReceiverEvAta,
  baseMint: stvState.baseMint, tokenProgram: TOKEN_PROGRAM_ID,
  amount, minShares,
  // The three fields a broken example once dropped — always spread the FULL ctx:
  remainingAccounts: ctx.remainingAccounts,
  protocolAumCount: ctx.protocolAumCount,
  autoRouteCount: ctx.autoRouteCount,
});

const sig = await sendSmartTx(
  connection,
  [
    ...ctx.preInstructions,
    createIdempotentAtaIx(user, evMint, user, TOKEN_PROGRAM_ID), // no-op if userEvAta already exists
    ix,
  ],
  user,
  signTransaction,
);

A parity test (src/p-stv-core/__tests__/readme-deposit-example.test.ts) builds this exact instruction and asserts it is account-for-account and byte-identical to the mainnet-proven p-stv-core/scripts/deposit-onyc-stv.ts's hand-built equivalent — so a regression here fails npm test, not a real user's transaction.

Staleness: preflight-and-block, or self-heal (opt-in)

The example above uses Pattern A — preflight and block: check checkDepositReadiness and disable the deposit button on "stale", adding no extra RPC call on the happy path. There is a second, opt-in pattern for a UI that would rather absorb the refresh itself than show a "try again shortly" message:

Pattern B — self-heal. Pass { selfHealStaleness: true } as buildDepositContext's 4th argument and it looks up the linked strategy's freshness CAPABILITY (round-6 — no age-sampling, no on-chain read of the strategy's last_updated at all): a needsExternalRefresh strategy (e.g. onyc) gets a registered refresh instruction bundled into ctx.preInstructions unconditionally, every call — not "only when a sampled timestamp looks stale" — because an extra refresh is idempotent and ~cheap on-chain, and unlike any age-sampling scheme it stays correct at the actual transaction-submission boundary, not just at the moment this function ran. A selfRefreshing strategy is a no-op. This is the SAME capability model buildProcessEpochContext("lockPps") already uses for the crank path (see below) — registerStrategyRefreshRequirement/getStrategyFreshnessCapability (@elmntl/jlpd-sdk/common) is the shared registry both go through.

import "@elmntl/jlpd-sdk/onyc-strategy"; // registers onyc's refresh_aum builder AND its needsExternalRefresh capability — required for self-heal to find it

const ctx = await buildDepositContext(connection, vault, payer, { selfHealStaleness: true });
// ctx.preInstructions is [] for a selfRefreshing strategy (or no strategy at
// all), or [refreshAumIx] for a needsExternalRefresh one like onyc — either
// way, spread it in.
const ix = createDepositIx({ /* ... */ remainingAccounts: ctx.remainingAccounts, protocolAumCount: ctx.protocolAumCount, autoRouteCount: ctx.autoRouteCount });
const sig = await sendSmartTx(connection, [...ctx.preInstructions, ix], user, signTransaction);

selfHealStaleness is off by default — omitting the 4th argument (or passing {} / { selfHealStaleness: false }) is byte-for-byte the same buildDepositContext behavior this SDK has always had; existing callers are unaffected unless they explicitly opt in. Like buildProcessEpochContext, this throws StrategyRefreshRequiredError — fails closed — if the strategy needs an external refresh and no refresh builder is registered for it, and throws a plain Error if the strategy's capability was never registered at all (UNKNOWN — round-6: there is no default in either direction; see common/strategy-refresh.ts's own doc for why an earlier "unregistered = self-refreshing" default silently misclassified canonical JLPD). Covered by src/p-stv-core/__tests__/deposit-self-heal.test.ts, including a dedicated back-compat suite proving the option's absence changes nothing.

Which instructions need the strategy tail?

remainingAccounts + protocolAumCount/autoRouteCount are not always required — only the instructions that compute a LIVE strategy NAV need them. Verified per-handler against p-stv-core/programs/p-stv-core/src (a "user vs. manager" grouping does NOT track this — check per instruction):

| Instruction | Needs the strategy tail? | Staleness threshold | | --- | --- | --- | | deposit | Yes, when stv.strategy != PublicKey.default and the STV is not an allocator | stv.strategyStalenessThreshold \|\| DEFAULT_STRATEGY_STALENESS_THRESHOLD (24 h default) | | instant_withdraw | Yes (same rule) | same 24 h-default field | | process_delayed_deposit | Yes (same rule) | same 24 h-default field | | deposit_to_strategy / withdraw_from_strategy (manager-only) | Yes, but via named strategyState/stvPosition args, not remainingAccounts | same 24 h-default field | | process_epochfirst call (stv.epochPps == 0) | Yes | PROCESS_EPOCH_STRATEGY_STALENESS_THRESHOLD30 seconds, not 24 h; buildProcessEpochContext(connection, vault, "lockPps") checks this itself and auto-bundles the refresh (see below) rather than leaving the caller to remember it | | process_epochcontinuation call (stv.epochPps != 0) | No — price is already locked; remainingAccounts is the list of WithdrawRequest PDAs to settle instead | n/a | | request_withdraw | No — no NAV computation at all | n/a | | claim_withdraw | No STRATEGY tail — pays out at the already-locked wr.pps, never CPIs the strategy. But NOT bare-instruction-safe on a lend-active vault (round-5 MED-1): core unconditionally parses claim_withdraw's 4 fixed LEND accounts before checking idle liquidity at all (claim.rs:348) — ALWAYS build through buildClaimWithdrawContext, never a bare createClaimWithdrawIx, regardless of checkClaimReadiness's status. See "Check withdrawal-request status" below. | n/a |

protocolAumCount/autoRouteCount/autoUnrouteCount are non-zero only when the vault has an active lendProgram (stv.lendProgram != PublicKey.default) — onyc/vault 16 has none, so all are always 0 for it. buildDepositContext / buildClaimWithdrawContext / buildDepositToStrategyContext / buildProcessEpochContext compute the correct non-zero values for vaults that DO have lend active — including, as of round-5, auto-unroute for a Kamino-registered protocol, not just a static Jupiter-pool lookup (MED-2; see buildClaimWithdrawContext's own doc comment in remaining-accounts.ts). Always take the counts from the same context object you took remainingAccounts from.

DEFAULT_STRATEGY_STALENESS_THRESHOLD and PROCESS_EPOCH_STRATEGY_STALENESS_THRESHOLD are exported from @elmntl/jlpd-sdk/p-stv-core — read the real values from there rather than hardcoding them a third time.

process_epoch's first call: the strategy refresh is bundled for you

The mainnet-proven crank (process-epoch-onyc.ts) always sends onyc's own refresh_aum immediately before process_epoch's first call in the SAME transaction — onyc's update_aum CPI (the one process_epoch itself triggers) does not stamp last_updated fresh for an already-solvent strategy, so without the standalone refresh_aum, the 30-second staleness check reverts StaleStrategyData on essentially every call.

buildProcessEpochContext(connection, vault, "lockPps") reproduces this for you: it checks the linked strategy's registered freshness CAPABILITY (round-6 — not a sampled last_updated timestamp) and, for a needsExternalRefresh strategy (onyc, JLPD), looks up a StrategyRefreshBuilder registered for vault.strategy and populates ctx.preInstructions with the refresh instruction UNCONDITIONALLY — every call, not only "when a sample looks stale" — since an extra refresh is idempotent and ~free on-chain, and this is the only way to stay correct at the actual submission boundary rather than at the moment this function happens to sample state. A selfRefreshing strategy is a no-op (no extra instruction, no extra RPC read). If no builder is registered for a needsExternalRefresh strategy program, it throws StrategyRefreshRequiredError — fails closed — rather than silently handing back a context that's guaranteed to revert on submission.

This is admin/keeper-only (epoch cranking) — buildProcessEpochContext and createProcessEpochIx live under the .admin fence (round-6 MED-1), not the LP root:

import { buildProcessEpochContext, createProcessEpochIx } from "@elmntl/jlpd-sdk/p-stv-core/admin";
// Registers onyc's refresh_aum builder as a side effect — required for the
// auto-bundling above to find it for an onyc-linked vault.
import "@elmntl/jlpd-sdk/onyc-strategy";

const ctx = await buildProcessEpochContext(connection, vault, "lockPps");
const ix = createProcessEpochIx({
  payer, config, stv, evMint, feeReceiverEvAta, escrowEvAta, vaultAta, tokenProgram,
  advanceEpoch: false,
  remainingAccounts: ctx.remainingAccounts,
  protocolAumCount: ctx.protocolAumCount,
});

// For a `needsExternalRefresh` strategy (onyc, jlpd) ctx.preInstructions ALWAYS
// contains the bundled refreshAumIx — the refresh is unconditional, never
// freshness-sampled. Spread it in verbatim; do not reimplement conditional logic.
const sig = await sendSmartTx(connection, [...ctx.preInstructions, ix], payer, signTransaction);

A parity test (src/p-stv-core/__tests__/process-epoch-context.test.ts) builds this exact two-instruction bundle and asserts it is account-for-account and byte-identical to process-epoch-onyc.ts's own hand-built [refreshAumIx, processEpochIx] pair.

For a strategy program other than onyc, call registerStrategyRefreshBuilder(programId, builder) (@elmntl/jlpd-sdk/common) yourself before calling buildProcessEpochContext — see onyc-strategy/refresh-aum.ts for the shape a builder implements.

Preflight a request-withdraw

request_withdraw has no strategy tail and no on-chain NAV computation — its own guard chain is a set of vault/protocol flags, the current epoch state, and the caller's own evX balance. checkRequestWithdrawReadiness mirrors that chain, in the SAME order request_withdraw.rs checks it, completing the preflight trilogy alongside checkDepositReadiness/checkClaimReadiness:

import {
  checkRequestWithdrawReadiness,
  findWithdrawRequestPda,
  createRequestWithdrawIx,
  fetchGlobalConfig,
} from "@elmntl/jlpd-sdk/p-stv-core";
import { findAta } from "@elmntl/jlpd-sdk/common";
import BN from "bn.js";

const shares = new BN(500_000); // evX to queue for withdrawal

const globalConfig = await fetchGlobalConfig(connection); // optional — omit to let the function fetch it itself
const readiness = await checkRequestWithdrawReadiness(connection, stvState, { user, shares, globalConfig });

switch (readiness.status) {
  case "ok":
    break; // proceed to build + send below
  case "insufficientEvBalance":
    console.log(`have ${readiness.available.toString()} evX, need ${readiness.requested.toString()}`);
    return;
  case "seedFloorViolation":
    console.log(`would drop supply to ${readiness.supplyAfterBurn.toString()}, below the seed floor of ${readiness.floor.toString()}`);
    return;
  case "epochAlreadyProcessed":
    console.log("the crank is mid-cycle — try again after it advances to the next epoch");
    return;
  default:
    // protocolPaused / vaultPaused / withdrawalsDisabled / vaultNotSeeded /
    // wrongWithdrawMode / noSharesRequested, plus the existingRequest*
    // family (round-7 MED-4 — a same-epoch top-up onto an already-open
    // WithdrawRequest that's frozen / migrate-kind / has invalid
    // flags-or-destination-fields / fails PDA self-verification / epoch- or
    // user-mismatched / already priced; see readiness.ts's own doc on
    // RequestWithdrawReadiness for the full list) — disable the button;
    // each needs a different admin action or client-side fix, not a retry.
    console.log(readiness.status);
    return;
}

const [config] = findConfigPda();
const [withdrawRequest, wrBump] = findWithdrawRequestPda(stv, user, stvState.currentEpochId);
const userEvAta = findAta(evMint, user, TOKEN_PROGRAM_ID);
const escrowEvAta = findAta(evMint, stv, TOKEN_PROGRAM_ID);

const ix = createRequestWithdrawIx({
  user, config, stv, withdrawRequest, userEvAta, escrowEvAta, evMint,
  tokenProgram: TOKEN_PROGRAM_ID, shares, wrBump,
});
const sig = await sendSmartTx(connection, [ix], user, signTransaction);

Covered by src/p-stv-core/__tests__/request-withdraw-readiness.test.ts — one case per guard, in handler order, plus the bigint/BN/number acceptance of the shares param.

Check withdrawal-request status and enumerate a user's requests

"claimable" and "claimableLendActive" mean ELIGIBLE TO ATTEMPT with a successfully built claim context — NEITHER status, by itself, guarantees claim_withdraw will succeed (round-5 MED-1). checkClaimReadiness is a unified, fail-closed preflight that mirrors execute_claim/claim_withdraw.rs's own check order exactly: protocol-wide pause, then wr.pps == 0, then migrate-kind, then frozen, then STV-paused, then the claim timelock, then the withdraw rate limit, then vault liquidity. A request can look superficially "ready" (priced, timelock passed) and still be guaranteed to revert for any of the earlier reasons — those are now first-class statuses (protocolPaused, migrateRequest, frozen, vaultPaused, rateLimited), not folded into a narrower "claimable" the way an earlier version of this helper did.

On ANY lend-active vault, "claimable" is not enough on its own — you MUST build through buildClaimWithdrawContext, never a bare createClaimWithdrawIx. Core unconditionally parses this instruction's 4 fixed lend accounts BEFORE it ever checks idle liquidity (claim.rs:348) — so even a plain "claimable" status reverts on a lend-active vault if you build a bare instruction without them. buildClaimWithdrawContext derives those accounts (plus the mandatory protocol-AUM segment and, if needed, an auto-unroute route) for you — see "Claim base from an unlocked WithdrawRequest" below and step 5 of the full walkthrough. This SDK cannot predict the protocol's own available liquidity, so neither status is a guarantee the on-chain unsweep will find enough — only that the TRANSACTION ITSELF is well-formed and eligible to attempt.

"claimableLendActive" is NOT a block, despite the name overlap with "vaultUnderfunded"'s shape. Both fire when idle vault_ata balance alone is short of the owed amount, but for a lend-active STV (stv.lendProgram != PublicKey.default), execute_claim refreshes lend AUM and unsweeps the exact deficit from the lend protocol BEFORE checking liquidity (utils/claim.rs:338) — only if the lend protocol itself can't cover the deficit does the claim actually revert. This SDK does not read protocol-specific available-liquidity state (Kamino reserve liquidity, Jupiter Lend pool liquidity, …) to predict that outcome, so it reports this distinct, non-blocking status instead of guessing wrong in either direction. Attempt on "claimable" OR "claimableLendActive" (always via buildClaimWithdrawContext); treat every other status as non-sendable. (Vault 16 / onyc has no lend program, so it can only ever report "claimable" or the genuine "vaultUnderfunded" block — this distinction matters for a lend-active vault.)

import {
  fetchUserWithdrawRequests,
  checkClaimReadiness,
  buildClaimWithdrawContext,
  fetchGlobalConfig,
} from "@elmntl/jlpd-sdk/p-stv-core";

// Fetch GlobalConfig once and pass it to both calls below — optional (each
// function fetches it itself when omitted), but saves an RPC round trip when
// checking readiness for more than one request in the same render.
const globalConfig = await fetchGlobalConfig(connection);

// All open (still-claimable-or-pending) requests for this user on this STV,
// each with a derived display status — pending / migrateRequest / frozen /
// vaultPaused / protocolPaused / priced / rateLimited / claimable /
// claimableLendActive / awaiting-liquidity — plus gross/net/fee payout
// figures. Returns [] once a request has been claimed — the WithdrawRequest
// account is closed on claim, so there is nothing left to decode a "claimed"
// status from; treat "present before, absent now" as claimed/closed in the UI.
const requests = await fetchUserWithdrawRequests(connection, { stv, user, globalConfig });

// Or, for a single already-known request:
const wr = await fetchWithdrawRequest(connection, stv, user, epochId);
const readiness = await checkClaimReadiness(connection, stvState, wr, globalConfig);
switch (readiness.status) {
  case "claimable":
  case "claimableLendActive":
    // Both are ELIGIBLE TO ATTEMPT — always build through
    // buildClaimWithdrawContext (never a bare createClaimWithdrawIx), so a
    // lend-active vault's mandatory lend accounts are present. See step 5
    // of the full walkthrough below for the complete build+send.
    break;
  case "vaultUnderfunded":
    console.log(`vault needs ${readiness.owed.sub(readiness.available).toString()} more base before this claim can pay out`);
    break;
  case "rateLimited":
    console.log(`daily withdraw cap reached: ${readiness.dailyWithdrawnBase.toString()} / ${readiness.dailyLimit.toString()} — try again after the window resets`);
    break;
  case "notYetClaimable":
    console.log(`claimable after ${new Date(readiness.availableAfter * 1000).toISOString()}`);
    break;
  case "migrateRequest":
  case "frozen":
  case "vaultPaused":
  case "protocolPaused":
  case "epochNotCranked":
    // disable the claim button; each of these needs a different admin/manager action, not a retry from the user
    break;
}

Render an LP dashboard (NAV, pps, and a user's position)

fetchVaultDashboard mirrors the EXACT NAV/pps formula p-stv-core's own on-chain code uses (utils/nav.rs::compute_nav/compute_pps) for a single-strategy, no-lend, non-allocator STV — onyc/vault 16 is exactly this shape. It returns both raw bigints (for further math) and pre-formatted decimal strings (for display) — see formatRawAmount's doc for why this module uses bigint instead of BN/Number (no float precision loss on large values). It throws for a lend-active or allocator vault rather than silently under-reporting NAV — read the thrown message for what's missing.

import { fetchVaultDashboard } from "@elmntl/jlpd-sdk/p-stv-core";

const dashboard = await fetchVaultDashboard(connection, { stv, user });

console.log(`NAV: ${dashboard.navFormatted}`);
console.log(`Price per share: ${dashboard.ppsFormatted}`);
console.log(`Total evX supply: ${dashboard.evSupplyFormatted}`);

if (dashboard.user) {
  console.log(`Your balance: ${dashboard.user.evBalanceFormatted} evX`);
  console.log(`Your value: ${dashboard.user.valueBaseFormatted}`);
  console.log(`Your share: ${(dashboard.user.shareOfVaultBps / 100).toFixed(2)}%`);
}

Staleness note: for a strategy-linked vault, this NAV is only as fresh as the strategy's last_updateddashboard.strategyLastUpdated is included at no extra RPC cost (the account is already being read for pps), but this helper does not gate on it. Pair it with checkDepositReadiness before enabling a deposit button; see that function's own doc/example above.

A parity test (src/p-stv-core/__tests__/readme-dashboard-example.test.ts) runs this exact snippet against a fixture connection and asserts every displayed line matches the documented values — including a cross-check against p-stv-core's own Rust NAV unit-test fixtures (instructions/user/deposit.rs's test_nav_with_single_strategy and friends), not just this SDK's own arithmetic.

End-to-end LP journey: connect → view → deposit → request-withdraw → (operator crank) → claim

Every piece above, chained into one walkthrough for vault 16. This is the full LP lifecycle — the pieces are individually parity-tested (see each section above); this section just shows them in sequence. Two steps (marked below) happen OFF the LP's device: an operator has to crank process_epoch before a queued request becomes claimable — that's the same operational dependency checkClaimReadiness's "epochNotCranked" status surfaces, not something the LP's own wallet can do.

import BN from "bn.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
  findConfigPda, findStvPda, findEvMintPda, findWithdrawRequestPda,
  fetchStv, fetchVaultDashboard, fetchUserWithdrawRequests,
  buildDepositContext, createDepositIx, checkDepositReadiness,
  createRequestWithdrawIx, checkRequestWithdrawReadiness,
  buildClaimWithdrawContext, createClaimWithdrawIx, checkClaimReadiness,
  sendSmartTx,
} from "@elmntl/jlpd-sdk/p-stv-core";
import { findAta, createIdempotentAtaIx, resolveProgramAwareError } from "@elmntl/jlpd-sdk/common";
import "@elmntl/jlpd-sdk/onyc-strategy"; // registers onyc's error map + refresh_aum builder

const VAULT_ID = 16; // onyc / USDC
const [config] = findConfigPda();
const [stv] = findStvPda(VAULT_ID);
const [evMint] = findEvMintPda(VAULT_ID);

// ---- 1. VIEW: connect wallet, render the LP's current position ----
const dashboard = await fetchVaultDashboard(connection, { stv, user });
console.log(`NAV ${dashboard.navFormatted}, your evX ${dashboard.user?.evBalanceFormatted ?? "0"}`);

// ---- 2. DEPOSIT ----
const stvState = await fetchStv(connection, VAULT_ID);
const depositReadiness = await checkDepositReadiness(connection, stvState);
// Exhaustive, not a single `=== "stale"` check — see "Deposit base into a
// vault from a frontend" above for why every non-"ok" status needs handling.
switch (depositReadiness.status) {
  case "ok":
    break;
  case "stale":
    throw new Error(depositReadiness.hint); // or use selfHealStaleness: true — see "Deposit base into a vault" above
  case "unsupportedAllocator":
    throw new Error("this helper does not preflight allocator vaults");
  case "unknownFreshnessCapability":
    throw new Error(`no freshness capability registered for strategy ${depositReadiness.strategyProgramId.toBase58()}`);
}
const depositCtx = await buildDepositContext(connection, {
  address: stv, baseMint: stvState.baseMint, lendProgram: stvState.lendProgram,
  strategy: stvState.strategy, tokenProgram: TOKEN_PROGRAM_ID,
});
try {
  const depositIx = createDepositIx({
    user, config, stv, evMint, tokenProgram: TOKEN_PROGRAM_ID,
    baseMint: stvState.baseMint,
    vaultAta: findAta(stvState.baseMint, stv, TOKEN_PROGRAM_ID),
    userBaseAta: findAta(stvState.baseMint, user, TOKEN_PROGRAM_ID),
    userEvAta: findAta(evMint, user, TOKEN_PROGRAM_ID),
    feeReceiverEvAta: findAta(evMint, stvState.feeReceiver, TOKEN_PROGRAM_ID),
    amount: new BN(1_000_000_000), // 1,000 USDC
    minShares: new BN(0), // set a real slippage floor in production
    remainingAccounts: depositCtx.remainingAccounts,
    protocolAumCount: depositCtx.protocolAumCount,
    autoRouteCount: depositCtx.autoRouteCount,
  });
  await sendSmartTx(
    connection,
    [...depositCtx.preInstructions, createIdempotentAtaIx(user, evMint, user, TOKEN_PROGRAM_ID), depositIx],
    user, signTransaction,
  );
} catch (err) {
  const { entry } = resolveProgramAwareError(err);
  showToast(entry.message);
}

// ---- 3. REQUEST WITHDRAW (some time later, when the LP wants out) ----
// RE-FETCH stvState here — do not reuse step 2's copy. Core derives the
// WithdrawRequest PDA (and enforces every stv.* gate in
// checkRequestWithdrawReadiness) against the CURRENT epoch/flags at
// submission time; a cached stvState from step 2 can point at a stale
// stvState.currentEpochId if any time (even just a slow user, let alone a
// crank in between) has passed since it was fetched — a stale-epoch PDA
// derivation silently targets the WRONG WithdrawRequest account.
const requestStvState = await fetchStv(connection, VAULT_ID);
const shares = new BN(500_000);
const requestReadiness = await checkRequestWithdrawReadiness(connection, requestStvState, { user, shares });
if (requestReadiness.status === "ok") {
  const [withdrawRequest, wrBump] = findWithdrawRequestPda(stv, user, requestStvState.currentEpochId);
  const requestIx = createRequestWithdrawIx({
    user, config, stv, withdrawRequest, evMint, tokenProgram: TOKEN_PROGRAM_ID, shares, wrBump,
    userEvAta: findAta(evMint, user, TOKEN_PROGRAM_ID),
    escrowEvAta: findAta(evMint, stv, TOKEN_PROGRAM_ID),
  });
  await sendSmartTx(connection, [requestIx], user, signTransaction);
}

// ---- 4. OPERATOR CRANK (OFF the LP's device — a keeper calls process_epoch;
//         see "process_epoch's first call" above) — the LP just polls status: ----
// The crank ADVANCES stv.currentEpochId and re-prices requests — this is
// exactly why step 5 below re-fetches stvState yet again rather than reusing
// requestStvState from step 3.
const myRequests = await fetchUserWithdrawRequests(connection, { stv, user });
for (const item of myRequests) {
  console.log(`epoch ${item.wr.epochId}: ${item.status}`); // "pending" until the crank prices it
}

// ---- 5. CLAIM (once an operator has cranked and the timelock has passed) ----
// RE-FETCH stvState here too, for the same reason as step 3 — the crank in
// step 4 may have changed stv.currentEpochId / lendProgram / pause flags
// since either earlier fetch. ALWAYS build through buildClaimWithdrawContext
// — never a bare createClaimWithdrawIx — even for a status of plain
// "claimable". Core unconditionally parses 4 fixed lend accounts before it
// ever checks idle liquidity (claim.rs:348), so a lend-active vault's claim
// fails outright without them; "claimable"/"claimableLendActive" mean
// ELIGIBLE TO ATTEMPT with a successfully built context, never "guaranteed
// to succeed with a bare instruction" (round-5 MED-1). Vault 16/onyc has no
// lend program, so this context is always empty for it — but the SAME code
// path here is what makes this snippet correct on a lend-active vault too,
// which is the point.
const claimStvState = await fetchStv(connection, VAULT_ID);
for (const item of myRequests) {
  const readiness = await checkClaimReadiness(connection, claimStvState, item.wr);
  if (readiness.status !== "claimable" && readiness.status !== "claimableLendActive") continue;
  // 3rd arg `user` (payer) is required for the Kamino stale-vault refresh
  // (round-6 MED-4) — WITHOUT it, a funded stale Kamino position's refresh
  // is silently omitted and the claim reverts StaleProtocolData for a
  // lend-active vault. Vault 16/onyc has no lend program, so this is a
  // no-op for it, but the SAME call here is what makes this snippet correct
  // on a lend-active vault too — never omit it, same discipline as the
  // deposit context's own payer argument below.
  const claimCtx = await buildClaimWithdrawContext(connection, {
    address: stv, baseMint: claimStvState.baseMint, lendProgram: claimStvState.lendProgram,
    strategy: claimStvState.strategy, tokenProgram: TOKEN_PROGRAM_ID,
  }, user);
  const claimIx = createClaimWithdrawIx({
    user, config, stv, tokenProgram: TOKEN_PROGRAM_ID,
    withdrawRequest: item.address, // fetchUserWithdrawRequests already gives you the WR's own PDA
    vaultAta: findAta(claimStvState.baseMint, stv, TOKEN_PROGRAM_ID),
    userBaseAta: findAta(claimStvState.baseMint, user, TOKEN_PROGRAM_ID),
    feeReceiverBaseAta: findAta(claimStvState.baseMint, claimStvState.feeReceiver, TOKEN_PROGRAM_ID),
    baseMint: claimStvState.baseMint,
    remainingAccounts: claimCtx.remainingAccounts,
    protocolAumCount: claimCtx.protocolAumCount,
    autoUnrouteCount: claimCtx.autoUnrouteCount,
  });
  await sendSmartTx(connection, [...claimCtx.preInstructions, claimIx], user, signTransaction);
}

(item.address/item.wr/item.status above are fetchUserWithdrawRequests's per-request UserWithdrawRequest fields — see "Check withdrawal-request status" above for the full shape. This snippet omits per-call error handling after step 2 for brevity; wrap every sendSmartTx call in the same try {} catch { resolveProgramAwareError(err) } pattern shown there. Steps 3 and 5 each re-fetch stvState rather than reusing an earlier copy — see the comments inline; this exact bug class (a cached, stale-epoch stvState producing a wrong WithdrawRequest PDA) is why src/p-stv-core/__tests__/readme-e2e-parity.test.ts transcribes and byte-verifies this full sequence, not just its individual steps.)

Turn a caught transaction error into a human-readable message

resolveProgramAwareError is the recommended entry point. A bare Custom(n) code is ambiguous without knowing which program raised it: p-stv-core CPIs into strategy programs as an UNMAPPED tail call (a strategy's revert propagates unchanged, in ITS OWN numbering), and p-stv-core's VaultError (0..=76) collides with onyc's OnycStrategyError (0..=29) on every code from 0-29 with unrelated meanings (Custom(0) = MathOverflow vs NotStvSigner; Custom(2) = Unauthorized vs StvStrategyMismatch). resolveProgramAwareError reads the transaction's log trail to determine which program actually threw — the FIRST Program <pid> failed: custom program error: 0x<hex> line, with the program id AND the code ALWAYS read from that SAME line (never a program id from one line paired with a code found elsewhere — a failed: line with no code of its own establishes nothing and is skipped; verified against real production log parsing, see common/program-error.ts's module doc) — and resolves against THAT program's registered map automatically, so a frontend calling this one function cannot reach for the wrong map.

import { resolveProgramAwareError } from "@elmntl/jlpd-sdk/common";
import "@elmntl/jlpd-sdk/onyc-strategy"; // side-effect import — registers onyc's map (see note below)

try {
  const sig = await sendSmartTx(connection, [ix], user, signTransaction);
} catch (err) {
  const { status, entry, programLabel } = resolveProgramAwareError(err);
  showToast(entry.message); // safe to render directly to the LP
  console.error(`[${status}${programLabel ? ` via ${programLabel}` : ""}] ${entry.hint}`); // for your own logs
}

Registration is a side effect of importing a program's module. Importing anything from @elmntl/jlpd-sdk/p-stv-core (which you already do for fetchVaultDashboard/deposit builders/etc.) registers VAULT_ERROR_MAP automatically — no extra step. Onyc's map is registered the same way by importing @elmntl/jlpd-sdk/onyc-strategy; if your app never otherwise imports anything from it, add the bare side-effect import shown above so resolveProgramAwareError can also recognize a revert that originated inside onyc's CPI (e.g. deposit/instant_withdraw/process_epoch, all of which CPI onyc's update_aum). getRegisteredProgramErrorMaps() lets you confirm what's currently registered; registerProgramErrorMap(programId, label, map) registers any additional program (jlpd-strategy, elemental-lend, or a third-party strategy) the same way.

status tells you how confident the result is — always check it before treating entry as definitive:

| status | Meaning | | --- | --- | | "resolved" | The origin program was identified from the logs and IS registered — entry/programId/programLabel are all populated. | | "unregisteredProgram" | The origin program WAS identified, but no map is registered for it — entry.hint names the program id so you can register it. | | "ambiguous" | A code was found but no genuine runtime Program <pid> failed: custom program error: 0x<hex> line was present to identify the origin (no logs at all, or logs that stop short of one — e.g. a truncated window showing only invoke frames). candidates lists every registered map's interpretation; the resolver never guesses from an unresolved invoke frame alone (an X invoke with no matching X failed/X success is NOT proof X is the one that failed — the omitted suffix could equally contain X success followed by a DIFFERENT program's failure). Fetch the transaction's full logs (connection.getTransaction) and re-resolve for a definitive answer. | | "unparsed" | No Custom(n) code could be found at all — likely not a program-level revert (blockhash expiry, insufficient SOL for fees, a wallet/RPC error). |

Narrower alternatives — use these directly ONLY when you already know for certain which single program raised the revert (e.g. request_withdraw, which never CPIs a strategy at all): resolveVaultError (p-stv-core only, from @elmntl/jlpd-sdk/p-stv-core) and resolveOnycStrategyError (onyc only, from @elmntl/jlpd-sdk/onyc-strategy) skip program identification entirely and resolve directly against one map. Both error maps carry a drift-guard test (src/p-stv-core/__tests__/errors.test.ts, src/onyc-strategy/__tests__/errors.test.ts) that re-parses each program's error.rs at test time and fails if the map and the Rust source disagree on even one code or name.

Parity tests (src/common/__tests__/readme-error-handling-example.test.ts, src/common/__tests__/program-aware-error.test.ts) run this exact catch block — and the collision case it exists to solve — against fixture errors and assert the rendered message is both correct AND free of raw program jargon (Custom(, 0x.., Rust type/enum names) an LP wouldn't understand.

Read a JLPD strategy state from a server job

import { fetchJlpStrategyState } from "@elmntl/jlpd-sdk/jlpd-strategy";

const state = await fetchJlpStrategyState(connection, baseMint);
console.log(`PPS: ${state.pps.toString()}`);
console.log(`base_loaned: ${state.baseLoaned.toString()}`);

Build a manager-only swap_jlp rebalance from base → JLP

import { buildSwapJlpTransaction } from "@elmntl/jlpd-sdk/jlpd-strategy";

const result = await buildSwapJlpTransaction({
  connection,
  manager,
  baseMint,
  vaultId,
  direction: "BaseToJlp",
  amount: 1_000_000n,
  slippageBps: 30,
});

const sig = await sendSmartTx(
  connection,
  [...result.preInstructions, ...result.instructions],
  manager,
  signTransaction,
  { additionalLuts: result.addressLookupTables },
);

Quality bar

This SDK is the artifact submitted for external audit. The following invariants are enforced:

  • tsc strict mode with noUnusedLocals and noUnusedParameters. Zero suppressions in source.
  • No any in source code. All wallet / connection seams use proper structural types.
  • No @deprecated exports. Deprecated aliases were removed during the audit cleanup; if you find one, it's a bug.
  • Buffer reads are bounds-checked. Every helper in common/buffer.ts throws on out-of-range offsets.
  • Discriminators are documented inline with their derivation (// sha256("global:deposit")[..8]).
  • Each module follows the same file shape so the audit pattern is the same per program.

Project structure

sdk/
├── README.md                       (this file)
├── package.json
├── tsconfig.json                   strict + noUnusedLocals + noUnusedParameters
├── src/
│   ├── index.ts                    namespace re-exports
│   ├── common/
│   │   ├── index.ts
│   │   ├── constants.ts            shared discriminators, PPS_DECIMALS, BPS, staleness
│   │   ├── connection.ts           SolanaConnection structural type
│   │   ├── buffer.ts               bounds-checked readers / writers
│   │   ├── ata.ts                  findAta()
│   │   ├── strategy-interface.ts   StrategyStateHeader + StvPosition + decoders + generic strategy PDA finders
│   │   └── program-error.ts        ProgramErrorEntry + extractCustomProgramErrorCode + resolveProgramError (generic, no program-specific data)
│   ├── p-stv-core/                 vault management — Pinocchio program (13 instructions, 0x00–0x0C)
│   │   ├── constants.ts, pda.ts, types.ts, accounts.ts, instructions.ts, events.ts
│   │   ├── remaining-accounts.ts   context builders for deposit/claim/epoch/strategy ops
│   │   ├── readiness.ts            checkDepositReadiness / checkClaimReadiness preflight helpers
│   │   ├── withdraw-requests.ts    fetchUserWithdrawRequests + per-request status derivation
│   │   ├── errors.ts               VAULT_ERROR_MAP + resolveVaultError — VaultError -> LP message (drift-guard tested vs error.rs)
│   │   ├── dashboard.ts            fetchVaultDashboard — live NAV/pps/evX-position reader for an LP dashboard
│   │   ├── send-tx.ts              smart tx: simulate CU → priority fee → LUT → sign → confirm
│   │   ├── sol-wrap.ts             native SOL wrap/unwrap for SOL-denominated vaults
│   │   └── prices.ts               Jupiter Quote API USD price fetcher
│   ├── elemental-lend/             idle-base lending — Anchor 0.32.1 program
│   │   ├── constants.ts, pda.ts, types.ts, accounts.ts, instructions.ts
│   │   ├── kamino-vault.ts         Kamino kVault account derivation + CPI + staleness check
│   │   ├── jupiter-lend.ts         Jupiter Lend pool configs + auto-route account builders
│   │   └── protocol-actions.ts     high-level protocol action tx builders
│   ├── jlpd-strategy/              JLP yield strategy — Anchor 0.32.1 program
│   │   ├── constants.ts, pda.ts, types.ts, accounts.ts, instructions.ts
│   │   ├── swap-jlp.ts             Jupiter swap tx builders (BaseToJlp / JlpToBase)
│   │   ├── settle-yield.ts         settle_yield instruction builder
│   │   ├── jlp-data.ts             JLP pool data reader (custody weights, AUM, prices)
│   │   ├── jlp-borrow.ts           Jupiter Lend Borrow constants + position reading
│   │   ├── jupusd-earn.ts           Jupiter Lend Earn position + APR helpers
│   │   └── adapter.ts              JLPD Lend Adapter instruction builders
│   └── onyc-strategy/              onyc/USDC strategy (STV 16) — errors only today, see "Module layout" above
│       └── errors.ts               ONYC_STRATEGY_ERROR_MAP + resolveOnycStrategyError (drift-guard tested vs onyc's error.rs)
├── dist/                           tsc output (published to npm; also committed for internal `file:` link consumers)
└── scripts/
    └── create-luts.ts              admin tool: create / extend address lookup tables