sta-sdk
v0.2.1
Published
TypeScript SDK for the Smart Treasury Account (STA) Soroban contracts: auth-entry construction, transaction preparation, typed event parsing, and typed state reads.
Maintainers
Readme
sta-sdk
TypeScript SDK for the Smart Treasury Account (STA) Soroban contracts:
smart_account custom-authorization (Entry A / Entry B) construction,
transaction preparation (prepare → simulate → sign → submit → poll), typed
#[contractevent] parsing, and typed state reads (policy version, replay
nonce, recovery state). Targets the live testnet and mainnet
deployments — see Versioning against a deployment.
Install
npm install sta-sdk @stellar/stellar-sdk@stellar/stellar-sdk (>=16.0.0) is a peer dependency — install it
yourself so your app controls the version and there is only ever one copy
of its classes (Address, xdr.ScVal, ...) loaded. Node >=20 (the
authorization nonce comes from Web Crypto, globalThis.crypto).
Why this SDK hand-encodes calls instead of using generated bindings
The Stellar CLI's stellar contract bindings typescript output is pinned
to whatever @stellar/stellar-sdk major was current when it was
generated. Depending on those generated bindings directly here would force
every consumer of this SDK onto that exact major, and loading two majors
of the same runtime classes in one bundle risks instanceof mismatches.
So this package encodes AuthPayload structs, call args, and typed reads
by hand against the contracts' real #[contracttype]/#[contractevent]
definitions, verified live — see each module's doc comment for specifics.
Usage
import {
TESTNET,
prepareTransferPayment,
signAndSubmit,
readPolicyVersion,
} from "sta-sdk";
import { Keypair } from "@stellar/stellar-sdk";
const signer = Keypair.fromSecret(process.env.SIGNER_SECRET!);
const expectedPolicyVersion = await readPolicyVersion(
TESTNET,
signer.publicKey(),
);
const tx = await prepareTransferPayment(
{
net: TESTNET,
feeSourceAddress: signer.publicKey(),
signerAddress: signer.publicKey(),
sign: signer,
},
{
asset: "CASSET...",
destination: "GDESTINATION...",
amount: 100_0000000n,
nonce: BigInt(Date.now()),
expectedPolicyVersion,
},
);
const result = await signAndSubmit(TESTNET, tx, signer);
console.log("ledger:", result.ledger);See examples/ for one runnable, documented example per
flow: read treasury state (no keys needed), transfer, split payment,
scheduled payment, and event parsing. Every example runs against testnet
by default and against mainnet with STA_NETWORK=mainnet.
Getting started on mainnet
There is deliberately no MAINNET constant to import. Unlike testnet, SDF
hosts no free public mainnet Soroban RPC — you pick a provider (see
the provider list),
and the SDK refuses to guess one for you:
import { mainnet, MAINNET_ASSETS, readAccountStatus } from "sta-sdk";
// Explicit URL, plus headers if your provider takes an API key that way.
const net = mainnet("https://<your-rpc-provider>", {
headers: { "x-api-key": "..." },
});
// Or configure it once in the environment and call `mainnet()` with no
// arguments: STA_MAINNET_RPC_URL, and optionally STA_MAINNET_RPC_HEADERS as
// a JSON object ({"x-api-key":"..."}).
const status = await readAccountStatus(net, "G...any existing mainnet account");mainnet() points at the example treasury documented in the
smart-contracts repo's
docs/MAINNET_DEPLOYMENT.md
(§5) — a funded, policy-configured treasury for integration testing whose
owner key is documented as non-confidential. Never hold real value there.
For your own treasury, call deploy_account on the mainnet
account_factory (MAINNET_CONTRACTS.accountFactory; §4 of that document
and DAPP_INTEGRATION_SPEC.md §12 explain the call) and pass the six
resulting addresses to buildMainnetConfig(contracts, rpcUrl).
MAINNET_ASSETS carries the XLM and USDC Stellar Asset Contract ids — the
two assets the example treasury's policy currently allows.
A NetworkConfig with network: "mainnet" and real signing keys submits
real, fee-paying, fund-moving transactions. Run
examples/read-treasury.ts (read-only, no keys) before anything that signs.
Modules
| Module | Exports |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| config | NetworkConfig, TESTNET, MAINNET_CONTRACTS, MAINNET_ASSETS, mainnet, buildMainnetConfig, MAINNET_NETWORK_PASSPHRASE |
| rpc | serverFor — the one rpc.Server factory, applies NetworkConfig.rpcHeaders |
| fee | inclusionFee — the market-driven inclusion bid every prepare* uses by default (see Fees) |
| auth | buildSmartAccountAuthEntries, buildExecutorAuthEntry, buildClassicAuthEntry, buildInvocation, countAuthContexts, selectInvocationForAddress, selectAllInvocationsForAddress |
| payments | prepareTransferPayment, prepareSplitPayment, prepareScheduledPayment, prepareCancelScheduledPayment, prepareRelayerExecution, discoverSmartAccountInvocation, submitTransaction, signAndSubmit, encode*Args |
| events | parseContractEvent, parseContractEvents, findEvent |
| state | typed reads: AccountStatus, ContextRule, ScheduledIntent, RecoveryRequest, WasmHashes; validatePolicy, readSignerId |
| scval | structScVal and the scalar encoders (addressScVal, i128ScVal, u64ScVal, bytesN32ScVal, signerDelegatedScVal, …) |
Fees
rpc.Server.prepareTransaction sets a transaction's resource fee from
simulation but leaves the inclusion bid at whatever the builder was given.
BASE_FEE (100 stroops) is the protocol minimum and mainnet refuses it
whenever there is any competition for ledger space — txInsufficientFee,
before the transaction reaches a ledger. Every prepare* helper therefore
bids inclusionFee(server): ten times the network's recent p99 Soroban
inclusion fee, floored at 2 000 stroops and capped at 100 000 (0.01 XLM).
Pass fee in PrepareOptions / RelayerExecuteOptions to pin a bid.
Authorization trees
A smart_account call that moves tokens — execute_transfer_payment,
execute_split_payment — has to be authorized over a two-node tree: the
entrypoint, and the Stellar Asset Contract's transfer the adapter reaches
beneath it. An Entry A built over the entrypoint alone is refused with
Error(Auth, InvalidAction). prepare* discovers the tree by simulating
the call in recording mode (discoverSmartAccountInvocation) and carries
one context_rule_id per node (countAuthContexts); contextRuleIds
accepts either one id, applied to every node, or exactly one per node.
Submitting from a browser
signAndSubmit signs the envelope with a Keypair. A dApp whose wallet
signs the envelope instead calls prepare*, hands tx.toXDR() to the
wallet, and passes new Transaction(signedXdr, net.networkPassphrase) to
submitTransaction, which does the sending and polling alone.
Versioning against a deployment
Each release states which contract deployment it targets. The contract
WASM behind both networks is the same source at the same hashes
(docs/MAINNET_DEPLOYMENT.md §3 in the smart-contracts repo; rebuild and
compare with scripts/verify_build.sh there).
| sta-sdk | Network | Deployment record | smart_account | account_factory |
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------------- |
| 0.2.1 | mainnet | MAINNET_DEPLOYMENT.md §4–§5 | CDTE6DBM…VL7W | CCFIPN4T…QAAV |
| 0.2.0 / 0.1.x | testnet | TESTNET_FACTORY_DEPLOYMENT.md §13.2 | CD6GY4UU…ULMQ | CAQQTRRY…GUZO |
0.2.1 makes prepare* usable against the deployed contracts and on
mainnet: recording-mode discovery of the authorization tree with one rule
id per node (0.2.0 built a single node, which every fund-moving call
rejects), a market inclusion fee instead of BASE_FEE (which mainnet
refuses), submitTransaction for wallet-signed envelopes, and the scalar
encoders, buildClassicAuthEntry, validatePolicy and readSignerId
exported so a consumer need not keep copies. Its Entry A/B bytes are pinned
by test to the output of the Smart Treasury dApp's own construction — the
code that moved real XLM on mainnet on 2026-09-10 — with the nonce held
equal. No breaking API change from 0.2.0.
0.2.0 replaced the MAINNET/NETWORKS placeholders from 0.1.x (both were
undefined for mainnet) with MAINNET_CONTRACTS + mainnet(), added
NetworkConfig.rpcHeaders, and requires Node >=20.
Known issue — multi-signer context rules
buildSmartAccountAuthEntries builds Entry A + Entry B for a single
required signer. For a context rule with more than one required signer
(a real M-of-N threshold), calling it once per signer and attaching each
pair does not currently match the documented design (one shared
Entry A carrying all signers' keys, plus N Entry Bs collected against it).
Don't rely on multi-signer thresholds through this function until that's
fixed — see the doc comment on that function.
Development
pnpm install
pnpm test
pnpm buildPublishing
npm version <patch|minor|major>
git push --follow-tagsprepublishOnly runs typecheck, tests, and build first. CI
(.github/workflows/publish.yml) publishes automatically on a pushed
v* tag through npm's OIDC trusted publishing — no NPM_TOKEN secret,
and none must be added (see the comment in that workflow).
