SpieldSpield
Developers

Reading protocol state

Query Spield's live state — positions, prices, solvency, and balances — with free Soroban RPC simulations, no wallet or fees required.

Reading Spield is free. Contract reads are simulated against a Soroban RPC node — no wallet, no signing, no fees. This page shows the pattern with @stellar/stellar-sdk (JS/TS).

Install

pnpm add @stellar/stellar-sdk

The read helper

Every read follows the same shape: build a dummy invocation, simulate it, and decode the result to a native JS value.

import {
  rpc,
  TransactionBuilder,
  Contract,
  Address,
  nativeToScVal,
  scValToNative,
  BASE_FEE,
} from '@stellar/stellar-sdk';

const RPC_URL = 'https://mainnet.sorobanrpc.com';
const NETWORK_PASSPHRASE = 'Public Global Stellar Network ; September 2015';
const server = new rpc.Server(RPC_URL);

// A throwaway source account is fine for read-only simulation.
const SIM_SOURCE = 'GADUMMY...'; // any valid account id

export async function readContract<T = unknown>(
  contractId: string,
  method: string,
  args: unknown[] = [],
): Promise<T> {
  const account = await server.getAccount(SIM_SOURCE).catch(() => null);
  const source = account ?? { accountId: () => SIM_SOURCE, sequenceNumber: () => '0', incrementSequenceNumber() {} } as never;

  const contract = new Contract(contractId);
  const tx = new TransactionBuilder(source as never, {
    fee: BASE_FEE,
    networkPassphrase: NETWORK_PASSPHRASE,
  })
    .addOperation(contract.call(method, ...(args as never[])))
    .setTimeout(30)
    .build();

  const sim = await server.simulateTransaction(tx);
  if (rpc.Api.isSimulationError(sim)) throw new Error(sim.error);
  return scValToNative(sim.result!.retval) as T;
}

Encoding arguments

Convert JS values to Soroban values (ScVal) for the call:

const u64 = (n: number | bigint) => nativeToScVal(BigInt(n), { type: 'u64' });
const i128 = (n: bigint) => nativeToScVal(n, { type: 'i128' });
const addr = (a: string) => new Address(a).toScVal();

Examples

// returns [backing, principal, unclaimed] as bigints (base units)
const [backing, principal, unclaimed] =
  await readContract<bigint[]>(WRAPPER_ID, 'solvency');

const solvent = backing >= principal; // always true by construction
const pos = await readContract<Record<string, unknown>>(
  WRAPPER_ID,
  'position_value',
  [u64(positionId)],
);
// { principal, claimable_yield, pt_amount, yt_amount, open }
const ptPrice = await readContract<bigint>(MARKET_ID, 'pt_price');     // 12-dp fixed point
const impliedApy = await readContract<bigint>(MARKET_ID, 'implied_apy'); // 12-dp fraction

const pricer = Number(ptPrice) / 1e12;          // e.g. 0.9987
const apyPct = (Number(impliedApy) / 1e12) * 100; // e.g. 1.56

Market reads are panic-free: a thin/empty pool or a matured market returns 0, which you should render as "no price / amount exceeds liquidity" rather than an error.

// SAC token balance for an account, in base units
const balance = await readContract<bigint>(
  USDC_ID,
  'balance',
  [addr(accountId)],
);

Read the token addresses from the engine (pt_token(), yt_token(), underlying()) so you never hard-code them.

Discovering a user's positions

The engine doesn't keep an on-chain owner→positions index (that would require unbounded iteration). To list a user's positions, scan position ids and keep the ones whose get_position(id).owner matches, stopping after a run of misses:

async function getOwnerPositions(owner: string, maxScan = 64) {
  const found = [];
  let misses = 0;
  for (let id = 0; id < maxScan && misses < 5; id++) {
    try {
      const pos = await readContract<Record<string, unknown>>(WRAPPER_ID, 'get_position', [u64(id)]);
      misses = 0;
      if (String(pos.owner) === owner) found.push(id);
    } catch {
      misses++; // non-existent id
    }
  }
  return found;
}

Or index events instead

For production dashboards, indexing events is more efficient than scanning — you reconstruct ownership from mint / transfer / redeem events.

Next: submitting transactions.

On this page