@spield/sdk
v0.4.3
Published
TypeScript SDK for Spield Protocol fixed-rate yield on Stellar Testnet.
Readme
@spield/sdk
TypeScript SDK for the Spield Protocol on Stellar: wrap USDC into yield-bearing shares, split them into principal and yield tokens, trade them, and buy a fixed rate — from one typed client.
v0.4.2 · Stellar Testnet · Node ≥ 18 and modern browsers · ESM + CJS + type declarations
Install
npm install @spield/sdk # or: pnpm add @spield/sdk / yarn add @spield/sdk@stellar/stellar-sdk v16 ships as a dependency — you do not install it separately.
Quick start
import { FreighterSigner, SpieldClient, testnet } from '@spield/sdk';
// Reads need no wallet. Omit `signer` for a read-only client.
const client = new SpieldClient({
network: testnet(), // the default; the SDK ships Testnet only
signer: new FreighterSigner(),
});
// Read
const health = await client.protocol.getHealth();
const srQuote = await client.sr.previewDeposit({ amount: '100' }); // USDC -> SR
const ptQuote = await client.router.quoteBuyPt({ usdcIn: '100' }); // USDC -> PT
// Write
await client.tokens.ensurePtTrustline(); // once per wallet, before receiving PT
await client.router.buyPt({ usdcIn: '100' }); // 1% slippage floor by defaultEvery write simulates before it asks for a signature, submits only after wallet approval, and
resolves to { hash, status: 'success', explorerUrl } once the network confirms it.
The protocol model
USDC wraps into SR, a yield-bearing share token over a Blend strategy. SR splits into PT (principal, redeems 1:1 at expiry) and YT (the yield until expiry). PT trades against SR on the market, and the fixed-rate vault sells a guaranteed coupon out of its PT inventory.
USDC ──sr.deposit──▶ SR ──yield.mintPy──▶ PT + YT
▲ │
└──yield.redeemPy─────┘SR is a share token, so one SR is not one USDC — use sr.previewDeposit / sr.previewRedeem,
or sr.srToUnderlying / sr.underlyingToSr with the current exchangeRate.
Namespaces
| Namespace | What it does |
|---|---|
| client.sr | Wrap USDC into SR shares; unwrap fully or partially; previews, exchange rate, deposit cap |
| client.strategy | Blend rate guard, position value, withdrawable liquidity, claimable emissions |
| client.yield | Mint/redeem PT + YT, read and claim interest, transfer YT, engine solvency |
| client.market | PT/SR quotes and trades, YT trades, LP add/remove, reserves and implied APY |
| client.vault | Fixed-rate quotes, receipts, deposit, redeem, harvest |
| client.router | One-transaction USDC routes for PT, redemption, and yield claims |
| client.tokens | USDC/SR/PT/YT balances and the single required PT trustline |
| client.portfolio | Wallet-wide balances, interest, LP position, vault receipts, in one call |
| client.protocol | Cross-contract health and yield-engine solvency |
Two orchestration helpers live on the client itself: client.buyYtFromUsdc() and
client.sellYtToUsdc() (see Two signatures for YT below).
See USAGE.md for a worked example of every flow.
What you need to know
Amounts. Strings and numbers are human amounts; a bigint is already in base units. Every
protocol token uses 7 decimals. Prices, APYs and exchange rates use 12-decimal fixed point and come
back as exact bigints (plus a …Number / …Pct field where a display value helps).
import { fromBaseUnits, toBaseUnits } from '@spield/sdk';
toBaseUnits('10.25'); // 102500000n
fromBaseUnits(102500000n); // "10.25"One trustline. PT is the only classic asset, so it is the only trustline a wallet needs. The
yield engine contract is the YT token — YT is a contract token and must not get a trustline.
Call client.tokens.ensurePtTrustline() once before any flow that delivers PT.
Quotes lag; execution does not. quote_* are views, and a view cannot synchronize SR's stored
rate — so a quote can slightly over-state a sale. The router already handles this: leave minOut
off and it simulates the real entry point (the same synchronized code path the submission runs) and
applies a 1% floor to that, falling back to the quote when simulation cannot run. Pass an explicit
minPtOut / minUsdcOut to override, or slippageBps to change the floor.
Two signatures for YT. The deployed Blend-backed stack cannot fit a YT wrap+trade or
trade+unwrap into a single Soroban transaction, so client.buyYtFromUsdc() and
client.sellYtToUsdc() deliberately use up to two wallet signatures and report progress:
await client.buyYtFromUsdc({
ytOut: '100',
onProgress: (step, i, total) => console.log(step, `${i}/${total}`),
});A deposit cap is in force. SR caps total deposits — currently 200 USDC on Testnet. Read
sr.getState() for depositCap, depositHeadroom and depositCapReached rather than assuming;
a reached cap blocks new deposits only, never redemptions.
Honest-value reads. sr.exchangeRate is a high-water mark that ratchets up and never falls, so
everything built on it can over-report once Blend takes a real loss. These read the venue with no
monotonicity guard and keep answering while guarded exits are frozen:
await client.sr.getRealizableRate(); // what a share is actually worth
await client.sr.getRealizableValue({ shares }); // a holder's actual pro-rata claim
await client.strategy.getPositionValueUnguarded(); // the whole Blend position, live rate
await client.strategy.getClaimableEmissions(); // unclaimed BLND (0 today, see below)Sitting slightly above the stored rate is normal; only the other direction is interesting.
getClaimableEmissions() reads 0 because Blend pays emissions to XLM suppliers and USDC
borrowers, not the USDC-supplier side Spield is on — allocations rotate each cycle, so that can
change. Claiming is permissionless and pays a destination fixed in contract storage, so an
integrator can trigger it but never redirect it.
Errors
Contract reverts are mapped to plain English from the protocol's own error enum, per contract, so
Error(Contract, #107) arrives as "The deposit would exceed the SR deposit cap. Check the
remaining headroom." The SDK exports SpieldValidationError, SpieldSignerError,
SpieldContractError, SpieldRpcError and SpieldConfigurationError, all carrying a code and
the original cause.
Confirmation polls RPC for up to two minutes by default, because Testnet can index an accepted
transaction well after ledger close. Tune it with confirmationTimeoutMs and
confirmationPollIntervalMs in the client options.
Live Testnet deployment
testnet() tracks the 2026-08-30 redeploy, verified live. src/config.ts is the source of
truth; these are the addresses it carries:
| | Contract |
|---|---|
| SR (share token) | CDYAM3NGY5I3SUGPCDQUS25MGCIWT2YOBDSWYT6SJNIPN6A6OOUSSCZY |
| Blend strategy | CDPNSWSBVBRF52SED6UD7T2VQH6XODLEHXSCFTZHQP73SNYTKIHP5R2B |
| Yield engine (also the YT token) | CDS2Q6L3QCUK4KX633M7QH53GC76EVOUAK7WJ54T3AP3J6IGXIM3LURD |
| PT/SR market | CBL7Z3BONITNSWO7NJLT67464HLVVQF5G3REI3XT6KUCK7YNHPICJX5A |
| Fixed-rate vault | CDKV7Z7FF3DA57LSO2JA6GFKAIDDIDMZD5XWCMV7G5I3E4ZA3NN2NMH7 |
| Router | CDP3VUYH3GEGNOF4XUHKMP5GBTH3SBCL5R3GM5ZTVQAGV7FOAPAQJTGE |
| PT (SAC) | CB3T6FOMAH77Z2FMSA2IEVLQEIYOQRRFP7JMJMJGMUCO6HGOOZJZJ7OC |
| USDC (SAC) | CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU |
PT trustline asset: SPLDPT7, issuer GDTM2UMJEO6LV5HE2SI56IEWNX5OAF5HV2XNZMVZEDXMMPHZUWXSSLQU.
These contracts carry the full current ABI — keep-alives (bumpHolder / bumpLp / bumpReceipt),
caller-bounded LP minting via addLiquidity({ minShares }), resumable vault redemption
(getRedeemRemaining, receipt.collected), vault.getSurplus(), and the honest-value reads above.
Where an older deployment lacks an entry point the SDK degrades instead of throwing: reads fall back
or return null, and addLiquidity uses the three-argument call when minShares is zero.
Scope
Testnet only. There is no mainnet configuration in this SDK. Point it at any other deployment with
customNetwork() — see USAGE.md for the full shape.
Development and verification
pnpm install
pnpm run typecheck
pnpm test # 116 unit + integration tests, no network
pnpm run build
pnpm run test:package # imports the built ESM and CJS bundles
pnpm run test:e2e:testnet
pnpm run test:all # typecheck + test + build + test:package + the local-stack suiteThe Testnet read suite checks SR, Blend liquidity, yield-engine solvency, PT/SR reserves and prices,
vault backing and router quotes against the checked-in live deployment, and cross-checks that the
engine, market and vault all agree on one series expiry. The write smoke test is skipped unless a
disposable funded SPIELD_E2E_SECRET is provided (copy .env.e2e.example); it only creates and
verifies the PT trustline.
Test app and examples
A browser console that exercises every SDK method against Testnet — from the SDK root:
pnpm demo # vite dev server on http://localhost:5173On a fresh clone, install the app's own dependencies first:
cd examples/sdk-test-app && npm install.
Reads work without a wallet. Writing uses the same multi-wallet connector as the Spield frontend (Freighter, xBull, Rabet, Hana, LOBSTR, Albedo) and needs a disposable funded Testnet account. See the demo README.
examples/ also holds six small runnable programs — amount-utils, protocol-health,
market-quotes, portfolio-snapshot, fixed-rate-vault and error-handling. All are read-only,
and amount-utils needs no network at all. Each installs and runs from its own directory:
cd examples/protocol-health && npm install && npm startThey are not pnpm workspace members, so a pnpm --filter from the SDK root will not find them.
