Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Whisky is an open-source Cardano Rust SDK built by SIDAN Lab. It provides a comprehensive set of tools for building Cardano DApps in Rust, with a chainable builder API inspired by MeshJS.

Modules

Whisky is organized as a Rust workspace with the following crates:

CrateDescription
whiskyThe main crate — re-exports everything you need for DApp development
whisky-commonShared types, interfaces, and utilities used across all crates
whisky-pallasTransaction serializer built on TxPipe’s Pallas (recommended)
whisky-cslLegacy serializer built on cardano-serialization-lib
whisky-providerProvider integrations for Blockfrost and Maestro
whisky-walletWallet signing and key management utilities
whisky-macrosProcedural macros for Plutus data encoding
whisky-jsWASM bindings for JavaScript/TypeScript usage

What You Can Do

With whisky, you can:

  • Build transactions with a chainable, cardano-cli-like API supporting complex DApp backends
  • Parse and edit transactions from raw CBOR hex
  • Sign transactions with key-based signing in Rust
  • Interact with the blockchain via Maestro and Blockfrost providers
  • Evaluate scripts off-chain using TxPipe’s uplc for execution unit estimation
  • Swap serializer backends between Pallas and CSL via dependency injection

Guide Overview

This guide walks you through:

  1. Installation — Adding whisky to your project
  2. Quick Start — Building your first transaction
  3. Transaction Builder — Simple sends, Plutus scripts, minting, staking
  4. Transaction Parser — Parsing and editing existing transactions
  5. Dependency Injection — Pluggable serializers and providers
  6. Migration: CSL to Pallas — Upgrading from the legacy backend

For full API reference, see the generated Rust docs.

Installation

Rust Library

Add whisky to your project:

cargo add whisky

Or add it directly to your Cargo.toml:

[dependencies]
whisky = "1.0.28-beta.1"

Feature Flags

By default, all features are enabled (full). You can selectively enable only what you need:

# Full (default) — includes wallet + provider
whisky = "1.0.28-beta.1"

# Just common types (minimal, no serializer backends)
whisky = { version = "1.0.28-beta.1", default-features = false }

# Wallet only (signing + key management)
whisky = { version = "1.0.28-beta.1", default-features = false, features = ["wallet"] }

# Provider only (Blockfrost/Maestro integrations)
whisky = { version = "1.0.28-beta.1", default-features = false, features = ["provider"] }
FeatureIncludesUse Case
full (default)wallet + providerFull DApp backend
walletSigning, key encryptionTransaction signing only
providerBlockfrost, MaestroBlockchain data fetching

JS / TS WASM Library

For JavaScript or TypeScript projects, whisky is available as a WASM package:

# For Node.js
yarn add @sidan-lab/whisky-js-nodejs

# For browser
yarn add @sidan-lab/whisky-js-browser

Prerequisites

For building from source, make sure LLVM is installed on your system:

  • macOS: brew install llvm
  • Ubuntu/Debian: apt install llvm-dev libclang-dev
  • Windows: Install via LLVM releases

Quick Start

This guide shows you how to build your first Cardano transaction with whisky.

Your First Transaction

The simplest transaction sends lovelace from one address to another. Here’s the complete example:

use whisky::*;

pub fn send_lovelace(
    recipient_address: &str,
    my_address: &str,
    inputs: &[UTxO],
) -> Result<String, WError> {
    let mut tx_builder = TxBuilder::new_core();
    tx_builder
        .tx_out(
            recipient_address,
            &[Asset::new_from_str("lovelace", "1000000")],
        )
        .change_address(my_address)
        .select_utxos_from(inputs, 5000000)
        .complete_sync(None)?;

    Ok(tx_builder.tx_hex())
}

Let’s break this down:

  1. TxBuilder::new_core() — Creates a new transaction builder with the default Pallas serializer
  2. .tx_out(address, assets) — Adds an output sending 1 ADA (1,000,000 lovelace) to the recipient
  3. .change_address(address) — Sets where leftover funds go after paying fees
  4. .select_utxos_from(inputs, threshold) — Automatically selects UTxOs from the provided set to cover outputs + fees. The threshold (5,000,000 lovelace) is extra headroom for fees and min UTxO
  5. .complete_sync(None) — Balances the transaction, calculates fees, and serializes to CBOR
  6. .tx_hex() — Returns the unsigned transaction as a hex-encoded CBOR string

Sync vs Async

Whisky offers both synchronous and asynchronous completion:

// Synchronous — no script evaluation, no provider calls
tx_builder.complete_sync(None)?;

// Asynchronous — evaluates Plutus scripts, can fetch data from providers
tx_builder.complete(None).await?;

Use complete_sync for simple transactions without scripts. Use complete (async) when your transaction includes Plutus scripts that need execution unit evaluation.

Signing

After building, sign the transaction with a private key:

let signed_tx = tx_builder
    .signing_key("your_signing_key_hex")
    .complete_sync(None)?
    .complete_signing()?;

The signed_tx string is ready for submission to the Cardano network.

