Find the Stellar Asset Contracts (SACs) referenced by a Stellar transaction or by Soroban authorization entries, and learn which asset each one wraps.
A signing UI receives CCW67TSZ… calling transfer. This library turns that into "USDC issued by GA5ZSEJY…".
npm install stellar-inspect @stellar/stellar-sdkThe package is published as ES modules. require("stellar-inspect") also works, since Node 22.12+ can load ES modules from CommonJS.
import { Networks } from "@stellar/stellar-sdk";
import { findSacs } from "stellar-inspect";
const sacs = await findSacs(txEnvelopeXdr, { networkPassphrase: Networks.PUBLIC });
// [
// { contractId: "CAS3J7GY…", asset: { type: "native" } },
// { contractId: "CCW67TSZ…", asset: { type: "credit_alphanum4", code: "USDC", issuer: "GA5ZSEJY…" } },
// ]findSacs accepts a Transaction, a FeeBumpTransaction, an xdr.TransactionEnvelope, an xdr.SorobanAuthorizationEntry, base64 XDR of either, or an array of any of those. Anything else throws a TypeError rather than being reported as a transaction without SACs.
networkPassphrase is the passphrase of the network the input belongs to, the same value you pass to TransactionBuilder. Any network works, including futurenet and local standalone networks, as long as the resolver can serve it.
It looks at:
- the contract invoked by each
InvokeHostFunctionoperation, - every
Address-typed argument, recursively through vectors and maps, - the full authorization tree of each auth entry, including sub-invocations,
- the Soroban footprint (contracts whose data is read or written),
createContractoperations: the deployer address, constructor arguments, and for SAC deployments the asset itself, taken from the transaction without a lookup.
Contract addresses are deduplicated and resolved in parallel. Anything that is not a SAC is dropped.
findSacs rejects, without partial results, when the input is unsupported or when any single lookup fails: a network error, a timeout, an abort, or an unexpected response from the resolver. A contract that does not exist or is not a SAC is not a failure; it is simply left out. Pass a signal to abort pending lookups, for example AbortSignal.timeout(5000) to bound the whole call:
await findSacs(tx, { networkPassphrase: Networks.PUBLIC, signal: AbortSignal.timeout(5000) });Resolution is pluggable through the Resolver interface: resolve(networkPassphrase, contractId, { signal }) returns the asset, null when the contract is not a SAC, and rejects when the lookup fails.
A resolver serves one network, like an RPC node does. findSacs verifies every result against the passphrase you pass, so a resolver pointed at the wrong network yields no SACs rather than wrong ones.
import { findSacs, httpResolver, rpcResolver, staticResolver } from "stellar-inspect";
// Default for mainnet and testnet: the public sac-resolver service (edge-cached, no RPC load on your side).
// Other networks have no default; pass a resolver.
await findSacs(tx, { networkPassphrase: Networks.PUBLIC });
// The same service addressed explicitly (`/mainnet` or `/testnet`), or your own deployment. One URL per
// network; the contract id is appended to it. Each request times out after 10 s; tune with `timeout`.
await findSacs(tx, { networkPassphrase: Networks.PUBLIC, resolver: httpResolver("https://sac-resolver.lightsail.network/mainnet", { timeout: 3000 }) });
// Read contract instances directly from a Soroban RPC node (a URL or an existing rpc.Server).
await findSacs(tx, { networkPassphrase: Networks.PUBLIC, resolver: rpcResolver("https://rpc.lightsail.network", { timeout: 5000 }) });
// Fully offline, from assets you already know (for example the user's trustlines).
await findSacs(tx, { networkPassphrase: Networks.PUBLIC, resolver: staticResolver(knownSacs) });A SAC address is sha256(networkId, asset), so findSacs re-derives the address of every asset a resolver returns, drops anything that does not match, and rebuilds what remains from the verified code and issuer alone. A misconfigured, compromised, or tampered-with resolver, including the public sac-resolver service, can only cause a SAC to be missed; it can never make an address show up as the wrong asset, and nothing it returns reaches you unverified. The RPC resolver applies the same check to the contract instance it reads, so a contract cannot misreport which asset it represents.
import { extractContracts, parseSacInstance } from "stellar-inspect";
// Pure, synchronous: every contract address in the input plus assets being deployed as SACs.
const { contractIds, deployedAssets } = extractContracts(txEnvelopeXdr);
// Parse a contract instance ledger entry yourself.
const asset = parseSacInstance(Networks.PUBLIC, contractId, ledgerEntryData); // AssetInfo | null- sac-resolver: the HTTP service behind
httpResolver.