Selecting UTXOs for Transactions

Selecting UTXOs for Transactions.

Overview

Unspent Transaction Outputs (UTXOs) are a fundamental concept in Cardano's accounting model. Unlike an account-based model that tracks balances, the UTXO model tracks individual outputs of transactions that haven't been spent yet.

When building a Cardano transaction with Anvil API, you have two options for specifying transaction inputs:

  1. Provide UTXO List - Explicitly specify which UTXOs to use (required in production)

  2. Automatic Selection - Let Anvil choose UTXOs for you (testing environments only)

Automatic UTXO Selection in Testing

In test environments (preprod, preview), the utxos parameter in the transaction building API is optional. If you don't provide it, Anvil will:

  • Automatically fetch UTXOs associated with the changeAddress

  • Select appropriate UTXOs to cover the transaction

  • Handle all input management for you

This simplifies development and testing by removing the need to manage UTXO selection logic until you're ready for production.

// Example: Let Anvil handle UTXO selection (test environments only)
const response = await fetch('https://preprod.api.ada-anvil.app/v2/services/transactions/build', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    // Just provide a change address - no UTXOs needed!
    changeAddress: 'addr_test...',
    outputs: [
      {
        address: 'recipient_address',
        lovelace: 5000000 // 5 ADA
      }
    ]
  })
});

Production Requirement

For production environments, you must explicitly provide UTXOs using the utxos parameter:

// Production example: Explicitly provide UTXOs
const response = await fetch('https://prod.api.ada-anvil.app/v2/services/transactions/build', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    changeAddress: 'addr...',
    // UTXO list is required in production
    utxos: ['8282...', '8282...'],
    outputs: [
      {
        address: 'recipient_address',
        lovelace: 5000000 // 5 ADA
      }
    ]
  })
});

Fetching UTXOs: Multiple Approaches

Here are three efficient ways to retrieve UTXOs in your Cardano applications:

Using Weld

Weld is a bridge to browser-based wallets that implements the CIP-30 standard. It provides a unified interface for wallet interactions. Here's how to retrieve UTXOs from a connected wallet:

const wallet = useWallet("isConnected", "handler");

const interact = async () => {
  if (!wallet.isConnected) {
    return;
  }
  const utxos = await wallet.handler.getUtxos();

  // Build Transaction using utxos

  // Sign Transaction
  const res = await wallet.handler.signTx("TX_CBOR_HERE");
};

This code snippet uses the Weld wallet connector to:

  1. Check if a wallet is connected using the isConnected property

  2. Retrieve the complete list of UTXOs associated with the wallet's addresses using getUtxos()

  3. Demonstrate how these UTXOs could then be used in transaction signing with signTx()

Weld simplifies wallet interactions by providing a consistent API regardless of which Cardano wallet the user has connected.

Using BlockFrost

BlockFrost is a popular API-as-a-service for Cardano that provides access to on-chain data. The following example demonstrates fetching UTXOs via the BlockFrost API and converting them to the format expected by cardano-serialization-lib (CSL):

import { Buffer } from "node:buffer";
import {
  TransactionUnspentOutput,
  TransactionInput,
  TransactionOutput,
  TransactionHash,
  TransactionUnspentOutputs,
  MultiAsset,
  Value,
  Assets,
  AssetName,
  BigNum,
  ScriptHash,
  Address,
} from "@emurgo/cardano-serialization-lib-nodejs";

export function hex_to_uint8(input: string): Uint8Array {
  return new Uint8Array(Buffer.from(input, "hex"));
}
export function assets_to_value(
  multi_asset: MultiAsset,
  assets: Asset[],
): Value {
  const qt = assets.find((asset) => asset.unit === "lovelace")?.quantity;
  if (!qt) {
    throw new Error("No lovelace found in the provided utxo");
  }

  const assets_to_add: Record<string, Assets> = {};

  for (const asset of assets) {
    if (asset.unit !== "lovelace") {
      const policy_hex = asset.unit.slice(0, 56);
      const asset_hex = hex_to_uint8(asset.unit.slice(56));

      if (!assets_to_add[policy_hex]) {
        assets_to_add[policy_hex] = Assets.new();
      }

      assets_to_add[policy_hex].insert(
        AssetName.new(asset_hex),
        BigNum.from_str(asset.quantity),
      );
    }
  }

  for (const key of Object.keys(assets_to_add)) {
    multi_asset.insert(ScriptHash.from_hex(key), assets_to_add[key]);
  }

  return Value.new_with_assets(BigNum.from_str(qt), multi_asset);
}

export type Asset = {
  unit: string;
  quantity: string;
};
export type Utxo = {
  address: string;
  tx_hash: string;
  output_index: number;
  amount: Asset[];
  block?: string;
  data_hash?: string | null;
  inline_datum?: string | null;
  reference_script_hash?: string | null;
};
export async function get_utxos_api(
  blockfrost_base_url: string,
  blockfrost_api_key: string,
  address: string,
): Promise<Utxo[]> {
  const get = async (data: object[] = [], page = 1) => {
    const res = await fetch(
      `${blockfrost_base_url}/addresses/${address}/utxos?page=${page}`,
      {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
          project_id: blockfrost_api_key,
        },
      },
    );

    if (res.status < 199 || res.status > 200) {
      throw new Error("Unable to get utxos for specified address");
    }

    const json = await res.json();
    if (json.length > 0) {
      return await get([...data, ...json], page + 1);
    }

    return [...data, ...json];
  };

  return await get();
}

