Mesh LogoMesh

Migrate from Lucid to Mesh

Step-by-step migration guide from Lucid to Mesh SDK with API mapping, code examples, and common patterns.

Overview

In this guide, you migrate an existing Cardano application from Lucid to Mesh SDK. Mesh separates transaction building, wallet interaction, and blockchain queries into distinct components, giving you more flexibility in how you structure your application.

What you will learn

  • How Lucid concepts map to Mesh equivalents
  • Key differences in API design and architecture
  • Step-by-step migration patterns for common operations
  • How to update tests and smart contract code

Prerequisites

  • An existing application using Lucid
  • Node.js 18+ installed
  • Basic TypeScript knowledge

Time to complete

2 hours (varies based on application complexity)

Quick Start

Install Mesh packages alongside your existing Lucid installation:

npm install @meshsdk/core @meshsdk/react

You can migrate incrementally, using both libraries during the transition.

Step-by-Step Guide

Step 1: Understand the architectural differences

Lucid combines provider and wallet in a single configured instance:

// Lucid pattern
const lucid = await Lucid.new(
  new Blockfrost("https://cardano-preprod.blockfrost.io/api", "key"),
  "Preprod"
);
await lucid.selectWallet(walletApi);
const tx = await lucid.newTx().payToAddress(...).complete();

Mesh separates these concerns:

// Mesh pattern
import { BlockfrostProvider, MeshTxBuilder } from "@meshsdk/core";
import { MeshCardanoBrowserWallet } from "@meshsdk/wallet";

const provider = new BlockfrostProvider("key");
const wallet = await MeshCardanoBrowserWallet.enable("eternl");
const txBuilder = new MeshTxBuilder({ fetcher: provider });

Why this matters: You can swap providers without changing wallet code, and test transactions without blockchain connectivity.

Step 2: Install Mesh packages

Add Mesh to your project:

npm install @meshsdk/core @meshsdk/react

For React applications, @meshsdk/react provides wallet components and hooks.

What to expect: Both Lucid and Mesh can coexist during migration.

Step 3: Replace provider setup

Lucid:

import { Blockfrost, Lucid } from "lucid-cardano";

const lucid = await Lucid.new(
  new Blockfrost("https://cardano-preprod.blockfrost.io/api", "project_key"),
  "Preprod"
);

Mesh:

import { BlockfrostProvider } from "@meshsdk/core";

// Preprod
const provider = new BlockfrostProvider("project_key_preprod");

// Mainnet
const providerMainnet = new BlockfrostProvider("project_key_mainnet");

// Preview
const providerPreview = new BlockfrostProvider("project_key_preview");

Mesh auto-detects the network from your API key prefix. Alternative providers:

import { KoiosProvider, MaestroProvider, OgmiosProvider } from "@meshsdk/core";

const koios = new KoiosProvider("preprod");
const maestro = new MaestroProvider({ apiKey: "key", network: "Preprod" });
const ogmios = new OgmiosProvider("ws://localhost:1337");

What to expect: Provider instances ready to pass to transaction builders.

Step 4: Update wallet connection

Lucid:

const api = await window.cardano.eternl.enable();
lucid.selectWallet(api);

const address = await lucid.wallet.address();
const utxos = await lucid.wallet.getUtxos();

Mesh:

import { MeshCardanoBrowserWallet } from "@meshsdk/wallet";

const wallet = await MeshCardanoBrowserWallet.enable("eternl");

const address = await wallet.getChangeAddressBech32();
const usedAddresses = await wallet.getUsedAddressesBech32();
const utxos = await wallet.getUtxosMesh();

React hooks (Mesh):

import { CardanoWallet, useWallet } from "@meshsdk/react";

function WalletConnect() {
  const { wallet, connected, connect } = useWallet();

  if (!connected) {
    return <CardanoWallet />;
  }

  return <div>Connected</div>;
}

What to expect: Wallet instances with CIP-30 standard methods.

Step 5: Migrate transaction building

Lucid payment transaction:

const tx = await lucid
  .newTx()
  .payToAddress("addr_test1...", { lovelace: 5000000n })
  .complete();

const signedTx = await tx.sign().complete();
const txHash = await signedTx.submit();

Mesh payment transaction:

import { MeshTxBuilder } from "@meshsdk/core";

const txBuilder = new MeshTxBuilder({ fetcher: provider });

const utxos = await wallet.getUtxosMesh();
const changeAddress = await wallet.getChangeAddressBech32();

