Lesson 2: Multi-signature Transactions
Build multi-signature transactions on Cardano to mint tokens with collaborative signing.
Learning Objectives
By the end of this lesson, you will:
- Understand how multi-signature transactions work on Cardano
- Set up a Next.js application with Mesh React components
- Create a native minting script with multiple signing conditions
- Build and execute a multi-sig token minting transaction
- Connect a CIP-30 browser wallet using the CardanoWallet component
Prerequisites
Before you begin, make sure you have:
- Completed Lesson 1: Hello World
- A CIP-30 compatible wallet extension installed (choose one here)
- Your wallet restored with the mnemonic from Lesson 1
- A Blockfrost API key for preprod
- Node.js 18+ installed on your machine
Key Concepts
What is a Multi-signature Transaction?
A multi-signature (multi-sig) transaction requires more than one party to sign before you can submit it to the blockchain. Think of it like a joint bank account where both account holders must approve withdrawals.
Multi-sig transactions can require:
- Two or more wallet signatures
- Script conditions (time locks, etc.)
- A combination of both
Use Cases
| Use Case | Description |
|---|---|
| Shared treasuries | Organizations require multiple approvals for spending |
| Escrow services | Release funds only when both parties agree |
| Token policies | Control minting through multiple administrators |
Native Script Types
| Script Type | Description |
|---|---|
all | All nested conditions must be satisfied |
any | At least one condition must be satisfied |
atLeast | A minimum number of conditions must be satisfied |
sig | Requires a signature from a specific key hash |
before | Valid only before a specific slot |
after | Valid only after a specific slot |
Step 1: Create a Next.js Project
Create a new Next.js application with TypeScript and Tailwind CSS.
npx create-next-app@latest --typescript mesh-multisigWhen prompted, select these options:
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like your code inside a `src/` directory? Yes
Would you like to use App Router? No
Would you like to use Turbopack for next dev? No
Would you like to customize the import alias? NoNavigate to the project and install Mesh packages:
cd mesh-multisig
npm install @meshsdk/core @meshsdk/reactStep 2: Configure MeshProvider
Wrap your application with MeshProvider to enable React hooks for wallet interaction.
Replace the contents of src/pages/_app.tsx:
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import "@meshsdk/react/styles.css";
import { MeshProvider } from "@meshsdk/react";
export default function App({ Component, pageProps }: AppProps) {
return (
<MeshProvider>
<Component {...pageProps} />
</MeshProvider>
);
}Step 3: Add Wallet Connection
Add the CardanoWallet component to connect browser wallets.
Replace the contents of src/pages/index.tsx:
import { CardanoWallet, useWallet } from "@meshsdk/react";
export default function Home() {
const { wallet, connected } = useWallet();
return (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
<h1 className="text-2xl font-bold mb-8">Multi-sig Token Minting</h1>
<CardanoWallet isDark={true} />
{connected && (
<p className="mt-4 text-green-600">Wallet connected!</p>
)}
</div>
);
}Start the development server:
npm run devVisit http://localhost:3000 and test connecting your wallet.
Step 4: Define the Minting Script
Add the minting configuration to your index.tsx file, before the Home component:
import { CardanoWallet, useWallet } from "@meshsdk/react";
import {
BlockfrostProvider,
MeshTxBuilder,
ForgeScript,
NativeScript,
deserializeAddress,
resolveScriptHash,
stringToHex,
} from "@meshsdk/core";
import type { UTxO } from "@meshsdk/core";
import { MeshCardanoHeadlessWallet, AddressType } from "@meshsdk/wallet";
// Configuration - Replace with your Blockfrost API key
const provider = new BlockfrostProvider("YOUR_BLOCKFROST_API_KEY");
const demoAssetMetadata = {
name: "Mesh Token",
image: "ipfs://QmRzicpReutwCkM6aotuKjErFCUD213DpwPq6ByuzMJaua",
mediaType: "image/jpg",
description: "This NFT was minted by Mesh (https://meshjs.dev/).",
};
// Replace with your minting wallet mnemonic
const mintingWallet = ["your", "mnemonic", "phrases", "here"];Understand Native Scripts
A native script defines who can mint tokens and under what conditions:
const nativeScript: NativeScript = {
type: "all", // All conditions must be met
scripts: [
{
type: "before",
slot: "99999999", // Must mint before this slot
},
{
type: "sig",
keyHash: keyHash, // Must be signed by this key
},
],
};Step 5: Build the Minting Transaction
Create a function that builds the multi-sig minting transaction:
async function buildMintTx(inputs: UTxO[], changeAddress: string) {
// Initialize the minting wallet
const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({
networkId: 0,
walletAddressType: AddressType.Base,
mnemonic: mintingWallet,
});
// Get the minting wallet's public key hash
const { pubKeyHash: keyHash } = deserializeAddress(
await wallet.getChangeAddressBech32()
);
// Create the native script with time lock and signature requirement
const nativeScript: NativeScript = {
type: "all",
scripts: [
{
type: "before",
slot: "99999999",
},
{
type: "sig",
keyHash: keyHash,
},
],
};
const forgingScript = ForgeScript.fromNativeScript(nativeScript);
// Derive policy ID and set up metadata
const policyId = resolveScriptHash(forgingScript);
const tokenName = "MeshToken";
const tokenNameHex = stringToHex(tokenName);
const metadata = { [policyId]: { [tokenName]: { ...demoAssetMetadata } } };
// Build the transaction
const txBuilder = new MeshTxBuilder({
fetcher: provider,
verbose: true,
});
const unsignedTx = await txBuilder
.mint("1", policyId, tokenNameHex)
.mintingScript(forgingScript)
.metadataValue(721, metadata)
.changeAddress(changeAddress)
.invalidHereafter(99999999)
.requiredSignerHash(keyHash)
.selectUtxosFrom(inputs)
.complete();
// Sign with the minting wallet (first signature)
const signedTx = await wallet.signTx(unsignedTx, true);
return signedTx;
}Transaction Builder Methods
| Method | Description |
|---|---|
mint(quantity, policyId, tokenName) | Specifies the token to mint |
mintingScript(script) | Attaches the minting policy script |
metadataValue(label, metadata) | Adds CIP-25 NFT metadata (label 721) |
invalidHereafter(slot) | Sets transaction expiry slot |
requiredSignerHash(keyHash) | Declares a required signer |
selectUtxosFrom(utxos) | Provides UTXOs for fees |
Step 6: Execute the Transaction
Add the minting function that coordinates both signatures:
async function mint() {
if (!connected) {
alert("Please connect your wallet first");
return;
}
try {
// Get UTXOs and change address from the connected browser wallet
const inputs = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
// Build transaction and get first signature from minting wallet
const partiallySignedTx = await buildMintTx(inputs, changeAddress);
// Add second signature from the connected browser wallet
const fullySignedTx = await wallet.signTx(partiallySignedTx, true);
// Submit the transaction
const txHash = await wallet.submitTx(fullySignedTx);
console.log("Transaction hash:", txHash);
alert(`Success! Transaction hash: ${txHash}`);
} catch (error) {
console.error("Minting failed:", error);
alert("Minting failed. Check console for details.");
}
}How Multi-sig Works
- First signature: The minting wallet signs the transaction (satisfies the
sigcondition in the native script) - Second signature: The browser wallet signs (provides the fee payment authorization)
- Submission: Submit the fully signed transaction to the network
The signTx(tx, true) parameter true indicates partial signing, allowing you to add additional signatures.
Step 7: Add the Mint Button
Update your component to include the mint functionality:
export default function Home() {
const { wallet, connected } = useWallet();
async function mint() {
if (!connected) {
alert("Please connect your wallet first");
return;
}
try {
const inputs = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const partiallySignedTx = await buildMintTx(inputs, changeAddress);
const fullySignedTx = await wallet.signTx(partiallySignedTx, true);
const txHash = await wallet.submitTx(fullySignedTx);
console.log("Transaction hash:", txHash);
alert(`Success! Transaction hash: ${txHash}`);
} catch (error) {
console.error("Minting failed:", error);
alert("Minting failed. Check console for details.");
}
}
return (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
<h1 className="text-2xl font-bold mb-8">Multi-sig Token Minting</h1>
<CardanoWallet isDark={true} />
{connected && (
<div className="mt-8">
<button
onClick={mint}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Mint Token
</button>
</div>
)}
</div>
);
}Complete Working Example
Here is the complete src/pages/index.tsx file:
import { CardanoWallet, useWallet } from "@meshsdk/react";
import {
BlockfrostProvider,
MeshTxBuilder,
ForgeScript,
NativeScript,
deserializeAddress,
resolveScriptHash,
stringToHex,
} from "@meshsdk/core";
import type { UTxO } from "@meshsdk/core";
import { MeshCardanoHeadlessWallet, AddressType } from "@meshsdk/wallet";
const provider = new BlockfrostProvider("YOUR_BLOCKFROST_API_KEY");
const demoAssetMetadata = {
name: "Mesh Token",
image: "ipfs://QmRzicpReutwCkM6aotuKjErFCUD213DpwPq6ByuzMJaua",
mediaType: "image/jpg",
description: "This NFT was minted by Mesh (https://meshjs.dev/).",
};
const mintingWallet = ["your", "mnemonic", "phrases", "here"];
async function buildMintTx(inputs: UTxO[], changeAddress: string) {
const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({
networkId: 0,
walletAddressType: AddressType.Base,
mnemonic: mintingWallet,
});
const { pubKeyHash: keyHash } = deserializeAddress(
await wallet.getChangeAddressBech32()
);
const nativeScript: NativeScript = {
type: "all",
scripts: [
{
type: "before",
slot: "99999999",
},
{
type: "sig",
keyHash: keyHash,
},
],
};
const forgingScript = ForgeScript.fromNativeScript(nativeScript);
const policyId = resolveScriptHash(forgingScript);
const tokenName = "MeshToken";
const tokenNameHex = stringToHex(tokenName);
const metadata = { [policyId]: { [tokenName]: { ...demoAssetMetadata } } };
const txBuilder = new MeshTxBuilder({
fetcher: provider,
verbose: true,
});
const unsignedTx = await txBuilder
.mint("1", policyId, tokenNameHex)
.mintingScript(forgingScript)
.metadataValue(721, metadata)
.changeAddress(changeAddress)
.invalidHereafter(99999999)
.requiredSignerHash(keyHash)
.selectUtxosFrom(inputs)
.complete();
const signedTx = await wallet.signTx(unsignedTx, true);
return signedTx;
}
export default function Home() {
const { wallet, connected } = useWallet();
async function mint() {
if (!connected) {
alert("Please connect your wallet first");
return;
}
try {
const inputs = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const partiallySignedTx = await buildMintTx(inputs, changeAddress);
const fullySignedTx = await wallet.signTx(partiallySignedTx, true);
const txHash = await wallet.submitTx(fullySignedTx);
console.log("Transaction hash:", txHash);
alert(`Success! Transaction hash: ${txHash}`);
} catch (error) {
console.error("Minting failed:", error);
alert("Minting failed. Check console for details.");
}
}
return (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
<h1 className="text-2xl font-bold mb-8">Multi-sig Token Minting</h1>
<CardanoWallet isDark={true} />
{connected && (
<div className="mt-8">
<button
onClick={mint}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Mint Token
</button>
</div>
)}
</div>
);
}Key Concepts Explained
ForgeScript
ForgeScript converts a native script into a format usable by the transaction builder:
const forgingScript = ForgeScript.fromNativeScript(nativeScript);Policy ID
The policy ID is a hash of the minting script. All tokens minted under this policy share the same policy ID:
const policyId = resolveScriptHash(forgingScript);CIP-25 Metadata
Label 721 follows the CIP-25 standard for NFT metadata:
const metadata = {
[policyId]: {
[tokenName]: {
name: "Token Name",
image: "ipfs://...",
// ... other properties
}
}
};Exercises
-
2-of-3 Multi-sig: Create a native script that requires 2 out of 3 possible signers to approve minting. Use the
atLeastscript type. -
Time-locked minting: Modify the script to only allow minting between two specific dates using both
beforeandafterconditions. -
Display minted token: After successful minting, query the wallet's assets and display the newly minted token in the UI.
Next Steps
You have successfully:
- Built a Next.js app with Mesh wallet integration
- Created a multi-signature minting policy
- Minted a token requiring two signatures
In the next lesson, you learn the fundamentals of Aiken smart contracts for more complex on-chain logic.