Skip to main content

Query the Chain

Reading is the other half of building. Before you build a transaction you need UTXOs and protocol parameters; after you submit one you wait for confirmation; a dApp UI shows balances, datums, and delegation. All of it comes from querying the chain through a provider, so you don't have to run and index a node yourself.

The conceptual model (UTXOs, datums) is in Transactions and eUTXO; this page is the read-side how-to.

Choosing a provider

A provider is the data source your SDK talks to. Most SDKs support several behind one unified interface, so the query methods stay the same no matter which you pick:

ProviderHostingAPI keyRate limits
BlockfrostHostedRequiredYes (free tier limited)
MaestroHostedRequiredYes (free tier limited)
KoiosHosted (community) or self-hostedOptionalYes (higher with a key)
KupmiosSelf-hosted (Ogmios + Kupo)Not applicableNone (your own infra)

Configure one when you make the client:

import { mainnet, Client } from "@evolution-sdk/evolution"

// Blockfrost (hosted)
const bf = Client.make(mainnet).withBlockfrost({
baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
projectId: process.env.BLOCKFROST_PROJECT_ID!
})

// Kupmios (self-hosted Ogmios + Kupo)
const kupmios = Client.make(mainnet).withKupmios({
ogmiosUrl: "http://localhost:1337",
kupoUrl: "http://localhost:1442"
})

// Maestro (hosted)
const maestro = Client.make(mainnet).withMaestro({
baseUrl: "https://mainnet.gomaestro-api.org/v1",
apiKey: process.env.MAESTRO_API_KEY!
})

// Koios (community)
const koios = Client.make(mainnet).withKoios({ baseUrl: "https://api.koios.rest/api/v1" })

