TypeScript SDK
Start the API sandbox (apps/api/README.md, "Sandbox"): a local chain, the contracts, test tokens and the API, with nothing real in it.
docker compose up -d --wait
(cd contracts/evm && forge build)
pnpm install && pnpm build
pnpm --filter @rwa-router/api sandboxThen make the Attestation, ask for a Route preview, sign the Order it proposes and watch it fill. This is examples/quickstart.ts; the tests run it as it stands here.
import { StreamClient, createClient, signAttestation, signOrder, toEvmDomain, toEvmOrder, type Order } from '@rwa-router/sdk';
import { keccak256, toHex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
// The API sandbox. Its first funded test wallet has a publicly known key: never use it anywhere else.
const apiUrl = process.env.RWA_API_URL ?? 'http://127.0.0.1:3000';
const account = privateKeyToAccount(keccak256(toHex('rwa-router sandbox: wallet-1')));
const client = createClient({ baseUrl: apiUrl });
// 1. The Attestation, once per wallet.
await client.submitAttestation(await signAttestation(account, account.address));
// 2. A Route preview for selling 0.5 GOLD: every candidate Source, and the Order to sign.
const gold = await client.getAsset({ id: 'sandbox-gold' });
const market = gold.markets[0]!;
const quote = await client.quote({
chainId: gold.chainId,
owner: account.address,
sellToken: market.assetToken,
buyToken: market.paymentToken,
sellAmount: 5n * 10n ** 17n, // base units, always bigint
});
if (!quote.order || !quote.typedData) throw new Error('no Source can fill this Order now');
for (const c of quote.candidates) console.log(`${c.source}: ${c.amountOut} after ${c.platformFee} Platform Fee, in ${c.settlementSeconds} s`);
// 3. Sign the Order. The signature authorises exactly this limit price, expiry and maximum fee.
const order = toEvmOrder(quote.order);
const signature = await signOrder(account, order, toEvmDomain(quote.typedData.domain));
// 4. Watch the wallet's Orders, then submit.
const stream = new StreamClient(`${apiUrl.replace(/^http/, 'ws')}/v1/stream`);
let done!: (order: Order) => void;
const filled = new Promise<Order>((resolve) => (done = resolve));
await stream.subscribe(`orders:${account.address}`, (update) => {
console.log(`Order ${update.hash}: ${update.status}`);
if (update.hash === quote.orderHash && update.status === 'filled') done(update);
});
await client.submitOrder({ chainId: quote.chainId, order, signature });
// 5. Filled: the Settlement transaction is confirmed and the tokens moved wallet to wallet.
export const result = await filled;
console.log(`sold ${result.filledSellAmount}, received ${result.filledBuyAmount} from ${result.fills.map((f) => f.source).join(', ')}`);
await stream.close();All amounts are base-unit integers as bigint, in what you send and in what you get back. The API carries them as decimal strings; the client converts both ways, and a JSON number never stands for an amount.
From packages/sdk-ts/README.md.