Build NFT Marketplace Smart Contract Transactions
Build transactions for listing, purchasing, canceling, and updating NFTs with Cardano smart contracts using Mesh SDK.
Overview
In this guide, you build the transaction patterns for an NFT marketplace on Cardano. You learn how to construct listing, purchasing, canceling, and updating transactions using Plutus smart contracts.
What you will build
- Listing transactions that lock NFTs at a script address
- Purchase transactions that pay sellers and transfer ownership
- Cancel and update transactions for seller management
What you will learn
- Locking assets at script addresses with datums
- Spending from scripts with redeemers
- Authorization patterns with required signers
Prerequisites
- A deployed Plutus smart contract (see Aiken guide)
- Next.js application with Mesh SDK
- Understanding of datums and redeemers
Time to complete
60 minutes
Quick Start
This guide uses a simple marketplace contract. View the complete example:
Step-by-Step Guide
Step 1: Load the Plutus script
Load your compiled marketplace contract:
import { resolvePlutusScriptAddress } from "@meshsdk/core";
import type { PlutusScript, Data } from "@meshsdk/core";
// Your compiled Plutus script in CBOR format
const scriptCbor = "590795..."; // Full CBOR hex string
const script: PlutusScript = {
code: scriptCbor,
version: "V2",
};
// Get the script address (0 = testnet, 1 = mainnet)
const scriptAddress = resolvePlutusScriptAddress(script, 0);What to expect: A script address where marketplace UTxOs are stored.
Step 2: Define the datum structure
The datum stores sale information:
import { resolvePaymentKeyHash } from "@meshsdk/core";
function createListingDatum(
sellerAddress: string,
priceLovelace: string,
policyId: string,
assetName: string
): Data {
return {
alternative: 0,
fields: [
resolvePaymentKeyHash(sellerAddress), // seller pubkey hash
priceLovelace, // price in lovelace
policyId, // NFT policy ID
assetName, // NFT asset name (hex)
],
};
}What to expect: A datum constructor for marketplace listings.
Step 3: List an asset for sale
Send the NFT to the script address with the listing datum:
import { MeshTxBuilder, KoiosProvider, resolvePaymentKeyHash } from "@meshsdk/core";
import { useWallet } from "@meshsdk/react";
async function listNFT(
policyId: string,
assetName: string,
priceLovelace: string
) {
const provider = new KoiosProvider("preprod");
const { wallet } = useWallet();
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const sellerAddress = (await wallet.getUsedAddresses())[0];
// Create the listing datum
const datum = createListingDatum(
sellerAddress,
priceLovelace,
policyId,
assetName
);
const assetUnit = policyId + assetName;
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
// Send NFT to script with datum
.txOut(scriptAddress, [{ unit: assetUnit, quantity: "1" }])
.txOutDatumHashValue(datum)
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
console.log("Listed NFT:", txHash);
return txHash;
}What to expect: The NFT is locked at the script address with sale information in the datum.
Step 4: Find a listing UTxO
Helper function to find a specific listing:
import { resolveDataHash } from "@meshsdk/core";
async function findListingUtxo(
scriptAddress: string,
assetUnit: string,
datum: Data
) {
const provider = new KoiosProvider("preprod");
const utxos = await provider.fetchAddressUTxOs(scriptAddress, assetUnit);
if (utxos.length === 0) {
throw new Error("No listing found for this asset");
}
// Match by datum hash
const dataHash = resolveDataHash(datum);
const listingUtxo = utxos.find(
(utxo) => utxo.output.dataHash === dataHash
);
if (!listingUtxo) {
throw new Error("Listing not found with matching datum");
}
return listingUtxo;
}What to expect: A function to locate specific marketplace listings.
Step 5: Cancel a listing
Only the seller can cancel. Use redeemer alternative 1:
async function cancelListing(
policyId: string,
assetName: string,
priceLovelace: string
) {
const provider = new KoiosProvider("preprod");
const { wallet } = useWallet();
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const sellerAddress = (await wallet.getUsedAddresses())[0];
const collateral = await wallet.getCollateral();
// Recreate the original datum
const datum = createListingDatum(
sellerAddress,
priceLovelace,
policyId,
assetName
);
const assetUnit = policyId + assetName;
const listingUtxo = await findListingUtxo(scriptAddress, assetUnit, datum);
// Cancel redeemer (alternative 1)
const redeemer: Data = { alternative: 1, fields: [] };
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
.spendingPlutusScriptV2()
.txIn(listingUtxo.input.txHash, listingUtxo.input.outputIndex)
.txInDatumValue(datum)
.txInRedeemerValue(redeemer)
.txInScript(script.code)
// Return NFT to seller
.txOut(sellerAddress, listingUtxo.output.amount)
// Seller must sign
.requiredSignerHash(resolvePaymentKeyHash(sellerAddress))
.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);
const txHash = await wallet.submitTx(signedTx);
console.log("Cancelled listing:", txHash);
return txHash;
}What to expect: The NFT returns to the seller's wallet.
Step 6: Purchase a listed asset
Buyer pays the seller and receives the NFT. Use redeemer alternative 0:
async function purchaseNFT(
sellerAddress: string,
policyId: string,
assetName: string,
priceLovelace: string
) {
const provider = new KoiosProvider("preprod");
const { wallet } = useWallet();
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const buyerAddress = (await wallet.getUsedAddresses())[0];
const collateral = await wallet.getCollateral();
// Reconstruct the listing datum
const datum = createListingDatum(
sellerAddress,
priceLovelace,
policyId,
assetName
);
const assetUnit = policyId + assetName;
const listingUtxo = await findListingUtxo(scriptAddress, assetUnit, datum);
// Buy redeemer (alternative 0)
const redeemer: Data = { alternative: 0, fields: [] };
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
.spendingPlutusScriptV2()
.txIn(listingUtxo.input.txHash, listingUtxo.input.outputIndex)
.txInDatumValue(datum)
.txInRedeemerValue(redeemer)
.txInScript(script.code)
// Send NFT to buyer
.txOut(buyerAddress, [{ unit: assetUnit, quantity: "1" }])
// Pay seller
.txOut(sellerAddress, [{ unit: "lovelace", quantity: priceLovelace }])
// Buyer must sign
.requiredSignerHash(resolvePaymentKeyHash(buyerAddress))
.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);
const txHash = await wallet.submitTx(signedTx);
console.log("Purchased NFT:", txHash);
return txHash;
}What to expect: The buyer receives the NFT and the seller receives payment.
Step 7: Update listing price
Redeem the UTxO and re-list with a new price:
async function updateListingPrice(
policyId: string,
assetName: string,
currentPrice: string,
newPrice: string
) {
const provider = new KoiosProvider("preprod");
const { wallet } = useWallet();
const utxos = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const sellerAddress = (await wallet.getUsedAddresses())[0];
const collateral = await wallet.getCollateral();
// Current listing datum
const currentDatum = createListingDatum(
sellerAddress,
currentPrice,
policyId,
assetName
);
// New listing datum with updated price
const newDatum = createListingDatum(
sellerAddress,
newPrice,
policyId,
assetName
);
const assetUnit = policyId + assetName;
const listingUtxo = await findListingUtxo(scriptAddress, assetUnit, currentDatum);
// Update uses cancel redeemer (alternative 1)
const redeemer: Data = { alternative: 1, fields: [] };
const txBuilder = new MeshTxBuilder({ fetcher: provider });
const unsignedTx = await txBuilder
.spendingPlutusScriptV2()
.txIn(listingUtxo.input.txHash, listingUtxo.input.outputIndex)
.txInDatumValue(currentDatum)
.txInRedeemerValue(redeemer)
.txInScript(script.code)
// Re-list at script with new datum
.txOut(scriptAddress, [{ unit: assetUnit, quantity: "1" }])
.txOutDatumHashValue(newDatum)
.requiredSignerHash(resolvePaymentKeyHash(sellerAddress))
.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);
const txHash = await wallet.submitTx(signedTx);
console.log("Updated listing price:", txHash);
return txHash;
}What to expect: The listing is updated with the new price while remaining at the script address.
Complete Example
Here is a complete React component with all marketplace actions:
import { useState } from "react";
import { CardanoWallet, useWallet } from "@meshsdk/react";
import {
MeshTxBuilder,
KoiosProvider,
resolvePaymentKeyHash,
resolveDataHash,
resolvePlutusScriptAddress,
} from "@meshsdk/core";
import type { PlutusScript, Data, UTxO } from "@meshsdk/core";
// Your marketplace script
const script: PlutusScript = {
code: "590795...", // Your compiled script
version: "V2",
};
const scriptAddress = resolvePlutusScriptAddress(script, 0);
export default function Marketplace() {
const { wallet, connected } = useWallet();
const [loading, setLoading] = useState(false);
// Implement list, cancel, purchase, update functions here
// using the patterns shown above
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">NFT Marketplace</h1>
<CardanoWallet />
{connected && (
<div className="mt-4 space-y-4">
<button
onClick={() => listNFT("policyId", "assetName", "10000000")}
disabled={loading}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
List NFT for 10 ADA
</button>
{/* Add more action buttons */}
</div>
)}
</div>
);
}Next Steps
Troubleshooting
Script validation failed
Cause: Datum or redeemer does not match contract expectations.
Solution: Ensure the datum fields match your contract's expected structure. Check that the redeemer alternative matches the action (0 for buy, 1 for cancel in this example).
UTxO not found
Cause: The listing was already purchased or cancelled.
Solution: Verify the listing exists before building the transaction. Implement a refresh mechanism in your UI.
Collateral required error
Cause: No collateral set in the wallet.
Solution: Enable collateral in wallet settings. Most wallets require at least 5 ADA:
const collateral = await wallet.getCollateral();
if (collateral.length === 0) {
throw new Error("Please set collateral in your wallet");
}Authorization failed
Cause: The signer does not match the datum's seller address.
Solution: Ensure requiredSignerHash matches the original seller for cancel/update operations.
Related Links
Build a Custom Blockchain Provider
Create custom providers to connect Mesh SDK to any data source. Implement IFetcher and ISubmitter interfaces for your own infrastructure.
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.