const utxos = await get_utxos_api(
  "https://cardano-mainnet.blockfrost.io/api/v0",
  Deno.env.get("BLOCKFROST_PROJECT_ID"),
  "addr1q96gmvs93t96txjv43sw5gtf6pfzwmsu07t635pmlvwsucup2645cqyrknctcsd3s4e6wc23fxqsr3mywdlx9lnme6sshlyndu",
);
console.log("UTXOs Found:", utxos.length);

const parsedUtxo: TransactionUnspentOutputs = TransactionUnspentOutputs.new();

for (const utxo of utxos) {
  const multi_assets = MultiAsset.new();
  const { tx_hash, output_index, amount, address } = utxo as Utxo;

  // If we have enough utxos, we can proceed.
  parsedUtxo.add(
    TransactionUnspentOutput.new(
      TransactionInput.new(TransactionHash.from_hex(tx_hash), output_index),
      TransactionOutput.new(
        Address.from_bech32(address),
        assets_to_value(multi_assets, amount),
      ),
    ),
  );
}

This code performs several key operations:

  1. Defines utility functions to convert between different data formats, including the critical assets_to_value function that transforms BlockFrost's asset representation to CSL's Value type

  2. Implements a paginated API request to BlockFrost's /addresses/{address}/utxos endpoint with proper authentication

  3. Constructs TransactionUnspentOutput objects from the returned data, which include:

    • Transaction inputs (with transaction hash and output index)

    • Transaction outputs (with address and value information)

  4. Combines these into a TransactionUnspentOutputs collection that can be used for transaction building

This approach is particularly useful for server-side applications where you need to query UTXOs without a connected wallet.

Using Dolos

Dolos is a specialized Cardano "Data Node" designed for efficient data access. It works with UTXOrpc, a standardized protocol for querying UTXO-based blockchains. The example below shows how to use the UTXOrpc SDK to retrieve UTXOs:

import { CardanoQueryClient } from "@utxorpc/sdk";
import { Buffer } from "node:buffer";
import {
  TransactionUnspentOutput,
  TransactionInput,
  TransactionOutput,
  TransactionHash,
  TransactionUnspentOutputs,
} from "@emurgo/cardano-serialization-lib-nodejs";

const queryClient = new CardanoQueryClient({
  // Dolos Endpoint
  uri: Deno.env.get("DOLOS_ENDPOINT"),
});

console.time("dolos");
const utxos = await queryClient.searchUtxosByAddress(
  Buffer.from(
    // Fetched from browser using wallet.getChangeAddress()
    "01748db2058acba59a4cac60ea2169d052276e1c7f97a8d03bfb1d0e638156ab4c0083b4f0bc41b18573a76151498101c764737e62fe7bcea1",
    "hex",
  ),
);
console.log("UTXOs Found:", utxos.length);

const parsedUtxo: TransactionUnspentOutputs = TransactionUnspentOutputs.new();

for (const utxo of utxos) {
  // UTXOs from dolos
  const bytes = utxo.nativeBytes;
  if (!bytes) {
    throw new Error("No native bytes");
  }
  parsedUtxo.add(
    TransactionUnspentOutput.new(
      TransactionInput.new(
        TransactionHash.from_bytes(utxo.txoRef.hash),
        utxo.txoRef.index,
      ),
      TransactionOutput.from_bytes(bytes),
    ),
  );
}

const utxoHexes: string[] = [];
for (let i = 0; i < parsedUtxo.len(); i++) {
  utxoHexes.push(parsedUtxo.get(i).to_hex());
}
console.log(utxoHexes);

This example demonstrates:

  1. Initializing a CardanoQueryClient from the UTXOrpc SDK, pointing to a Dolos endpoint

  2. Querying UTXOs for a specific address (the address is provided in hex format)

  3. Converting the returned UTXOs directly to TransactionUnspentOutput objects using the native byte format

  4. Creating a collection of UTXOs that can be used for transaction building

  5. Converting each UTXO to its hexadecimal representation for debugging or further processing

Dolos with UTXOrpc offers a more lightweight and efficient approach to data retrieval compared to running a full Cardano node. It's particularly well-suited for applications that need high performance UTXO retrieval with lower resource requirements.

Comparison and Considerations

  • Weld: Best for client-side/browser applications where a user's wallet is already connected. Simplest API for direct wallet interaction.

  • BlockFrost: Good for server-side applications or when no wallet is connected. Requires an API key and performs well for most use cases.

  • Dolos: Optimal for high-performance applications. Requires running a Dolos instance or connecting to a hosted one. Provides native format UTXOs that minimize conversion overhead.

Choose the approach that best fits your application architecture, performance requirements, and deployment environment.

TransactionUtility Functions

Last updated

Was this helpful?