Running the Tests

Whisky includes comprehensive integration tests you can run to see these patterns in action:

# Run all tests
cargo test

# Run Pallas integration tests specifically
cargo test --package whisky --test pallas_integration_tests

# Run a specific test with output
cargo test --package whisky --test pallas_integration_tests test_simple_spend -- --nocapture

Next Steps

Transaction Builder

The TxBuilder is the core API for constructing Cardano transactions in whisky. It uses a chainable builder pattern where you compose a transaction step by step, then finalize it.

Overview

use whisky::*;

let mut tx_builder = TxBuilder::new_core();
tx_builder
    .tx_in(tx_hash, tx_index, amount, address)  // Add inputs
    .tx_out(address, assets)                      // Add outputs
    .change_address(my_address)                   // Set change address
    .complete_sync(None)?;                        // Balance and serialize

let tx_hex = tx_builder.tx_hex();

Key Concepts

Builder Pattern

Every method returns &mut Self, allowing you to chain calls fluently. The builder accumulates state until you call complete_sync() or complete().

Inputs and UTxO Selection

You can specify inputs explicitly with .tx_in() or let whisky select them automatically:

// Explicit input
tx_builder.tx_in(tx_hash, tx_index, amount, address);

// Automatic selection from a pool of UTxOs
tx_builder.select_utxos_from(&available_utxos, 5000000);

The select_utxos_from threshold (e.g., 5,000,000 lovelace) tells the selector to pick enough UTxOs to cover all outputs plus this extra amount for fees and change output min UTxO.

Outputs

// Send lovelace
tx_builder.tx_out(address, &[Asset::new_from_str("lovelace", "2000000")]);

// Send native tokens
tx_builder.tx_out(address, &[
    Asset::new_from_str("lovelace", "2000000"),
    Asset::new("policy_id_hex".to_string() + "token_name_hex", "1".to_string()),
]);

Completion

MethodSync/AsyncScript EvaluationUse Case
complete_sync(None)SyncNoSimple transactions
complete(None).awaitAsyncYes (offline)Plutus script transactions

Common Methods

MethodPurpose
.tx_in()Add a specific UTxO as input
.tx_out()Add an output
.change_address()Set the change address
.select_utxos_from()Auto-select UTxOs from a pool
.signing_key()Add a signing key
.required_signer_hash()Add a required signer
.invalid_before()Set validity start slot
.invalid_hereafter()Set validity end slot
.metadata_value()Attach transaction metadata
.complete_signing()Sign and return the final tx hex

Chapters

Simple Transactions

These examples cover basic transaction patterns that don’t involve Plutus scripts.

Send Lovelace

The most basic transaction: send ADA from one address to another.

use whisky::*;

pub fn send_lovelace(
    recipient_address: &str,
    my_address: &str,
    inputs: &[UTxO],
) -> Result<String, WError> {
    let mut tx_builder = TxBuilder::new_core();
    tx_builder
        .tx_out(
            recipient_address,
            &[Asset::new_from_str("lovelace", "1000000")],
        )
        .change_address(my_address)
        .select_utxos_from(inputs, 5000000)
        .complete_sync(None)?;

    Ok(tx_builder.tx_hex())
}

Note: You don’t need to manually add inputs — select_utxos_from automatically picks UTxOs to cover the output amount plus the threshold for fees.

Lock Funds at a Script Address

Send funds to a script address with an inline datum attached:

use whisky::*;

pub fn lock_fund(
    script_address: &str,
    datum: &str,
    my_address: &str,
    inputs: &[UTxO],
) -> Result<String, WError> {
    let mut tx_builder = TxBuilder::new_core();
    tx_builder
        .tx_out(script_address, &[])
        .tx_out_inline_datum_value(&WData::JSON(datum.to_string()))
        .change_address(my_address)
        .select_utxos_from(inputs, 5000000)
        .complete_sync(None)?;

    Ok(tx_builder.tx_hex())
}

Key points:

  • tx_out_inline_datum_value attaches an inline datum (stored on-chain) to the output
  • The datum is provided as a JSON-encoded Plutus data string (e.g., {"constructor": 0, "fields": []})
  • Use tx_out_datum_hash_value instead if you only want to store the datum hash on-chain

Delegate Stake

Register a stake key and delegate to a pool in a single transaction:

use whisky::*;

pub fn delegate_stake(
    stake_key_hash: &str,
    pool_id: &str, // e.g., "pool1..."
    my_address: &str,
    inputs: &[UTxO],
) -> Result<String, WError> {
    let mut tx_builder = TxBuilder::new_core();
    tx_builder
        .register_stake_certificate(stake_key_hash)
        .delegate_stake_certificate(stake_key_hash, pool_id)
        .change_address(my_address)
        .select_utxos_from(inputs, 5000000)
        .complete_sync(None)?;

    Ok(tx_builder.tx_hex())
}

Validity Ranges and Metadata

You can set time bounds and attach metadata to any transaction:

