Skip to Content
Scalus Club is now open! Join us to get an early access to new features 🎉

How to Build Your First Cardano Transaction

This guide walks you through building a simple Cardano transaction using Scalus TxBuilder. You’ll learn the fundamental workflow: setting up the environment, building, and signing a transaction.

Set Up the CardanoInfo

CardanoInfo encapsulates everything TxBuilder needs to construct valid transactions:

  • Protocol parameters for fee calculation, execution limits, and deposits
  • Network identifier (Mainnet or Testnet) for address validation
  • Slot configuration for time-based validity constraints

Pre-built Configurations

Scalus provides ready-to-use configurations with embedded protocol parameters:

import scalus.cardano.ledger.* import scalus.cardano.txbuilder.* // Mainnet (production) val cardanoInfo = CardanoInfo.mainnet // Preprod testnet val preprodInfo = CardanoInfo.preprod // Preview testnet val previewInfo = CardanoInfo.preview

Custom Configuration (Yaci DevKit Example)

For local development with Yaci DevKit or other custom environments, load protocol parameters from JSON and configure the slot timing:

// Load params from Blockfrost API or cardano-cli val params = ProtocolParams.fromBlockfrostJson(blockfrostJsonString) // Or: ProtocolParams.fromCardanoCliJson(cliJsonString) // Yaci DevKit uses slot length of 1 second and start time of 0 val yaciSlotConfig = SlotConfig( zeroTime = 0L, zeroSlot = 0L, slotLength = 1000 ) val yaciDevKit = CardanoInfo( protocolParams = params, network = Network.Testnet, slotConfig = yaciSlotConfig )

The pre-built configurations use embedded protocol parameters that are updated periodically. For production use, consider querying current parameters from a node or API.

Working with Addresses

Scalus provides convenient string interpolators for parsing Cardano bech32 addresses:

import scalus.cardano.address.Address.{addr, stake} import scalus.cardano.address.StakeAddress // Parse any Cardano address (Shelley, Stake, or Byron) val recipient = addr"addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x" val testnet = addr"addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs3xyj0d" // Parse stake addresses specifically (returns StakeAddress type) val stakeAddr: StakeAddress = stake"stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"

These interpolators validate the address format at runtime and throw an exception for invalid addresses.

The addr interpolator returns Address (the base trait), while stake returns StakeAddress specifically. Use stake when you need the more specific type.

Build the Transaction

Use TxBuilder’s fluent API to specify inputs, outputs, and change handling:

import scalus.cardano.address.Address.addr import scalus.cardano.ledger.Value // Your UTxOs and addresses val myUtxo: Utxo = // ... UTxO to spend val recipientAddress = addr"addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer..." val changeAddress = addr"addr1qy..." val builder = TxBuilder(cardanoInfo) .spend(myUtxo) // Add input .payTo(recipientAddress, Value.ada(10)) // Send 10 ADA .build(changeTo = changeAddress) // Finalize transaction with change

The build() method:

  • Calculates the transaction fee based on size
  • Creates a change output with the remaining value
  • Validates that the transaction is balanced
  • Returns a new builder with the finalized transaction

Value.ada(10) creates a Value with 10 million lovelace (10 ADA) and no extra tokens.

Sign the Transaction

Add signatures to authorize spending the inputs. You can create a TransactionSigner in two ways:

From a mnemonic phrase and derivation path:

import scalus.crypto.Bip32PrivateKey val mnemonic = "test " * 24 + "sauce" val derivationPath = "m/1852'/1815'/0'/0/0" // Standard Cardano derivation path for the first ADA wallet val signer = BloxbeanAccount(network, mnemonic, derivationPath).signerForUtxos

From a specific keypair:

val keyPair: KeyPair = // ... your keypair val signer = TransactionSigner(Set(keyPair))

Then sign the transaction:

val signedBuilder = builder.sign(signer) val transaction = signedBuilder.transaction

The sign() method adds the signature to the transaction’s witness set. You can chain multiple sign() calls if multiple signatures are needed.

Submit the Transaction

Finally, submit the signed transaction to the Cardano network:

import scalus.cardano.node.Provider import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* val provider: Provider = // ... blockfrost, ogmios, etc. provider.submit(transaction).await(30.seconds) match { case Right(txHash) => println(s"Transaction submitted: ${txHash.toHex}") case Left(error) => println(s"Submission failed: $error") }

Complete Example

Here’s the full workflow in one place:

import scalus.cardano.ledger.* import scalus.cardano.txbuilder.* import scalus.cardano.address.Address import scalus.cardano.node.Provider import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* // Setup val cardanoInfo = CardanoInfo.mainnet val myUtxo: Utxo = // ... val recipientAddress: Address = // ... val changeAddress: Address = // ... val signer: TransactionSigner = // ... val provider: Provider = // ... // Build, sign, and submit val transaction = TxBuilder(cardanoInfo) .spend(myUtxo) .payTo(recipientAddress, Value.ada(10)) .build(changeTo = changeAddress) .sign(signer) .transaction provider.submit(transaction).await(30.seconds)

Using Automatic Completion

For simpler cases, use complete() to let TxBuilder handle input selection automatically:

val transaction = TxBuilder(cardanoInfo) .payTo(recipientAddress, Value.ada(10)) .complete(provider, sponsorAddress) // Automatic input selection and balancing .sign(signer) .transaction

The complete() method:

  • Queries the provider for UTxOs at the sponsor address
  • Selects inputs to cover the payment and fees
  • Adds collateral if the transaction includes script execution
  • Creates change outputs to return excess value
  • Balances the transaction (no need to call .build() afterward)

Cross-Platform Async Completion

The complete() method returns a Future[TxBuilder] and works on both JVM and JavaScript platforms:

import scala.concurrent.ExecutionContext.Implicits.global val futureTransaction: Future[Transaction] = TxBuilder(cardanoInfo) .payTo(recipientAddress, Value.ada(10)) .complete(asyncProvider, sponsorAddress) .map(_.sign(signer).transaction)

This is the recommended approach for cross-platform code and async workflows.

Completion with Pre-fetched UTXOs

If you already have UTXOs available in memory (e.g., from a previous query or a custom source), you can use the synchronous complete() overload that accepts UTXOs directly:

// Pre-fetched or cached UTXOs from any source val availableUtxos: Utxos = Map( TransactionInput(txHash, 0) -> TransactionOutput(sponsorAddress, Value.ada(100)), TransactionInput(txHash, 1) -> TransactionOutput(sponsorAddress, Value.ada(50)) ) // Synchronous - no Future, no async provider query val transaction = TxBuilder(cardanoInfo) .payTo(recipientAddress, Value.ada(10)) .complete(availableUtxos, sponsorAddress) // Immediate completion .sign(signer) .transaction

This variant is useful when:

  • You’ve already queried UTXOs and want to avoid redundant network calls
  • You’re building transactions in a batch with shared UTXO state
  • You’re working with a custom UTXO source (e.g., in-memory ledger state)

The behavior is identical to the provider-based complete() - it selects inputs, adds collateral if needed, and balances the transaction.

Next Steps

Last updated on