diff --git a/docs/reference/assembled-tx-examples.md b/docs/reference/assembled-tx-examples.md new file mode 100644 index 000000000..96dff58e7 --- /dev/null +++ b/docs/reference/assembled-tx-examples.md @@ -0,0 +1,523 @@ +# AssembledTransaction Examples + +This guide demonstrates how to use the various static methods of `AssembledTransaction` for different smart contract interaction scenarios. + +**Note: It is recommended to use [contract bindings](https://developers.stellar.org/docs/learn/fundamentals/contract-development/types/fully-typed-contracts#stellar-js-sdk-contractclient) generated by the [stellar cli](https://developers.stellar.org/docs/tools/cli/install-cli) to handle the creation of `AssembledTransaction`** + +## Table of Contents + +- [Basic Usage with `build()`](#basic-usage-with-build) +- [Custom Operations with `buildWithOp()`](#custom-operations-with-buildwithop) +- [Serialization with `toJSON()` and `fromJSON()`](#serialization-with-tojson-and-fromjson) +- [XDR Serialization with `toXDR()` and `fromXDR()`](#xdr-serialization-with-toxdr-and-fromxdr) +- [Multi-Auth Workflows](#multi-auth-workflows) +- [Error Handling](#error-handling) + +--- + +## Basic Configuration +This will be the base configuration reused throughout these examples +```typescript +const invoker = Keypair.random() / KeyPair.fromSecret("S...."); +const config = { + networkPassphrase: Networks.TESTNET, + rpcUrl: 'https://soroban-testnet.stellar.org', + publicKey: invoker.publicKey(), + signTransaction: basicNodeSigner(invoker, Networks.TESTNET).signTransaction, + signAuthEntry: basicNodeSigner(invoker, Networks.TESTNET).signAuthEntry + parseResultXdr: (xdr: xdr.ScVal) => scValToNative(xdr), +} +``` + +## Basic Usage with `build()` + +The `build()` static method is the most common way to create an `AssembledTransaction`. It constructs and simulates a contract method invocation. + +### Simple Read Call + +```typescript +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; +import { Networks } from '@stellar/stellar-sdk'; + + +// Read-only call (no signing needed) +const tx = await AssembledTransaction.build({ + method: 'get_balance', + args: [ + nativeToScVal(invoker.publicKey(), {type: "address"}) + ], + contractId: Asset.native().contractId(Networks.TESTNET), + ...config +}); + +// Access the result immediately (simulation only) +console.log('Balance:', tx.result); +``` + +### Simple Write Call + +```typescript +import { Keypair, basicNodeSigner, nativeToScVal, Asset } from '@stellar/stellar-sdk'; +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +// Write call (requires signing and sending) +const alice = Keypair.random(); +const tx = await AssembledTransaction.build({ + method: 'transfer', + args: [ + nativeToScVal(invoker.publicKey(), { type: 'address' }), // from + nativeToScVal(alice.publicKey(), { type: 'address' }), // to + nativeToScVal('100', { type: 'i128' }), // amount + ], + contractId: Asset.native().contractId(Networks.TESTNET), + ...config +}); + +// Sign and send the transaction +const sentTx = await tx.signAndSend(); +console.log('Transaction hash:', sentTx.hash); + +// Wait for the result +const result = await sentTx.getTransactionResponse; +console.log('Result:', result); +``` + +### Skipping Simulation + +```typescript +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +// Build without automatic simulation +const tx = await AssembledTransaction.build({ + method: 'my_method', + args: [], + contractId: 'CCONT...', + simulate: false, // Skip automatic simulation + ...config, +}); + +// Now simulate +await tx.simulate(); + +console.log('Simulated result:', tx.result); +``` + +--- + +## Custom Operations with `buildWithOp()` + +Use `buildWithOp()` for non-contract-invoking transactions such as restoring, contract uploading and deploying. + +The following `Operations` are allowed +- invokeHostFunction + - createCustomContract + - createStellarAssetContract + - uploadContractWasm + - invokeContractFunction +- restoreFootprint +- extendFootprintTtl + + +### Deploying a Contract + +```typescript +import { Operation, Asset } from '@stellar/stellar-sdk'; +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +// Deploy a Stellar Asset Contract +const assetOp = Operation.createStellarAssetContract({ + asset: new Asset('TEST', invoker.publicKey()), + source: invoker.publicKey() +}); + +const deployTx = await AssembledTransaction.buildWithOp( + assetOp, + { + publicKey: issuer.publicKey(), + contractId: '', // Not needed for deployment + method: 'deploy', // Method name for tracking + ...config + } +); + +const sentTx = await deployTx.signAndSend(); +console.log('Contract deployed at:', sentTx.result); +``` + +### Upload Contract WASM + +```typescript +import { Operation } from '@stellar/stellar-sdk'; +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; +import fs from 'fs'; + +const wasmBytes = fs.readFileSync('./contract.wasm'); +const installOp = Operation.uploadContractWasm({ + wasm: wasmBytes, + source: invoker.publicKey() +}); + +const installTx = await AssembledTransaction.buildWithOp( + installOp, + { + contractId: '', + method: 'install_wasm', + ...config + } +); + +const result = await installTx.signAndSend(); +console.log('WASM hash:', result.result); +``` + +--- + +## Serialization with `toJSON()` and `fromJSON()` + +These methods are essential for multi-auth workflows where a transaction needs to be passed between different parties for signing. + +### Serializing a Transaction + +```typescript +import { Operation, Keypair } from '@stellar/stellar-sdk'; +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +// Invoker creates and simulates a transaction +const bob = Keypair.random(); +const tx = await AssembledTransaction.build({ + method: 'multi_sig_transfer', + args: [ + nativeToScVal(invoker.publicKey(), {type: "address"}), + nativeToScVal(bob.publicKey(), type: "address"), + nativeToScVal(1000n, {type: "i128"}) + ], + contractId: 'CCONT...', + ...config +}); + +// Check who else needs to sign +const needsSigning = tx.needsNonInvokerSigningBy(); +console.log('Needs signatures from:', needsSigning); // ['GBOB...'] + +// Serialize to JSON for transmission +const txJson = tx.toJSON(); + +// Send to Bob (via WebSocket, HTTP, etc.) +sendToBob(txJson); +``` + +### Deserializing a Transaction + +```typescript +import { Operation, Keypair } from '@stellar/stellar-sdk'; +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; +// Bob receives the JSON from the Invoker +const txJson = receiveFromAlice(); +const bob = Keypair.random(); +// Deserialize back to AssembledTransaction +const tx = AssembledTransaction.fromJSON( + { + contractId: '', // Not required + method: '', // Not required + networkPassphrase: Networks.TESTNET, + rpcUrl: 'https://soroban-testnet.stellar.org', + publicKey: bob.publicKey(), + parseResultXdr: (xdr) => scValToNative(xdr), + signAuthEntry: basicNodeSigner(bob, Networks.TESTNET).signAuthEntry + }, + JSON.parse(txJson) +); + +// Bob signs the auth entries +await tx.signAuthEntries(); + +// Serialize and send back to Alice +const signedJson = tx.toJSON(); +sendToInvoker(signedJson); +``` + +## XDR Serialization with `toXDR()` and `fromXDR()` + +XDR serialization provides a more compact format than JSON, useful for storage or bandwidth-constrained scenarios. + +### Serializing to XDR + +```typescript +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +const tx = await AssembledTransaction.build({ + method: 'increment', + args: [], + contractId: 'CCONT...', + ...config +}); + +// Serialize to base64-encoded XDR +const xdrString = tx.toXDR(); +console.log('XDR:', xdrString); + +``` + +### Deserializing from XDR + +```typescript +import { Spec, rpc } from '@stellar/stellar-sdk/contract'; +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; +// Need a contract spec to parse the result +const server = new rpc.Server("https://soroban-testnet.stellar.org") +const contractWasm = await server.getContractWasmByContractId("CCONT..."); +const spec = await Spec.fromWasm(contractWasm); + +// Deserialize +const tx = AssembledTransaction.fromXDR( + { + contractId: '', // The contract id will retrieved when deserialized + ...config + }, + xdrString, + spec +); + +// Requires resimulation +await tx.simulate(); + +// Continue with signing and sending +await tx.signAndSend(); +``` + +--- + +## Multi-Auth Workflows + +Soroban supports multi-signature transactions where multiple parties must approve. + +### Checking Who Needs to Sign + +```typescript +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; +const tx = await AssembledTransaction.build({ + method: 'multi_party_operation', + args: [ + nativeToScVal("GPARTY1...", {type: "address"}), + nativeToScVal("GPARTY2...", {type: "address"}), + nativeToScVal("GPARTY3...", {type: "address"}), + ], + contractId: 'CCONT...', + ...config +}); + +// Check who needs to sign (excluding the invoker) +const signers = tx.needsNonInvokerSigningBy(); +console.log('Required signatures:', signers); +// ['GPARTY1...', 'GPARTY2...', 'GPARTY3...'] + +// Include already-signed entries if needed +const allSigners = tx.needsNonInvokerSigningBy({ + includeAlreadySigned: true +}); + +// Send the transaction to GPARTY1 for signing +await sendToGparty1(tx.toJSON()); + +const jsonTx = await receiveTxFromInvoker(); +const tx = AssembledTransaction.fromJSON( + { + + contractId: '', // Not required + methid: '', // Not required + networkPassphrase: Networks.TESTNET, + rpcUrl: 'https://soroban-testnet.stellar.org', + publicKey: gParty1Pubkey.publicKey(), + parseResultXdr: (xdr) => scValToNative(xdr), + signAuthEntry: basicNodeSigner(gParty1Pubkey, Networks.TESTNET).signAuthEntry + }, + JSON.parse(jsonTx) +); + +// GPARTY1 signs the auth entries +await tx.signAuthEntries(); + +const signedTx = tx.toJSON(); + +// ...The transaction then gets sent to the remaining signers needed + +// NOTE: the transaction must be sent back to the invoker to sign and then send the fully authorized transaction. + + + +``` + +--- + +## Error Handling + +`AssembledTransaction` provides specific error types for common failure scenarios. + +### Handling Specific Errors + +```typescript +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +try { + const tx = await AssembledTransaction.build({ + method: 'some_method', + args: [], + contractId: 'CCONT...', + ...config, + }); + + await tx.signAndSend(); + +} catch (error) { + if (error instanceof AssembledTransaction.Errors.ExpiredState) { + console.error('Contract state expired, needs restoration'); + // Handle restoration + + } else if (error instanceof AssembledTransaction.Errors.NeedsMoreSignatures) { + console.error('Transaction needs additional signatures:', error.message); + // Collect more signatures + + } else if (error instanceof AssembledTransaction.Errors.NoSignatureNeeded) { + console.log('This is a read-only call'); + // Just use the result + + } else if (error instanceof AssembledTransaction.Errors.SimulationFailed) { + console.error('Simulation failed:', error.message); + // Check contract state or arguments + + } else if (error instanceof AssembledTransaction.Errors.UserRejected) { + console.log('User rejected the transaction'); + // Show user-friendly message + + } else { + console.error('Unexpected error:', error); + } +} +``` + +### Manual State Restoration + +```typescript +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; +import { Api } from '@stellar/stellar-sdk/rpc' +const tx = await AssembledTransaction.build({ + method: 'method_with_storage', + args: [], + contractId: 'CCONT...', + ...config, +}); + +// Check if restoration is needed +if (tx.simulation && Api.isSimulationRestore(tx.simulation)) { + console.log('State restoration required'); + + // Manually restore + const restoreResult = await tx.restoreFootprint( + tx.simulation.restorePreamble + ); + + if (restoreResult.status === Api.GetTransactionStatus.SUCCESS) { + console.log('State restored successfully'); + + // Re-simulate with restored state + await tx.simulate({ restore: false }); + } +} + +await tx.signAndSend(); +``` + +### Read Call Detection + +```typescript +import { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +const tx = await AssembledTransaction.build({ + method: 'get_data', + args: [], + contractId: 'CCONT...', + ...config +}); + +// Check if this is a read-only call +if (tx.isReadCall) { + console.log('Read-only call, no signing needed'); + console.log('Result:', tx.result); +} else { + console.log('Write call, needs signing'); + await tx.signAndSend(); +} +``` + +--- + +## Advanced Patterns + +### Transaction Watchers + +```typescript +import { AssembledTransaction, Watcher } from '@stellar/stellar-sdk/contract'; + +const watcher: Watcher = { + onSubmitted: (hash) => { + console.log('Transaction submitted:', hash); + updateUI({ status: 'pending', hash }); + }, + onProgress: (attempt, timeWaiting) => { + console.log(`Attempt ${attempt}, waiting ${timeWaiting}ms`); + updateUI({ status: 'waiting', attempt, timeWaiting }); + } +}; + +const tx = await AssembledTransaction.build({ + method: 'transfer', + args: [ + nativeToScVal(invoker.publicKey(), {type: "address"}), + nativeToScVal("GTO...", {type: "address"}), + nativeToScVal(1000n, {type: "i128"}), + ], + contractId: Asset.native().contractId(Networks.TESTNET), + ...config +}); + +const sentTx = await tx.signAndSend({ watcher }); +console.log('Transaction completed:', sentTx.hash); +``` + +### Custom Timeouts and Fees + +```typescript +const tx = await AssembledTransaction.build({ + method: 'expensive_operation', + args: [], + contractId: 'CCONT...', + fee: '10000', // Higher fee for priority + timeoutInSeconds: 60, // 1 minute timeout instead of default 5 minutes + ...config +}); + +await tx.signAndSend(); +``` + +### Separate Signing and Sending + +```typescript +const tx = await AssembledTransaction.build({ + method: 'transfer', + args: [], + contractId: 'CCONT...', + ...config +}); + +// Sign first +await tx.sign(); + +// Inspect the signed transaction +console.log('Signed transaction:', tx.signed?.toXDR()); + +// Send later +const sentTx = await tx.send(); + +// Or use a watcher when sending +const sentTxWithWatcher = await tx.send(watcher); +``` diff --git a/docs/reference/examples.md b/docs/reference/horizon-examples.md similarity index 99% rename from docs/reference/examples.md rename to docs/reference/horizon-examples.md index 7fde9f797..79f89516e 100644 --- a/docs/reference/examples.md +++ b/docs/reference/horizon-examples.md @@ -1,6 +1,6 @@ ---- -title: Basic Examples ---- + +# Basic Examples + - [Creating a payment Transaction](#creating-a-payment-transaction) - [Loading an account's transaction history](#loading-an-accounts-transaction-history) diff --git a/docs/reference/rpc-examples.md b/docs/reference/rpc-examples.md new file mode 100644 index 000000000..77134f389 --- /dev/null +++ b/docs/reference/rpc-examples.md @@ -0,0 +1,844 @@ +# Soroban RPC Server Examples + +Comprehensive examples for all methods in the `Server` class (also known as `RpcServer`). + +## Table of Contents + +- [Server Initialization](#server-initialization) +- [Account Methods](#account-methods) +- [Ledger Entry Methods](#ledger-entry-methods) +- [Contract Methods](#contract-methods) +- [Transaction Methods](#transaction-methods) +- [Network Methods](#network-methods) +- [Event Methods](#event-methods) +- [Utility Methods](#utility-methods) + +--- + +## Server Initialization + +### Basic Setup + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +// Testnet +const server = new Server('https://soroban-testnet.stellar.org'); + +// Mainnet (Production) +const server = new Server('https://soroban.stellar.org', { + allowHttp: false // Always false in production! +}); + +// Local development +const server = new Server('http://localhost:8000/soroban/rpc', { + allowHttp: true // Only for local development +}); +``` + +### With Custom Headers + +```typescript +const server = new Server('https://soroban-testnet.stellar.org', { + headers: { + 'Authorization': 'Bearer your-token', + 'X-Custom-Header': 'value' + } +}); +``` + +### Using HTTP Client Interceptors + +```typescript +const server = new Server('https://soroban-testnet.stellar.org'); + +// Add request logging +server.httpClient.interceptors.request.use((config) => { + console.log('Request:', config.url); + return config; +}); + +// Add response logging +server.httpClient.interceptors.response.use((response) => { + console.log('Response:', response.status); + return response; +}); +``` + +--- + +## Account Methods + +### `getAccount()` + +Fetch account details needed for building transactions. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { TransactionBuilder, Networks } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const accountId = "GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4"; + +const account = await server.getAccount(accountId); +console.log("Sequence number:", account.sequenceNumber()); +console.log("Account ID:", account.accountId()); + +// Use with TransactionBuilder +const transaction = new TransactionBuilder(account, { + fee: '100', + networkPassphrase: Networks.TESTNET +}) + .setTimeout(30) + .build(); +``` + +### `getAccountEntry()` + +Fetch the full on-chain account entry. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const accountId = "GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4"; + +const entry = await server.getAccountEntry(accountId); +console.log("Balance:", entry.balance().toString()); +console.log("Sequence:", entry.seqNum().toString()); +console.log("Number of subentries:", entry.numSubEntries()); +console.log("Flags:", entry.flags()); +``` + +### `getTrustline()` + +Fetch a specific trustline for an account. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { Asset } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const accountId = "GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4"; +const asset = new Asset( + "TEST", + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" +); + +const trustline = await server.getTrustline(accountId, asset); +console.log("Balance:", trustline.balance().toString()); +console.log("Limit:", trustline.limit().toString()); +console.log("Flags:", trustline.flags()); +``` + +--- + +## Ledger Entry Methods + +### `getLedgerEntries()` + +Fetch arbitrary ledger entries directly. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { xdr, Address, nativeToScVal, scValToNative } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +// Contract data key +const contractId = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM'; +const key = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: new Address(contractId).toScAddress(), + key: nativeToScVal('counter', { type: 'symbol' }), + durability: xdr.ContractDataDurability.persistent(), + }) +); + +const response = await server.getLedgerEntries(key, key); +const entry = response.entries[0]; +console.log('Latest ledger:', response.latestLedger); +if (entry) { + const entryValue = entry.val; + const contractData = entryValue.contractData(); + + const key = contractData.key(); + const val = contractData.val(); + console.log('Key:', scValToNative(key)); + console.log('Val:', scValToNative(val)); + console.log('Last modified:', entry.lastModifiedLedgerSeq); + console.log('Live until:', entry.liveUntilLedgerSeq); +} +``` + +### `getLedgerEntry()` + +Fetch a single ledger entry. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { xdr, scValToNative } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const key = xdr.LedgerKey.contractData(/* ... */); +const entry = await server.getLedgerEntry(key); + +if (entry) { + const entryValue = entry.val; + const contractData = entryValue.contractData(); + + const key = contractData.key(); + const val = contractData.val(); + console.log('Key:', scValToNative(key)); + console.log('Val:', scValToNative(val)); + console.log('Last modified:', entry.lastModifiedLedgerSeq); + console.log('Live until:', entry.liveUntilLedgerSeq); +} else { + console.log("Entry not found"); +} +``` + +### `getClaimableBalance()` + +Fetch a claimable balance entry. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +// Using hex ID +const hexId = "00000000178826fbfe339e1f5c53417c6fedfe2c05e8bec14303143ec46b38981b09c3f9"; +const entry = await server.getClaimableBalance(hexId); + +console.log("Asset:", entry.asset()); +console.log("Amount:", entry.amount().toString()); +console.log("Claimants:", entry.claimants()); + +// Using strkey ID (B...) +const strkeyId = "BAAAAAAA..."; +const entry2 = await server.getClaimableBalance(strkeyId); +``` + +--- + +## Contract Methods + +### `getContractData()` + +Read contract storage data directly. + +```typescript +import { Server, Durability } from '@stellar/stellar-sdk/rpc'; +import { nativeToScVal, scValToNative } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const contractId = 'CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5'; +const key = nativeToScVal('example-key', { type: 'symbol' }); + +// Persistent storage (default) +const data = await server.getContractData(contractId, key); +console.log('Value:', scValToNative(data.val.contractData().val())); +console.log('Live until ledger:', data.liveUntilLedgerSeq); + +// Temporary storage +const tempData = await server.getContractData(contractId, key, Durability.Temporary); +console.log('Temp value:', scValToNative(tempData.val.contractData().val())); + +``` + +### `getContractWasmByContractId()` + +Fetch WASM bytecode by contract ID. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import fs from 'fs'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const contractId = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5"; + +const wasmBuffer = await server.getContractWasmByContractId(contractId); +console.log("WASM size:", wasmBuffer.length, "bytes"); + +``` + +### `getContractWasmByHash()` + +Fetch WASM bytecode by hash. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +// Using Buffer +const wasmHash = Buffer.from("abc123...", 'hex'); +const wasmBuffer = await server.getContractWasmByHash(wasmHash); + +// Using hex string +const wasmBuffer2 = await server.getContractWasmByHash( + "abc123...", + "hex" +); + +// Using base64 string +const wasmBuffer3 = await server.getContractWasmByHash( + "YWJjMTIz...", + "base64" +); + +console.log("WASM bytecode:", wasmBuffer); +``` + +--- + +## Transaction Methods + +### `sendTransaction()` + +Submit a transaction to the network. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { Keypair, TransactionBuilder, Networks } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const keypair = Keypair.random(); +const account = await server.getAccount(keypair.publicKey()); + +const transaction = new TransactionBuilder(account, { + fee: '100', + networkPassphrase: Networks.TESTNET +}) + .addOperation(/* ... */) + .setTimeout(30) + .build(); + +transaction.sign(keypair); + +const response = await server.sendTransaction(transaction); +console.log("Status:", response.status); +console.log("Hash:", response.hash); +console.log("Latest ledger:", response.latestLedger); +console.log("Latest ledger close time:", response.latestLedgerCloseTime); + +if (response.status === "ERROR") { + console.error("Error:", response.errorResult); +} +``` + +### `getTransaction()` + +Fetch transaction status and details. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const txHash = "c4515e3bdc0897f21cc5dbec8c82cf0a936d4741cb74a8e158eb51b9fb00411a"; + +const tx = await server.getTransaction(txHash); +console.log("Status:", tx.status); +console.log("Application order:", tx.applicationOrder); +console.log("Fee charged:", tx.feeBump); +console.log("Envelope XDR:", tx.envelopeXdr); +console.log("Result XDR:", tx.resultXdr); +console.log("Result meta XDR:", tx.resultMetaXdr); + +if (tx.status === "SUCCESS") { + console.log("Ledger:", tx.ledger); + console.log("Created at:", tx.createdAt); +} +``` + +### `pollTransaction()` + +Poll for transaction completion. + +```typescript +import { Server, BasicSleepStrategy, LinearSleepStrategy } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const txHash = "c4515e3bdc0897f21cc5dbec8c82cf0a936d4741cb74a8e158eb51b9fb00411a"; + +// Basic polling (1 second between attempts) +const result = await server.pollTransaction(txHash, { + attempts: 10, + sleepStrategy: BasicSleepStrategy +}); + +console.log("Final status:", result.status); + +// Linear backoff (1s, 2s, 3s, etc.) +const result2 = await server.pollTransaction(txHash, { + attempts: 5, + sleepStrategy: LinearSleepStrategy +}); + +// Custom sleep strategy +const customStrategy = (iter: number) => 500 * iter; // 500ms, 1s, 1.5s, etc. +const result3 = await server.pollTransaction(txHash, { + attempts: 10, + sleepStrategy: customStrategy +}); +``` + +### `simulateTransaction()` + +Simulate a transaction before sending. + +```typescript +import { Api, Server } from '@stellar/stellar-sdk/rpc'; +import { TransactionBuilder, Networks, Contract, Keypair } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const sourceKeypair = Keypair.random(); +const contractId = 'CCONT...'; + +const account = await server.getAccount(sourceKeypair.publicKey()); +const contract = new Contract(contractId); + +const transaction = new TransactionBuilder(account, { + fee: '100', + networkPassphrase: Networks.TESTNET, +}) + .addOperation(contract.call('increment')) + .setTimeout(30) + .build(); + +const simulation = await server.simulateTransaction(transaction); + +if (Api.isSimulationSuccess(simulation) && simulation.result) { + console.log('Return value:', simulation.result.retval); + console.log('Events:', simulation.events); + console.log('Minimum resource fee:', simulation.minResourceFee); +} + +if (Api.isSimulationError(simulation)) { + console.error('Simulation error:', simulation.error); +} +``` + +### `prepareTransaction()` + +Prepare a transaction for submission (simulate + assemble). + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { TransactionBuilder, Networks, Contract, Keypair } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const sourceKeypair = Keypair.random(); +const contractId = "CCONT..."; +const account = await server.getAccount(sourceKeypair.publicKey()); +const contract = new Contract(contractId); + +const transaction = new TransactionBuilder(account, { + fee: '100', + networkPassphrase: Networks.TESTNET +}) + .addOperation(contract.call('transfer', ...[])) + .setTimeout(30) + .build(); + +const prepared = await server.prepareTransaction(transaction); + +prepared.sign(sourceKeypair); +const response = await server.sendTransaction(prepared); +console.log("Transaction sent:", response.hash); +``` + +### `getTransactions()` + +Fetch transactions in a ledger range. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +// Get transactions starting from a specific ledger +const response = await server.getTransactions({ + startLedger: 100000, + pagination: { limit: 10 }, +}); + +console.log('Latest ledger:', response.latestLedger); +console.log('Oldest ledger:', response.oldestLedger); +console.log('Cursor:', response.cursor); + +response.transactions.forEach((tx) => { + console.log('Hash:', tx.txHash); + console.log('Status:', tx.status); + console.log('Ledger:', tx.ledger); + console.log('Application order:', tx.applicationOrder); +}); + +// Pagination using cursor +const nextPage = await server.getTransactions({ + pagination: { cursor: response.cursor, limit: 10 }, +}); + +``` + +--- + +## Network Methods + +### `getHealth()` + +Check server health status. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const health = await server.getHealth(); +console.log("Status:", health.status); // "healthy" +``` + +### `getNetwork()` + +Get network information. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const network = await server.getNetwork(); +console.log("Friendbot URL:", network.friendbotUrl); +console.log("Passphrase:", network.passphrase); +console.log("Protocol version:", network.protocolVersion); +``` + +### `getLatestLedger()` + +Get the latest ledger information. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const ledger = await server.getLatestLedger(); +console.log("Sequence:", ledger.sequence); +console.log("Hash:", ledger.id); +console.log("Protocol version:", ledger.protocolVersion); +``` + +### `getLedgers()` + +Fetch ledger details in a range. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +// Get specific ledgers +const response = await server.getLedgers({ + startLedger: 1000, + pagination: { + limit: 5, + }, +}); + +console.log('Latest ledger:', response.latestLedger); +console.log('Oldest ledger:', response.oldestLedger); +console.log('Cursor:', response.cursor); + +response.ledgers.forEach((ledger) => { + console.log('Sequence:', ledger.sequence); + console.log('Hash:', ledger.hash); + console.log('Close time:', ledger.ledgerCloseTime); + console.log('Metadata XDR:', ledger.metadataXdr); +}); + +// Pagination +const nextPage = await server.getLedgers({ + pagination: { + cursor: response.cursor, + limit: 5, + }, +}); + +``` + +### `getVersionInfo()` + +Get RPC server version information. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +const version = await server.getVersionInfo(); +console.log("Version:", version.version); +console.log("Commit hash:", version.commitHash); +console.log("Build timestamp:", version.buildTimestamp); +console.log("Captive core version:", version.captiveCoreVersion); +console.log("Protocol version:", version.protocolVersion); +``` + +### `getFeeStats()` + +Get fee statistics for the network. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +const feeStats = await server.getFeeStats(); +console.log("Soroban inclusion fee:", { + max: feeStats.sorobanInclusionFee.max, + min: feeStats.sorobanInclusionFee.min, + mode: feeStats.sorobanInclusionFee.mode, + p10: feeStats.sorobanInclusionFee.p10, + p50: feeStats.sorobanInclusionFee.p50, + p90: feeStats.sorobanInclusionFee.p90, + p99: feeStats.sorobanInclusionFee.p99 +}); + +console.log("Inclusion fee:", feeStats.inclusionFee); +console.log("Latest ledger:", feeStats.latestLedger); +``` + +--- + +## Event Methods + +### `getEvents()` + +Query contract events. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { nativeToScVal } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +// Get all events for a contract +const events = await server.getEvents({ + startLedger: 1000, + endLedger: 2000, + filters: [ + { + contractIds: ['CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5'], + }, + ], + limit: 100, +}); + +console.log('Latest ledger:', events.latestLedger); +events.events.forEach((event) => { + console.log('Type:', event.type); + console.log('Ledger:', event.ledger); + console.log('Contract ID:', event.contractId); + console.log('Topic:', event.topic); + console.log('Value:', event.value); + console.log('In successful contract call:', event.inSuccessfulContractCall); +}); + +// Filter by topic +const filteredEvents = await server.getEvents({ + startLedger: 1000, + endLedger: 2000, + + filters: [ + { + contractIds: ['CCONT...'], + topics: [ + [ + nativeToScVal('transfer', { type: 'symbol' }).toXDR('base64'), + nativeToScVal('G...', { type: 'address' }).toXDR('base64'), + ], + ], + }, + ], +}); + +// Pagination +const nextEvents = await server.getEvents({ + filters: [], + cursor: events.cursor, + limit: 100, +}); + +``` + +--- + +## Utility Methods + +### `requestAirdrop()` + +Request testnet tokens from friendbot. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { Keypair } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +const keypair = Keypair.random(); +console.log('Public key:', keypair.publicKey()); +console.log('Secret:', keypair.secret()); + +// Request airdrop +const account = await server.requestAirdrop(keypair.publicKey()); +console.log('Account funded!'); +console.log('Sequence:', account.sequenceNumber()); + +// Custom friendbot URL +const account2 = await server.requestAirdrop( + keypair.publicKey(), + 'https://friendbot-futurenet.stellar.org/' +); +``` + +### `getSACBalance()` + +Get Stellar Asset Contract (SAC) balance for an account. + +```typescript +import { Server } from '@stellar/stellar-sdk/rpc'; +import { Asset, Networks, Address } from '@stellar/stellar-sdk'; + +const server = new Server('https://soroban-testnet.stellar.org'); + +const address = 'GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4'; +const usdc = new Asset('USDC', 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5'); + +const balance = await server.getSACBalance(address, usdc, Networks.TESTNET); + +console.log('Balance:', balance.balanceEntry); +console.log('Latest ledger:', balance.latestLedger); + +// Using Address object +const addr = new Address(address); +const balance2 = await server.getSACBalance(addr, usdc, Networks.TESTNET); +``` + +--- + +## Complete Examples + +### Simple Contract Call Flow + +```typescript +import { Keypair, Contract, TransactionBuilder, Networks, xdr } from '@stellar/stellar-sdk'; +import { Api, Server } from '@stellar/stellar-sdk/rpc'; + +const server = new Server('https://soroban-testnet.stellar.org'); +const keypair = Keypair.random(); +const contractId = 'CCONT...'; + +// 1. Get account +const account = await server.getAccount(keypair.publicKey()); + +// 2. Build transaction +const contract = new Contract(contractId); +const transaction = new TransactionBuilder(account, { + fee: '100', + networkPassphrase: Networks.TESTNET, +}) + .addOperation(contract.call('increment')) + .setTimeout(30) + .build(); + +// 3. Simulate +const simulation = await server.simulateTransaction(transaction); +if (Api.isSimulationSuccess(simulation)) { + if (simulation.result) { + console.log('Simulated result:', simulation.result.retval); + } +} else { + throw new Error('Simulation failed'); +} + +// 4. Prepare (adds soroban data and auth) +const prepared = await server.prepareTransaction(transaction); + +// 5. Sign +prepared.sign(keypair); + +// 6. Send +const response = await server.sendTransaction(prepared); +console.log('Sent:', response.hash); + +// 7. Poll for result +const result = await server.pollTransaction(response.hash); +console.log('Final status:', result.status); + +``` + +### Deploy Contract Example + +```typescript +import { + Keypair, + Operation, + TransactionBuilder, + Networks, + Address, + scValToNative, + xdr, +} from '@stellar/stellar-sdk'; +import { Server } from '@stellar/stellar-sdk/rpc'; +import fs from 'fs'; + +const server = new Server('https://soroban-testnet.stellar.org'); + const keypair = Keypair.random(); + + // 1. Fund account + await server.requestAirdrop(keypair.publicKey()); + + // 2. Upload WASM + const account = await server.getAccount(keypair.publicKey()); + const wasmBytes = fs.readFileSync('./contract.wasm'); + + const uploadTx = new TransactionBuilder(account, { + fee: '1000000', + networkPassphrase: Networks.TESTNET, + }) + .addOperation(Operation.uploadContractWasm({ wasm: wasmBytes })) + .setTimeout(30) + .build(); + + const preparedUpload = await server.prepareTransaction(uploadTx); + preparedUpload.sign(keypair); + + const uploadResponse = await server.sendTransaction(preparedUpload); + const uploadResult = await server.pollTransaction(uploadResponse.hash); + console.log('WASM uploaded:', uploadResult.status); + + // 3. Deploy contract + const account2 = await server.getAccount(keypair.publicKey()); + const wasmHash = + uploadResult.status === 'SUCCESS' && + scValToNative(uploadResult.returnValue || xdr.ScVal.scvVoid()); + + const deployTx = new TransactionBuilder(account2, { + fee: '100000', + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.createCustomContract({ + address: Address.fromString(keypair.publicKey()), + wasmHash, + salt: Buffer.alloc(32), + }) + ) + .setTimeout(30) + .build(); + + const preparedDeploy = await server.prepareTransaction(deployTx); + preparedDeploy.sign(keypair); + + const deployResponse = await server.sendTransaction(preparedDeploy); + const deployResult = await server.pollTransaction(deployResponse.hash); + console.log('Contract deployed:', deployResult.status); +``` + diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index a61ecdd51..8d4be91cf 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -14,7 +14,6 @@ import { import type { AssembledTransactionOptions, ClientOptions, - MethodOptions, Tx, WalletError, XDR_BASE64, @@ -43,197 +42,7 @@ import { Spec } from "./spec"; * bindings typescript` command, these also wraps `Client` and return * `AssembledTransaction` instances. * - * Let's look at examples of how to use `AssembledTransaction` for a variety of - * use-cases: - * - * #### 1. Simple read call - * - * Since these only require simulation, you can get the `result` of the call - * right after constructing your `AssembledTransaction`: - * - * ```ts - * const { result } = await AssembledTransaction.build({ - * method: 'myReadMethod', - * args: spec.funcArgsToScVals('myReadMethod', { - * args: 'for', - * my: 'method', - * ... - * }), - * contractId: 'C123…', - * networkPassphrase: '…', - * rpcUrl: 'https://…', - * publicKey: undefined, // irrelevant, for simulation-only read calls - * parseResultXdr: (result: xdr.ScVal) => - * spec.funcResToNative('myReadMethod', result), - * }) - * ``` - * - * While that looks pretty complicated, most of the time you will use this in - * conjunction with {@link Client}, which simplifies it to: - * - * ```ts - * const { result } = await client.myReadMethod({ - * args: 'for', - * my: 'method', - * ... - * }) - * ``` - * - * #### 2. Simple write call - * - * For write calls that will be simulated and then sent to the network without - * further manipulation, only one more step is needed: - * - * ```ts - * const assembledTx = await client.myWriteMethod({ - * args: 'for', - * my: 'method', - * ... - * }) - * const sentTx = await assembledTx.signAndSend() - * ``` - * - * Here we're assuming that you're using a {@link Client}, rather than - * constructing `AssembledTransaction`'s directly. - * - * Note that `sentTx`, the return value of `signAndSend`, is a - * {@link SentTransaction}. `SentTransaction` is similar to - * `AssembledTransaction`, but is missing many of the methods and fields that - * are only relevant while assembling a transaction. It also has a few extra - * methods and fields that are only relevant after the transaction has been - * sent to the network. - * - * Like `AssembledTransaction`, `SentTransaction` also has a `result` getter, - * which contains the parsed final return value of the contract call. Most of - * the time, you may only be interested in this, so rather than getting the - * whole `sentTx` you may just want to: - * - * ```ts - * const tx = await client.myWriteMethod({ args: 'for', my: 'method', ... }) - * const { result } = await tx.signAndSend() - * ``` - * - * #### 3. More fine-grained control over transaction construction - * - * If you need more control over the transaction before simulating it, you can - * set various {@link MethodOptions} when constructing your - * `AssembledTransaction`. With a {@link Client}, this is passed as a - * second object after the arguments (or the only object, if the method takes - * no arguments): - * - * ```ts - * const tx = await client.myWriteMethod( - * { - * args: 'for', - * my: 'method', - * ... - * }, { - * fee: '10000', // default: {@link BASE_FEE} - * simulate: false, - * timeoutInSeconds: 20, // default: {@link DEFAULT_TIMEOUT} - * } - * ) - * ``` - * - * Since we've skipped simulation, we can now edit the `raw` transaction and - * then manually call `simulate`: - * - * ```ts - * tx.raw.addMemo(Memo.text('Nice memo, friend!')) - * await tx.simulate() - * ``` - * - * If you need to inspect the simulation later, you can access it with - * `tx.simulation`. - * - * #### 4. Multi-auth workflows - * - * Soroban, and Stellar in general, allows multiple parties to sign a - * transaction. - * - * Let's consider an Atomic Swap contract. Alice wants to give 10 of her Token - * A tokens to Bob for 5 of his Token B tokens. - * - * ```ts - * const ALICE = 'G123...' - * const BOB = 'G456...' - * const TOKEN_A = 'C123…' - * const TOKEN_B = 'C456…' - * const AMOUNT_A = 10n - * const AMOUNT_B = 5n - * ``` - * - * Let's say Alice is also going to be the one signing the final transaction - * envelope, meaning she is the invoker. So your app, from Alice's browser, - * simulates the `swap` call: - * - * ```ts - * const tx = await swapClient.swap({ - * a: ALICE, - * b: BOB, - * token_a: TOKEN_A, - * token_b: TOKEN_B, - * amount_a: AMOUNT_A, - * amount_b: AMOUNT_B, - * }) - * ``` - * - * But your app can't `signAndSend` this right away, because Bob needs to sign - * it first. You can check this: - * - * ```ts - * const whoElseNeedsToSign = tx.needsNonInvokerSigningBy() - * ``` - * - * You can verify that `whoElseNeedsToSign` is an array of length `1`, - * containing only Bob's public key. - * - * Then, still on Alice's machine, you can serialize the - * transaction-under-assembly: - * - * ```ts - * const json = tx.toJSON() - * ``` - * - * And now you need to send it to Bob's browser. How you do this depends on - * your app. Maybe you send it to a server first, maybe you use WebSockets, or - * maybe you have Alice text the JSON blob to Bob and have him paste it into - * your app in his browser (note: this option might be error-prone 😄). - * - * Once you get the JSON blob into your app on Bob's machine, you can - * deserialize it: - * - * ```ts - * const tx = swapClient.txFromJSON(json) - * ``` - * - * Or, if you're using a client generated with `soroban contract bindings - * typescript`, this deserialization will look like: - * - * ```ts - * const tx = swapClient.fromJSON.swap(json) - * ``` - * - * Then you can have Bob sign it. What Bob will actually need to sign is some - * _auth entries_ within the transaction, not the transaction itself or the - * transaction envelope. Your app can verify that Bob has the correct wallet - * selected, then: - * - * ```ts - * await tx.signAuthEntries() - * ``` - * - * Under the hood, this uses `signAuthEntry`, which you either need to inject - * during initial construction of the `Client`/`AssembledTransaction`, - * or which you can pass directly to `signAuthEntries`. - * - * Now Bob can again serialize the transaction and send back to Alice, where - * she can finally call `signAndSend()`. - * - * To see an even more complicated example, where Alice swaps with Bob but the - * transaction is invoked by yet another party, check out - * [test-swap.js](../../test/e2e/src/test-swap.js). - * + * @see {@link https://github.com/stellar/js-stellar-sdk/blob/master/docs/reference/assembled-tx-examples.md | AssembledTransaction Examples} * @memberof module:contract */ export class AssembledTransaction { diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 5c651493e..f79f260c1 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -173,6 +173,7 @@ function findCreatedAccountSequenceInTransactionMeta( * @param {Record} [opts.headers] Allows setting custom headers * * @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods | API reference docs} + * @see {@link https://github.com/stellar/js-stellar-sdk/blob/master/docs/reference/rpc-examples.md | Server Examples} */ export class RpcServer { public readonly serverURL: URI;