Transaction Builder Basics
Build and customize Cardano transactions with the MeshTxBuilder low-level API.
Overview
MeshTxBuilder is a powerful, low-level API for constructing Cardano transactions. It provides fine-grained control over every aspect of transaction building—from simple ADA transfers to complex multi-signature and smart contract interactions.
When to use MeshTxBuilder:
- Building custom transaction logic beyond standard transfers
- Creating multi-signature transactions
- Interacting with Plutus smart contracts
- Optimizing transaction fees and execution units
Quick Start
import { MeshTxBuilder, BlockfrostProvider } from "@meshsdk/core";
// Initialize provider and builder
const provider = new BlockfrostProvider("<YOUR_API_KEY>");
const txBuilder = new MeshTxBuilder({
fetcher: provider,
verbose: true,
});
// Build a simple transfer
const unsignedTx = await txBuilder
.txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
// Sign and submit
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);Configuration
Constructor Options
const txBuilder = new MeshTxBuilder({
fetcher: provider, // Required for auto-fetching UTXO data
submitter: provider, // Optional: for direct submission via builder
evaluator: provider, // Optional: for script execution unit optimization
serializer: serializer, // Optional: custom serializer (default: CSLSerializer)
params: protocolParams, // Optional: custom protocol parameters
isHydra: false, // Optional: use Hydra protocol parameters
verbose: true, // Optional: enable detailed logging
});| Option | Type | Required | Description |
|---|---|---|---|
fetcher | IFetcher | Recommended | Provider for fetching blockchain data |
submitter | ISubmitter | No | Provider for submitting transactions |
evaluator | IEvaluator | No | Provider for evaluating script execution |
serializer | IMeshTxSerializer | No | Custom transaction serializer |
params | Partial<Protocol> | No | Custom protocol parameters |
isHydra | boolean | No | Enable Hydra-specific parameters |
verbose | boolean | No | Enable detailed console logging |
Sending ADA
Basic Transfer
Send ADA to a recipient address:
import { MeshTxBuilder, BlockfrostProvider } from "@meshsdk/core";
const provider = new BlockfrostProvider("<YOUR_API_KEY>");
const txBuilder = new MeshTxBuilder({
fetcher: provider,
verbose: true,
});
// Get wallet data
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
// Build transaction
const unsignedTx = await txBuilder
.txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
// Sign and submit
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);Send Multiple Outputs
Send to multiple recipients in a single transaction:
const unsignedTx = await txBuilder
.txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
.txOut("addr_test1qr...", [{ unit: "lovelace", quantity: "3000000" }])
.txOut("addr_test1qs...", [{ unit: "lovelace", quantity: "2000000" }])
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();Send Native Assets
Include native assets (tokens/NFTs) in the output:
const unsignedTx = await txBuilder
.txOut("addr_test1qz...", [
{ unit: "lovelace", quantity: "2000000" },
{ unit: "d9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e", quantity: "1" }
])
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();Input Selection
Automatic Selection
Let Mesh automatically select UTXOs to cover the transaction:
const unsignedTx = await txBuilder
.txOut(recipientAddress, [{ unit: "lovelace", quantity: "5000000" }])
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();Selection Strategies
Configure how UTXOs are selected:
txBuilder.selectUtxosFrom(
utxos, // Available UTXOs
"largestFirst", // Selection strategy
"5000000", // Minimum threshold (lovelace)
true // Include transaction fees in calculation
);| Strategy | Description |
|---|---|
experimental | Mesh's optimized selection algorithm |
keepRelevant | Prefer UTXOs with relevant assets |
largestFirst | Select largest UTXOs first |
largestFirstMultiAsset | Optimized for transactions with multiple assets |
Manual Input Selection
Explicitly specify which UTXOs to spend:
const unsignedTx = await txBuilder
.txIn(
"abc123...", // Transaction hash
0, // Output index
[{ unit: "lovelace", quantity: "10000000" }], // Amount
"addr_test1qz..." // Address
)
.txOut("addr_test1qr...", [{ unit: "lovelace", quantity: "5000000" }])
.changeAddress(changeAddress)
.complete();Multi-Signature Transactions
Create transactions requiring multiple signatures:
import { MeshTxBuilder, ForgeScript, resolveScriptHash, stringToHex } from "@meshsdk/core";
import { MeshCardanoHeadlessWallet, AddressType } from "@meshsdk/wallet";
// Create minting wallet
const mintingWallet = await MeshCardanoHeadlessWallet.fromMnemonic({
networkId: 0,
walletAddressType: AddressType.Base,
fetcher: provider,
submitter: provider,
mnemonic: ["your", "mnemonic", "words", "..."],
});
// Create forging script
const forgingScript = ForgeScript.withOneSignature(
await mintingWallet.getChangeAddressBech32()
);
const policyId = resolveScriptHash(forgingScript);
const assetName = "MeshToken";
// Get user wallet data
const address = (await wallet.getUsedAddresses())[0];
const utxos = await wallet.getUtxos();
// Build multi-sig transaction
const txBuilder = new MeshTxBuilder({
fetcher: provider,
verbose: true,
});
const unsignedTx = await txBuilder
.mint("1", policyId, stringToHex(assetName))
.mintingScript(forgingScript)
.metadataValue(721, {
[policyId]: {
[assetName]: { name: "Mesh Token", image: "ipfs://..." }
}
})
.changeAddress(address)
.selectUtxosFrom(utxos)
.complete();
// Sign with both wallets (partial signing)
const signedTx1 = await wallet.signTx(unsignedTx, true, false);
const signedTx2 = await mintingWallet.signTx(signedTx1, true, false);
// Submit final transaction
const txHash = await wallet.submitTx(signedTx2);Native Script Multi-Sig
Spend from a native script address requiring multiple signatures:
import { deserializeAddress, serializeNativeScript, NativeScript } from "@meshsdk/core";
// Get key hashes from addresses
const { pubKeyHash: keyHash1 } = deserializeAddress(walletAddress1);
const { pubKeyHash: keyHash2 } = deserializeAddress(walletAddress2);
// Create native script requiring both signatures
const nativeScript: NativeScript = {
type: "all",
scripts: [
{ type: "sig", keyHash: keyHash1 },
{ type: "sig", keyHash: keyHash2 },
],
};
// Serialize script to get address
const { address: scriptAddress, scriptCbor } = serializeNativeScript(nativeScript);
// Fetch UTXOs from script address
const scriptUtxos = await provider.fetchAddressUTxOs(scriptAddress);
const utxo = scriptUtxos[0];
// Build transaction spending from script
const unsignedTx = await txBuilder
.txIn(
utxo.input.txHash,
utxo.input.outputIndex,
utxo.output.amount,
utxo.output.address
)
.txInScript(scriptCbor)
.txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "2000000" }])
.changeAddress(scriptAddress)
.selectUtxosFrom(scriptUtxos)
.complete();
// Both wallets must sign
const signedTx1 = await wallet1.signTx(unsignedTx, true, false);
const signedTx2 = await wallet2.signTx(signedTx1, true, false);
const txHash = await wallet1.submitTx(signedTx2);Transaction Metadata
Add Messages
Attach messages to transactions using label 674 (CIP-20):
const unsignedTx = await txBuilder
.txOut(recipientAddress, [{ unit: "lovelace", quantity: "5000000" }])
.metadataValue(674, {
msg: [
"Invoice-No: 1234567890",
"Customer-No: 555-1234"
]
})
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();Each message string must be at most 64 bytes when UTF-8 encoded.
Custom Metadata
Add any JSON-compatible metadata:
const unsignedTx = await txBuilder
.metadataValue(customLabel, {
key: "value",
nested: { data: [1, 2, 3] }
})
.txOut(address, amount)
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();Transaction Validity
Set Expiration (TTL)
Make a transaction invalid after a specific slot:
import { resolveSlotNo } from "@meshsdk/core";
// Transaction expires in 5 minutes
const expirationTime = new Date(Date.now() + 5 * 60 * 1000);
const slot = resolveSlotNo("mainnet", expirationTime.getTime());
const unsignedTx = await txBuilder
.txOut(address, amount)
.invalidHereafter(Number(slot))
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();Set Start Time
Make a transaction invalid before a specific slot:
const startTime = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes from now
const startSlot = resolveSlotNo("mainnet", startTime.getTime());
const unsignedTx = await txBuilder
.txOut(address, amount)
.invalidBefore(Number(startSlot))
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();Required Signers
Specify required signers for smart contract transactions:
import { deserializeAddress } from "@meshsdk/core";
const { pubKeyHash } = deserializeAddress(walletAddress);
const unsignedTx = await txBuilder
// ... transaction details
.requiredSignerHash(pubKeyHash)
.complete();Network Configuration
Set Network
Configure the network for proper cost model calculation:
txBuilder.setNetwork("mainnet"); // or "testnet", "preview", "preprod"Custom Protocol Parameters
Use specific protocol parameters:
const params = await provider.fetchProtocolParameters();
const txBuilder = new MeshTxBuilder({
fetcher: provider,
params: params,
});Manual Fee Control
Override automatic fee calculation:
const unsignedTx = await txBuilder
.txOut(address, amount)
.changeAddress(changeAddress)
.setFee("200000") // Set exact fee in lovelace
.complete();Build with JSON
Alternative method using a JSON object:
import { MeshTxBuilderBody } from "@meshsdk/core";
const meshTxBody: Partial<MeshTxBuilderBody> = {
outputs: [
{
address: recipientAddress,
amount: [{ unit: "lovelace", quantity: "5000000" }],
},
],
changeAddress: changeAddress,
extraInputs: utxos,
selectionConfig: {
threshold: "5000000",
strategy: "largestFirst",
includeTxFees: true,
},
};
const unsignedTx = await txBuilder.complete(meshTxBody);Complete Example
import {
MeshTxBuilder,
BlockfrostProvider,
resolveSlotNo
} from "@meshsdk/core";
// Setup
const provider = new BlockfrostProvider("<YOUR_API_KEY>");
const txBuilder = new MeshTxBuilder({
fetcher: provider,
verbose: true,
});
// Get wallet data
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
// Set expiration (5 minutes)
const expiration = new Date(Date.now() + 5 * 60 * 1000);
const expirationSlot = resolveSlotNo("preprod", expiration.getTime());
// Build transaction
const unsignedTx = await txBuilder
// Send 5 ADA to recipient
.txOut("addr_test1qz...", [{ unit: "lovelace", quantity: "5000000" }])
// Send 3 ADA + token to another recipient
.txOut("addr_test1qr...", [
{ unit: "lovelace", quantity: "3000000" },
{ unit: "policyId...", quantity: "1" }
])
// Add transaction message
.metadataValue(674, { msg: ["Payment for services"] })
// Set expiration
.invalidHereafter(Number(expirationSlot))
// Configure change and UTXO selection
.changeAddress(changeAddress)
.selectUtxosFrom(utxos, "largestFirst", "5000000", true)
// Build the transaction
.complete();
// Sign and submit
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
console.log(`Transaction submitted: ${txHash}`);Troubleshooting
"Insufficient funds" error
- Ensure UTXOs have enough ADA to cover outputs + fees + min UTxO values
- Check that
selectUtxosFromis provided with current UTXOs - For native asset outputs, ensure minimum ADA (typically ~1.5 ADA) is included
"Missing input" error
- The
fetcherprovider is required for automatic UTXO data fetching - If using manual
txIn, provide all four parameters (txHash, index, amount, address)
Transaction too large
- Split into multiple transactions
- Reduce the number of outputs
- Use UTXO consolidation first
Invalid slot number
- Ensure you're using the correct network in
resolveSlotNo - Mainnet and testnets have different slot calculations
Multi-sig transaction fails
- Ensure
partialSign: trueis set for all signers except the last - Verify all required signers have signed
- Check that signing order doesn't matter (unless script requires specific order)
Related
- Minting Tokens — Mint native assets
- Smart Contracts — Interact with Plutus scripts
- Staking — Delegation and rewards
- Governance — Voting and proposals
- Blockfrost Provider — Provider setup