@stellar/stellar-sdk
Version:
A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.
1,282 lines (1,280 loc) • 56.3 kB
JavaScript
import { Buffer } from 'buffer';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/int.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/hyper.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-int.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-hyper.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/xdr-type.js';
import types from '../base/generated/curr_generated.js';
import '@noble/hashes/sha2.js';
import '../base/signing.js';
import { Keypair } from '../base/keypair.js';
import { StrKey } from '../base/strkey.js';
import '../base/util/continued_fraction.js';
import '../base/util/bignumber.js';
import { Address } from '../base/address.js';
import '../base/transaction_builder.js';
import { Account } from '../base/account.js';
import '../base/muxed_account.js';
import { Contract } from '../base/contract.js';
import { nativeToScVal, scValToNative } from '../base/scval.js';
import '../base/numbers/uint128.js';
import '../base/numbers/uint256.js';
import '../base/numbers/int128.js';
import '../base/numbers/int256.js';
import { createHttpClient } from './axios.js';
import { postObject } from './jsonrpc.js';
import { Api } from './api.js';
import { assembleTransaction } from './transaction.js';
import { parseRawLedgerEntries, parseTransactionInfo, parseRawTransactions, parseRawEvents, parseRawLatestLedger, parseRawSimulation, parseRawSendTransaction, parseRawLedger } from './parsers.js';
import { Utils } from '../utils.js';
var Durability = /* @__PURE__ */ ((Durability2) => {
Durability2["Temporary"] = "temporary";
Durability2["Persistent"] = "persistent";
return Durability2;
})(Durability || {});
const DEFAULT_GET_TRANSACTION_TIMEOUT = 30;
const BasicSleepStrategy = (_iter) => 1e3;
const LinearSleepStrategy = (iter) => 1e3 * iter;
function findCreatedAccountSequenceInTransactionMeta(meta) {
let operations = [];
switch (meta.switch()) {
case 0:
operations = meta.operations();
break;
case 1:
case 2:
case 3:
case 4:
operations = meta.value().operations();
break;
default:
throw new Error("Unexpected transaction meta switch value");
}
const sequenceNumber = operations.flatMap((op) => op.changes()).find(
(c) => c.switch() === types.LedgerEntryChangeType.ledgerEntryCreated() && c.created().data().switch() === types.LedgerEntryType.account()
)?.created()?.data()?.account()?.seqNum()?.toString();
if (sequenceNumber) {
return sequenceNumber;
}
throw new Error("No account created in transaction");
}
function contractSpecTypeName(td) {
if (td.switch().value === types.ScSpecType.scSpecTypeUdt().value) {
return td.udt().name().toString();
}
return td.switch().name.replace(/^scSpecType/, "");
}
class RpcServer {
serverURL;
/**
* HTTP client instance for making requests to Horizon.
* Exposes interceptors, defaults, and other configuration options.
*
* @example
* ```ts
* // Add authentication header
* server.httpClient.defaults.headers['Authorization'] = 'Bearer token';
*
* // Add request interceptor
* server.httpClient.interceptors.request.use((config) => {
* console.log('Request:', config.url);
* return config;
* });
* ```
*/
httpClient;
constructor(serverURL, opts = {}) {
this.serverURL = new URL(serverURL);
this.httpClient = createHttpClient(opts.headers);
if (this.serverURL.protocol !== "https:" && !opts.allowHttp) {
throw new Error(
"Cannot connect to insecure Soroban RPC server if `allowHttp` isn't set"
);
}
}
/**
* Fetch a minimal set of current info about a Stellar account.
*
* Needed to get the current sequence number for the account so you can build
* a successful transaction with {@link TransactionBuilder}.
*
* @param address - The public address of the account to load.
* @returns A promise which resolves to the {@link Account}
* object with a populated sequence number
*
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgerEntries | getLedgerEntries docs}
*
* @example
* ```ts
* const accountId = "GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4";
* server.getAccount(accountId).then((account) => {
* console.log("sequence:", account.sequence);
* });
* ```
*/
async getAccount(address) {
const entry = await this.getAccountEntry(address);
return new Account(address, entry.seqNum().toString());
}
/**
* Fetch the full account entry for a Stellar account.
*
* @param address - The public address of the account to load.
* @returns Resolves to the full on-chain account
* entry
*
* @see
* {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgerEntries | getLedgerEntries docs}
*
* @example
* ```ts
* const accountId = "GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4";
* server.getAccountEntry(accountId).then((account) => {
* console.log("sequence:", account.balance().toString());
* });
* ```
*/
async getAccountEntry(address) {
const ledgerKey = types.LedgerKey.account(
new types.LedgerKeyAccount({
accountId: Keypair.fromPublicKey(address).xdrPublicKey()
})
);
try {
const resp = await this.getLedgerEntry(ledgerKey);
return resp.val.account();
} catch {
throw new Error(`Account not found: ${address}`);
}
}
/**
* Fetch the full trustline entry for a Stellar account.
*
* @param account - The public address of the account whose trustline it is
* @param asset - The trustline's asset
* @returns Resolves to the full on-chain trustline
* entry
*
* @see
* {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgerEntries | getLedgerEntries docs}
*
* @deprecated Use {@link getAssetBalance}, instead
* @example
* ```ts
* const accountId = "GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4";
* const asset = new Asset(
* "USDC",
* "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
* );
* server.getTrustline(accountId, asset).then((entry) => {
* console.log(`{asset.toString()} balance for ${accountId}:", entry.balance().toString());
* });
* ```
*/
async getTrustline(account, asset) {
const trustlineLedgerKey = types.LedgerKey.trustline(
new types.LedgerKeyTrustLine({
accountId: Keypair.fromPublicKey(account).xdrAccountId(),
asset: asset.toTrustLineXDRObject()
})
);
try {
const entry = await this.getLedgerEntry(trustlineLedgerKey);
return entry.val.trustLine();
} catch {
throw new Error(
`Trustline for ${asset.getCode()}:${asset.getIssuer()} not found for ${account}`
);
}
}
/**
* Fetch the full claimable balance entry for a Stellar account.
*
* @param id - The strkey (`B...`) or hex (`00000000abcde...`) (both
* IDs with and without the 000... version prefix are accepted) of the
* claimable balance to load
* @returns Resolves to the full on-chain
* claimable balance entry
*
* @see
* {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgerEntries | getLedgerEntries docs}
*
* @example
* ```ts
* const id = "00000000178826fbfe339e1f5c53417c6fedfe2c05e8bec14303143ec46b38981b09c3f9";
* server.getClaimableBalance(id).then((entry) => {
* console.log(`Claimable balance {id.substr(0, 12)} has:`);
* console.log(` asset: ${Asset.fromXDRObject(entry.asset()).toString()}`;
* console.log(` amount: ${entry.amount().toString()}`;
* });
* ```
*/
async getClaimableBalance(id) {
let balanceId;
if (StrKey.isValidClaimableBalance(id)) {
const buffer = StrKey.decodeClaimableBalance(id);
const v = Buffer.concat([
Buffer.from("\0\0\0"),
buffer.subarray(0, 1)
]);
balanceId = types.ClaimableBalanceId.fromXDR(
Buffer.concat([v, buffer.subarray(1)])
);
} else if (id.match(/[a-f0-9]{72}/i)) {
balanceId = types.ClaimableBalanceId.fromXDR(id, "hex");
} else if (id.match(/[a-f0-9]{64}/i)) {
balanceId = types.ClaimableBalanceId.fromXDR(id.padStart(72, "0"), "hex");
} else {
throw new TypeError(`expected 72-char hex ID or strkey, not ${id}`);
}
const trustlineLedgerKey = types.LedgerKey.claimableBalance(
new types.LedgerKeyClaimableBalance({ balanceId })
);
try {
const entry = await this.getLedgerEntry(trustlineLedgerKey);
return entry.val.claimableBalance();
} catch {
throw new Error(`Claimable balance ${id} not found`);
}
}
/**
* Fetch the balance of an asset held by an account or contract.
*
* The `address` argument may be provided as a string (as a {@link StrKey}),
* {@link Address}, or {@link Contract}.
*
* @param address - The account or contract whose
* balance should be fetched.
* @param asset - The asset whose balance you want to inspect.
* @param networkPassphrase - (optional) optionally, when requesting the
* balance of a contract, the network passphrase to which this token
* applies. If omitted and necessary, a request about network information
* will be made (see {@link getNetwork}), since contract IDs for assets are
* specific to a network. You can refer to {@link Networks} for a list of
* built-in passphrases, e.g., `Networks.TESTNET`.
* @returns Resolves with balance entry details
* when available.
*
* @throws If the supplied `address` is not a valid account or
* contract strkey.
*
* @example
* ```ts
* const usdc = new Asset(
* "USDC",
* "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
* );
* const balance = await server.getAssetBalance("GD...", usdc);
* console.log(balance.balanceEntry?.amount);
* ```
*/
async getAssetBalance(address, asset, networkPassphrase) {
let addr = address;
if (typeof address === "string") {
addr = address;
} else if (address instanceof Address) {
addr = address.toString();
} else if (address instanceof Contract) {
addr = address.toString();
} else {
throw new TypeError(`invalid address: ${address}`);
}
if (StrKey.isValidEd25519PublicKey(addr)) {
const [tl, ll] = await Promise.all([
this.getTrustline(addr, asset),
this.getLatestLedger()
]);
return {
latestLedger: ll.sequence,
balanceEntry: {
amount: tl.balance().toString(),
// Extract actual flags from the coalesced value.
authorized: Boolean(tl.flags() & 1),
// AUTHORIZED_FLAG
clawback: Boolean(tl.flags() & 4),
// TRUSTLINE_CLAWBACK_ENABLED_FLAG
authorizedToMaintainLiabilities: Boolean(tl.flags() & 2),
// AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
revocable: Boolean(tl.flags() & 2)
// AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG (deprecated, will be removed in a future major release)
}
};
} else if (StrKey.isValidContract(addr)) {
return this.getSACBalance(addr, asset, networkPassphrase);
}
throw new Error(`invalid address: ${address}`);
}
/**
* General node health check.
*
* @returns A promise which resolves to the
* {@link Api.GetHealthResponse} object with the status of the
* server (e.g. "healthy").
*
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getHealth | getLedgerEntries docs}
*
* @example
* ```ts
* server.getHealth().then((health) => {
* console.log("status:", health.status);
* });
* ```
*/
async getHealth() {
return postObject(
this.httpClient,
this.serverURL.toString(),
"getHealth"
);
}
/**
* Reads the current value of contract data ledger entries directly.
*
* Allows you to directly inspect the current state of a contract. This is a
* backup way to access your contract data which may not be available via
* events or {@link rpc.Server.simulateTransaction}.
*
* @param contract - The contract ID containing the
* data to load as a strkey (`C...` form), a {@link Contract}, or an
* {@link Address} instance
* @param key - The key of the contract data to load
* @param durability - (optional) The "durability
* keyspace" that this ledger key belongs to, which is either 'temporary'
* or 'persistent' (the default), see {@link rpc.Durability}.
* @returns The current data value
*
* **Warning:** If the data entry in question is a 'temporary' entry, it's
* entirely possible that it has expired out of existence.
*
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgerEntries | getLedgerEntries docs}
*
* @example
* ```ts
* const contractId = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5";
* const key = xdr.ScVal.scvSymbol("counter");
* server.getContractData(contractId, key, Durability.Temporary).then(data => {
* console.log("value:", data.val);
* console.log("liveUntilLedgerSeq:", data.liveUntilLedgerSeq);
* console.log("lastModified:", data.lastModifiedLedgerSeq);
* console.log("latestLedger:", data.latestLedger);
* });
* ```
*/
async getContractData(contract, key, durability = "persistent" /* Persistent */) {
let scAddress;
if (typeof contract === "string") {
scAddress = new Contract(contract).address().toScAddress();
} else if (contract instanceof Address) {
scAddress = contract.toScAddress();
} else if (contract instanceof Contract) {
scAddress = contract.address().toScAddress();
} else {
throw new TypeError(`unknown contract type: ${contract}`);
}
let xdrDurability;
switch (durability) {
case "temporary" /* Temporary */:
xdrDurability = types.ContractDataDurability.temporary();
break;
case "persistent" /* Persistent */:
xdrDurability = types.ContractDataDurability.persistent();
break;
default:
throw new TypeError(`invalid durability: ${durability}`);
}
const contractKey = types.LedgerKey.contractData(
new types.LedgerKeyContractData({
key,
contract: scAddress,
durability: xdrDurability
})
);
try {
return await this.getLedgerEntry(contractKey);
} catch {
throw {
code: 404,
message: `Contract data not found for ${Address.fromScAddress(
scAddress
).toString()} with key ${key.toXDR("base64")} and durability: ${durability}`
};
}
}
/**
* Retrieves the deployed contract instance for a given contract ID.
*
* The instance describes the contract's executable — either a Wasm hash or
* the built-in Stellar Asset Contract — along with its instance storage.
*
* @param contractId - The contract ID (`C...`) to look up
* @returns The contract's `xdr.ScContractInstance`
* @throws If the contract instance cannot be found on the network.
*
* @example
* ```ts
* const instance = await server.getContractInstance(
* "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5",
* );
* console.log(instance.executable().switch().name);
* ```
*/
async getContractInstance(contractId) {
const contractLedgerKey = new Contract(contractId).getFootprint();
const response = await this.getLedgerEntries(contractLedgerKey);
if (!response.entries.length || !response.entries[0]?.val) {
return Promise.reject({
code: 404,
message: "Could not obtain contract instance from server"
});
}
return response.entries[0].val.contractData().val().instance();
}
/**
* Retrieves the WASM bytecode for a given contract.
*
* This method allows you to fetch the WASM bytecode associated with a contract
* deployed on the Soroban network. The WASM bytecode represents the executable
* code of the contract.
*
* This only works for Wasm-based contracts. A built-in Stellar Asset Contract
* (SAC) has no Wasm bytecode on-chain, so this throws for a SAC; use
* {@link contract.Client.from} to build a client from the embedded SAC spec.
*
* @param contractId - The contract ID containing the WASM bytecode to retrieve
* @returns A Buffer containing the WASM bytecode
* @throws If the contract or its associated WASM bytecode cannot be
* found on the network, or if the contract is a Stellar Asset Contract (SAC).
*
* @example
* ```ts
* const contractId = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5";
* server.getContractWasmByContractId(contractId).then(wasmBuffer => {
* console.log("WASM bytecode length:", wasmBuffer.length);
* // ... do something with the WASM bytecode ...
* }).catch(err => {
* console.error("Error fetching WASM bytecode:", err);
* });
* ```
*/
async getContractWasmByContractId(contractId) {
const instance = await this.getContractInstance(contractId);
if (instance.executable().switch() === types.ContractExecutableType.contractExecutableStellarAsset()) {
return Promise.reject({
code: 400,
message: `Contract ${contractId} is a Stellar Asset Contract (SAC), which has no Wasm bytecode. Use contract.Client.from() to build a client from the built-in SAC spec instead.`
});
}
return this.getContractWasmByHash(instance.executable().wasmHash());
}
/**
* Retrieves the WASM bytecode for a given contract hash.
*
* This method allows you to fetch the WASM bytecode associated with a contract
* deployed on the Soroban network using the contract's WASM hash. The WASM bytecode
* represents the executable code of the contract.
*
* @param wasmHash - The WASM hash of the contract
* @returns A Buffer containing the WASM bytecode
* @throws If the contract or its associated WASM bytecode cannot be
* found on the network.
*
* @example
* ```ts
* const wasmHash = Buffer.from("...");
* server.getContractWasmByHash(wasmHash).then(wasmBuffer => {
* console.log("WASM bytecode length:", wasmBuffer.length);
* // ... do something with the WASM bytecode ...
* }).catch(err => {
* console.error("Error fetching WASM bytecode:", err);
* });
* ```
*/
async getContractWasmByHash(wasmHash, format = void 0) {
const wasmHashBuffer = typeof wasmHash === "string" ? Buffer.from(wasmHash, format) : wasmHash;
const ledgerKeyWasmHash = types.LedgerKey.contractCode(
new types.LedgerKeyContractCode({
hash: wasmHashBuffer
})
);
const responseWasm = await this.getLedgerEntries(ledgerKeyWasmHash);
if (!responseWasm.entries.length || !responseWasm.entries[0]?.val) {
return Promise.reject({
code: 404,
message: "Could not obtain contract wasm from server"
});
}
const wasmBuffer = responseWasm.entries[0].val.contractCode().code();
return wasmBuffer;
}
/**
* Performs a read-only call to a contract method and returns the decoded result.
*
* This is a convenience wrapper for one-line contract state queries: it builds
* a {@link contract.Client} for the contract, simulates the method call, and
* returns the spec-decoded return value — no manual transaction assembly,
* signing, or submission required.
*
* Works for both Wasm contracts and built-in Stellar Asset Contracts (SACs):
* the embedded SAC spec is used automatically for SACs (see
* {@link contract.Client.from}). The query reuses this server's transport
* (headers, interceptors, `allowHttp`).
*
* @typeParam T - the expected (decoded) return type of the method
* @param contractId - the contract to query (`C...`)
* @param method - the contract method to call
* @param args - named arguments for the method, keyed by parameter name
* (omit for methods that take no arguments)
* @param networkPassphrase - (optional) the network passphrase. If omitted, a
* request about network information will be made (see {@link getNetwork}).
* You can refer to {@link Networks} for a list of built-in passphrases,
* e.g., `Networks.TESTNET`.
* @returns An object with the method's decoded return value (`result`) and
* `isReadCall`: whether this specific call is a side-effect-free read that
* needs no signature (it wrote no state and required no authorization).
* `isReadCall` is per-call, not per-method: it reflects the given `args`.
* Since `queryContract` never signs or sends, `isReadCall: false` means the
* `result` is a simulation preview of a call that would change state.
* @throws If the contract has no such method, or if the simulation fails.
*
* @example
* ```ts
* const { result: decimals, isReadCall } = await server.queryContract<number>(
* "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5",
* "decimals",
* );
* const { result: balance } = await server.queryContract<bigint>(
* "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5",
* "balance",
* { id: "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ" },
* );
* ```
*/
async queryContract(contractId, method, args = {}, networkPassphrase) {
const passphrase = networkPassphrase ?? (await this.getNetwork()).passphrase;
const { Client } = await import('../contract/client.js');
const client = await Client.from({
contractId,
rpcUrl: this.serverURL.toString(),
networkPassphrase: passphrase,
server: this
});
const isContractMethod = client.spec.funcs().some((fn) => fn.name().toString() === method);
const { sanitizeIdentifier } = await import('../bindings/utils.js');
const invoke = client[sanitizeIdentifier(method)];
if (!isContractMethod || typeof invoke !== "function") {
throw new TypeError(`Contract ${contractId} has no method '${method}'`);
}
const assembled = await invoke(args);
return { result: assembled.result, isReadCall: assembled.isReadCall };
}
/**
* Lists a contract's callable methods and their signatures.
*
* A discovery helper for tooling, dapps, and agents that need to inspect an
* arbitrary contract without knowing its interface up front. It resolves the
* contract's spec (embedded in the Wasm for regular contracts, or the
* built-in spec for Stellar Asset Contracts — see {@link contract.Client.from})
* and reports each declared function's name, inputs, and outputs. No method
* is invoked or simulated; this performs only the spec lookup.
*
* The complement to {@link queryContract}: list methods here, then call a
* read-only one with `server.queryContract(contractId, method, args?)`.
*
* @param contractId - the contract to inspect (`C...`)
* @param networkPassphrase - (optional) the network passphrase. If omitted, a
* request about network information will be made (see {@link getNetwork}).
* You can refer to {@link Networks} for a list of built-in passphrases,
* e.g., `Networks.TESTNET`.
* @returns The contract's methods, in the order they appear in the spec
*
* @example
* ```ts
* const methods = await server.getContractMethods(
* "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5",
* );
* // [
* // { name: "decimals", inputs: [], outputs: ["U32"] },
* // { name: "balance", inputs: [{ name: "id", type: "Address" }], outputs: ["I128"] },
* // { name: "transfer", inputs: [...], outputs: [] },
* // ]
* ```
*/
async getContractMethods(contractId, networkPassphrase) {
const passphrase = networkPassphrase ?? (await this.getNetwork()).passphrase;
const { Client } = await import('../contract/client.js');
const client = await Client.from({
contractId,
rpcUrl: this.serverURL.toString(),
networkPassphrase: passphrase,
server: this
});
return client.spec.funcs().map((fn) => {
const doc = fn.doc().toString();
const method = {
name: fn.name().toString(),
inputs: fn.inputs().map((input) => ({
name: input.name().toString(),
type: contractSpecTypeName(input.type())
})),
outputs: fn.outputs().map(contractSpecTypeName)
};
if (doc) {
method.doc = doc;
}
return method;
});
}
/**
* Reads the current value of arbitrary ledger entries directly.
*
* Allows you to directly inspect the current state of contracts, contract's
* code, accounts, or any other ledger entries.
*
* To fetch a contract's WASM byte-code, built the appropriate
* {@link xdr.LedgerKeyContractCode} ledger entry key (or see
* {@link Contract.getFootprint}).
*
* @param keys - One or more ledger entry keys to load
* @returns The current on-chain
* values for the given ledger keys
*
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgerEntries | getLedgerEntries docs}
* @see RpcServer._getLedgerEntries
* @example
* ```ts
* const contractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM";
* const key = xdr.LedgerKey.contractData(new xdr.LedgerKeyContractData({
* contractId: StrKey.decodeContract(contractId),
* key: xdr.ScVal.scvSymbol("counter"),
* }));
*
* server.getLedgerEntries([key]).then(response => {
* const ledgerData = response.entries[0];
* console.log("key:", ledgerData.key);
* console.log("value:", ledgerData.val);
* console.log("liveUntilLedgerSeq:", ledgerData.liveUntilLedgerSeq);
* console.log("lastModified:", ledgerData.lastModifiedLedgerSeq);
* console.log("latestLedger:", response.latestLedger);
* });
* ```
*/
getLedgerEntries(...keys) {
return this._getLedgerEntries(...keys).then(parseRawLedgerEntries);
}
_getLedgerEntries(...keys) {
return postObject(
this.httpClient,
this.serverURL.toString(),
"getLedgerEntries",
{
keys: keys.map((k) => k.toXDR("base64"))
}
);
}
async getLedgerEntry(key) {
const results = await this._getLedgerEntries(key).then(
parseRawLedgerEntries
);
if (results.entries.length !== 1) {
throw new Error(`failed to find an entry for key ${key.toXDR("base64")}`);
}
return results.entries[0];
}
/**
* Poll for a particular transaction with certain parameters.
*
* After submitting a transaction, clients can use this to poll for
* transaction completion and return a definitive state of success or failure.
*
* @param hash - the transaction you're polling for
* @param opts - (optional) polling options
* - `attempts` (optional): (optional) the number of attempts to make
* before returning the last-seen status. By default or on invalid inputs,
* try 5 times.
* - `sleepStrategy` (optional): (optional) the amount of time
* to wait for between each attempt. By default, sleep for 1 second between
* each attempt.
*
* @returns the response after a "found"
* response (which may be success or failure) or the last response obtained
* after polling the maximum number of specified attempts.
*
* @example
* ```ts
* const h = "c4515e3bdc0897f21cc5dbec8c82cf0a936d4741cb74a8e158eb51b9fb00411a";
* const txStatus = await server.pollTransaction(h, {
* attempts: 100, // I'm a maniac
* sleepStrategy: rpc.LinearSleepStrategy
* }); // this will take 5,050 seconds to complete
* ```
*/
async pollTransaction(hash, opts) {
const maxAttempts = (opts?.attempts ?? 0) < 1 ? DEFAULT_GET_TRANSACTION_TIMEOUT : opts?.attempts ?? DEFAULT_GET_TRANSACTION_TIMEOUT;
let foundInfo;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
foundInfo = await this.getTransaction(hash);
if (foundInfo.status !== Api.GetTransactionStatus.NOT_FOUND) {
return foundInfo;
}
await Utils.sleep((opts?.sleepStrategy ?? BasicSleepStrategy)(attempt));
}
return foundInfo;
}
/**
* Fetch the details of a submitted transaction.
*
* After submitting a transaction, clients should poll this to tell when the
* transaction has completed.
*
* @param hash - Hex-encoded hash of the transaction to check
* @returns The status, result, and
* other details about the transaction
*
* @see
* {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getTransaction | getTransaction docs}
*
* @example
* ```ts
* const transactionHash = "c4515e3bdc0897f21cc5dbec8c82cf0a936d4741cb74a8e158eb51b9fb00411a";
* server.getTransaction(transactionHash).then((tx) => {
* console.log("status:", tx.status);
* console.log("envelopeXdr:", tx.envelopeXdr);
* console.log("resultMetaXdr:", tx.resultMetaXdr);
* console.log("resultXdr:", tx.resultXdr);
* });
* ```
*/
async getTransaction(hash) {
return this._getTransaction(hash).then((raw) => {
const foundInfo = {};
if (raw.status !== Api.GetTransactionStatus.NOT_FOUND) {
Object.assign(foundInfo, parseTransactionInfo(raw));
}
const result = {
status: raw.status,
txHash: hash,
latestLedger: raw.latestLedger,
latestLedgerCloseTime: raw.latestLedgerCloseTime,
oldestLedger: raw.oldestLedger,
oldestLedgerCloseTime: raw.oldestLedgerCloseTime,
...foundInfo
};
return result;
});
}
async _getTransaction(hash) {
return postObject(
this.httpClient,
this.serverURL.toString(),
"getTransaction",
{
hash
}
);
}
/**
* Fetch transactions starting from a given start ledger or a cursor. The end ledger is the latest ledger
* in that RPC instance.
*
* @param request - The request parameters.
* @returns - A promise that resolves to the transactions response.
*
* @see https://developers.stellar.org/docs/data/rpc/api-reference/methods/getTransactions
* @example
* ```ts
* server.getTransactions({
* startLedger: 10000,
* limit: 10,
* }).then((response) => {
* console.log("Transactions:", response.transactions);
* console.log("Latest Ledger:", response.latestLedger);
* console.log("Cursor:", response.cursor);
* });
* ```
*/
async getTransactions(request) {
return this._getTransactions(request).then(
(raw) => {
const result = {
transactions: (raw.transactions || []).map(parseRawTransactions),
latestLedger: raw.latestLedger,
latestLedgerCloseTimestamp: raw.latestLedgerCloseTimestamp,
oldestLedger: raw.oldestLedger,
oldestLedgerCloseTimestamp: raw.oldestLedgerCloseTimestamp,
cursor: raw.cursor
};
return result;
}
);
}
async _getTransactions(request) {
return postObject(
this.httpClient,
this.serverURL.toString(),
"getTransactions",
request
);
}
/**
* Fetch all events that match a given set of filters.
*
* The given filters (see {@link Api.EventFilter}
* for detailed fields) are combined only in a logical OR fashion, and all of
* the fields in each filter are optional.
*
* To page through events, use the `pagingToken` field on the relevant
* {@link Api.EventResponse} object to set the `cursor` parameter.
*
* @param request - Event filters {@link Api.GetEventsRequest},
* @returns A paginatable set of the events
* matching the given event filters
*
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getEvents | getEvents docs}
*
* @example
* ```ts
*
* server.getEvents({
* startLedger: 1000,
* endLedger: 2000,
* filters: [
* {
* type: "contract",
* contractIds: [ "deadb33f..." ],
* topics: [[ "AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc=", "*" ]]
* }, {
* type: "system",
* contractIds: [ "...c4f3b4b3..." ],
* topics: [[ "*" ], [ "*", "AAAAAQB6Mcc=" ]]
* }, {
* contractIds: [ "...c4f3b4b3..." ],
* topics: [[ "AAAABQAAAAh0cmFuc2Zlcg==" ]]
* }, {
* type: "diagnostic",
* topics: [[ "AAAAAQB6Mcc=" ]]
* }
* ],
* limit: 10,
* });
* ```
*/
async getEvents(request) {
return this._getEvents(request).then(parseRawEvents);
}
async _getEvents(request) {
return postObject(
this.httpClient,
this.serverURL.toString(),
"getEvents",
{
filters: request.filters ?? [],
pagination: {
...request.cursor && { cursor: request.cursor },
// add if defined
...request.limit && { limit: request.limit }
},
...request.startLedger && {
startLedger: request.startLedger
},
...request.endLedger && {
endLedger: request.endLedger
}
}
);
}
/**
* Fetch metadata about the network this Soroban RPC server is connected to.
*
* @returns Metadata about the current
* network this RPC server is connected to
*
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getNetwork | getNetwork docs}
*
* @example
* ```ts
* server.getNetwork().then((network) => {
* console.log("friendbotUrl:", network.friendbotUrl);
* console.log("passphrase:", network.passphrase);
* console.log("protocolVersion:", network.protocolVersion);
* });
* ```
*/
async getNetwork() {
return postObject(
this.httpClient,
this.serverURL.toString(),
"getNetwork"
);
}
/**
* Fetch the latest ledger meta info from network which this Soroban RPC
* server is connected to.
*
* @returns metadata about the
* latest ledger on the network that this RPC server is connected to
*
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLatestLedger | getLatestLedger docs}
*
* @example
* ```ts
* server.getLatestLedger().then((response) => {
* console.log("hash:", response.id);
* console.log("sequence:", response.sequence);
* console.log("protocolVersion:", response.protocolVersion);
* });
* ```
*/
async getLatestLedger() {
return this._getLatestLedger().then(parseRawLatestLedger);
}
async _getLatestLedger() {
return postObject(
this.httpClient,
this.serverURL.toString(),
"getLatestLedger"
);
}
/**
* Submit a trial contract invocation to get back return values, expected
* ledger footprint, expected authorizations, and expected costs.
*
* @param tx - the transaction to simulate,
* which should include exactly one operation (one of
* {@link xdr.InvokeHostFunctionOp}, {@link xdr.ExtendFootprintTtlOp}, or
* {@link xdr.RestoreFootprintOp}). Any provided footprint or auth
* information will be ignored.
* @param addlResources - (optional) any additional resources
* to add to the simulation-provided ones, for example if you know you will
* need extra CPU instructions
* @param authMode - (optional) optionally, specify the type of
* auth mode to use for simulation: `enforce` for enforcement mode,
* `record` for recording mode, or `record_allow_nonroot` for recording
* mode that allows non-root authorization
* @param useUpgradedAuth - (optional) opt simulation into recording
* v2 address credentials (CAP-71) instead of the legacy v1 address
* credentials. Best-effort: it only affects the recording auth modes and
* is silently ignored on protocol versions whose host cannot emit v2
* credentials.
*
* **Deprecated**: this flag is transitional. Once the network returns v2
* credentials by default (protocol 28), it becomes a no-op — do not rely
* on omitting it to keep receiving the legacy v1 format.
*
* @returns An object with the
* cost, footprint, result/auth requirements (if applicable), and error of
* the transaction
*
* @see
* {@link https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/operations-and-transactions | transaction docs}
* @see
* {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/simulateTransaction | simulateTransaction docs}
* @see
* {@link https://developers.stellar.org/docs/learn/fundamentals/contract-development/contract-interactions/transaction-simulation#authorization | authorization modes}
* @see module:rpc.Server#prepareTransaction
* @see module:rpc.assembleTransaction
*
* @example
* ```ts
* const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE';
* const contract = new StellarSdk.Contract(contractId);
*
* // Right now, this is just the default fee for this example.
* const fee = StellarSdk.BASE_FEE;
* const transaction = new StellarSdk.TransactionBuilder(account, { fee })
* // Uncomment the following line to build transactions for the live network. Be
* // sure to also change the horizon hostname.
* //.setNetworkPassphrase(StellarSdk.Networks.PUBLIC)
* .setNetworkPassphrase(StellarSdk.Networks.FUTURENET)
* .setTimeout(30) // valid for the next 30s
* // Add an operation to call increment() on the contract
* .addOperation(contract.call("increment"))
* .build();
*
* server.simulateTransaction(transaction).then((sim) => {
* console.log("cost:", sim.cost);
* console.log("result:", sim.result);
* console.log("error:", sim.error);
* console.log("latestLedger:", sim.latestLedger);
* });
* ```
*/
async simulateTransaction(tx, addlResources, authMode, useUpgradedAuth) {
return this._simulateTransaction(
tx,
addlResources,
authMode,
useUpgradedAuth
).then(parseRawSimulation);
}
async _simulateTransaction(transaction, addlResources, authMode, useUpgradedAuth) {
return postObject(
this.httpClient,
this.serverURL.toString(),
"simulateTransaction",
{
transaction: transaction.toXDR(),
authMode,
...useUpgradedAuth !== void 0 && { useUpgradedAuth },
...addlResources !== void 0 && {
resourceConfig: {
instructionLeeway: addlResources.cpuInstructions
}
}
}
);
}
/**
* Submit a trial contract invocation, first run a simulation of the contract
* invocation as defined on the incoming transaction, and apply the results to
* a new copy of the transaction which is then returned. Setting the ledger
* footprint and authorization, so the resulting transaction is ready for
* signing & sending.
*
* The returned transaction will also have an updated fee that is the sum of
* fee set on incoming transaction with the contract resource fees estimated
* from simulation. It is advisable to check the fee on returned transaction
* and validate or take appropriate measures for interaction with user to
* confirm it is acceptable.
*
* You can call the {@link rpc.Server.simulateTransaction} method
* directly first if you want to inspect estimated fees for a given
* transaction in detail first, then re-assemble it manually or via
* {@link rpc.assembleTransaction}.
*
* @param tx - the transaction to
* prepare. It should include exactly one operation, which must be one of
* {@link xdr.InvokeHostFunctionOp}, {@link xdr.ExtendFootprintTtlOp},
* or {@link xdr.RestoreFootprintOp}.
*
* Any provided footprint will be overwritten. However, if your operation
* has existing auth entries, they will be preferred over ALL auth entries
* from the simulation. In other words, if you include auth entries, you
* don't care about the auth returned from the simulation. Other fields
* (footprint, etc.) will be filled as normal.
* @returns A copy of the
* transaction with the expected authorizations (in the case of
* invocation), resources, and ledger footprints added. The transaction fee
* will also automatically be padded with the contract's minimum resource
* fees discovered from the simulation.
* @throws * If simulation fails
*
* @see module:rpc.assembleTransaction
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/simulateTransaction | simulateTransaction docs}
*
* @example
* ```ts
* const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE';
* const contract = new StellarSdk.Contract(contractId);
*
* // Right now, this is just the default fee for this example.
* const fee = StellarSdk.BASE_FEE;
* const transaction = new StellarSdk.TransactionBuilder(account, { fee })
* // Uncomment the following line to build transactions for the live network. Be
* // sure to also change the horizon hostname.
* //.setNetworkPassphrase(StellarSdk.Networks.PUBLIC)
* .setNetworkPassphrase(StellarSdk.Networks.FUTURENET)
* .setTimeout(30) // valid for the next 30s
* // Add an operation to call increment() on the contract
* .addOperation(contract.call("increment"))
* .build();
*
* const preparedTransaction = await server.prepareTransaction(transaction);
*
* // Sign this transaction with the secret key
* // NOTE: signing is transaction is network specific. Test network transactions
* // won't work in the public network. To switch networks, use the Network object
* // as explained above (look for StellarSdk.Network).
* const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecretKey);
* preparedTransaction.sign(sourceKeypair);
*
* server.sendTransaction(transaction).then(result => {
* console.log("hash:", result.hash);
* console.log("status:", result.status);
* console.log("errorResultXdr:", result.errorResultXdr);
* });
* ```
*/
async prepareTransaction(tx) {
const simResponse = await this.simulateTransaction(tx);
if (Api.isSimulationError(simResponse)) {
throw new Error(simResponse.error);
}
return assembleTransaction(tx, simResponse).build();
}
/**
* Submit a real transaction to the Stellar network.
*
* Unlike Horizon, RPC does not wait for transaction completion. It
* simply validates the transaction and enqueues it. Clients should call
* {@link rpc.Server.getTransaction} to learn about transaction
* success/failure.
*
* @param transaction - to submit
* @returns the
* transaction id, status, and any error if available
*
* @see {@link https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/operations-and-transactions | transaction docs}
* @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/sendTransaction | sendTransaction docs}
*
* @example
* ```ts
* const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE';
* const contract = new StellarSdk.Contract(contractId);
*
* // Right now, this is just the default fee for this example.
* const fee = StellarSdk.BASE_FEE;
* const transaction = new StellarSdk.TransactionBuilder(account, { fee })
* // Uncomment the following line to build transactions for the live network. Be
* // sure to also change the horizon hostname.
* //.setNetworkPassphrase(StellarSdk.Networks.PUBLIC)
* .setNetworkPassphrase(StellarSdk.Networks.FUTURENET)
* .setTimeout(30) // valid for the next 30s
* // Add an operation to call increment() on the contract
* .addOperation(contract.call("increment"))
* .build();
*
* // Sign this transaction with the secret key
* // NOTE: signing is transaction is network specific. Test network transactions
* // won't work in the public network. To switch networks, use the Network object
* // as explained above (look for StellarSdk.Network).
* const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecretKey);
* transaction.sign(sourceKeypair);
*
* server.sendTransaction(transaction).then((result) => {
* console.log("hash:", result.hash);
* console.log("status:", result.status);
* console.log("errorResultXdr:", result.errorResultXdr);
* });
* ```
*/
async sendTransaction(transaction) {
return this._sendTransaction(transaction).then(parseRawSendTransaction);
}
async _sendTransaction(transaction) {
return postObject(
this.httpClient,
this.serverURL.toString(),
"sendTransaction",
{
transaction: transaction.toXDR()
}
);
}
/**
* Fund a new account using the network's Friendbot faucet, if any.
*
* @param address - The address or account instance that we
* want to create and fund with Friendbot
* @param friendbotUrl - (optional) Optionally, an explicit address for
* friendbot (by default: this calls the Soroban RPC
* {@link rpc.Server.getNetwork | getNetwork} method to try to
* discover this network's Friendbot url).
* @returns An {@link Account} object for the created
* account, or the existing account if it's already funded with the
* populated sequence number (note that the account will not be "topped
* off" if it already exists)
* @throws If Friendbot is not configured on this network or request failure
*
* @see {@link https://developers.stellar.org/docs/learn/fundamentals/networks#friendbot | Friendbot docs}
* @see {@link Friendbot.Api.Response}
*
* @deprecated Use {@link Server.fundAddress} instead, which supports both
* account (G...) and contract (C...) addresses.
*
* @example
* ```ts
* server
* .requestAirdrop("GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4")
* .then((accountCreated) => {
* console.log("accountCreated:", accountCreated);
* }).catch((error) => {
* console.error("error:", error);
* });
* ```
*/
async requestAirdrop(address, friendbotUrl) {
const account = typeof address === "string" ? address : address.accountId();
friendbotUrl = friendbotUrl || (await this.getNetwork()).friendbotUrl;
if (!friendbotUrl) {
throw new Error("No friendbot URL configured for current network");
}
try {
const response = await this.httpClient.post(
`${friendbotUrl}?addr=${encodeURIComponent(account)}`
);
let meta;
if (!response.data.result_meta_xdr) {
const txMeta = await this.getTransaction(response.data.hash);
if (txMeta.status !== Api.GetTransactionStatus.SUCCESS) {
throw new Error(`Funding account ${address} failed`);
}
meta = txMeta.resultMetaXdr;
} else {
meta = types.TransactionMeta.fromXDR(
response.data.result_meta_xdr,
"base64"
);
}
const sequence = findCreatedAccountSequenceInTransactionMeta(meta);
return new Account(account, sequence);
} catch (error) {
if (error.response?.status === 400) {
if (error.response.data?.detail?.includes("createAccountAlreadyExist")) {
return this.getAccount(account);
}
}
throw error;
}
}
/**
* Fund an address using the network's Friendbot faucet, if any.
*
* This method supports both account (G...) and contract (C...) addresses.
*
* @param address - The address to fund. Can be either a Stellar
* account (G...) or contract (C...) address.
* @param friendbotUrl - (optional) Optionally, an explicit Friendbot URL
* (by default: this calls the Stellar RPC
* {@link rpc.Server.getNetwork | getNetwork} method to try to
* discover this network's Friendbot url).
* @returns The transaction
* response from the Friendbot funding transaction.
* @throws If Friendbot is not configured on this network or the
* funding transaction fails.
*
* @see {@link https://developers.stellar.org/docs/learn/fundamentals/networks#friendbot | Friendbot docs}
*
* @example
* ```ts
* // Funding an account (G... address)
* const tx = await server.fundAddress("GBZC6Y2Y7...");
* console.log("Funded! Hash:", tx.txHash);
* // If you need the Account object:
* const account = await server.getAccount("GBZC6Y2Y7...");
* ```
*
* @example
* ```ts
* // Funding a contract (C... address)
* const tx = await server.fundAddress("CBZC6Y2Y7...");
* console.log("Contract funded! Hash:", tx.txHash);
* ```
*/
async fundAddress(address, friendbotUrl) {
if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address)) {
throw new Error(
`Invalid address: ${address}. Expected a Stellar account (G...) or contract (C...) address.`
);
}
friendbotUrl = friendbotUrl || (await this.getNetwork()).friendbotUrl;
if (!friendbotUrl) {
throw new Error("No friendbot URL configured for current network");
}
try {
const response = await this.httpClient.post(
`${friendbotUrl}?addr=${encodeURIComponent(address)}`
);
const txResponse = await this.getTransaction(response.data.hash);
if (txResponse.status !== Api.GetTransactionStatus.SUCCESS) {
throw new Error(
`Funding address ${address} failed: transaction status ${txResponse.status}`