const unsignedTx = await txBuilder
  .txOut("addr_test1...", [{ unit: "lovelace", quantity: "5000000" }])
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

const signedTx = await wallet.signTx(unsignedTx, false);
const txHash = await wallet.submitTx(signedTx);

Key differences:

  • Mesh uses explicit input/output methods (txIn, txOut)
  • Amounts are strings, not BigInt
  • Signing returns a new string, does not mutate
  • Change address must be set explicitly

What to expect: Transaction hashes on successful submission.

Step 6: Migrate native token minting

Lucid:

const { paymentCredential } = lucid.utils.getAddressDetails(address);
const mintingPolicy = lucid.utils.nativeScriptFromJson({
  type: "sig",
  keyHash: paymentCredential.hash,
});
const policyId = lucid.utils.mintingPolicyToId(mintingPolicy);

const tx = await lucid
  .newTx()
  .mintAssets({ [policyId + tokenName]: 1n })
  .validTo(Date.now() + 100000)
  .attachMintingPolicy(mintingPolicy)
  .complete();

Mesh:

import {
  MeshTxBuilder,
  ForgeScript,
  resolveScriptHash,
  stringToHex,
} from "@meshsdk/core";

const address = await wallet.getChangeAddressBech32();
const forgingScript = ForgeScript.withOneSignature(address);
const policyId = resolveScriptHash(forgingScript);
const tokenNameHex = stringToHex("MyToken");

const txBuilder = new MeshTxBuilder({ fetcher: provider });
const utxos = await wallet.getUtxosMesh();

const unsignedTx = await txBuilder
  .mint("1", policyId, tokenNameHex)
  .mintingScript(forgingScript)
  .txOut(address, [{ unit: policyId + tokenNameHex, quantity: "1" }])
  .changeAddress(address)
  .selectUtxosFrom(utxos)
  .complete();

const signedTx = await wallet.signTx(unsignedTx, false);
const txHash = await wallet.submitTx(signedTx);

What to expect: NFTs minted with Mesh's ForgeScript utilities.

Step 7: Migrate smart contract interactions

Lucid locking funds:

const tx = await lucid
  .newTx()
  .payToContract(scriptAddress, { inline: datum }, { lovelace: 5000000n })
  .complete();

Mesh locking funds:

const txBuilder = new MeshTxBuilder({ fetcher: provider });

const unsignedTx = await txBuilder
  .txOut(scriptAddress, [{ unit: "lovelace", quantity: "5000000" }])
  .txOutInlineDatumValue(datum)
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

Lucid unlocking funds:

const tx = await lucid
  .newTx()
  .collectFrom([utxo], redeemer)
  .attachSpendingValidator(validator)
  .complete();

Mesh unlocking funds:

const txBuilder = new MeshTxBuilder({ fetcher: provider });

const unsignedTx = await txBuilder
  .spendingPlutusScriptV2()
  .txIn(utxo.input.txHash, utxo.input.outputIndex)
  .txInInlineDatumPresent()
  .txInRedeemerValue(redeemer)
  .txInScript(scriptCbor)
  .txOut(recipientAddress, utxo.output.amount)
  .requiredSignerHash(pubKeyHash)
  .txInCollateral(
    collateral.input.txHash,
    collateral.input.outputIndex,
    collateral.output.amount,
    collateral.output.address
  )
  .changeAddress(changeAddress)
  .selectUtxosFrom(utxos)
  .complete();

const signedTx = await wallet.signTx(unsignedTx, true); // true for partial signing

What to expect: Smart contract transactions with explicit script and datum handling.

Step 8: Migrate data serialization

Lucid:

import { Data } from "lucid-cardano";

const datum = Data.to({ owner: pubKeyHash });
const parsed = Data.from(onchainDatum);

Mesh:

import { serializePlutusData, deserializePlutusData } from "@meshsdk/core";
import type { Data } from "@meshsdk/core";

// For simple datums, use the Data type directly
const datum: Data = {
  alternative: 0,
  fields: [pubKeyHash],
};

// For complex serialization
const cbor = serializePlutusData(datum);
const parsed = deserializePlutusData(onchainDatum);

What to expect: CBOR-encoded datum and redeemer values.

Step 9: Migrate tests

Lucid Emulator:

import { Emulator, Lucid } from "lucid-cardano";

const emulator = new Emulator([{ address, assets: { lovelace: 100_000_000n } }]);
const lucid = await Lucid.new(emulator);

Mesh testing:

