diff --git a/skills/dapp/SKILL.md b/skills/dapp/SKILL.md index f6c0eed..9642bd2 100644 --- a/skills/dapp/SKILL.md +++ b/skills/dapp/SKILL.md @@ -42,7 +42,7 @@ Client-side development with `@stellar/stellar-sdk`, wallet connection, signing, ## Recommended Dependencies -> **Requires Node.js 20+** — the Stellar SDK dropped Node 18 support. +> **Requires Node.js 22+.** As of SDK v16, Node 22 is the minimum (older Node produces an `EBADENGINE` warning). v16 also folded `@stellar/stellar-base` into `@stellar/stellar-sdk`, is ESM-first, and uses native `fetch` instead of axios. If you still import `@stellar/stellar-base` directly, switch the import to `@stellar/stellar-sdk` and uninstall the base package (keeping both breaks `instanceof` checks). See the [migration guide](https://stellar.github.io/js-stellar-sdk/guides/00-migration). ```bash npm install @stellar/stellar-sdk @stellar/freighter-api @@ -50,6 +50,8 @@ npm install @stellar/stellar-sdk @stellar/freighter-api npm install @stellar/stellar-sdk @creit.tech/stellar-wallets-kit ``` +> **Sourcing:** SDK mechanics below (init, transaction building, contract invocation, submission, data fetching, error handling) track the official [JS SDK docs](https://stellar.github.io/js-stellar-sdk/) (which also publish [`llms.txt`](https://stellar.github.io/js-stellar-sdk/llms.txt) / [`llms-full.txt`](https://stellar.github.io/js-stellar-sdk/llms-full.txt) bundles for agents). Wallet integrations (Freighter, Stellar Wallets Kit), passkey smart accounts, and the OpenZeppelin relayer are separate packages, not part of the JS SDK — verify those against their own upstream docs. + ## SDK Initialization > For the full API reference (RPC methods, Horizon endpoints, migration guide), see the [data skill](../data/SKILL.md). @@ -272,7 +274,57 @@ export async function buildPaymentTx( } ``` -### Smart Contract Invocation +### Smart Contract Invocation (`contract.Client`) + +The canonical way to call a Soroban contract from JS is the `contract.Client`, not hand-built `Contract.call` + `assembleTransaction`. The client reads the contract's interface from the network, so each method is callable by name and returns an `AssembledTransaction`. You get a native JS result and don't build ScVals by hand. + +```typescript +import { contract } from "@stellar/stellar-sdk"; +import { config } from "@/lib/stellar"; + +// Describe just the methods you call. `Client.from()` uses this to type +// the returned client, so calls are checked and autocompleted — no codegen. +// For a contract with many methods, generate this interface from its spec +// with the SDK's binding CLI instead of writing it by hand. +interface CounterContract { + increment: ( + options?: contract.MethodOptions, + ) => Promise>; +} + +// `signTransaction` comes from the wallet (e.g. Freighter/Wallets Kit in the +// browser). `contract.basicNodeSigner(keypair, networkPassphrase)` is the +// Node equivalent for scripts and tests. +export async function getCounterClient( + contractId: string, + publicKey: string, + signTransaction: contract.ClientOptions["signTransaction"], +) { + return contract.Client.from({ + contractId, + rpcUrl: config.rpcUrl, + networkPassphrase: config.networkPassphrase, + publicKey, + signTransaction, + }); +} + +// Preview (free simulation) then sign + send to apply on-chain. +export async function increment(client: contract.Client & CounterContract) { + const tx = await client.increment(); + console.log("preview:", tx.result); // predicted return value, no signature + const sent = await tx.signAndSend(); // submits and polls to completion + return sent.result; +} +``` + +`AssembledTransaction` also supports fine-grained control (`{ fee, simulate, timeoutInSeconds }` as a second arg) and multi-party auth via `tx.needsNonInvokerSigningBy()` / `tx.signAuthEntries()`. See [Invoke a Contract](https://stellar.github.io/js-stellar-sdk/guides/06-invoke-a-contract) and [Authorize a Contract Call](https://stellar.github.io/js-stellar-sdk/guides/07-contract-auth). + +
+Advanced: low-level invocation without a client + +Use this only when you need direct control over the transaction (e.g. batching a contract call with classic operations). Otherwise prefer `contract.Client` above. + ```typescript import * as StellarSdk from "@stellar/stellar-sdk"; import { rpc, config } from "@/lib/stellar"; @@ -284,10 +336,9 @@ export async function invokeContract( args: StellarSdk.xdr.ScVal[] ) { const account = await rpc.getAccount(sourceAddress); - const contract = new StellarSdk.Contract(contractId); - let transaction = new StellarSdk.TransactionBuilder(account, { + const transaction = new StellarSdk.TransactionBuilder(account, { fee: StellarSdk.BASE_FEE, networkPassphrase: config.networkPassphrase, }) @@ -295,28 +346,18 @@ export async function invokeContract( .setTimeout(180) .build(); - // Simulate to get resource estimates - const simulation = await rpc.simulateTransaction(transaction); - - if (StellarSdk.rpc.Api.isSimulationError(simulation)) { - throw new Error(`Simulation failed: ${simulation.error}`); - } - - // Assemble with proper resources - transaction = StellarSdk.rpc.assembleTransaction( - transaction, - simulation - ).build(); - - return transaction.toXDR(); + // `prepareTransaction` simulates and applies footprint/auth/fees in one step. + // (Equivalent to simulateTransaction + rpc.assembleTransaction.) + const prepared = await rpc.prepareTransaction(transaction); + return prepared.toXDR(); } ``` -### Building ScVal Arguments +**Building ScVal arguments by hand** (only needed for the low-level path — `contract.Client` converts native JS args for you): + ```typescript import * as StellarSdk from "@stellar/stellar-sdk"; -// Common conversions const addressVal = StellarSdk.Address.fromString(address).toScVal(); const i128Val = StellarSdk.nativeToScVal(BigInt(amount), { type: "i128" }); const u32Val = StellarSdk.nativeToScVal(42, { type: "u32" }); @@ -334,10 +375,15 @@ const structVal = StellarSdk.nativeToScVal( } ); -// Vec -const vecVal = StellarSdk.nativeToScVal([1, 2, 3], { type: "i128" }); +// Vec of i128 — the element type is applied to each item +const vecVal = StellarSdk.nativeToScVal( + [1, 2, 3].map((n) => BigInt(n)), + { type: "i128" } +); ``` +
+ ## Transaction Submission ### Submit and Wait for Confirmation @@ -369,15 +415,13 @@ async function submitSorobanTransaction(signedXdr: string) { const response = await rpc.sendTransaction(transaction); if (response.status === "ERROR") { - throw new Error(`Transaction failed: ${response.errorResult}`); + throw new Error(`Send failed: ${response.errorResult}`); } - // Poll for completion - let getResponse = await rpc.getTransaction(response.hash); - while (getResponse.status === "NOT_FOUND") { - await new Promise((resolve) => setTimeout(resolve, 1000)); - getResponse = await rpc.getTransaction(response.hash); - } + // Poll for completion. pollTransaction handles the retry loop (default 5 + // attempts, 1s apart — tune with { attempts, sleepStrategy }) instead of a + // hand-rolled while loop that can spin forever. + const getResponse = await rpc.pollTransaction(response.hash); if (getResponse.status === "SUCCESS") { return { @@ -551,6 +595,7 @@ export default function RootLayout({ ### Account Balance ```typescript +import { NotFoundError } from "@stellar/stellar-sdk"; import { horizon } from "@/lib/stellar"; export async function getBalance(address: string) { @@ -561,15 +606,43 @@ export async function getBalance(address: string) { ); return nativeBalance?.balance || "0"; } catch (error) { - if (error.response?.status === 404) { - return "0"; // Account not funded + // loadAccount rejects with the typed NotFoundError for an unfunded account. + if (error instanceof NotFoundError) { + return "0"; // Account not funded yet } throw error; } } ``` +> For submission failures, Horizon returns result codes under `error.response?.data?.extras?.result_codes` (`transaction` + per-`operation`). See [Handle Errors](https://stellar.github.io/js-stellar-sdk/guides/05-handle-errors). + ### Contract State + +For a read-only contract call, `rpc.Server` has one-line shortcuts that build the contract interface for you (including the built-in spec for Stellar Asset Contracts), so no client setup or manual ScVal work is needed: + +```typescript +import { rpc } from "@/lib/stellar"; + +// Run a read-only method and get the decoded result directly. +const { result: balance, isReadCall } = await rpc.queryContract( + tokenId, + "balance", + { id: "G..." } // named args, keyed by parameter name; omit for no-arg methods +); + +// Discover a contract's callable methods from just its ID. +const methods = await rpc.getContractMethods(tokenId); +// [{ name: "balance", inputs: [{ name: "id", type: "Address" }], outputs: ["I128"] }, ...] +``` + +`isReadCall` is per-call: `false` means the `result` is only a simulation preview of a call that would change state (apply it by signing a transaction via `contract.Client`). + +
+Advanced: read a raw ledger entry + +Reach for `getLedgerEntries` only when you need a specific storage key that isn't exposed as a contract method. + ```typescript import * as StellarSdk from "@stellar/stellar-sdk"; import { rpc } from "@/lib/stellar"; @@ -598,6 +671,8 @@ export async function getContractData( } ``` +
+ ## Smart Accounts (Passkey Wallets) For passwordless authentication using WebAuthn passkeys, use Smart Account Kit.