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

@instadapp/fluid-sui-read-sdk

v0.3.0

Published

Read-only TypeScript SDK for Fluid on Sui, shaped like @jup-ag/lend-read

Downloads

738

Readme

fluid-sui-read-sdk

A read-only TypeScript SDK for Fluid on Sui. It has the same shape and field names as the Solana reader @jup-ag/lend-read (jup-lend-read-sdk): client.liquidity, client.lending, client.vault, plus client.oracle. It's built on @mysten/sui v2 (the core API).

npm i @instadapp/fluid-sui-read-sdk @mysten/sui
// index.mjs (ESM; runs with plain `node index.mjs`)
import { Client } from "@instadapp/fluid-sui-read-sdk";

const client = new Client(undefined, { network: "staging" }); // network is required: no default
console.log("vaults", await client.vault.getTotalVaults());
const vault = await client.vault.getVaultEntireData(1);
console.log("vault 1", vault.constantViews.supplyToken, "->", vault.constantViews.borrowToken);
const fUsdc = await client.lending.getFTokenDetails(
  "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
);
console.log("fUSDC totalAssets", fUsdc.totalAssets);
import { Client, fluidRead } from "@instadapp/fluid-sui-read-sdk";
import { SuiGrpcClient } from "@mysten/sui/grpc";

const failover = new Client([                        // endpoint failover (+ retry with backoff)
  "https://fullnode.mainnet.sui.io:443",             //   gRPC
  "jsonrpc+https://my-node.example:9000",            //   JSON-RPC (self-hosted)
], { network: "staging" });
const pos = await failover.vault.getPositionByVaultId(1, "<Position object id>");

// or as an extension of an existing Sui client
const sui = new SuiGrpcClient({ network: "mainnet", baseUrl: "https://fullnode.mainnet.sui.io:443" }).$extend(fluidRead({ network: "staging" }));
await sui.fluidRead.vault.getAllVaults();

Get my positions (per wallet)

The default client reads pure on-chain state and can't enumerate a wallet's vault positions yet: getAllUserPositions(user) throws NeedsDiscoveryError, and totalPositions / nextPositionId come back as -1. Pass discovery: "fullnode":

import { Client } from "@instadapp/fluid-sui-read-sdk";

const client = new Client(undefined, { network: "staging", discovery: "fullnode" });
const mine = await client.vault.getAllUserPositions("0x<your wallet address>");
for (const p of mine) console.log(p.vault.vault, p.nftId, p.supply, p.borrow);

This stops being needed once the on-chain PositionIndex ships (reads become pure state again).

Lending state for integrators (Merkl etc.)

const client = new Client(undefined, { network: "staging", discovery: "indexer" }); // indexer only for holders/history

// per fToken: identity, accrued + stored exchange prices, staleness, TVL, rates (bps), rewards schedule
const pools = await client.lending.getLendingStateData();          // all pools, one batched read

// wallet holders of Coin<FToken<T>>, cross-checked with getBalance; totalSupply is returned for comparison
const h = await client.lending.getFTokenHolders(pools[0].asset);

// the same state at a past checkpoint (GraphQL object versions, clock = checkpoint timestamp)
const past = await client.atCheckpoint(333_000_000);
const then = await past.lending.getLendingStateData();
  • getLendingStateData fields: fTokenType, asset, decimals, timestamp (chain time used), tokenExchangePrice (rewards included) and liquidityExchangePrice (rewards excluded) accrued to timestamp, storedTokenExchangePrice / storedLiquidityExchangePrice + lastUpdateS / liquidityLastUpdateS (staleness), totalShares, totalAssets (token units), supplyRate (organic), rewardsRate, totalRate (bps), rewards (start/end/yearly + queued next window).
  • getFTokenHolders counts only wallet-owned coins (AddressOwner / ConsensusAddressOwner). Coins owned by objects, shared coins, wrapped Balance<FToken<T>> and address balances are not attributed to a wallet, so the sum of wallet balances can be below totalSupply.
  • atCheckpoint serves object state only; Move views, oracle quotes and balances throw CheckpointReadError. How far back works depends on the GraphQL service's retention.
  • Polling: state changes per checkpoint, so poll one batched read at most once per checkpoint; public endpoints are rate-limited. Reads retry with exponential backoff (ClientOptions.retry).

