Mesh LogoMesh

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 CaseDescription
Shared treasuriesOrganizations require multiple approvals for spending
Escrow servicesRelease funds only when both parties agree
Token policiesControl minting through multiple administrators

Native Script Types

Script TypeDescription
allAll nested conditions must be satisfied
anyAt least one condition must be satisfied
atLeastA minimum number of conditions must be satisfied
sigRequires a signature from a specific key hash
beforeValid only before a specific slot
afterValid 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-multisig

When 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? No

Navigate to the project and install Mesh packages:

cd mesh-multisig
npm install @meshsdk/core @meshsdk/react

Step 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 dev

Visit 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

MethodDescription
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

  1. First signature: The minting wallet signs the transaction (satisfies the sig condition in the native script)
  2. Second signature: The browser wallet signs (provides the fee payment authorization)
  3. 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

  1. 2-of-3 Multi-sig: Create a native script that requires 2 out of 3 possible signers to approve minting. Use the atLeast script type.

  2. Time-locked minting: Modify the script to only allow minting between two specific dates using both before and after conditions.

  3. 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.

On this page