Client (@caatinga/client)
@caatinga/client is the alpha browser/client-side interop layer for generated Stellar CLI TypeScript bindings. It connects:
- generated contract bindings
caatinga.artifacts.json- RPC URL and network passphrase
- a wallet adapter
It does not replace Stellar CLI, Stellar SDK, Soroban SDK, generated bindings, or wallet signing. It does not serialize SCVal manually, parse XDR manually, or store secret keys.
Scope
Single-invoker scope (until v1.0)
Browser wallet support is single-invoker only until v1.0. @caatinga/client invoke() signs with one connected wallet as the transaction invoker.
Contracts requiring delegated AddressV2 / non-invoker signAuthEntry credentials will fail with CAATINGA_MULTI_AUTH_REQUIRED; orchestration is application code today.
Included in alpha:
- artifact-based
contractIdlookup - generated binding registration
CaatingaWalletAdapter- Stellar Wallets Kit adapter
- Freighter adapter compatibility subpath
createWalletSession— framework-agnostic connection state, persistence, silent restore@caatinga/client/react—WalletProvider+useWallethooks (optional React peer)read()simulate()invoke()buildXdr()- explicit
debugXdranddebugRaw CAATINGA_XDR_*, binding, wallet, and artifact errors
Not included:
- CLI XDR commands
caatinga generate --interop- custom SCVal serialization
- multisig orchestration
- backend signing
Install
pnpm add @caatinga/client @creit.tech/stellar-wallets-kit@caatinga/client depends on @caatinga/core and imports only the browser-safe subpath @caatinga/core/browser (errors and artifact types). That entry excludes Node-only modules such as execa, so Vite and webpack bundles do not pull in the CLI shell layer.
Application code that needs the same types in the browser should import from @caatinga/core/browser rather than the root @caatinga/core package entry.
Counter Example
import { createCaatingaClient } from "@caatinga/client";
import { createStellarWalletsKitAdapter } from "@caatinga/client/stellar-wallets-kit";
import * as Counter from "./contracts/generated/counter";
import artifacts from "../caatinga.artifacts.json";
const wallet = createStellarWalletsKitAdapter();
const client = createCaatingaClient({
network: {
name: "testnet",
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: "Test SDF Network ; September 2015",
},
artifacts,
wallet,
contracts: {
counter: {
binding: Counter,
},
},
});
const before = await client.contract("counter").read<number>("get");
const increment = await client.contract("counter").invoke<number>("increment");
const after = increment.result ?? (await client.contract("counter").read<number>("get"));Minimal successful result:
{
"status": "confirmed",
"contract": "counter",
"method": "increment",
"contractId": "C...",
"transactionHash": "..."
}If a contract ID is not passed explicitly, the client resolves it from:
artifacts.networks[network].contracts[contract].contractIdTo override artifacts:
const client = createCaatingaClient({
network,
artifacts,
wallet,
contracts: {
counter: {
binding: Counter,
contractId: "C...",
},
},
});Arguments
Arguments are forwarded as one object to the generated binding method:
await client.contract("token").invoke("transfer", {
to: receiverAddress,
amount: 100n,
});Read and Simulate
| API | When to use |
|---|---|
read() | Read-only methods; returns the parsed value directly |
simulate() | Read-only methods when you need status, contractId, or debugRaw |
invoke() | State-changing methods that must be signed and submitted |
Use read() for read-only contract methods when the UI only needs the returned value:
const value = await client.contract("counter").read<number>("get");Use simulate() when the UI or diagnostics need metadata:
const result = await client.contract("counter").simulate<number>("get", {
debugRaw: true,
});
console.log(result.status);
console.log(result.contractId);
console.log(result.result);
console.log(result.raw);simulate() prepares the generated binding transaction and returns the parsed binding result. It calls wallet.getPublicKey() to build the generated client, but it does not call wallet.signTransaction(). If the simulated method does not expose a result, the client throws CAATINGA_READ_RESULT_MISSING.
Calling invoke() on a read-only binding method may fail with a hint to use read() or simulate() instead.
Project layout
Keep Caatinga client wiring in a dedicated module with static imports:
// src/caatinga.ts — static imports only
import { createCaatingaClient } from "@caatinga/client";
import artifactsJson from "../caatinga.artifacts.json";
import * as Counter from "./contracts/generated/counter";
import { stellarWalletAdapter } from "./wallet.js";
export const caatingaClient = createCaatingaClient({
/* ... */
});// src/App.tsx — import the pre-built client
import { caatingaClient } from "./caatinga.js";Avoid dynamic import() of bindings or caatinga.artifacts.json inside React components. Vite needs static import paths to bundle JSON artifacts and generated contract modules correctly. The official react-vite-counter template follows this pattern in src/caatinga.ts.
XDR Debug
XDR is omitted by default and only returned when debugXdr is enabled.
const result = await client.contract("counter").invoke("increment", {
debugXdr: true,
});
console.log(result.xdr?.unsigned);
console.log(result.xdr?.prepared);
console.log(result.xdr?.signed);Raw binding or submit output is omitted unless debugRaw is enabled:
const result = await client.contract("counter").invoke("increment", {
debugXdr: true,
debugRaw: true,
});
console.log(result.raw);Build XDR Only
const tx = await client.contract("counter").buildXdr("increment");
console.log(tx.unsignedXdr);
console.log(tx.preparedXdr);buildXdr() creates the generated binding client, so it may call wallet.getPublicKey(). It does not call wallet.signTransaction().
Implementing a wallet adapter
export interface CaatingaWalletAdapter {
getPublicKey(): Promise<string>;
signTransaction(input: { xdr: string; networkPassphrase: string }): Promise<string>;
}Contract:
- Reject on dismissal:
getPublicKeyandsignTransactionmust reject when the user cancels or dismisses the wallet UI. Do not leave the promise pending indefinitely. - Adapter timeouts: Your adapter may apply its own timeout before rejecting.
- Caatinga timeout: Caatinga does not impose a default timeout. Pass optional
walletTimeout(milliseconds) onCaatingaClientConfigto capgetPublicKeyandsignTransaction; when exceeded, the client throwsCAATINGA_WALLET_TIMEOUT.
The Stellar Wallets Kit adapter is exported from:
import { createStellarWalletsKitAdapter } from "@caatinga/client/stellar-wallets-kit";The Freighter adapter remains available for compatibility:
import { freighterWalletAdapter } from "@caatinga/client/freighter";For the full adapter guide — bundled adapters, custom adapters, capability methods, and Stellar Wallets Kit bundler workarounds — see Wallets.
Vite apps using Stellar Wallets Kit get reusable bundler helpers from @caatinga/client/vite: walletStubViteAliases, walletStubOverrides, and walletStubPnpmWorkspaceYaml. These avoid copying 15+ lines of overrides by hand — see Wallets — Adding SWK to a custom Vite app.
Wallet session
createWalletSession(adapter, options?) wraps any adapter with connection state (disconnected / connecting / connected), subscriptions, optional persistence, and silent restore — usable from any framework or plain TypeScript:
import { createWalletSession } from "@caatinga/client";
const session = createWalletSession(wallet, { persist: true });
session.subscribe(() => render(session.getState()));
await session.connect(); // modal when the adapter has one, else getPublicKey()
await session.restore(); // silent reconnect on page load — never throwsReact hooks
React apps (react >= 18, optional peer) get a provider and hook from the react subpath:
import { WalletProvider, useWallet } from "@caatinga/client/react";
<WalletProvider adapter={wallet} options={{ persist: true }}>
<App />
</WalletProvider>;
const { publicKey, connected, connecting, error, connect, disconnect, session } = useWallet();useWallet is backed by useSyncExternalStore; WalletProvider restores persisted sessions on mount automatically. Full reference and examples: Wallets.
Binding Contract
The default binding adapter expects generated bindings to:
- export
Client - accept
contractId,publicKey,rpcUrl, andnetworkPassphrase - expose contract methods on the client instance
- return a transaction-like object with
toXDR() - optionally expose
prepare() - expose
signAndSend({ signTransaction }), wheresignTransactionreturns{ signedTxXdr }
Caatinga adapts CaatingaWalletAdapter.signTransaction({ xdr, networkPassphrase }) into generated transaction signTransaction(xdr, opts), and does not parse XDR or serialize Soroban values.
If @stellar/stellar-sdk generate changes this generated shape, the compatibility fix belongs in the binding adapter/client integration layer, not in application code.
SDK v16 transaction submission
@caatinga/client calls assembledTx.signAndSend({ signTransaction }) on generated bindings. SDK v16 accepts optional force, signTransaction, and watcher; Caatinga passes only signTransaction.
The return is a SentTransaction<T>. Caatinga normalizes sendTransactionResponse.hash → transactionHash and reads the result getter → result on invoke responses.
Multi-auth: when a simulated transaction has unsigned non-invoker Soroban auth entries (delegated address credentials / "AddressV2"), signing raises NeedsMoreSignaturesError. Caatinga surfaces CAATINGA_MULTI_AUTH_REQUIRED. Full signAuthEntry orchestration is not supported until v1.0 — handle multi-signer flows in application code.
Failure behavior
Client failures use public CAATINGA_* codes. The most common are:
CAATINGA_CONTRACT_ARTIFACT_NOT_FOUNDCAATINGA_BINDING_CLIENT_NOT_FOUNDCAATINGA_BINDING_METHOD_NOT_FOUNDCAATINGA_WALLET_NOT_CONNECTEDCAATINGA_WALLET_TIMEOUTCAATINGA_XDR_BUILD_FAILEDCAATINGA_XDR_PREPARE_FAILEDCAATINGA_XDR_SIGN_FAILEDCAATINGA_XDR_SUBMIT_FAILEDCAATINGA_XDR_RESULT_FAILEDCAATINGA_READ_RESULT_MISSINGCAATINGA_PLACEHOLDER_BINDING
See errors.md for the full table.