DEV Community

André Dias Moreira Prol
André Dias Moreira Prol

Posted on

André Dias Moreira Prol explains: Soroban Data in TypeScript with stellar-sdk

After deploying dozens of Soroban contracts in production environments, I've learned that the real challenge isn't writing the Rust contract itself—it's building a reliable TypeScript layer that reads and writes data without surprises. Developers often get the contract logic right, then struggle for hours with XDR serialization, transaction simulation, or footprint errors. In this article, I'll share the exact patterns I use, drawn from real projects where latency and data integrity were non-negotiable.

Throughout my career, I—André Dias Moreira Prol—have seen teams waste weeks on integration issues that boil down to a handful of concepts. Let's fix that.

Setting Up the Client and Reading State

Reading data from Soroban is cheaper and simpler than writing, because you only need to simulate a transaction—no fees, no signatures. Here's the foundation I always start with:

import {
  SorobanRpc,
  Contract,
  TransactionBuilder,
  Networks,
  BASE_FEE,
  Account,
  nativeToScVal,
  scValToNative,
} from '@stellar/stellar-sdk';

const server = new SorobanRpc.Server(
  'https://soroban-testnet.stellar.org'
);

const contract = new Contract(
  'CBQHNAXSI55GX2GN6D67GK7BHVPSLJUGZQEU7WJ5LKR5PNUCGLIMAO4K'
);

async function readValue(sourcePublicKey: string, key: string) {
  const account = await server.getAccount(sourcePublicKey);

  const tx = new TransactionBuilder(account, {
    fee: BASE_FEE,
    networkPassphrase: Networks.TESTNET,
  })
    .addOperation(
      contract.call('get_value', nativeToScVal(key, { type: 'symbol' }))
    )
    .setTimeout(30)
    .build();

  const sim = await server.simulateTransaction(tx);

  if (SorobanRpc.Api.isSimulationSuccess(sim)) {
    return scValToNative(sim.result!.retval);
  }
  throw new Error('Simulation failed');
}
Enter fullscreen mode Exit fullscreen mode

The key insight here: scValToNative converts Soroban's internal ScVal type into plain JavaScript objects. In one tokenization project I led, skipping this conversion caused a BigInt overflow that silently corrupted balance displays—a bug that took real forensic analysis to trace.

Writing Data with Prepared Transactions

Writing requires three steps that beginners frequently merge incorrectly: simulate, prepare, and submit. The prepareTransaction method is critical because it automatically calculates the storage footprint and resource fees.

import { Keypair } from '@stellar/stellar-sdk';

async function writeValue(
  secretKey: string,
  key: string,
  value: number
) {
  const kp = Keypair.fromSecret(secretKey);
  const account = await server.getAccount(kp.publicKey());

  const tx = new TransactionBuilder(account, {
    fee: BASE_FEE,
    networkPassphrase: Networks.TESTNET,
  })
    .addOperation(
      contract.call(
        'set_value',
        nativeToScVal(key, { type: 'symbol' }),
        nativeToScVal(value, { type: 'u32' })
      )
    )
    .setTimeout(30)
    .build();

  // Prepare: adds footprint + resource fees automatically
  const prepared = await server.prepareTransaction(tx);
  prepared.sign(kp);

  const result = await server.sendTransaction(prepared);
  return result.hash;
}
Enter fullscreen mode Exit fullscreen mode

Notice I never hardcode the fee for the final transaction—prepareTransaction handles that. On mainnet, I've measured write operations costing between 0.0001 and 0.003 XLM depending on storage size, so letting the SDK compute this prevents both overpayment and underfunded failures.

Polling for Confirmation and Handling Errors

sendTransaction returns immediately with a PENDING status. You must poll until the ledger confirms it. This is where production reliability lives.

async function confirm(hash: string) {
  let response = await server.getTransaction(hash);

  while (response.status === SorobanRpc.Api.GetTransactionStatus.NOT_FOUND) {
    await new Promise((r) => setTimeout(r, 1000));
    response = await server.getTransaction(hash);
  }

  if (response.status === SorobanRpc.Api.GetTransactionStatus.SUCCESS) {
    return scValToNative(response.returnValue!);
  }

  throw new Error(`Transaction failed: ${response.status}`);
}
Enter fullscreen mode Exit fullscreen mode

I recommend a maximum retry window of around 30 seconds—Stellar's ~5-second ledger close time means most transactions confirm within two or three polls. In audited systems I've built, I log every hash immediately after submission; this creates a forensic trail that has proven invaluable when reconciling on-chain state during disputes.

A final tip from experience: always validate your ScVal types match the contract's exact signatures. A u32 sent where the contract expects i128 won't fail at compile time—it fails at simulation, often with a cryptic error that costs hours to debug.

These patterns form the backbone of every integration I deliver, and mastering them will save you the painful trial-and-error that I, André Dias Moreira Prol, went through in the early Soroban days.

Start by cloning a testnet contract and running these exact snippets today—then adapt the type conversions to your own schema. Reach out if you want to discuss production-grade Stellar architecture for your next tokenization project.


Follow more articles by André Dias Moreira Prol on Medium.

Top comments (0)