Build Your First Aiken Smart Contract
Write a Cardano smart contract with Aiken and interact with it using Mesh SDK. Learn to lock and unlock assets on-chain.
Overview
In this guide, you build a complete "Hello World" smart contract on Cardano. You write the on-chain validator in Aiken, then use Mesh SDK to create transactions that lock and unlock assets.
What you will build
- An Aiken smart contract with datum and redeemer validation
- A transaction that locks ADA at the script address
- A transaction that unlocks ADA with the correct redeemer
What you will learn
- Aiken syntax for Cardano validators
- How datums and redeemers work in Plutus contracts
- Building Plutus transactions with Mesh SDK
Prerequisites
- Rust installed (for Aiken)
- Node.js 18+ installed
- Basic TypeScript knowledge
- A Cardano wallet browser extension
Resources
Time to complete
60 minutes
Quick Start
Clone the complete template:
git clone https://github.com/MeshJS/aiken-next-ts-template
cd aiken-next-ts-template
npm install
npm run devOpen http://localhost:3000 to interact with the deployed contract.
Step-by-Step Guide
Step 1: Install Aiken
On Linux or macOS:
curl -sSfL https://install.aiken-lang.org | bash
aikupOn any platform (via Cargo):
cargo install aikenVerify the installation:
aiken -VWhat to expect: Aiken version information displays in the terminal.
Step 2: Create an Aiken project
Create a new Aiken project:
aiken new meshjs/hello_world
cd hello_worldVerify the project structure:
aiken checkWhat to expect: A new directory with aiken.toml and a validators folder.
Step 3: Write the validator
Create validators/hello_world.ak:
use aiken/hash.{Blake2b_224, Hash}
use aiken/list
use aiken/transaction.{ScriptContext}
use aiken/transaction/credential.{VerificationKey}
/// The datum stores the owner's public key hash
type Datum {
owner: Hash<Blake2b_224, VerificationKey>,
}
/// The redeemer must contain the magic message
type Redeemer {
msg: ByteArray,
}
/// The validator allows spending only when:
/// 1. The redeemer message is "Hello, World!"
/// 2. The transaction is signed by the owner
validator {
fn hello_world(datum: Datum, redeemer: Redeemer, context: ScriptContext) -> Bool {
let must_say_hello = redeemer.msg == "Hello, World!"
let must_be_signed =
list.has(context.transaction.extra_signatories, datum.owner)
must_say_hello && must_be_signed
}
}What to expect: A validator file that enforces two conditions for spending.
Step 4: Compile the contract
Build the Aiken project:
aiken buildWhat to expect: A plutus.json file is generated. This is the CIP-0057 Plutus blueprint containing your compiled validator.
Step 5: Set up the frontend
Create a new Next.js project with Mesh SDK (see the Next.js guide):
npx meshjs my-aiken-app
cd my-aiken-app
npm install cborCopy plutus.json from your Aiken project to src/data/plutus.json.
What to expect: A Next.js project ready for smart contract integration.
Step 6: Load the contract
Create src/lib/contract.ts:
import {
resolvePlutusScriptAddress,
resolvePaymentKeyHash,
resolveDataHash,
} from "@meshsdk/core";
import type { PlutusScript, Data } from "@meshsdk/core";
import cbor from "cbor";
import plutusBlueprint from "@/data/plutus.json";
// Encode the compiled code to CBOR format for Mesh
const scriptCbor = cbor
.encode(Buffer.from(plutusBlueprint.validators[0].compiledCode, "hex"))
.toString("hex");
// Create the PlutusScript object
export const script: PlutusScript = {
code: scriptCbor,
version: "V2",
};
// Get the script address (0 = testnet, 1 = mainnet)
export const scriptAddress = resolvePlutusScriptAddress(script, 0);
// Helper to create the datum
export function createDatum(ownerPubKeyHash: string): Data {
return {
alternative: 0,
fields: [ownerPubKeyHash],
};
}
// Helper to create the redeemer
export function createRedeemer(): Data {
return {
alternative: 0,
fields: ["Hello, World!"],
};
}What to expect: Utility functions for working with the contract.
Step 7: Lock assets at the script
Create a component that locks ADA at the script address:
import { MeshTxBuilder, KoiosProvider, resolvePaymentKeyHash } from "@meshsdk/core";
import { useWallet } from "@meshsdk/react";
import { script, scriptAddress, createDatum } from "@/lib/contract";
export function LockFunds() {
const { wallet } = useWallet();
async function lockAda() {
const provider = new KoiosProvider("preprod");
// Get wallet info
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const usedAddresses = await wallet.getUsedAddresses();
const ownerPubKeyHash = resolvePaymentKeyHash(usedAddresses[0]);
// Create the datum with owner's public key hash
const datum = createDatum(ownerPubKeyHash);
// Build the transaction
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
.txOut(scriptAddress, [{ unit: "lovelace", quantity: "5000000" }])
.txOutDatumHashValue(datum)
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
// Sign and submit
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
console.log("Locked 5 ADA at script. TX:", txHash);
return txHash;
}
return (
<button onClick={lockAda}>
Lock 5 ADA
</button>
);
}What to expect: A button that sends 5 ADA to the script address with a datum containing your public key hash.
Step 8: Unlock assets from the script
Create a component that unlocks ADA from the script:
import {
MeshTxBuilder,
KoiosProvider,
resolvePaymentKeyHash,
resolveDataHash,
} from "@meshsdk/core";
import { useWallet } from "@meshsdk/react";
import { script, scriptAddress, createDatum, createRedeemer } from "@/lib/contract";
export function UnlockFunds() {
const { wallet } = useWallet();
async function unlockAda(lockTxHash: string) {
const provider = new KoiosProvider("preprod");
// Get wallet info
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const collateral = await wallet.getCollateral();
const usedAddresses = await wallet.getUsedAddresses();
const ownerPubKeyHash = resolvePaymentKeyHash(usedAddresses[0]);
// Recreate the original datum
const datum = createDatum(ownerPubKeyHash);
const dataHash = resolveDataHash(datum);
// Find the UTxO at the script address
const scriptUtxos = await provider.fetchAddressUTxOs(scriptAddress, "lovelace");
const lockedUtxo = scriptUtxos.find(
(utxo) => utxo.output.dataHash === dataHash
);
if (!lockedUtxo) {
throw new Error("No locked UTxO found for this wallet");
}
// Create the redeemer with the magic message
const redeemer = createRedeemer();
// Build the unlock transaction
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
.spendingPlutusScriptV2()
.txIn(lockedUtxo.input.txHash, lockedUtxo.input.outputIndex)
.txInDatumValue(datum)
.txInRedeemerValue(redeemer)
.txInScript(script.code)
.txOut(changeAddress, lockedUtxo.output.amount)
.requiredSignerHash(ownerPubKeyHash)
.txInCollateral(
collateral[0].input.txHash,
collateral[0].input.outputIndex,
collateral[0].output.amount,
collateral[0].output.address
)
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
// Sign with partial signing (required for Plutus scripts)
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);
console.log("Unlocked ADA from script. TX:", txHash);
return txHash;
}
return (
<button onClick={() => unlockAda("your-lock-tx-hash")}>
Unlock ADA
</button>
);
}What to expect: A button that withdraws the locked ADA using the correct redeemer message.
Complete Example
Here is a complete page component:
import { useState } from "react";
import { CardanoWallet, useWallet } from "@meshsdk/react";
import {
MeshTxBuilder,
KoiosProvider,
resolvePaymentKeyHash,
resolveDataHash,
} from "@meshsdk/core";
import { script, scriptAddress, createDatum, createRedeemer } from "@/lib/contract";
export default function HelloWorldContract() {
const { wallet, connected } = useWallet();
const [lockTxHash, setLockTxHash] = useState<string>("");
const [loading, setLoading] = useState(false);
const provider = new KoiosProvider("preprod");
async function lockAda() {
setLoading(true);
try {
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const usedAddresses = await wallet.getUsedAddresses();
const ownerPubKeyHash = resolvePaymentKeyHash(usedAddresses[0]);
const datum = createDatum(ownerPubKeyHash);
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
.txOut(scriptAddress, [{ unit: "lovelace", quantity: "5000000" }])
.txOutDatumHashValue(datum)
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
setLockTxHash(txHash);
} finally {
setLoading(false);
}
}
async function unlockAda() {
setLoading(true);
try {
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const collateral = await wallet.getCollateral();
const usedAddresses = await wallet.getUsedAddresses();
const ownerPubKeyHash = resolvePaymentKeyHash(usedAddresses[0]);
const datum = createDatum(ownerPubKeyHash);
const dataHash = resolveDataHash(datum);
const scriptUtxos = await provider.fetchAddressUTxOs(scriptAddress, "lovelace");
const lockedUtxo = scriptUtxos.find((u) => u.output.dataHash === dataHash);
if (!lockedUtxo) throw new Error("No locked UTxO found");
const redeemer = createRedeemer();
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
.spendingPlutusScriptV2()
.txIn(lockedUtxo.input.txHash, lockedUtxo.input.outputIndex)
.txInDatumValue(datum)
.txInRedeemerValue(redeemer)
.txInScript(script.code)
.txOut(changeAddress, lockedUtxo.output.amount)
.requiredSignerHash(ownerPubKeyHash)
.txInCollateral(
collateral[0].input.txHash,
collateral[0].input.outputIndex,
collateral[0].output.amount,
collateral[0].output.address
)
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
const signedTx = await wallet.signTx(unsignedTx, true);
await wallet.submitTx(signedTx);
} finally {
setLoading(false);
}
}
return (
<main className="p-8">
<h1 className="text-2xl font-bold mb-4">Hello World Contract</h1>
<CardanoWallet />
{connected && (
<div className="mt-4 space-x-4">
<button
onClick={lockAda}
disabled={loading}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Lock 5 ADA
</button>
<button
onClick={unlockAda}
disabled={loading}
className="px-4 py-2 bg-green-500 text-white rounded"
>
Unlock ADA
</button>
</div>
)}
{lockTxHash && (
<p className="mt-4">
Lock TX: <a href={`https://preprod.cardanoscan.io/transaction/${lockTxHash}`}>{lockTxHash}</a>
</p>
)}
</main>
);
}Next Steps
- Aiken documentation - Learn more Aiken syntax
- Build a vesting contract - Time-locked fund release
- NFT marketplace transactions - List, buy, cancel patterns
- Transaction Builder API - Advanced transaction patterns
Troubleshooting
Script validation failed
Cause: The redeemer message does not match "Hello, World!" exactly.
Solution: Ensure the redeemer message is exactly "Hello, World!" with the comma and exclamation mark.
No collateral available
Cause: The wallet has no collateral set.
Solution: Enable collateral in your wallet settings. Most wallets require at least 5 ADA as collateral.
UTxO not found
Cause: The locked UTxO was already spent or the datum hash does not match.
Solution: Ensure you are using the same wallet that locked the funds. The datum includes the owner's public key hash.
CBOR encoding error
Cause: The cbor package is not installed or import is incorrect.
Solution: Install the package:
npm install cborAiken check fails
Cause: Syntax error in the Aiken code.
Solution: Run aiken check to see detailed error messages. Common issues:
- Missing imports
- Type mismatches
- Incorrect function signatures
Related Links
Build NFT Marketplace Smart Contract Transactions
Build transactions for listing, purchasing, canceling, and updating NFTs with Cardano smart contracts using Mesh SDK.
Run Standalone Cardano Scripts with TypeScript
Execute TypeScript scripts directly to interact with Cardano using Mesh SDK. Build, sign, and submit transactions without a framework.