tx_builder
    .tx_out(address, &[Asset::new_from_str("lovelace", "2000000")])
    .invalid_before(100)          // Transaction valid from slot 100
    .invalid_hereafter(200)       // Transaction invalid after slot 200
    .metadata_value("674", "{\"msg\": [\"Hello, Cardano!\"]}")
    .change_address(my_address)
    .complete_sync(None)?;

Signing and Submitting

After building, sign with one or more keys:

let signed_tx = tx_builder
    .signing_key("ed25519_sk_hex_key_1")
    .signing_key("ed25519_sk_hex_key_2")  // Multiple signers
    .complete_sync(None)?
    .complete_signing()?;

// signed_tx is ready for submission

Plutus Script Transactions

This chapter covers spending from Plutus script addresses — unlocking funds guarded by validators.

Unlock Funds from a Script

To spend a UTxO locked at a Plutus script address, you must provide the script, datum, and redeemer:

use whisky::*;

pub async fn unlock_fund(
    script_utxo: &UTxO,
    redeemer: &str,
    script: &ProvidedScriptSource,
    my_address: &str,
    inputs: &[UTxO],
    collateral: &UTxO,
) -> Result<String, WError> {
    let mut tx_builder = TxBuilder::new_core();
    let pub_key_hash = deserialize_address(my_address)?.pub_key_hash;

    tx_builder
        .spending_plutus_script_v3()
        .tx_in(
            &script_utxo.input.tx_hash,
            script_utxo.input.output_index,
            &script_utxo.output.amount,
            &script_utxo.output.address,
        )
        .tx_in_inline_datum_present()
        .tx_in_redeemer_value(&WRedeemer {
            data: WData::JSON(redeemer.to_string()),
            ex_units: Budget { mem: 0, steps: 0 },
        })
        .tx_in_script(&script.script_cbor)
        .change_address(my_address)
        .required_signer_hash(&pub_key_hash)
        .tx_in_collateral(
            &collateral.input.tx_hash,
            collateral.input.output_index,
            &collateral.output.amount,
            &collateral.output.address,
        )
        .input_for_evaluation(script_utxo)
        .select_utxos_from(inputs, 5000000)
        .complete(None)
        .await?;

    Ok(tx_builder.tx_hex())
}

The Script Spending Pattern

Every Plutus spend follows the same sequence:

  1. Declare the script version: .spending_plutus_script_v3() (or _v2(), _v1())
  2. Add the script input: .tx_in(hash, index, amount, address)
  3. Provide the datum: .tx_in_inline_datum_present() or .tx_in_datum_value(&datum)
  4. Provide the redeemer: .tx_in_redeemer_value(&redeemer)
  5. Provide the script: .tx_in_script(&script_cbor) or use a reference script

Datum Handling

Whisky supports two datum modes:

// The UTxO already has an inline datum — just declare it's present
.tx_in_inline_datum_present()

// Provide the datum value explicitly (e.g., when only the datum hash is on-chain)
.tx_in_datum_value(&WData::JSON(datum_json.to_string()))

Redeemer Values

Redeemers are Plutus data values passed to the validator. Set execution units to 0 and let the evaluator calculate them:

