Lesson #02: Multi-signature Transactions
A multi-signature (multi-sig) transaction requires more than one user to sign a transaction prior to the transaction being broadcast on a blockchain. You can think of it like a husband and wife savings account, where both signatures are required to spend the funds, preventing one spouse from spending the money without the approval of the other. For a multi-sig transaction, you can include 2 or more required signers, these signers can be wallets or script.
In this lesson, we will learn to build multi-signature transactions to mint a token. We will also setup a NextJS app to build a simple web interface to interact with the Cardano blockchain.
System setup
Download CIP30 wallet extension
In order to interact with the blockchain, we need a wallet extension. You can use any wallet that supports the CIP30 standard, which you can choose one to download here.
After downloading the wallet, you can restore the wallet using the seed phrase you created in the previous lesson.
Setup NextJS and Mesh
Open the Terminal and execute this command to create a new NextJs application:
npx create-next-app@latest --typescript mesh-multisig
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
Change directory (cd mesh-multisig
) to the newly created folder and install the latest version of Mesh with npm:
npm install @meshsdk/core @meshsdk/react
Add MeshProvider
In order to use Mesh React, we need to wrap our application with the MeshProvider
component. Open the src/app/layout.tsx
file and add the following code:
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
We can add a wallet React component to the application. This component will allow us to connect to the wallet and interact with the Cardano blockchain.
Open the src/pages/index.tsx
file, you can delete the existing code and replace it with the following code:
import { CardanoWallet, useWallet } from "@meshsdk/react";
export default function Home() {
const { wallet, connected } = useWallet();
return (
<div>
<CardanoWallet isDark={true} />
</div>
);
}
Start the development server with:
npm run dev
Visit http://localhost:3000 to view your application. CTRL+C to stop the application.
You should see a connect wallet component, which will allow you to connect to the wallet. Try connecting to your wallet.
Minting script
In this section, we will create a minting script that will mint a token using a multi-signature transaction.
Define the minting script
First, lets define a few constants that we will use in the minting script.
// Set up the blockchain provider with your key
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"];
- You will need to replace with your API key, replace
YOUR_KEY_HERE
- You can define the asset metadata
demoAssetMetadata
- You can generate a new mnemonic and use it for your minting script
mintingWallet
Create minting application wallet
Next, lets create a new function that will create a minting transaction. This function will take the asset metadata and the minting
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()
);
}
- this function takes the inputs as a parameter, this inputs will be fed from your wallet to pay for minting fees
- initializes the application wallet with the mnemonic
- derive the pubkeyhash of the wallet, which will be used to create the minting script
Create native script
Then, lets create the native script that will be used to mint the token.
// create minting script
const nativeScript: NativeScript = {
type: "all",
scripts: [
{
type: "before",
slot: "99999999",
},
{
type: "sig",
keyHash: keyHash,
},
],
};
const forgingScript = ForgeScript.fromNativeScript(nativeScript);
nativeScript
define the parameters of the scriptForgeScript.fromNativeScript
create the forging script
Define asset metadata
Next, we define the asset metadata.
const policyId = resolveScriptHash(forgingScript);
const tokenName = "MeshToken";
const tokenNameHex = stringToHex(tokenName);
const metadata = { [policyId]: { [tokenName]: { ...demoAssetMetadata } } };
policyId
is the policy ID of the minting script, we derive it from the forging script usingresolveScriptHash
tokenName
is the name of the token, you can change it to whatever you wantmetadata
is the asset metadata, we can use thedemoAssetMetadata
defined above
Create transaction
Finally, we are ready to 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)
.selectUtxosFrom(inputs)
.complete();
MeshTxBuilder
is the transaction builder that will be used to build the minting transactionmint
takes the amount, policy ID and token name as parameters. Note that the token name is in hex format.mintingScript
add the minting script to the transactionmetadataValue
add the asset metadata to the transaction, and721
is the standard for the asset metadatachangeAddress
add the change address to the transaction, this is the address that will receive the change after the transaction is completedinvalidHereafter
add the invalid hereafter to the transaction, this is the slot number that the transaction will be invalid afterselectUtxosFrom
select the UTxOs from the inputs, this is the UTxOs that will be used to pay for the minting feescomplete
will complete the transaction and return the unsigned transaction
Sign the transaction (application wallet)
Because this is a multi-signature transaction, we need to sign the transaction with the minting wallet. We can do this by calling the signTx
function on the wallet.
const signedTx = await wallet.signTx(unsignedTx);
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)
.selectUtxosFrom(inputs)
.complete();
const signedTx = await wallet.signTx(unsignedTx);
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);
const txHash = await wallet.submitTx(signedTx);
console.log("Transaction hash:", txHash);
}
}
- first we check if the wallet is connected
- then we get the UTxOs from the wallet, this is the UTxOs that will be used to pay for the minting fees
- then we get the change address from the wallet
- then we call the
buildMintTx
function to build the minting transaction - then we call the
signTx
function to sign the transaction - finally we call the
submitTx
function to submit the transaction to the blockchain
Source code
The source code for this lesson is available on GitHub.
Challenge
Create a multi-signature wallet that requires 2 out of 3 signers to approve a transaction. Create a transaction to spend from that multi-sig wallet and sign it with the 2 signers. Submit the transaction to the blockchain and verify that the transaction was successful.