import { OfflineFetcher, MeshTxBuilder } from "@meshsdk/core";

// Mock blockchain state
const offlineFetcher = new OfflineFetcher();
offlineFetcher.addUTxOs([
  {
    input: { txHash: "abc...", outputIndex: 0 },
    output: { address, amount: [{ unit: "lovelace", quantity: "100000000" }] },
  },
]);

const txBuilder = new MeshTxBuilder({ fetcher: offlineFetcher });

For local development, use Yaci:

import { YaciProvider } from "@meshsdk/core";

const yaci = new YaciProvider("http://localhost:8080");

What to expect: Test environments without live blockchain connectivity.

API Mapping Reference

LucidMeshNotes
Lucid.new()new BlockfrostProvider() + new MeshTxBuilder()Separate provider and builder
lucid.selectWallet()MeshCardanoBrowserWallet.enable()Returns wallet instance
lucid.newTx()new MeshTxBuilder()Builder pattern
tx.payToAddress()txBuilder.txOut()Output construction
tx.payToContract()txBuilder.txOut() + txOutInlineDatumValue()With datum
tx.mintAssets()txBuilder.mint() + mintingScript()Minting
tx.collectFrom()txBuilder.txIn() + script methodsScript spending
tx.attachSpendingValidator()txBuilder.txInScript()Attach validator
tx.complete()txBuilder.complete()Build transaction
tx.sign().complete()wallet.signTx()Returns signed CBOR
signedTx.submit()wallet.submitTx()Submit to network
Data.to()serializePlutusData()Serialize datum
Data.from()deserializePlutusData()Parse datum
EmulatorOfflineFetcherTesting

Complete Example

Here is a complete migration example for a payment transaction:

Before (Lucid):

import { Blockfrost, Lucid } from "lucid-cardano";

async function sendPayment() {
  const lucid = await Lucid.new(
    new Blockfrost("https://cardano-preprod.blockfrost.io/api", "key"),
    "Preprod"
  );

  const api = await window.cardano.eternl.enable();
  lucid.selectWallet(api);

  const tx = await lucid
    .newTx()
    .payToAddress("addr_test1...", { lovelace: 5000000n })
    .complete();

  const signedTx = await tx.sign().complete();
  const txHash = await signedTx.submit();

  console.log("TX:", txHash);
}

After (Mesh):

import { BlockfrostProvider, MeshTxBuilder } from "@meshsdk/core";
import { MeshCardanoBrowserWallet } from "@meshsdk/wallet";

async function sendPayment() {
  const provider = new BlockfrostProvider("key_preprod");
  const wallet = await MeshCardanoBrowserWallet.enable("eternl");

  const utxos = await wallet.getUtxosMesh();
  const changeAddress = await wallet.getChangeAddressBech32();

  const txBuilder = new MeshTxBuilder({ fetcher: provider, submitter: provider });

  const unsignedTx = await txBuilder
    .txOut("addr_test1...", [{ unit: "lovelace", quantity: "5000000" }])
    .changeAddress(changeAddress)
    .selectUtxosFrom(utxos)
    .complete();

  const signedTx = await wallet.signTx(unsignedTx, false);
  const txHash = await wallet.submitTx(signedTx);

  console.log("TX:", txHash);
}

Troubleshooting

Type errors with amounts

Cause: Lucid uses BigInt for amounts, Mesh uses strings.

Solution: Convert amounts when migrating:

// Lucid
{ lovelace: 5000000n }

// Mesh
[{ unit: "lovelace", quantity: "5000000" }]

Transaction signing fails

Cause: Missing the partial signing flag for Plutus scripts.

Solution: Pass true as the second argument to signTx():

const signedTx = await wallet.signTx(unsignedTx, true);

Missing change address error

Cause: Mesh requires explicit change address.

Solution: Always set the change address:

const changeAddress = await wallet.getChangeAddress();
txBuilder.changeAddress(changeAddress);

Script validation fails

Cause: Datum or redeemer structure does not match contract expectations.

Solution: Verify your Data structure matches the Aiken/Plutus types:

const datum: Data = {
  alternative: 0, // Constructor index
  fields: [value1, value2], // Field values
};

UTxO not found errors

Cause: UTxOs fetched at different times may be spent.

Solution: Fetch UTxOs immediately before building the transaction:

const utxos = await wallet.getUtxosMesh();
// Build transaction immediately after
const unsignedTx = await txBuilder
  .selectUtxosFrom(utxos)
  .complete();

On this page