.tx_in_redeemer_value(&WRedeemer {
    data: WData::JSON(r#"{"constructor": 0, "fields": []}"#.to_string()),
    ex_units: Budget { mem: 0, steps: 0 },
})

When you call .complete(None).await, the built-in offline evaluator runs the script and fills in the actual execution units.

Script Sources

You can provide the script in two ways:

// Embed the script CBOR directly in the transaction
.tx_in_script(&script_cbor)

// Reference a script already on-chain (more efficient — saves tx size)
.spending_tx_in_reference(tx_hash, tx_index, script_hash, script_size)

Collateral

Plutus transactions require collateral — a UTxO that gets consumed if the script fails:

.tx_in_collateral(
    &collateral.input.tx_hash,
    collateral.input.output_index,
    &collateral.output.amount,
    &collateral.output.address,
)

You can also set a specific total collateral amount:

.tx_in_collateral(tx_hash, tx_index, amount, address)
.set_total_collateral("5000000")

Input for Evaluation

When using offline evaluation, provide the script UTxO context so the evaluator can resolve inputs:

.input_for_evaluation(script_utxo)

This is required for the offline evaluator to correctly simulate script execution.

Minting

This chapter covers minting and burning native tokens using Plutus minting policies.

Mint Tokens

Mint tokens using a Plutus V3 minting policy:

use whisky::*;

pub async fn mint_tokens(
    to_mint_asset: &Asset,
    redeemer: &str,
    script: &ProvidedScriptSource,
    my_address: &str,
    inputs: &[UTxO],
    collateral: &UTxO,
) -> Result<String, WError> {
    let mut tx_builder = TxBuilder::new_core();

    tx_builder
        .mint_plutus_script_v3()
        .mint(
            to_mint_asset.quantity_i128(),
            &to_mint_asset.policy(),
            &to_mint_asset.name(),
        )
        .minting_script(&script.script_cbor)
        .mint_redeemer_value(&WRedeemer {
            data: WData::JSON(redeemer.to_string()),
            ex_units: Budget { mem: 0, steps: 0 },
        })
        .change_address(my_address)
        .tx_in_collateral(
            &collateral.input.tx_hash,
            collateral.input.output_index,
            &collateral.output.amount,
            &collateral.output.address,
        )
        .select_utxos_from(inputs, 5000000)
        .complete(None)
        .await?;

    Ok(tx_builder.tx_hex())
}

The Minting Pattern

Every Plutus mint follows this sequence:

  1. Declare the script version: .mint_plutus_script_v3() (or _v2(), _v1())
  2. Specify the mint: .mint(quantity, policy_id, asset_name)
  3. Provide the script: .minting_script(&script_cbor) or use a reference script
  4. Provide the redeemer: .mint_redeemer_value(&redeemer)

Reference Script Minting

Instead of embedding the script, reference one already on-chain:

tx_builder
    .mint_plutus_script_v2()
    .mint(1, policy_id, token_name_hex)
    .mint_tx_in_reference(
        "reference_tx_hash",
        0,              // reference tx index
        policy_id,      // script hash to validate against
        100,            // script size in bytes
    )
    .mint_redeemer_value(&WRedeemer {
        data: WData::JSON(r#"{"constructor": 0, "fields": []}"#.to_string()),
        ex_units: Budget { mem: 3386819, steps: 1048170931 },
    })

Burning Tokens

To burn tokens, use a negative quantity:

tx_builder
    .mint_plutus_script_v3()
    .mint(-1, policy_id, token_name_hex)  // Negative = burn
    .minting_script(&script.script_cbor)
    .mint_redeemer_value(&redeemer)

Multiple Mints in One Transaction

You can mint tokens from multiple policies in a single transaction by chaining mint blocks:

use whisky::*;

tx_builder
    // First mint
    .mint_plutus_script_v2()
    .mint(
        to_mint_asset_1.quantity_i128(),
        &to_mint_asset_1.policy(),
        &to_mint_asset_1.name(),
    )
    .mint_redeemer_value(&WRedeemer {
        data: WData::JSON(redeemer_1.to_string()),
        ex_units: Budget { mem: 0, steps: 0 },
    })
    .minting_script(&script_1.script_cbor)
    // Second mint
    .mint_plutus_script_v2()
    .mint(
        to_mint_asset_2.quantity_i128(),
        &to_mint_asset_2.policy(),
        &to_mint_asset_2.name(),
    )
    .mint_redeemer_value(&WRedeemer {
        data: WData::JSON(redeemer_2.to_string()),
        ex_units: Budget { mem: 0, steps: 0 },
    })
    .minting_script(&script_2.script_cbor)
    .change_address(my_address)
    .tx_in_collateral(/* ... */)
    .select_utxos_from(inputs, 5000000)
    .complete(None)
    .await?;

Each mint block starts with .mint_plutus_script_vN() and is independent — you can mix V1, V2, and V3 policies in the same transaction.

Complex Transaction: Spend + Mint

You can combine script spending and minting in one transaction:

tx_builder
    // Script spend
    .spending_plutus_script_v2()
    .tx_in(/* script UTxO */)
    .tx_in_inline_datum_present()
    .tx_in_redeemer_value(&spend_redeemer)
    .tx_in_script(&spend_script_cbor)
    // Mint
    .mint_plutus_script_v2()
    .mint(1, policy_id, token_name)
    .mint_redeemer_value(&mint_redeemer)
    .minting_script(&mint_script_cbor)
    // Finalize
    .change_address(my_address)
    .tx_in_collateral(/* ... */)
    .input_for_evaluation(script_utxo)
    .select_utxos_from(inputs, 5000000)
    .complete(None)
    .await?;

Staking & Governance

This chapter covers stake operations and Conway-era governance actions.

Stake Registration

Register a stake key on-chain:

use whisky::*;

let mut tx_builder = TxBuilder::new_core();
tx_builder
    .register_stake_certificate(stake_key_hash)
    .change_address(my_address)
    .select_utxos_from(inputs, 5000000)
    .complete_sync(None)?;

Stake Delegation

Delegate to a stake pool (can be combined with registration):

tx_builder
    .register_stake_certificate(stake_key_hash)
    .delegate_stake_certificate(stake_key_hash, pool_id)
    .change_address(my_address)
    .select_utxos_from(inputs, 5000000)
    .complete_sync(None)?;

Stake Deregistration

Deregister a stake key to reclaim the deposit:

tx_builder
    .deregister_stake_certificate(stake_key_hash)
    .change_address(my_address)
    .select_utxos_from(inputs, 5000000)
    .complete_sync(None)?;

Withdrawals

Withdraw staking rewards:

tx_builder
    .tx_in(tx_hash, tx_index, amount, address)
    .change_address(my_address)
    .withdrawal(stake_address, 0)  // stake_address e.g., "stake_test1ur..."
    .required_signer_hash(pub_key_hash)
    .signing_key(signing_key_hex)
    .complete_sync(None)?
    .complete_signing()?;

Governance: DRep Registration

Register as a Delegated Representative (Conway era):

tx_builder
    .drep_registration(drep_id, deposit_amount)
    .change_address(my_address)
    .select_utxos_from(inputs, 5000000)
    .complete_sync(None)?;

Governance: Vote Delegation

Delegate your voting power to a DRep:

tx_builder
    .vote_delegation(stake_key_hash, drep)
    .change_address(my_address)
    .select_utxos_from(inputs, 5000000)
    .complete_sync(None)?;

Governance: Voting

Cast a governance vote:

tx_builder
    .vote(voter, ref_tx_hash, ref_tx_index, vote_kind)
    .change_address(my_address)
    .select_utxos_from(inputs, 5000000)
    .complete_sync(None)?;

For script-based voting, use the Plutus vote pattern:

tx_builder
    .vote_plutus_script_v3()
    .vote(voter, ref_tx_hash, ref_tx_index, vote_kind)
    .vote_script(&script_cbor)
    .vote_redeemer_value(&redeemer)
    .tx_in_collateral(/* ... */)
    .complete(None)
    .await?;

Transaction Parser

The transaction parser lets you deserialize a raw CBOR transaction hex back into a TxBuilderBody, inspect it, edit it, and rebuild it into a new transaction.

Parsing a Transaction

Use the parse function to deserialize a transaction:

use whisky::*;
use whisky_pallas::tx_parser::parse;

let tx_hex = "84a700d90102..."; // Raw transaction CBOR hex
let utxos = vec![/* resolved UTxOs referenced by the transaction */];

let body = parse(tx_hex, &utxos).unwrap();

The parse function returns a TxBuilderBody containing all the transaction’s inputs, outputs, mints, certificates, withdrawals, and metadata.

Important: You must provide the resolved UTxOs that the transaction references as inputs. The parser needs these to reconstruct the full input context.

The Parse-Edit-Rebuild Pattern

A powerful pattern is to parse an existing transaction, modify it, and rebuild:

use whisky::*;
use whisky_pallas::tx_parser::parse;

// 1. Parse the original transaction
let utxos = vec![utxo_1, utxo_2, utxo_3];
let tx_hex = "84a700d90102...";
let mut body = parse(tx_hex, &utxos).unwrap();

// 2. Edit the body
body.outputs.pop();           // Remove last output
body.reference_inputs.pop();  // Remove a reference input

// 3. Rebuild with a new TxBuilder
let mut tx_builder = TxBuilder::new_core();
tx_builder.tx_builder_body = body;

// 4. Add new elements and rebalance
let new_tx_hex = tx_builder
    .tx_out(
        "addr_test1zp...",
        &[Asset::new_from_str("lovelace", "5000000")],
    )
    .invalid_before(100)
    .invalid_hereafter(200)
    .required_signer_hash("3f1b5974f4f09f0974be655e4ce94f8a2d087df378b79ef3916c26b2")
    .complete_sync(None)
    .unwrap()
    .tx_hex();

The resulting transaction is automatically rebalanced with proper fee calculation and a new change output.

TxParsable Trait

The parser implements the TxParsable trait, which provides:

pub trait TxParsable {
    fn parse(&mut self, tx_hex: &str, resolved_utxos: &[UTxO]) -> Result<(), WError>;
    fn get_required_inputs(&mut self, tx_hex: &str) -> Result<Vec<UtxoInput>, WError>;
    fn get_builder_body(&self) -> TxBuilderBody;
    fn get_builder_body_without_change(&self) -> TxBuilderBody;
    fn to_tester(&self) -> TxTester;
}
MethodPurpose
parseDeserialize tx hex into internal state
get_required_inputsExtract input references without full parsing
get_builder_bodyGet the full TxBuilderBody from parsed state
get_builder_body_without_changeGet the body excluding the change output
to_testerConvert to a TxTester for making assertions

Checking Required Signers

You can also inspect a transaction’s required signers:

use whisky_pallas::tx_parser::check_tx_required_signers;

let signers = check_tx_required_signers(tx_hex);

Transaction Evaluation

For parsed transactions that include Plutus scripts, you can evaluate execution units:

use uplc::tx::script_context::SlotConfig;
use whisky_common::{Network, UTxO};
use whisky_pallas::utils::evaluate_tx_scripts;

let result = evaluate_tx_scripts(
    tx_hex,
    &utxos,
    &[],  // additional chained transactions
    &Network::Mainnet,
    &SlotConfig::default(),
);

This returns execution units (memory and CPU steps) for each script in the transaction.

Dependency Injection

Whisky uses a trait-based dependency injection pattern that lets you swap out core components: the serializer backend, blockchain data fetcher, script evaluator, and transaction submitter.

TxBuilderParam

When creating a TxBuilder, you can inject dependencies via TxBuilderParam:

use whisky::*;
use whisky_pallas::WhiskyPallas;

let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(WhiskyPallas::new(None)),  // Required
    evaluator: None,    // Optional — defaults to OfflineTxEvaluator
    fetcher: None,      // Optional — for blockchain data
    submitter: None,    // Optional — for tx submission
    params: None,       // Optional — protocol parameters
});

