@1delta/calldata-sdk-sol
v0.0.11
Published
⚒️ Builds Solana transactions for the lenders the SDK serves — the peer of @1delta/calldata-sdk: same entrypoints, instructions instead of composer calldata, artifacts that expire. No viem, no Solana runtime.
Downloads
1,365
Readme
@1delta/calldata-sdk-sol
Moved from
lending-sdks-sol/packages/calldata-sdkon 2026-10-02. Paths are relative to this package; the prototype's@1delta/sol-*names map to@1delta/calldata-sdk-sol,margin-fetcher-sol,svm-kit.
The peer of @1delta/calldata-sdk: the same entrypoints, building Solana
transactions instead of composer calldata. What the EVM package encodes for one
contract, this one fetches from each lender's hosted builder as instructions,
adds what those builders leave out, and seals — a transaction that expires in
~60–90 s, delivered in the SvmTransaction envelope the EVM API already defines
for chainType: 'svm'.
There is no composer on Solana (SOLANA_ARCHITECTURE_PLAN §3). The names are kept
anyway — ComposerDirectLending.composeDirectMoneyMarketAction,
ComposerQuickActions.swapAndDeposit, SvmTxUtilsLite — so the API routes port
by import path, the way the tracer ports yield-tracer's ingest.
src/
types/ SvmCallParams (instructions + tables + splits) ← EVMCallParams; mergeCallParams
lending/
types/ LendingOperation, QuickActionType, LendingMode — the EVM shape, reduced
actions/ KaminoLending (/ktx/klend/*-instructions), JupiterLendLending (/borrow/operate-instructions),
SaveLending (the program's OWN instructions — no hosted builder; SAVE_FINANCE.md §3)
direct/ ComposerDirectLending: the single entrypoint + depositAndBorrow / repayAndWithdraw
utils/ getLenderData (lender key → market/reserve, vault, or Save market/reserve), toDecimalAmount
consts/ U64_MAX, MIN_I128, the API bases
spot/ ComposerSpot.jupiterSwap — Jupiter /swap-instructions, the instruction form
quickActions/ ComposerQuickActions — swapAndDeposit, swapDepositAndBorrow, swapAndRepay, borrowAndSwap, …
vaults/ ComposerVaultActions — kvault deposit/withdraw, Jupiter Lend earn deposit/withdraw/mint/redeem
vaults/exponent/ ComposerExponentActions — Exponent PT trades from the VENDORED IDL, the program as the quoter (EXPONENT.md §3)
vaults/solstice/ ComposerSolsticeActions — Solstice deposits, the three exits and their claims from the app-bundled IDLs; the program as the quoter by post-simulation balances (SOLSTICE.md §6)
vaults/huma/ ComposerHumaActions — Huma 2.0 PST deposit (lender setup prepended), the queued redemption and its cancel, the instant withdrawal quoted by simulation (docs/HUMA.md §8)
txUtils/ SvmTxUtilsLite — assemble: tables, blockhash, pack, simulate, budget, price, seal; `indexPatches` resolve instruction indexes a flash pair carries
flashloan/ the flash providers (Jupiter Lend's program, Kamino's pair, Save's pair, Project 0's bracket) + `ComposerMargin` — open / close / adjust / migrate a loop on any venue (LOOPING.md)
lending/actions/loopscale/ the venue-built loop: `/creditbook/flash_borrow` with our swap as its CPI, the returned message enveloped and simulatedOne direct action
import {
ComposerDirectLending,
QuickActionType,
LendingMode,
SvmTxUtilsLite,
} from '@1delta/calldata-sdk-sol'
import { createSvmRpc, createSvmFetchers } from '@1delta/svm-kit'
const bundle = await ComposerDirectLending.composeDirectMoneyMarketAction({
amount: 10_000_000n, // raw units, as on EVM
lender: 'KAMINO_7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF',
chainId: 'solana',
actionType: QuickActionType.Deposit,
lendingMode: LendingMode.NONE,
account: wallet, // signer, fee payer, receiver
callerAssetAddress: 'So11111111111111111111111111111111111111112',
lenderAssetAddress: 'd4A2prbA2whesmvHaL88BH6Ewn5N4bTSU2Ze8P6Bc4Q', // the marketUid ref: Kamino's RESERVE
decimals: 9, // Kamino's builder speaks decimal amounts
})
const fetchers = createSvmFetchers(createSvmRpc())
const { transactions, fees } = await SvmTxUtilsLite.assemble(bundle, {
fetchers,
payer: wallet,
})
// transactions[0]: { chainType: 'svm', transaction, lastValidBlockHeight, expiresAt, addressLookupTables, instructions }isAll on a withdraw / repay sends the lender's own sentinel — Kamino
u64::MAX (in decimal form, 18446744073.709551615 at 9 decimals), Jupiter
Lend MIN_I128, Save raw u64::MAX (a withdraw's is in cTOKENS; a partial
withdraw is converted at the reserve's current exchange rate, floored). Jupiter
Lend actions other than a fresh deposit need jupiterLendParams.positionId
(positions are NFTs; the user-data layer lists them). Save needs nothing beyond
the lender key and the reserve: the obligation is DERIVED from the wallet
(saveParams.obligationIndex picks a sub-account) and read from the chain; a
wallet with none gets the allocation
InitObligationin front of its first deposit.
Save — built from the program, not a builder
SaveLending (actions/save/) has no counterparty to call. It resolves the
market's address book from /v1/markets/configs, the wallet's obligation from
the chain, and the refresh set from the obligation's own legs, then emits ONE
atomic list per action: RefreshReserve × every reserve the obligation touches
→ RefreshObligation → the action, with the obligation's deposit reserves as
trailing accounts in obligation order. Its 11 encoders are pinned byte-exact
against @solendprotocol/[email protected]
(test/fixtures/save-instructions.json), and test/save.live.test.ts proves
the LISTS by simulating a borrow, a withdraw and a fresh-wallet deposit against
mainnet through assemble. Before returning, the builder reads the Pyth
receiver accounts of the refresh set and refuses (SaveStaleOracleError) when a
price is older than 100 s — the program would refuse the refresh at 120 s, and
posting a price update is not built. SaveLending.flashLoanPair wraps a body in
FlashBorrow … FlashRepay(index); both must stay top-level (the program
rejects CPI) and index is the borrow's ABSOLUTE position in the transaction.
Only the assembler knows that (it prepends one ComputeBudget instruction on the
simulation probe and two on the sealed message, and drops duplicate idempotent
ATA creates), so a builder never writes it: it emits the bundle-relative index
plus an indexPatches entry ({ ix, dataOffset, target, width }) and
assemble rewrites the bytes per segment — refusing a bracket that would span a
cut. Project 0's start_flashloan(end_index) is the same mechanism with a
u64. Both pairs are simulated on mainnet through it (save.live.test.ts,
project0.live.test.ts).
Huma — the queue's id and the instant path
ComposerHumaActions (vaults/huma/) encodes from the recovered on-chain IDL.
Three facts the program never states in it, each pinned by a test: a new
redemption request's id is last_request_id + 1 (next_request_id is the
processor's FIFO head — an existing request's id whenever the queue is
non-empty); the request PDA seed is the u128 id big-endian; and a wallet
without a LenderState needs create_lender_accounts_v2 before its first
deposit (the builder reads the state and prepends it). The instant exit is
permissionless with a progressive fee by liquid-asset ratio, so
instantWithdraw quotes it by running it (quoteInstantWithdraw, the Solstice
quoter's mechanism) and seals with max_fee = quote × (1 + tolerance).
ComposerVaultActions routes provider: 'huma' for deposit and redeemShares;
test/huma.live.test.ts simulates all three paths on mainnet.
What the hosted builders do and do not do
| | Kamino /ktx/*-instructions | Jupiter Lend /borrow/operate-instructions |
| ----------------------------- | ----------------------------------- | --------------------------------------------------------------------- |
| amount | decimal string | raw, signed (+ deposit/borrow, − withdraw/repay) |
| ATA creation, SOL wrap/unwrap | included | absent — this package adds them (actions/jupiter-lend/setup.ts) |
| compute-unit limit | included (stripped, we set our own) | absent |
| priority fee | absent | absent |
| lookup tables | contents included (lutsByAddress) | addresses only — resolved by RPC |
| one-shot (deposit + borrow) | two lists, concatenated | one operate with both legs |
| "all" sentinel | u64::MAX | MIN_I128 per side |
Measured 2026-09-16 (fixtures in test/fixtures/). Jupiter Lend's builder
simulates to learn a fresh position's nftId, so the signer must hold SOL;
a wallet that cannot run the transaction answers
{"message":"No return data found in logs"}.
Composed actions and the byte cap
A quick action is a concatenation: swap leg, lending leg(s), each from its own
builder, with a split recorded at every boundary. The assembler packs
greedily — as many instructions as fit in 1,232 bytes and 64 account locks — and
cuts only at a split; a swap → Kamino deposit fit in one 928-byte
transaction live (tables carry it), a cut is the fallback and Assembled.split
plus the description suffix (i/n) say so. A segment that depends on an
earlier one (a deposit of a swap's output) cannot be simulated by the chain; it
is budgeted at fallbackComputeUnits and the fee report says
simulated: false.
The lending leg is sized on the swap's guaranteed output
(otherAmountThreshold for ExactIn, outAmount for ExactOut) because a hosted
lender instruction takes a fixed amount — there is no "deposit the contract
balance". The surplus stays in the wallet. Prefer ExactOut quotes for a repay.
Fee policy
SvmFeePolicy in svm-kit/fees.ts: simulate → unitsConsumed × (1 + margin) →
getRecentPrioritizationFees over the transaction's writable accounts → the
median sample, floored at 1 000 µL/CU, capped at 2 M, and a hard cap of 0.02 SOL
on the priority fee (refused, not clipped). Every number is overridable per
call.
Testing
pnpm test— offline. The hosted builders' responses are fixtures; the message compiler is proven against them insvm-kit(both venues serve the same request sealed and unsealed, and our compile of their instructions is byte-identical to their transaction —order: 'address'for Kamino's@solana/kit, first-appearance for Jupiter'sweb3.js).pnpm test:live— the builders and a public RPC: Kamino deposit, Jupiter Lend deposit with our setup, swap → deposit; each assembled, simulated and budgeted. Nothing is sent.
Exponent — a venue with no hosted builder
Exponent (Pendle on Solana) publishes no quote or build endpoint; its SDK is
AGPL on web3.js. So this package builds the leg itself: the CLMM and core IDLs
are vendored (vaults/exponent/idl/, the ABI, address-pinned), MarketThree /
Vault decode through svm-kit's IDL codec, and trade_pt /
trade_pt_exact_out / buy_pt / sell_pt are encoded from the IDL with every
account taken off the market account (the SY program's CPI accounts are ALT
indexes on it).
import {
ComposerExponentActions,
readMarketThree,
} from '@1delta/calldata-sdk-sol'
const market = await readMarketThree(fetchers, clmmMarket) // from the tracer's exponent_meta.market_address
const q = await ComposerExponentActions.quote(
{ market, direction: 'SyToPt', amountIn: 1_000_000_000n }, // no wallet needed: runs as the pool
{ fetchers },
)
// q = { amountOut: 1014764897n, exact: true, source: 'simulation', ptPriceInSy: 0.98545, unitsConsumed: 127806 }
const leg = await ComposerExponentActions.tradePt(
{
market: clmmMarket,
account: wallet,
direction: 'SyToPt',
amountIn: q.amountIn,
amountConstraint: (q.amountOut * 995n) / 1000n,
}, // 50 bps
{ fetchers, market },
)
const { transactions } = await SvmTxUtilsLite.assemble(leg, {
fetchers,
payer: wallet,
})The quote IS a simulation of the instruction: trade_pt returns
TradePtReturnData and the RPC hands it back, so fees, tick crossings and spot
limits are the program's own. A size-only quote borrows the pool's identity
(sigVerify: false), a wallet quote runs as the wallet. midQuote is the
no-RPC estimate off the API mid, marked exact: false. buy_pt / sell_pt
(base ↔ PT) are encoded but not yet simulation-verified on a funded wallet —
EXPONENT.md §4.
Solstice — three exits, one composer
Solstice (USX yield tokens: eUSX, SR-/JR-strcUSX) publishes no IDL and no public
builder; the app bundles both programs' IDLs and those are vendored
(@1delta/margin-fetcher-sol's vaults/solstice/idl/). Every user instruction
is encoded from them and checked against them position by position in
test/solstice.test.ts. What makes the composer more than an encoder is the
exit: eUSX unlocks into a 7-day escrow, senior leaves instantly at a fee or
through a 15-day cooldown, junior queues into a settlement batch — and the
per-user records behind the queued paths must be inited once.
import {
ComposerSolsticeActions,
ComposerVaultActions,
} from '@1delta/calldata-sdk-sol'
import { fetchSolsticeVaults } from '@1delta/margin-fetcher-sol'
const ctx = { rpc, vaults: await fetchSolsticeVaults({ rpc }) }
const lock = await ComposerSolsticeActions.deposit(
{ vault: 'eUSX', account, amount: 1_000_000_000n },
ctx,
)
const q = await ComposerSolsticeActions.simulate(
{ vault: 'JR-strcUSX', direction: 'deposit', amount: 1_000_000_000n },
ctx,
) // the program's number
const req = await ComposerSolsticeActions.requestRedeem(
{ vault: 'SR-strcUSX', account, shares },
ctx,
) // init + senior_unlock_queued
const now = await ComposerSolsticeActions.redeemInstant(
{ vault: 'SR-strcUSX', account, shares },
ctx,
) // 10 bps
const usx = await ComposerSolsticeActions.claim(
{ vault: 'JR-strcUSX', account, batch: 'AUG-26-26' },
ctx,
)
// or through the uniform surface:
await ComposerVaultActions.deposit(
{ provider: 'solstice', vault: 'eUSX', account, amount, decimals: 6 },
{ solstice: ctx },
)solstice.live.test.ts simulates every one of them on mainnet as a borrowed
program-owned identity — deposits agree with the ledger estimate to 1e-6, the
exits land where the row says, and the paths that must fail fail on the
program's own check.
Leverage — ComposerMargin
Every venue loops through its OWN stack (LOOPING.md): a flash pair (Jupiter
Lend's flashloan program at 0 bps, Kamino's reserve pair, Save's) or a bracket
(Project 0's start_flashloan … end_flashloan) around depositAndBorrow and a
Jupiter swap, in one atomic list. The composer sizes from the caller's oracle
price and the venues' fees (origination fees are grossed up — they come OUT of
the borrowed amount), quotes the swap under the 64-lock budget, tries every
flash candidate cheapest-first × two routes until the sealed message fits 1,232
bytes, and hands one SvmCallParams to assemble, which simulates it.
import {
ComposerMargin,
MarginTradeType,
SvmTxUtilsLite,
} from '@1delta/calldata-sdk-sol'
const build = await ComposerMargin.createMarginFlashLoan(
{
account: wallet,
marginData: {
marginTradeType: MarginTradeType.Open,
collateral: {
lender: 'KAMINO_7u3He…',
lenderAssetAddress: SOL_RESERVE,
mint: WRAPPED_SOL,
decimals: 9,
},
debt: {
lender: 'KAMINO_7u3He…',
lenderAssetAddress: USDC_RESERVE,
mint: USDC,
decimals: 6,
},
},
input: { mint: WRAPPED_SOL, amount: 100_000_000n }, // 0.1 SOL
targetLeverage: 2,
slippageBps: 100,
priceCollInDebtE18: usdcPerSolRawE18, // from the rows' oracle prices
elevationGroup: 2, // Kamino: requested between the deposit and the borrow
},
{ rpc, jupiterSwapApi, save: { rpc }, project0: { rpc, publicData } },
)
// build.meta: { flash: { provider, feeBps }, sizing: { side, flashAmount, … }, swap, lockBudget, route, bytes, accountKeys }
const { transactions } = await SvmTxUtilsLite.assemble(build, {
fetchers,
payer: wallet,
})What the live gate found (test/margin.live.test.ts, mainnet simulation,
2026-09-20): a 2× SOL/USDC open fits one transaction on Jupiter Lend (vault 1),
Kamino main (existing obligation), Save main and Project 0; a 3× JitoSOL/SOL
open on Kamino requests elevation group 2 in the list. A Kamino loop on a wallet
with no obligation is refused (KaminoObligationRequired — the hosted borrow
builder needs it to exist; deposit first). A same-asset MIGRATE Kamino → Save
measures 1,243 B — 11 over — and is refused with the measurement
(TransactionTooLarge); a lookup table of ours for the pairs' keys is the
lever. Loopscale loops are venue-built: LoopscaleLending.openLoop (or
ComposerMargin.createLoopscaleLoop), /creditbook/flash_borrow with the
wallet in a user-wallet header and cpiIxs[].data in base64 (the docs say
base58; base58 is a 500).
Public RPCs 429 a full run of the live gate (each build reads the venue's
accounts, quotes three flash providers and measures); set SVM_RPC_URLS or run
the cases one at a time.
Not here
- Exponent YT, liquidity, orderbook, post-maturity redeem — one more IDL instruction each, on the same method (EXPONENT.md §4).
- The 1delta lookup tables themselves —
flashloan/lut.tsplans them andpnpm flash-lut --keypair <file> --sendcreates them; until the addresses are inONEDELTA_FLASH_LOOKUP_TABLESnothing compresses beyond the venues' own tables. - A plain spot swap — SOLANA_PLAN §5.1 relays Jupiter's sealed transaction; the instruction form here exists for composition.
- Token-2022 detection — the ATA derivation needs the mint's program; pass
jupiterLendParams.tokenProgramfor a Token-2022 mint (PYUSD). The token list knows.