Use the matching network base URL for Preprod/Preview (e.g. https://cardano-preprod.blockfrost.io/api/v0). For a hosted Kupmios like Demeter, pass the API keys through the connection. With Evolution that is the headers option on withKupmios:

const client = Client.make(mainnet).withKupmios({
ogmiosUrl: "https://ogmios.demeter.run",
kupoUrl: "https://kupo.demeter.run",
headers: {
ogmiosHeader: { "dmtr-api-key": process.env.DEMETER_API_KEY! },
kupoHeader: { "dmtr-api-key": process.env.DEMETER_API_KEY! }
}
})

Mesh has no single Kupmios provider; pair OgmiosProvider with Kupo and pass the Demeter keys through each provider's connection options.

Because the interface is unified, switching provider (e.g. Blockfrost in dev, self-hosted Kupmios in prod) is a one-line change. The query calls stay the same. For setting up the provider infrastructure itself (Blockfrost projects, running your own node + Kupo + Ogmios, Demeter), see the API providers reference and production infrastructure.

Privacy and trust

A hosted provider sees every address you query and every transaction you submit, along with your IP. It's a third party in your data path, with rate limits and an uptime you don't control. Self-hosting (your own node + Kupo + Ogmios, or Kupmios) keeps that data private and removes the dependency, at the cost of running the infrastructure. Pick based on how sensitive your queries are and how much ops you want to own.

Provider-only, read-only, or signing client

How you configure the client decides what it can do:

ClientConfigured withQuery any addressQuery own walletBuild txSign
Provider-onlyproviderYes---
Read-onlyprovider + addressYesYesYes (unsigned)-
Signingprovider + wallet (seed/key/CIP-30)YesYesYesYes

A provider-only client is all you need to read the chain, a block explorer, a submission service, a monitor. Add a wallet address (read-only) to also build unsigned transactions for a specific user (the backend-builds pattern); add a wallet to sign.

Querying chain data

You'll read a handful of things off the chain, each a single query through the client.

Off-chain helpers you'll reach for

Querying gives you raw chain data; turning addresses, datums, and assets into the hashes and identifiers your code needs is the other half. Both SDKs ship the same family of pure helpers for this, so you can call them in a backend without a provider. The calls differ in name, not in what they return:

import { Address, Unit, Time } from "@evolution-sdk/evolution"

// Address -> credentials
const { paymentCredential, stakingCredential, networkId } = Address.getAddressDetails("addr_test1...")
const payment = Address.getPaymentCredential("addr_test1...") // payment credential only

// Unit -> policy id + asset name
const { policyId, assetName, label } = Unit.fromUnit(unit)

// Time -> slot for a network
const slot = Time.unixTimeToSlot(Date.now(), slotConfig)

// CIP-14 fingerprint: compute from policy + name (no one-call helper)

Mesh additionally ships one-call helpers like resolveDataHash (datum hash), serializeNativeScript, and resolveScriptHashDRepId; in Evolution you reach the same results through its Data, NativeScripts, and credential modules. Either way these are pure (network-aware only for slot conversion), so they belong in a backend without a provider.

UTXOs and balances

import { Address } from "@evolution-sdk/evolution"

// Any address
const utxos = await client.getUtxos(Address.fromBech32("addr_test1..."))

// Your wallet, and its total ADA
const mine = await client.getWalletUtxos()
const balance = mine.reduce((sum, u) => sum + u.assets.lovelace, 0n)

// Find UTXOs holding a specific asset, or the single UTXO holding an NFT
const withToken = await client.getUtxosWithUnit(Address.fromBech32("addr_test1..."), unit)
const nftUtxo = await client.getUtxoByUnit(unit) // unit = policyId + assetNameHex

Datums

A UTXO with an inline datum carries it directly, on the UTXO you already fetched. A UTXO with only a datum hash needs a separate lookup to recover the datum behind it:

// Inline datum: already attached to the fetched UTXO
const utxos = await client.getUtxos(scriptAddress)
const inline = utxos[0].datumOption // present when the output carries an inline datum

// Datum hash: resolve the datum behind it through the provider
const datum = await client.getDatum(datumHash)

Inline datums (Plutus V2+) avoid the extra round-trip. Prefer them when designing contracts. See Datum, redeemer & context. Mesh reads inline datums straight off the fetched UTXO and has no separate datum-hash lookup, so for a hash-only UTXO you supply the datum off-chain when you spend it, another reason to prefer inline datums.

Protocol parameters

The builder fetches these automatically, but you can read them, fees, size limits, deposits, Plutus costs:

const params = await client.getProtocolParameters()
console.log(params.minFeeA, params.maxTxSize, params.keyDeposit, params.coinsPerUtxoByte)

Delegation and confirmation

// Which pool a reward address delegates to, and its reward balance
const delegation = await client.getDelegation(rewardAddress) // { poolId, rewards }

// Wait for a submitted transaction to appear on-chain (poll every 3s)
const confirmed = await client.awaitTx(txHash, 3000)

Delegation queries underpin the staking UI; awaitTx is the confirmation step after your first transaction.

Submitting transactions

A provider also broadcasts signed transactions and can evaluate script costs before you submit:

import { Transaction } from "@evolution-sdk/evolution"

// Submit signed CBOR (e.g. returned from a frontend wallet)
const signedTx = Transaction.fromCBORHex(signedTxCbor)
const txHash = await client.submitTx(signedTx)
const confirmed = await client.awaitTx(txHash)

// Estimate script execution units before submitting
const redeemers = await client.evaluateTx(Transaction.fromCBORHex(unsignedTxCbor))

Common rejection reasons from the node:

ErrorMeaningRetryable?
BadInputsUTxOA chosen UTXO was already spentNo: rebuild with fresh UTXOs
OutsideValidityIntervalUTxOThe transaction expiredNo: rebuild with a new validity window
ValueNotConservedUTxOInputs ≠ outputs + feeNo: fix the transaction
FeeTooSmallUTxOFee too lowNo: rebuild
Network timeoutProvider unreachableYes: retry after a delay

BadInputsUTxO from indexer lag is the classic one. Handle it with the retry-safe pattern, which re-reads chain state on every attempt.

Inspect a transaction

Sometimes you have a transaction in hand (one you built, or one you pulled from the chain) and you want to read it back: its inputs, outputs, fee, mint, and validity interval. Both SDKs decode transaction CBOR into an inspectable structure.

Evolution decodes CBOR straight into typed transaction objects:

import { Transaction, TransactionBody } from "@evolution-sdk/evolution"

const tx = Transaction.fromCBORHex(txHex) // the whole transaction
const body = TransactionBody.fromCBORHex(bodyHex) // or just the body
// read inputs, outputs, fee, mint, and the validity interval off the decoded body

Next steps