Or use the convenience constructor with all defaults:

let mut tx_builder = TxBuilder::new_core();
// Equivalent to: WhiskyPallas serializer, offline evaluator, no fetcher/submitter

The TxBuilder Struct

pub struct TxBuilder {
    pub serializer: Box<dyn TxBuildable>,       // Serializes tx body to CBOR
    pub fetcher: Option<Box<dyn Fetcher>>,      // Fetches blockchain data
    pub evaluator: Option<Box<dyn Evaluator>>,  // Evaluates Plutus scripts
    pub submitter: Option<Box<dyn Submitter>>,  // Submits transactions
    pub protocol_params: Option<Protocol>,      // Network parameters
    // ... internal state fields
}

Why Dependency Injection?

This design enables:

  • Swappable serializers: Use Pallas (recommended) or CSL backend without changing transaction logic
  • Testing: Mock fetchers and evaluators in unit tests
  • Custom providers: Implement your own blockchain data source
  • Offline mode: Build transactions without any network dependency (the default)
  • Full pipeline: Wire up fetcher + evaluator + submitter for end-to-end transaction handling

Trait Overview

TraitPurposeBuilt-in Implementations
TxBuildableSerialize transaction body to CBORWhiskyPallas, WhiskyCSL
FetcherFetch UTxOs, protocol params, block infoMaestroProvider, BlockfrostProvider
EvaluatorEvaluate Plutus script execution unitsOfflineTxEvaluator
SubmitterSubmit signed transactionsMaestroProvider, BlockfrostProvider

