I underwent an extensive refactoring process that involved switching from Lucid-Cardano to Mesh.js for our comprehensive Cardano NFT minting and wallet management system. "Swap one library for another" wasn't the only approach taken. It was more like to acknowledge that rather than using our instruments, we were fighting against them.
Allow me to explain what I discovered, the rationale behind our switch, and some real code from our migration. I hope this spares someone else the trouble of having to figure things out from the beginning.
What's the Deal with Lucid and Mesh.js?
Lucid-Cardano
Lucid has existed for some time. This TypeScript module streamlines the process of creating Cardano transactions by automating tedious tasks, including fee calculation and transaction balancing. Initially created with Deno, it provides an appealing layer of abstraction over the unprocessed blockchain data.
Here's what you should know:
- Uses Rust-based serialization libraries (WASM under the hood)
- You need WebAssembly support
- Pretty simple API for basic transactions
- Not super actively maintained anymore (last npm update was like 9 months ago)
- Documentation is... fine, but sparse
Mesh.js
The newest kid that has truly grown up is Mesh.js. It is a complete TypeScript SDK that includes all the necessary components for Cardano development. Lots of documentation, an active community, and real maintenance.
What makes it different:
- Now runs on IOG's cardano-sdk—pure TypeScript, no WebAssembly nonsense.
- Comes with React components and hooks out of the box
- Documentation is actually good, with working examples you can copy-paste
- Regular updates and an active community
- Multiple wallet types are supported natively.
- A transaction builder that actually makes sense
Why the switch
1. Nobody's Home
Open source developers owe us nothing, I realize. However, you need to know that the tools you rely on are being maintained when you're developing production apps. Mesh.js has real funding from Project Catalyst and receives frequent upgrades. Lucid? Not as much now.
2. Developer Experience Matters
Mesh just feels better to work with. You get:
- React components that actually work.
- Docs with live examples (seriously, this is huge)
- No fighting with WebAssembly configurations
- TypeScript everywhere, properly typed
3. It's Actually Production-Ready
Here, we're not working on little projects. Mesh offers reliable solutions for the things we really need, such as wallet management, transactions, and NFT minting. It is designed for practical apps.
4. The API Just Makes Sense
The remaining part of Mesh follows a similar pattern once you have mastered one. Every procedure felt like a tiny puzzle when Lucid was involved.
The Code: Before and After
Example 1: Policy ID Generation
Before (Lucid):
@Post('policy-id')
public async policyId(@Req() request: Request) {
let lucid = await this.getConfigs(request);
lucid.selectWalletFromSeed(request?.body?.seed);
const mintingPolicy = await this.getPolicy(lucid);
const policy: PolicyId = lucid.utils.mintingPolicyToId(mintingPolicy);
return { policy };
}
protected async getPolicy(lucid: Lucid) {
const { paymentCredential } = lucid.utils.getAddressDetails(
await lucid.wallet.address(),
);
const mintingPolicy: MintingPolicy = lucid.utils.nativeScriptFromJson({
type: 'all',
scripts: [
{
type: 'before',
slot: lucid.utils.unixTimeToSlot(1761942369100),
},
{
type: 'sig',
keyHash: paymentCredential?.hash!,
},
],
});
return mintingPolicy;
}
After (Mesh.js):
@Post('policy-id')
public async policyId(@Req() request: Request) {
const wallet = await this.getWallet(request);
const nativeScript = await this.getNativeScript(wallet);
const policyId = resolveNativeScriptHash(nativeScript);
return { policy: policyId };
}
protected async getNativeScript(wallet) {
const usedAddress = await wallet.getUsedAddresses();
const address = usedAddress[0];
const { pubKeyHash: keyHash } = deserializeAddress(address);
const nativeScript: NativeScript = {
type: "all",
scripts: [
{
type: "sig",
keyHash: keyHash,
},
],
};
return nativeScript;
}
What changed:
- Way more straightforward with resolveNativeScriptHash
- Cleaner native script creation
- Less ceremony, more actual work getting done
Example 2: NFT Minting
Before (Lucid):
@Post('mint')
public async mintNft(@Req() request: Request) {
const nft = request.body.nft;
let lucid = await this.getConfigs(request);
lucid.selectWalletFromSeed(request?.body?.seed);
const mintingPolicy = await this.getPolicy(lucid);
const policyId: PolicyId = lucid.utils.mintingPolicyToId(mintingPolicy);
const unit: Unit = policyId + fromText(nft.key);
const tx = await lucid
.newTx()
.payToAddress(nft.owner, { lovelace: BigInt(2000000), [unit]: 1n })
.mintAssets({ [unit]: 1n })
.validTo(Date.now() + 100000)
.attachMintingPolicy(mintingPolicy)
.attachMetadata(721, { [policyId]: { [nft.key]: nft.metadata } })
.complete();
const signedTx = await tx.sign().complete();
const hash = await signedTx.submit();
return { hash };
}
After (Mesh.js):
@Post('mint')
public async mintNft(@Req() request: Request) {
const nft = request.body.nft;
const wallet = await this.getWallet(request);
const address = request.body.address;
let tx = new Transaction({ initiator: wallet });
const nativeScript = await this.getNativeScript(wallet);
const forgingScript = ForgeScript.fromNativeScript(nativeScript);
let metadata = nft.metadata;
const asset: Mint = {
assetName: nft.key,
assetQuantity: '1',
metadata: metadata,
label: '721',
recipient: address,
};
tx = await tx.mintAsset(forgingScript, asset);
const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
return { hash: txHash };
}
What improved:
- The Mint interface is way more intuitive.
- The ForgeScript utility handles the script-related tasks for you.
- The transaction flow is cleaner—build, sign, and submit.
- Metadata just works with the right label structure.
Example 3: Wallet Management
Before (Lucid):
@Post('address')
async wallet(@Req() request: Request) {
let lucid;
lucid = await this.getConfigs(request);
lucid.selectWalletFromSeed(request?.body?.seed);
return {
address: await lucid.wallet.address(),
};
}
After (Mesh.js):
@Post('address')
async wallet(@Req() request: Request) {
const wallet = await this.getWallet(request);
return {
address: await wallet.getUsedAddresses()[0],
};
}
protected async getWallet(request: Request): Promise<MeshWallet> {
const wallet = new MeshWallet({
networkId: network,
fetcher: provider,
submitter: provider,
key: {
type: 'mnemonic',
words: request?.body?.seed.split(" "),
},
});
return wallet;
}
Why it's better:
- Cleaner separation—create wallet, use wallet
- Proper TypeScript types everywhere
- You explicitly control the network and provider setup.
The Upsetting Elements
Seed Phrase Format
This one caught me off guard. Lucid and Mesh handle seed phrases differently.
The fix:
- Lucid wants commas: seed.split(",")
- Mesh wants spaces: seed.split(" ")
Just normalize your input format before you pass it to either library.
Imports
Different imports, different types. You'll need to update everything.
// Old Lucid
import { Lucid, MintingPolicy, PolicyId } from 'lucid-cardano';
// New Mesh
import {
MeshWallet,
Transaction,
ForgeScript,
NativeScript,
resolveNativeScriptHash
} from '@meshsdk/core';
Transaction Building
The patterns are different. Instead of chaining with lucid.newTx(), you use Mesh's Transaction class. Build, sign, and submit are separate steps.
Performance
Mesh v1.9+ dropped WebAssembly. It's pure TypeScript using IOG's cardano-sdk.
- Bundle gets smaller (under 60kB)
- App starts faster
- Works with any frontend framework
- Debugging is easier
Migration Steps
- Start with wallet stuff
- Then tackle transactions
- Test on testnet
- Clean up dependencies
- Use Mesh's utilities
- Read the docs
Final Thoughts
Was it worth it? Absolutely. The code is cleaner, easier to maintain, and we're using an actively developed library.
The migration took work, but our codebase is in a much better place now.
Links
- Mesh.js: https://meshjs.dev/
- Mesh NPM: https://www.npmjs.com/package/@meshsdk/core
- Lucid NPM: https://www.npmjs.com/package/lucid-cardano
- Mesh GitHub: https://github.com/MeshJS/m
最初に感想をシェアしてみましょう!