Invoke a Contract
This guide calls a method on a deployed Soroban contract from JavaScript. You will connect over RPC, preview a call for free with simulation, then sign and send it to change on-chain state. Everything runs on testnet, so it is free and safe to repeat.
Prerequisites
- A funded testnet account and its keypair. If you need one, see Connect and Fund an Account.
- A deployed contract and its contract ID (a
C...string). This guide uses the increment contract. Deploying is a one-time setup with a different toolchain (the Stellar CLI and Rust): follow Stellar’s Deploy the Increment Contract tutorial once (about 20 to 30 minutes), then paste the contract ID it prints intocontractIdbelow. You will not touch the CLI again in this guide. This guide types the client with a small hand-written interface; generating one from a contract’s spec is covered later in the series. - The examples use testnet RPC at
https://soroban-testnet.stellar.org.
Connect and load the contract
Contracts are reached over
Soroban RPC, not Horizon, so
this guide connects over RPC instead of using the Horizon.Server from earlier
guides. Build a contract.Client
from your deployed contract ID. The client reads the contract’s interface from
the network, which is what lets you call its methods by name:
import { contract, Keypair, Networks } from "@stellar/stellar-sdk";
const rpcUrl = "https://soroban-testnet.stellar.org";const networkPassphrase = Networks.TESTNET;
// Describe just the methods you call. `Client.from<T>()` uses this to type the// returned client, so the calls below are checked and autocompleted — no code// generation needed.interface IncrementContract { increment: ( options?: contract.MethodOptions, ) => Promise<contract.AssembledTransaction<number>>;}
const { signTransaction } = contract.basicNodeSigner(keypair, networkPassphrase);
const client = await contract.Client.from<IncrementContract>({ contractId, rpcUrl, networkPassphrase, publicKey: keypair.publicKey(), signTransaction,});Here keypair is your funded account from
Connect and Fund an Account and contractId is
your deployed contract’s C... ID. The client is built from the live contract at
runtime, so TypeScript cannot infer its methods on its own. Passing an interface
to Client.from<T>() types them:
client.increment() below is fully typed and autocompleted, with no code
generation. For a contract with many methods, generate that interface from its
spec (covered later in the series) rather than writing it by hand.
Query contract state
Sometimes you only want to inspect a contract or read a value from it, not
change anything. For that, rpc.Server has two one-line shortcuts that build the
contract’s interface for you — including the built-in spec for Stellar Asset
Contracts (SACs) — so they work from just a contract ID, with no client setup.
getContractMethods
lists a contract’s callable methods and their signatures, which is handy when you
are inspecting a contract you did not write. The spec it reports carries no
read/write flag, so to learn whether a specific call would change state, invoke
it with queryContract and read its isReadCall (see below).
queryContract
runs a read-only
call and returns the decoded result. It simulates the call the same way the preview
below does, so it needs no signing or fee, but it hands you the value directly. Here
both run against a token contract — discover its methods, then read one:
import { rpc } from "@stellar/stellar-sdk";
const server = new rpc.Server(rpcUrl);
// Discover what the contract exposes, from just its ID.const methods = await server.getContractMethods(tokenId);// [// { name: "decimals", inputs: [], outputs: ["U32"] },// { name: "balance", inputs: [{ name: "id", type: "Address" }], outputs: ["I128"] },// { name: "transfer", inputs: [...], outputs: [] },// ]
// Read one of its read-only methods in a single line.const { result: decimals, isReadCall } = await server.queryContract<number>( tokenId, "decimals",);
const { result: balance } = await server.queryContract<bigint>( tokenId, "balance", { id: "G...", // named arguments, keyed by the method's parameter names },);Alongside the decoded result, queryContract returns isReadCall: whether
this call — for the exact arguments given — wrote no state and needed no
signature. It is per-call, not a fixed property of the method. Since
queryContract never signs or sends, isReadCall: false means the result is
only a simulation preview of a call that would change state; to apply such a
change you build a client and sign a transaction, as shown next.
Preview a call with simulation
Calling a contract method does not send anything yet: it builds a transaction and
simulates it. The RPC server runs the call against the current ledger state
and returns the result without committing anything, so a preview is free and needs
no signature. Read the predicted return value from
tx.result:
const tx = await client.increment();
tx.result; // the value the call would return; nothing has been sentNothing changed on-chain: simulate again and you get the same answer. A read-only
method (one that does not change state) stops here. tx.isReadCall is true, and
tx.result is your final answer with no signing or fee, because a read touches no
state and needs no authorization. increment does change state, so tx.isReadCall
is false and this is only a preview. To apply it, you sign and send.
Sign and send to apply it
To apply the state change, sign and send the transaction. The signer is
the basicNodeSigner you
passed to the client (a simple Node signer for scripts and tests; a browser app
swaps in a wallet such as Freighter). signAndSend submits the transaction and
waits for the network, returning a
SentTransaction whose
result is the value the contract returned on-chain:
const sent = await tx.signAndSend();
sent.result; // the applied result; send again and the counter advancesIf a method depends on contract state that has expired, pass restore: true in
the method options and simulation will restore it before the call; see
State Archival.
Put it together
The whole flow as one runnable script. Set contractId to your deployed
increment contract (see Prerequisites); the script funds a throwaway source
account with friendbot so it runs end to end. In your app, replace the
Keypair.random() call with your existing funded keypair.
import { contract, rpc, Keypair, Networks } from "@stellar/stellar-sdk";
const rpcUrl = "https://soroban-testnet.stellar.org";const networkPassphrase = Networks.TESTNET;const contractId = "C..."; // your deployed increment contract (see Prerequisites)
interface IncrementContract { increment: ( options?: contract.MethodOptions, ) => Promise<contract.AssembledTransaction<number>>;}
async function main() { const server = new rpc.Server(rpcUrl); const keypair = Keypair.random(); const { signTransaction } = contract.basicNodeSigner( keypair, networkPassphrase, );
try { // Fund a throwaway account to invoke from (the RPC-side friendbot). await server.fundAddress(keypair.publicKey());
const client = await contract.Client.from<IncrementContract>({ contractId, rpcUrl, networkPassphrase, publicKey: keypair.publicKey(), signTransaction, });
// Preview the call for free with simulation. const tx = await client.increment(); console.log("preview:", tx.result);
// Sign and send to apply it on-chain. const sent = await tx.signAndSend(); console.log("applied:", sent.result); } catch (e) { console.error("Invocation failed:", e); }}
main().catch(console.error);You can now read from and write to a deployed contract from JavaScript. Next, learn to authorize calls that more than one account must sign.