Chapters

  • Serializer BackendsTxBuildable trait, WhiskyPallas vs WhiskyCSL
  • Providers — Fetcher, Evaluator, Submitter traits and implementations

Serializer Backends

The TxBuildable trait abstracts transaction serialization. Whisky ships with two implementations: WhiskyPallas (recommended) and WhiskyCSL (legacy).

TxBuildable Trait

pub trait TxBuildable: Debug + Send + Sync {
    fn set_protocol_params(&mut self, protocol_params: Protocol);
    fn set_tx_builder_body(&mut self, tx_builder: TxBuilderBody);
    fn reset_builder(&mut self);

    fn serialize_tx_body(&mut self) -> Result<String, WError>;
    fn unbalanced_serialize_tx_body(&mut self) -> Result<String, WError>;
    fn complete_signing(&mut self) -> Result<String, WError>;
    fn set_tx_hex(&mut self, tx_hex: String);
    fn tx_hex(&mut self) -> String;
    fn tx_evaluation_multiplier_percentage(&self) -> u64;

    fn add_tx_in(&mut self, input: PubKeyTxIn) -> Result<(), WError>;
}

The TxBuilder calls these methods internally — you interact with the high-level builder API, not the trait directly.

The Pallas-based serializer is the default and recommended backend. It uses TxPipe’s Pallas library for CBOR serialization.

use whisky_pallas::WhiskyPallas;

// With default protocol parameters
let serializer = WhiskyPallas::new(None);

// With custom protocol parameters
let serializer = WhiskyPallas::new(Some(protocol_params));

Use it with TxBuilder:

use whisky::*;
use whisky_pallas::WhiskyPallas;

let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(WhiskyPallas::new(None)),
    evaluator: None,
    fetcher: None,
    submitter: None,
    params: None,
});

Or simply:

let mut tx_builder = TxBuilder::new_core();
// new_core() uses WhiskyPallas by default

Why Pallas?

  • Pure Rust implementation — no C dependencies
  • Actively maintained and updated for hard forks
  • Better alignment with the Cardano Rust ecosystem

WhiskyCSL (Legacy)

The CSL-based serializer uses cardano-serialization-lib. It’s available for backward compatibility.

use whisky_csl::WhiskyCSL;

let serializer = WhiskyCSL::new(None);

Note: The main whisky crate no longer includes CSL by default. To use it, add whisky-csl directly:

[dependencies]
whisky-csl = "1.0.28-beta.1"

Implementing a Custom Serializer

You can implement TxBuildable for your own serializer:

use whisky_common::*;

#[derive(Debug)]
struct MySerializer {
    // your fields
}

impl TxBuildable for MySerializer {
    fn set_protocol_params(&mut self, protocol_params: Protocol) { /* ... */ }
    fn set_tx_builder_body(&mut self, tx_builder: TxBuilderBody) { /* ... */ }
    fn reset_builder(&mut self) { /* ... */ }
    fn serialize_tx_body(&mut self) -> Result<String, WError> { /* ... */ }
    fn unbalanced_serialize_tx_body(&mut self) -> Result<String, WError> { /* ... */ }
    fn complete_signing(&mut self) -> Result<String, WError> { /* ... */ }
    fn set_tx_hex(&mut self, tx_hex: String) { /* ... */ }
    fn tx_hex(&mut self) -> String { /* ... */ }
    fn tx_evaluation_multiplier_percentage(&self) -> u64 { 110 }
    fn add_tx_in(&mut self, input: PubKeyTxIn) -> Result<(), WError> { /* ... */ }
}