How it reads: on-chain state, no indexer by default

  • Objects, not indexers. Every Fluid object id is derived offline from derived_object and dynamic-field ids, the Sui equivalent of Solana PDAs. Objects are fetched in batches of 50 and decoded with BCS layouts transcribed from the deployed packages (src/bcs.ts).
  • Maths in TS, bit-exact. These are ports of the Move code (src/math/*), accrued to the current second:
    • exchange prices, revenue and limits;
    • fToken accrual, including rewards, and previews;
    • tick ratios;
    • position settlement after liquidation.
  • Simulation only where needed. The oracle price comes from simulating the oracle views view::get_exchange_rate_operate / get_exchange_rate_liquidate (Oracle + RedStone PriceAdapter + Pyth PriceInfoObject + Clock). Nothing is sent and no gas is spent, and the result is exactly what operate / liquidate would consume.

Oracle (RedStone main, Pyth guard)

  • client.oracle.getOraclePrice(id) → { operate, liquidate }, 1e15 debt-per-collateral.

    • operate = min(main, guard); it aborts on-chain if either route is older than 600s or invalid. The SDK then returns operate: null plus operateError; it never substitutes a price.
    • liquidate = min of the routes still usable at 7200s; null only when both are dead.
  • client.oracle.getOracleRoutes(id) reads each route on its own: RedStone via price_adapter::price_and_timestamp, Pyth by decoding the PriceInfoObject. Each route returns its raw price, 1e15 rate, age at the chain clock, and whether it passes the operate / liquidate checks (mirrors *_core::reading_error). This shows which route sets the price and whether the guard is live.

  • client.oracle.getOracle(id) → { nonce, main, guard }; derive.oracleId(nonce) gives the id offline.

  • Both providers are pull oracles: freshness depends on someone pushing updates. The SDK reports staleness; it doesn't push.

  • Discovery levels (new Client(rpc, { discovery })):

    • "state" (default): object reads by derived id, plus view simulation. No list or scan queries, and no GraphQL.
    • "fullnode": also the fullnode's own indexes (listDynamicFields, listOwnedObjects). These use the same endpoints and failover.
    • "indexer": also GraphQL event scans. Used only as a last resort.

    A read that state alone can't answer throws NeedsDiscoveryError naming the on-chain fix. It never quietly falls back to an indexer.

  • Failover. If an endpoint throws (network error, timeout, 5xx), the read moves on to the next one. The last healthy endpoint is tried first next time.

What is pure state today, and the one contract change still needed, is covered in docs/onchain-position-index.md.

| Read | Default (state) | |---|---| | Vaults, ticks, branches, oracle, exchange prices, fToken data | pure | | Token list | pure: config.tokens plus every vault's collateral and debt types (vault ids 1..next_vault_id-1). discoverTokens() reports untrackedCaps, from Registry.active_caps.size. | | Liquidity user positions | pure for every vault and lending holder. getAllUserPositionsDetailed().untracked, from Table.size, counts holders outside Fluid's own markets. | | Vault position counts / ids | needs the on-chain PositionIndex. Until then use discovery: "fullnode". That reads the derived_object::Claimed fields, one per position ever opened, with no event indexer involved. | | Positions owned by a user | pure once the index ships. getUserPositionsByCapIds is always pure. |

Units

Same convention as Solana:

  • Vault-side amounts (position supply and borrow, vault totals) are in the vault's internal 1e9 space.
  • Liquidity-side amounts are in token units.
  • Vault.unscale(x, decimals) converts between the two.
  • Oracle rates are debt-per-collateral × 1e15 (Oracle.toNumber).
  • Every rate field is bps (1e4 = 100% per year), floored: liquidity borrowRate / supplyRate, vault rates, and lending supplyRate (organic), rewardsRate, totalRate (= supplyRate + rewardsRate), maxRate. On chain the two rates use different scales (supply: 1e12 = 100%; rewards: 1e14 = 100%), which is why the SDK converts both; unfloored raw values: liquidityMath.supplyRateAt (1e12) and lendingMath.currentRewardsRate (1e14).
  • supplyRate is the contract's own formula (reserve::calculate_exchange_prices: interest-free ratios, both exchange prices accrued to now, fee), not an approximation.
  • Time comes from the on-chain Clock (0x6) by default, never the server clock (ClientOptions.now).
  • config.mathVariant picks the on-chain maths: "pre-audit" for the 2026-09-18 staging liquidity/lending packages (checked against deployed bytecode), "audited" (default) for fluid-packages-sui 742ea21 and later.

Differences from the Solana reader

| Solana | Sui | |---|---| | nftId: number | nftId / positionId: Position object id, plus capId (PositionOwnerCap) | | PublicKey / BN | string (object id or coin type) / bigint | | jlToken | fToken (Coin<FToken<T>>): getFToken*, FTokenDetails, fTokenShares, fTokenDetails. getJlToken* / JlToken* remain only as @deprecated aliases | | totalPositions, nextPositionId | stored on-chain once PositionIndex ships (source: "state"); until then discovery: "fullnode" (source: "fullnode-index") | | DEX / smart col-debt | not on Sui yet: null / false |

Position counts (totalPositions, nextPositionId)

The deployed VaultMarket has no counter. A Position is a derived object keyed by its random PositionOwnerCap id. vault.getVaultPositionCounts(id) resolves the counts in this order:

  1. state: the PositionIndex dynamic field on the vault, from the proposed Move module in docs/move/. It holds the stored next_position_id and a Table<u64, cap id>, read entry by entry via derived ids. The SDK uses it automatically once it exists.
  2. fullnode: 0x2::derived_object::Claimed(position_id) fields on the vault. Claims are never removed, so their count equals positions ever opened, which matches EVM totalPositions.
  3. indexer: LogPositionOpened / LogClosePosition events. This is incremental, deduped and retried, but opt-in only.

In every tier, openPositions counts only ids whose Position object still exists, because close deletes the object. getVaultState / getVaultEntireData report -1 when the active level can't resolve counts, and the rest of the vault read still succeeds.

Tests

npm test            # offline: derivations, maths, on-chain index reader (BCS fixtures), failover
npm run test:live   # mainnet, read-only
npm run example     # prints every resolver against mainnet
  • test/live/pure-state.test.ts runs with the GraphQL host blocked at fetch level and records every RPC method. It requires the default path to use only getObjects, simulateTransaction and getCoinMetadata. It also checks that:
    • state and fullnode token lists match;
    • liquidity positions plus untracked equal the full table;
    • vault 1 positions via claims (09-30 market, and the 09-18 market checked against an independent LogPositionOpened count, never a fixed number);
    • a dead first endpoint fails over.
  • test/live/parity.test.ts requires exact equality between the SDK and the deployed Move code at the same clock second, using one simulated PTB:
    • reserve::get_exchange_prices and user::get_exchange_prices;
    • fUSDC preview_* and total_assets;
    • oracle: getOraclePrice = an independent view::get_both_exchange_rate PTB = min over the two routes read separately; the two providers within 2%;
  • test/live/reconcile.test.ts: getLendingStateData against one simulated PTB per fToken (reserve::get_exchange_prices, user::update_rate, accounting::total_assets, stored prices, shares, rewards schedule) at the same clock second, with object versions checked unchanged before and after.
  • test/live/holders.test.ts: holder sum <= totalSupply at the same checkpoint, and every wallet = getBalance (no address balance).
  • test/unit/rate-goldens.test.ts / rewards-tvl.test.ts: hand-derived supply and rewards rate goldens (zero borrow, interest-free supply/borrow, window boundaries, TVL below start_tvl, 50% cap), pre-accrual TVL pricing per math variant.
  • test/live/positions.test.ts cross-checks the claim set against an independent raw event count, on the current market and on the 09-18 market (which still holds 9 positions, so position maths stays covered while the new market is empty).

Config

There is no default network: every client passes network explicitly.

  • network: "mainnet" = PROD (alias MAINNET): the production deployment (2026-11-02), same ids as the write SDK and fluid-packages-sui deployments/mainnet.json. Day one: fUSDC and vault 1 = VaultMarket<hBTC, USDC> 0x1aa7…a624 on oracle nonce 1 0x6f74…3784 (RedStone BTC main + Pyth BTC/USD guard).
  • network: "staging" = STAGING: the staging-on-mainnet deployment (staging gist, 2026-09-30): library / liquidity / lending from 2026-09-18, oracle 0xc1e7…4814 and vaults 0x08a3…6f20 from 2026-09-30. Vaults 1 SUI/USDC + 2 SUI/USDSUI, fUSDC + fUSDSUI. npx tsx scripts/smoke-staging.ts checks it with zero config.

oracle holds the RedStone adapter and the Pyth PriceInfoObject per feed id.

  • Don't depend on the built-in ids: build the config from a deployment record (fluid-packages-sui deployments/<network>.json, the write SDK's source) and pass it in:
    import { readFileSync } from "node:fs";
    import { Client, configFromDeployment, PROD } from "@instadapp/fluid-sui-read-sdk";
    const rec = JSON.parse(readFileSync("deployments/mainnet.json", "utf8"));
    const cfg = configFromDeployment(rec, { network: "mainnet", oracle: PROD.oracle });
    const client = new Client(undefined, { network: cfg });
    The record has no RedStone package or Pyth feed map, so oracle is passed in.
  • Pass new Client(rpc, { network: customConfig }) for testnet or after an upgrade.
  • packageId is the latest version (the call target). originalId is the first version (used in type tags and keys).
  • After the position-index upgrade, set positionIndexKeyType to the upgraded package's position_index::PositionIndexKey.
  • A new Pyth feed needs its PriceInfoObject added to oracle.pythPriceInfoObjects (the Oracle object stores the feed id, not the object id).