Lesson #02: Multi-signature Transactions
A multi-signature (multi-sig) transaction requires more than one user to sign a transaction before it is broadcast on the blockchain. Think of it like a joint savings account where both parties must approve spending. Multi-sig transactions can include two or more required signers, which can be wallets or scripts.
In this lesson, you’ll learn how to:
- Build multi-signature transactions to mint a token.
- Set up a NextJS app with a simple web interface to interact with the Cardano blockchain.
System setup
Download CIP30 Wallet Extension
To interact with the blockchain, you’ll need a wallet extension that supports the CIP30 standard. Choose and download one here.
After downloading the wallet, restore it using the seed phrase you created in the previous lesson.
Set Up NextJS and Mesh
Open your terminal and run the following command to create a new NextJS application:
npx create-next-app@latest --typescript mesh-multisig
Follow the prompts:
Need to install the following packages:
Ok to proceed? (y)
✔ 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 (@/* by default)? … No
Navigate to the newly created folder:
cd mesh-multisig
Install the latest version of Mesh:
npm install @meshsdk/core @meshsdk/react
Add MeshProvider
To use Mesh React, wrap your application with the MeshProvider
component. Open the src/app/layout.tsx
file and add:
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>
);
}
Add CardanoWallet Component
Add a wallet React component to connect to the wallet and interact with the blockchain. Open the src/pages/index.tsx
file, delete the existing code, and replace it with:
import { CardanoWallet, useWallet } from "@meshsdk/react";
export default function Home() {
const { wallet, connected } = useWallet();
return (
<div>
<CardanoWallet isDark={true} />
</div>
);
}
Start the development server:
npm run dev
Visit http://localhost:3000 to view your application. Press CTRL+C to stop the server.
You should see a “Connect Wallet” component. Try connecting to your wallet.
Minting Script
In this section, you’ll create a minting script to mint a token using a multi-signature transaction.
Define the Minting Script
Set up constants for the minting script:
const provider = new BlockfrostProvider("YOUR_KEY_HERE");
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", "...", "here"];
- Replace
YOUR_KEY_HERE
with your Blockfrost API key. - Define asset metadata in
demoAssetMetadata
. - Use a mnemonic for the minting wallet.
Create Minting Application Wallet
Create a function to build the minting transaction:
async function buildMintTx(inputs: UTxO[], changeAddress: string) {
const wallet = new MeshWallet({
networkId: 0,
key: {
type: "mnemonic",
words: mintingWallet,
},
});
const { pubKeyHash: keyHash } = deserializeAddress(
await wallet.getChangeAddress()
);
}
inputs
: UTxOs from your wallet to pay minting fees.- Initialize the wallet with the mnemonic.
- Derive the
pubKeyHash
for the minting script.
Create Native Script
Define the native script:
const nativeScript: NativeScript = {
type: "all",
scripts: [
{
type: "before",
slot: "99999999",
},
{
type: "sig",
keyHash: keyHash,
},
],
};
const forgingScript = ForgeScript.fromNativeScript(nativeScript);
nativeScript
: Parameters for the script.ForgeScript.fromNativeScript
: Create the forging script.
Define Asset Metadata
Set up asset metadata:
const policyId = resolveScriptHash(forgingScript);
const tokenName = "MeshToken";
const tokenNameHex = stringToHex(tokenName);
const metadata = { [policyId]: { [tokenName]: { ...demoAssetMetadata } } };
policyId
: Derived from the forging script.tokenName
: Name of the token.metadata
: Asset metadata.
Create Transaction
Build the minting 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();
mint
: Add token details.mintingScript
: Attach the minting script.metadataValue
: Add asset metadata.changeAddress
: Specify the change address.invalidHereafter
: Set transaction expiry.selectUtxosFrom
: Use UTxOs for fees.requiredSignerHash
to declare that the minter wallet pub key hash is required.complete
: Finalize the transaction.
Sign the Transaction
Sign the transaction with the minting wallet:
const signedTx = await wallet.signTx(unsignedTx, true);
Source code
Here is the complete code for building the minting transaction:
async function buildMintTx(inputs: UTxO[], changeAddress: string) {
// minting wallet
const wallet = new MeshWallet({
networkId: 0,
key: {
type: "mnemonic",
words: mintingWallet,
},
});
const { pubKeyHash: keyHash } = deserializeAddress(
await wallet.getChangeAddress()
);
// create minting script
const nativeScript: NativeScript = {
type: "all",
scripts: [
{
type: "before",
slot: "99999999",
},
{
type: "sig",
keyHash: keyHash,
},
],
};
const forgingScript = ForgeScript.fromNativeScript(nativeScript);
// create metadata
const policyId = resolveScriptHash(forgingScript);
const tokenName = "MeshToken";
const tokenNameHex = stringToHex(tokenName);
const metadata = { [policyId]: { [tokenName]: { ...demoAssetMetadata } } };
// create 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();
const signedTx = await wallet.signTx(unsignedTx, true);
return signedTx;
}
Execute the transaction
Now that we have the minting transaction, we can execute it.
async function mint() {
if (connected) {
const inputs = await wallet.getUtxos();
const changeAddress = await wallet.getChangeAddress();
const tx = await buildMintTx(inputs, changeAddress);
const signedTx = await wallet.signTx(tx, true);
const txHash = await wallet.submitTx(signedTx);
console.log("Transaction hash:", txHash);
}
}
- Check wallet connection.
- Get UTxOs and change address.
- Build, sign, and submit the transaction.
Source code
The source code for this lesson is available on GitHub.
Challenge
Create a multi-signature wallet requiring 2 out of 3 signers to approve a transaction. Build and sign a transaction with two signers, submit it, and verify success.