Then inject it into TxBuilder:

let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(MySerializer { /* ... */ }),
    evaluator: None,
    fetcher: None,
    submitter: None,
    params: None,
});

Providers

Providers implement the Fetcher, Evaluator, and Submitter traits to connect whisky to the Cardano blockchain.

Fetcher

The Fetcher trait provides blockchain data:

#[async_trait]
pub trait Fetcher: Send + Sync {
    async fn fetch_account_info(&self, address: &str) -> Result<AccountInfo, WError>;
    async fn fetch_address_utxos(
        &self,
        address: &str,
        asset: Option<&str>,
    ) -> Result<Vec<UTxO>, WError>;
    async fn fetch_asset_addresses(&self, asset: &str) -> Result<Vec<(String, String)>, WError>;
    async fn fetch_asset_metadata(
        &self,
        asset: &str,
    ) -> Result<Option<HashMap<String, serde_json::Value>>, WError>;
    async fn fetch_block_info(&self, hash: &str) -> Result<BlockInfo, WError>;
    async fn fetch_collection_assets(
        &self,
        policy_id: &str,
        cursor: Option<String>,
    ) -> Result<(Vec<(String, String)>, Option<String>), WError>;
    async fn fetch_protocol_parameters(&self, epoch: Option<u32>) -> Result<Protocol, WError>;
    async fn fetch_tx_info(&self, hash: &str) -> Result<TransactionInfo, WError>;
    async fn fetch_utxos(&self, hash: &str, index: Option<u32>) -> Result<Vec<UTxO>, WError>;
    async fn get(&self, url: &str) -> Result<serde_json::Value, WError>;
}

Evaluator

The Evaluator trait runs Plutus scripts off-chain to determine execution units:

#[async_trait]
pub trait Evaluator: Send {
    async fn evaluate_tx(
        &self,
        tx_hex: &str,
        inputs: &[UTxO],
        additional_txs: &[String],
        network: &Network,
        slot_config: &SlotConfig,
    ) -> Result<Vec<Action>, WError>;
}

By default, TxBuilder uses OfflineTxEvaluator, which evaluates scripts locally using TxPipe’s uplc library — no network calls needed.

Submitter

The Submitter trait submits signed transactions:

#[async_trait]
pub trait Submitter: Send + Sync {
    async fn submit_tx(&self, tx_hex: &str) -> Result<String, WError>;
}

Returns the transaction hash on success.

Built-in Providers

Maestro

use whisky_provider::MaestroProvider;

let provider = MaestroProvider::new("your_api_key", "preprod"); // or "mainnet"

let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(WhiskyPallas::new(None)),
    evaluator: None,
    fetcher: Some(Box::new(provider.clone())),
    submitter: Some(Box::new(provider)),
    params: None,
});

Blockfrost

use whisky_provider::BlockfrostProvider;

let provider = BlockfrostProvider::new("your_project_id", "preprod"); // or "mainnet"

let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(WhiskyPallas::new(None)),
    evaluator: None,
    fetcher: Some(Box::new(provider.clone())),
    submitter: Some(Box::new(provider)),
    params: None,
});

Note: Using providers requires the provider feature flag (enabled by default).

Wiring It All Together

A fully-wired TxBuilder can fetch UTxOs, evaluate scripts, and submit — all in one flow:

use whisky::*;
use whisky_pallas::WhiskyPallas;
use whisky_provider::MaestroProvider;

let provider = MaestroProvider::new("api_key", "preprod");

let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(WhiskyPallas::new(None)),
    evaluator: None,  // Uses OfflineTxEvaluator by default
    fetcher: Some(Box::new(provider.clone())),
    submitter: Some(Box::new(provider)),
    params: None,
});

// Build, evaluate, sign, and submit
tx_builder
    .tx_out(recipient, &[Asset::new_from_str("lovelace", "5000000")])
    .change_address(my_address)
    .select_utxos_from(&utxos, 5000000)
    .signing_key(skey_hex)
    .complete(None)
    .await?
    .complete_signing()?;

// Submit
let tx_hash = tx_builder
    .submit_tx(&tx_builder.tx_hex())
    .await?;

Migration: CSL to Pallas

This guide covers migrating from the legacy cardano-serialization-lib (CSL) backend to the pallas-based backend.

Why Migrate?

Both serialization libraries have similar functionality, but Pallas offers:

  • Better maintenance — actively updated for Cardano hard forks
  • Pure Rust — no C/WASM dependencies
  • Ecosystem alignment — used widely in the Cardano Rust ecosystem

The whisky main crate now defaults to Pallas. CSL has been removed from the default feature set.

Transaction Building

Before (CSL)

use whisky::*;
use whisky_csl::WhiskyCSL;

