Skip to content

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

SoroRail — frontend

The TypeScript half of SoroRail: the client SDK, the reference application, and the documentation site, in one pnpm workspace.

The contracts live in Sororail/sororail-contracts. Nothing here can be correct until those are, so that repo leads.

Unaudited. Testnet only.

The contracts this talks to have not been audited. Do not use them with real value. No mainnet addresses will be published before an audit.

Layout

frontend/
├── packages/
│   └── sdk/          @sororail/sdk — typed client        ✅ built
└── apps/
    ├── web/          Next.js reference application       ✅ built
    └── docs/         Astro Starlight documentation site  ⬜ not started

Status

Piece State
packages/sdk Clients for all five contracts, typed error decoding, Freighter + keypair signers, amount helpers. 28 unit tests, 5 runnable examples, typechecks against @stellar/stellar-sdk 17.0.1.
apps/web Overview, payroll, streams, vesting and escrow screens. Builds and serves; typechecks. Wallet-connected flows have not been click-tested against a real wallet.
apps/docs Not started.

Verified end-to-end against live testnet. examples/stream-lifecycle.ts created and withdrew from a real stream on 2026-09-07 — transactions 520c753b and 30d080d7 — and the contract's conservation invariant held on-chain.

Not yet verified live: the escrow, vesting, recurring and batch examples. Only the stream one has been run by hand. All five now assert what they demonstrate and are run by the scheduled integration job in ci.yml, which deploys a fresh instance of each contract to testnet — but that job has not yet reported a run, so treat those four as unrun until it does.

Not yet done: the SDK is unpublished and the @sororail npm scope is not reserved. The app has no indexer, no database and no history feed — it is entirely client-side against Soroban RPC — and its wallet flows have not been exercised with a real Freighter extension.

Running the app

pnpm --filter @sororail/web dev

Then open http://localhost:3000. You will need Freighter and a funded testnet account.

The app is configured through environment variables, all optional:

Variable Default Purpose
NEXT_PUBLIC_RPC_URL https://soroban-testnet.stellar.org Soroban RPC endpoint.
NEXT_PUBLIC_EXPLORER_URL derived from the RPC URL Block explorer base for transaction and contract links, e.g. https://stellar.expert/explorer/testnet.

Without NEXT_PUBLIC_EXPLORER_URL, an RPC on a testnet host links to the testnet explorer; any other RPC (a local quickstart node, futurenet) gets no explorer links rather than links that 404.

Development

pnpm install
pnpm test          # all packages
pnpm typecheck
pnpm build
pnpm changeset     # describe a change for the next release

Node ≥20. pnpm 10.

Runnable examples live in packages/sdk/examples — they run against a real network and assert what they demonstrate, so they are the SDK's integration tests. pnpm --filter @sororail/sdk test:integration runs all five; see their README for setup.

Releases and changelog

Releases are managed with Changesets. A PR that changes the SDK's published behaviour adds a changeset (pnpm changeset); cutting a release (pnpm version-packages) turns the pending ones into an entry in packages/sdk/CHANGELOG.md, which also ships in the npm package. Nothing has been released yet, so the changelog is empty and upcoming changes are the files in .changeset/. The reference app is not published and has no changelog. See .changeset/README.md for the release steps.

The SDK

import {
  StreamClient,
  KeypairSigner,
  ContractError,
  NetworkError,
  SigningError,
  ValidationError,
} from "@sororail/sdk";
import { Networks } from "@stellar/stellar-sdk";

const signer = new KeypairSigner(process.env.SECRET_KEY!);
const stream = new StreamClient({
  contractId: "CBEE4SRXRGCJDWXP6DDOSX6FR4S2PJ5KHUQCHI3ABY3SQTCHYSA7CGC7",
  rpcUrl: "https://soroban-testnet.stellar.org",
  networkPassphrase: Networks.TESTNET,
  publicKey: signer.publicKey,
});

try {
  // Build and inspect before anyone signs.
  const call = await stream.withdraw();
  const wouldReceive = await call.simulate();

  // Then commit.
  const { hash, result } = await call.signAndSend(signer);
} catch (error) {
  if (error instanceof ContractError) {
    // The contract refused. `message` is a sentence to show a person;
    // branch on the variant (or the stable numeric `code`) to recover.
    if (error.is("StreamInsufficientAccrued")) {
      // Asked for more than has accrued: withdraw less, or wait.
    }
  } else if (error instanceof ValidationError) {
    // Rejected before anything was sent. Fix the arguments.
  } else if (error instanceof SigningError) {
    // No wallet, locked, declined, or on the wrong network.
  } else if (error instanceof NetworkError) {
    // Could not simulate or submit. The original failure is `error.cause`.
  } else {
    throw error;
  }
}

Every stage can fail, so the whole flow sits in one try:

Error Thrown by What it means
ValidationError building a call (stream.withdraw() etc.) Bad arguments, or a client without a publicKey. Nothing was sent.
ContractError simulate(), signAndSend() The contract refused. Carries code, variant, contract and a readable message.
SigningError signAndSend() The signer could not sign: no wallet, locked, declined, a switched account, or NetworkMismatchError for the wrong network.
NetworkError any stage The RPC could not be reached or rejected the request; the original failure is cause.

All of them extend SororailError, so anything else is a bug in the calling code rather than a failed call. The reference app's Feedback.tsx and recovery.ts show one way to turn these into UI: the message says what happened, the recovery text says what to do next.

Design rules

Signing is injected, never performed here. The SDK builds and submits transactions but never holds a secret key. Supply a Signer — FreighterSigner in a browser, KeypairSigner for tests and scripts. FreighterSigner re-reads the extension's selected account and network before every signature and refuses — with NetworkMismatchError for the wrong network — rather than signing as an account or on a network the transaction was not built for.

Every stage is separately accessible. prepare → simulate → signAndSend, rather than one opaque call. A confirmation screen can only be honest if it can show what a transaction will do before asking for a signature, so nothing collapses those steps.

Amounts are bigint, never number. Number loses integer precision above 2^53, which is well inside the range of a real balance. Use toStroops / fromStroops at the edges — and note the decimals trap: 7 is the default for classic Stellar assets, but a custom token can declare anything, so read decimals() off the token rather than assuming.

Errors are decoded, never bare codes. ContractError carries the ABI integer, the variant name, the owning contract, and a message written for a person. The code table mirrors sororail_common::errors and is tested against its reserved ranges.

Zero framework dependencies. No React. If React helpers are ever wanted they go in a separate packages/react.

Two contract behaviours to surface in any UI

Recurring charges do not accrue retroactively. A payee who forgets to charge for three months cannot then take three payments — the next charge is scheduled from now, and skipped periods are gone. This is deliberate consumer protection, and it surprises merchants. Say so in the UI.

Batch payouts are capped and all-or-nothing. Read BatchPayoutClient.maxRecipients() rather than hardcoding, and use BatchPayoutClient.chunk() to split a larger payroll. If any transfer fails, nobody is paid.

Contributing

See CONTRIBUTING.md.

License

Apache-2.0. See LICENSE.

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages