@stellarforge-protocol/sdk
v0.3.0
Published
TypeScript SDK for the StellarForge RWA protocol on Stellar/Soroban
Maintainers
Readme
@stellarforge-protocol/sdk
TypeScript client for the StellarForge Real World Asset tokenization protocol on Stellar/Soroban.
Clients for all four Phase 1 contracts: rwa-asset, compliance, registry
and governance.
npm install @stellarforge-protocol/sdkRequires Node.js 22.12 or newer, which is what @stellar/stellar-sdk 17 requires. Its CommonJS build loads ESM-only dependencies with require(), and Node supports that without a flag only from 22.12.
Reading
Every read is a simulation. Nothing is written, nothing is signed, and no account is needed.
import { RwaAssetClient, TESTNET_CONFIG, fromStroops } from "@stellarforge-protocol/sdk";
const client = new RwaAssetClient({
...TESTNET_CONFIG,
contracts: { rwaAsset: "C..." },
});
const meta = await client.metadata();
const supply = await client.totalSupply();
console.log(`${meta.symbol}: ${fromStroops(supply, meta.decimals)}`);Amounts are bigint, because the contracts store them as i128 and a number
would lose precision silently. Use toStroops and fromStroops to convert.
toStroops throws rather than guess. It rejects anything but plain decimal
notation, more decimal places than the asset allows, and a number that is not
a safe integer, so pass user-entered amounts as strings.
Networks
TESTNET_CONFIG includes SDF's public testnet RPC. MAINNET_CONFIG has no RPC
URL, because SDF doesn't run a public mainnet RPC. Supply one from an
RPC provider; TypeScript won't compile a mainnet config without it.
import { MAINNET_CONFIG, RwaAssetClient } from "@stellarforge-protocol/sdk";
const client = new RwaAssetClient({
...MAINNET_CONFIG,
rpcUrl: "https://your-mainnet-rpc-provider.example",
contracts: { rwaAsset: "C..." },
});Writing
Every write comes in two forms.
build*Tx returns a prepared but unsigned transaction for a wallet to
sign. No secret reaches this library, which is the only form usable in a
browser.
const tx = await client.buildTransferTx(from, to, toStroops("100", meta.decimals));
const signedXdr = await freighter.signTransaction(tx.toXdr(), { networkPassphrase });The bare method signs and submits, for server-side automation.
const client = new RwaAssetClient({ ...TESTNET_CONFIG, contracts, signerSecret });
const { hash, ledger } = await client.transfer(from, to, amount);A bare write waits for the transaction to settle, for up to its 180-second validity window plus 30 seconds. If it has not settled by then, the error type says whether retrying is safe:
TransactionExpiredError: the window closed without the transaction being included. It can never land, so rebuild and resubmit.TransactionOutcomeUnknownError: it may still land. Look uperr.hashbefore retrying, or the write may execute twice.
Two things worth knowing before you use either:
- The authorizing address also sources the transaction in these forms, so
its signature satisfies the contract's
require_auth. To have another account pay, see Sponsored writes. - Building simulates. A call the contract would reject — a bad amount, a paused asset, a party failing compliance — fails while building, with the contract's own error, before anything is signed or submitted.
Clients
| Client | Contract | Notes |
|---|---|---|
| RwaAssetClient | rwa-asset | Balances, metadata, allowances; mint, burn, burnFrom, transfer, approve (with an expiry ledger), transferFrom. Admin: setIssuer, setPaused, updateMetadata, setCompliance |
| ComplianceClient | compliance | isCompliant is the pure query, never the TTL-extending screen; getKyc reads a stored record. Admin: setKyc, revokeKyc |
| RegistryClient | registry | Asset directory, paged with listAssets(start, limit) or walked with listAllAssets(); register and setActive are admin-only |
| GovernanceClient | governance | Proposals and voting — see the warning below |
Sponsored writes
One account can source and pay for a write while another address only authorizes it, for example an issuer covering fees for its holders. The authorizer signs an authorization entry rather than the transaction, so the two keys can live on different machines.
import { rpc } from "@stellar/stellar-sdk";
import { DEFAULT_AUTH_VALIDITY_LEDGERS, authorizeEntries } from "@stellarforge-protocol/sdk";
// Fee payer: build the call. Nothing is signed yet.
const sponsored = await client.buildSponsoredTransferTx(holder, recipient, amount, {
feeSource: relayer,
});
// Authorizer: sign its entries, valid until a ledger it chooses.
const { sequence } = await new rpc.Server(rpcUrl).getLatestLedger();
const signed = await authorizeEntries(
sponsored.authEntries,
holderKeypair,
sequence + DEFAULT_AUTH_VALIDITY_LEDGERS,
networkPassphrase,
);
// Fee payer: attach the signatures, then sign and submit.
const { hash } = await client.submitSponsoredTx(sponsored, signed);- Transport. Entries cross machines as base64 through
authEntriesToXdrandauthEntriesFromXdr.finalizeSponsoredTxreturns the prepared transaction instead of submitting it, for a fee payer signing with a wallet. - Checks before submitting. Finalizing checks every signed entry against
the one built, and requires its expiry to be at least
MIN_AUTH_REMAINING_LEDGERS(40) and at mostMAX_AUTH_VALIDITY_LEDGERS(17,280) ledgers ahead.authorizeEntriesrefuses a key the entries don't name. - Network rejections. The network refuses a signature made with the wrong key, a replayed entry, and an entry reused for other arguments.
- The authorizer must have an account. It must already exist on the network, even though it pays nothing.
buildTransferAdminTx(admin, newAdmin), on every client, uses the same flow,
because the contract requires both admins to authorize. By default the current
admin pays, and the incoming admin signs its entry.
Governance results are advisory in Phase 1
The governance contract accepts whatever vote weight its caller declares and
verifies it against nothing, and finalize applies no quorum. Any address
can carry any proposal. Do not gate anything privileged on an outcome read
through GovernanceClient.
Weight is deliberately unverified rather than read from a live balance, which
would be worse: a holder could vote, transfer the same tokens onward, and vote
again from the recipient. Correct weighting needs balances snapshotted at
proposal creation, which arrives with SFORGE in Phase 2/3.
Status
Phase 1. The contracts are unaudited and not deployed to mainnet. See the roadmap.
License
Apache-2.0
