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:
| Crate | Description |
|---|---|
| whisky | The main crate — re-exports everything you need for DApp development |
| whisky-common | Shared types, interfaces, and utilities used across all crates |
| whisky-pallas | Transaction serializer built on TxPipe’s Pallas (recommended) |
| whisky-csl | Legacy serializer built on cardano-serialization-lib |
| whisky-provider | Provider integrations for Blockfrost and Maestro |
| whisky-wallet | Wallet signing and key management utilities |
| whisky-macros | Procedural macros for Plutus data encoding |
| whisky-js | WASM 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
uplcfor execution unit estimation - Swap serializer backends between Pallas and CSL via dependency injection
Guide Overview
This guide walks you through:
- Installation — Adding whisky to your project
- Quick Start — Building your first transaction
- Transaction Builder — Simple sends, Plutus scripts, minting, staking
- Transaction Parser — Parsing and editing existing transactions
- Dependency Injection — Pluggable serializers and providers
- 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"] }
| Feature | Includes | Use Case |
|---|---|---|
full (default) | wallet + provider | Full DApp backend |
wallet | Signing, key encryption | Transaction signing only |
provider | Blockfrost, Maestro | Blockchain 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:
TxBuilder::new_core()— Creates a new transaction builder with the default Pallas serializer.tx_out(address, assets)— Adds an output sending 1 ADA (1,000,000 lovelace) to the recipient.change_address(address)— Sets where leftover funds go after paying fees.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.complete_sync(None)— Balances the transaction, calculates fees, and serializes to CBOR.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 — Explore all transaction building patterns
- Transaction Parser — Parse and edit existing transactions
- Dependency Injection — Customize serializers and providers
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
| Method | Sync/Async | Script Evaluation | Use Case |
|---|---|---|---|
complete_sync(None) | Sync | No | Simple transactions |
complete(None).await | Async | Yes (offline) | Plutus script transactions |
Common Methods
| Method | Purpose |
|---|---|
.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 — Send lovelace, lock funds, delegate stake
- Plutus Script Transactions — Unlock funds from scripts, handle datums and redeemers
- Minting — Mint and burn native tokens with Plutus scripts
- Staking & Governance — Stake registration, delegation, withdrawals, governance
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_fromautomatically 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_valueattaches 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_valueinstead 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:
- Declare the script version:
.spending_plutus_script_v3()(or_v2(),_v1()) - Add the script input:
.tx_in(hash, index, amount, address) - Provide the datum:
.tx_in_inline_datum_present()or.tx_in_datum_value(&datum) - Provide the redeemer:
.tx_in_redeemer_value(&redeemer) - 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:
- Declare the script version:
.mint_plutus_script_v3()(or_v2(),_v1()) - Specify the mint:
.mint(quantity, policy_id, asset_name) - Provide the script:
.minting_script(&script_cbor)or use a reference script - 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;
}
| Method | Purpose |
|---|---|
parse | Deserialize tx hex into internal state |
get_required_inputs | Extract input references without full parsing |
get_builder_body | Get the full TxBuilderBody from parsed state |
get_builder_body_without_change | Get the body excluding the change output |
to_tester | Convert 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
| Trait | Purpose | Built-in Implementations |
|---|---|---|
TxBuildable | Serialize transaction body to CBOR | WhiskyPallas, WhiskyCSL |
Fetcher | Fetch UTxOs, protocol params, block info | MaestroProvider, BlockfrostProvider |
Evaluator | Evaluate Plutus script execution units | OfflineTxEvaluator |
Submitter | Submit signed transactions | MaestroProvider, BlockfrostProvider |
Chapters
- Serializer Backends —
TxBuildabletrait, 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.
WhiskyPallas (Recommended)
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
whiskycrate no longer includes CSL by default. To use it, addwhisky-csldirectly:[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
providerfeature 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
| Component | CSL | Pallas |
|---|---|---|
| Serializer | WhiskyCSL::new(None) | WhiskyPallas::new(None) |
| Default constructor | N/A | TxBuilder::new_core() |
| Parser | CSLParser::new() | parse(tx_hex, &utxos) |
| Crate | whisky-csl | whisky-pallas (included via whisky) |
| Feature flag | features = ["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
| Endpoint | Method | Description |
|---|---|---|
/send_lovelace | POST | Send ADA to a recipient |
/lock_fund | POST | Lock funds at a script address with datum |
/unlock_fund | POST | Unlock funds from a Plutus script |
/mint_tokens | POST | Mint 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/:
| File | Function | Type |
|---|---|---|
send_lovelace.rs | send_lovelace | Sync |
lock_fund.rs | lock_fund | Sync |
unlock_fund.rs | unlock_fund | Async |
mint_tokens.rs | mint_tokens | Async |
delegate_stake.rs | delegate_stake | Sync |
complex_transaction.rs | complex_transaction | Async |
collateral_return.rs | collateral_return | Async |
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
Key Entry Points
| Crate | Description | Link |
|---|---|---|
| whisky | Main crate — re-exports all public APIs | docs |
| whisky-common | Shared types, traits, and utilities | docs |
| whisky-pallas | Pallas-based serializer | docs |
| whisky-csl | Legacy CSL-based serializer | docs |
| whisky-provider | Blockfrost and Maestro integrations | docs |
| whisky-wallet | Wallet signing and key management | docs |
| whisky-macros | Procedural macros for Plutus data | docs |
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.