let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(WhiskyCSL::new(None)),
    evaluator: None,
    fetcher: None,
    submitter: None,
    params: None,
});

After (Pallas)

use whisky::*;
use whisky_pallas::WhiskyPallas;

// Option 1: Explicit
let mut tx_builder = TxBuilder::new(TxBuilderParam {
    serializer: Box::new(WhiskyPallas::new(None)),
    evaluator: None,
    fetcher: None,
    submitter: None,
    params: None,
});

// Option 2: Use the convenience constructor (defaults to Pallas)
let mut tx_builder = TxBuilder::new_core();

The transaction building API remains identical — only the serializer instantiation changes:

let signed_tx = tx_builder
    .tx_in(tx_hash, tx_index, amount, address)
    .change_address(my_address)
    .signing_key(skey_hex)
    .complete_sync(None)?
    .complete_signing()?;

Transaction Parsing

Before (CSL)

use whisky_csl::CSLParser;

let mut parser = CSLParser::new();
parser.parse(tx_hex, &utxos)?;
let body = parser.get_builder_body();

After (Pallas)

use whisky_pallas::tx_parser::parse;

let body = parse(tx_hex, &utxos)?;

Or using the trait-based approach:

use whisky_pallas::tx_parser::PallasParser;
use whisky_common::TxParsable;

let mut parser = PallasParser::new();
parser.parse(tx_hex, &utxos)?;
let body = parser.get_builder_body();

Transaction Evaluation

use uplc::tx::script_context::SlotConfig;
use whisky_common::{Network, UTxO};
use whisky_pallas::utils::evaluate_tx_scripts;

let result = evaluate_tx_scripts(
    tx_hex,
    &utxos,
    &[],                      // additional chained transactions
    &Network::Mainnet,
    &SlotConfig::default(),
);

Dependency Changes

If you were depending on whisky-csl directly, update your Cargo.toml:

Before

[dependencies]
whisky = "1.0.17"
# CSL was included by default

After

[dependencies]
whisky = "1.0.28-beta.1"
# Pallas is now the default — no extra dependency needed

# Only if you still need CSL:
# whisky-csl = "1.0.28-beta.1"

Summary of Changes

ComponentCSLPallas
SerializerWhiskyCSL::new(None)WhiskyPallas::new(None)
Default constructorN/ATxBuilder::new_core()
ParserCSLParser::new()parse(tx_hex, &utxos)
Cratewhisky-cslwhisky-pallas (included via whisky)
Feature flagfeatures = ["csl"]Included by default

The transaction building API (tx_in, tx_out, change_address, complete, etc.) is unchanged — migration only requires swapping the serializer backend.

Examples Server

The whisky-examples crate provides runnable transaction examples and an HTTP server that exposes them as API endpoints.

Running the Server

cargo run --package whisky-examples

The server starts on http://127.0.0.1:8080 with CORS enabled.

Available Endpoints

EndpointMethodDescription
/send_lovelacePOSTSend ADA to a recipient
/lock_fundPOSTLock funds at a script address with datum
/unlock_fundPOSTUnlock funds from a Plutus script
/mint_tokensPOSTMint tokens with a Plutus minting policy

Example: Send Lovelace

curl -X POST http://127.0.0.1:8080/send_lovelace \
  -H "Content-Type: application/json" \
  -d '{
    "recipientAddress": "addr_test1...",
    "myAddress": "addr_test1...",
    "inputs": [
      {
        "input": {
          "txHash": "abcdef...",
          "outputIndex": 0
        },
        "output": {
          "address": "addr_test1...",
          "amount": [{"unit": "lovelace", "quantity": "10000000"}]
        }
      }
    ]
  }'

Response:

{
  "txHex": "84a400..."
}

Example Functions

The transaction functions are in packages/whisky-examples/src/tx/:

FileFunctionType
send_lovelace.rssend_lovelaceSync
lock_fund.rslock_fundSync
unlock_fund.rsunlock_fundAsync
mint_tokens.rsmint_tokensAsync
delegate_stake.rsdelegate_stakeSync
complex_transaction.rscomplex_transactionAsync
collateral_return.rscollateral_returnAsync

These examples serve as both documentation and testable reference implementations. See the Transaction Builder guide for detailed explanations of each pattern.

API Documentation

Full API reference documentation is auto-generated from Rust doc comments using cargo doc.

Browse the API Docs

Open API Reference

Key Entry Points

CrateDescriptionLink
whiskyMain crate — re-exports all public APIsdocs
whisky-commonShared types, traits, and utilitiesdocs
whisky-pallasPallas-based serializerdocs
whisky-cslLegacy CSL-based serializerdocs
whisky-providerBlockfrost and Maestro integrationsdocs
whisky-walletWallet signing and key managementdocs
whisky-macrosProcedural macros for Plutus datadocs

Building Docs Locally

# Generate API reference
npm run rust:doc

# Serve locally
npx http-server ./docs

The docs are also published automatically to GitHub Pages on every push to the deployment branch.