Skip to content

Multisig

← Back to Transactions

This example demonstrates how to create and use a 2-of-3 multisig account. Key concepts:

  • Creating a MultisigAccount with version, threshold, and addresses
  • Deriving the multisig address from the participant addresses
  • Signing transactions with a subset of participants (2 of 3)
  • Demonstrating that insufficient signatures (1 of 3) will fail
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example transact/10-multisig.ts

View source on GitHub

10-multisig.ts
/**
* Example: Multisig
*
* This example demonstrates how to create and use a 2-of-3 multisig account.
*
* Key concepts:
* - Creating a MultisigAccount with version, threshold, and addresses
* - Deriving the multisig address from the participant addresses
* - Signing transactions with a subset of participants (2 of 3)
* - Demonstrating that insufficient signatures (1 of 3) will fail
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { AlgorandClient } from '@algorandfoundation/algokit-utils'
import {
assignFee,
MultisigAccount,
Transaction,
TransactionType,
type MultisigMetadata,
type PaymentTransactionFields,
} from '@algorandfoundation/algokit-utils/transact'
import {
createAlgodClient,
formatAlgo,
getAccountBalance,
printHeader,
printInfo,
printStep,
printSuccess,
shortenAddress,
waitForConfirmation,
} from '../shared/utils.js'
/**
* Gets a funded account from LocalNet's KMD wallet
*/
async function getLocalNetFundedAccount(algorand: AlgorandClient) {
return await algorand.account.kmd.getLocalNetDispenserAccount()
}
async function main() {
printHeader('Multisig Example (2-of-3)')
// Step 1: Initialize clients
printStep(1, 'Initialize Algod Client')
const algod = createAlgodClient()
const algorand = AlgorandClient.defaultLocalNet()
printInfo('Connected to LocalNet Algod')
// Step 2: Create 3 individual accounts using AlgorandClient helper
printStep(2, 'Create 3 Individual Accounts')
const account1 = algorand.account.random()
const account2 = algorand.account.random()
const account3 = algorand.account.random()
printInfo(`Account 1: ${shortenAddress(account1.addr.toString())}`)
printInfo(`Account 2: ${shortenAddress(account2.addr.toString())}`)
printInfo(`Account 3: ${shortenAddress(account3.addr.toString())}`)
// Step 3: Create MultisigAccount with version=1, threshold=2, and all 3 addresses
printStep(3, 'Create MultisigAccount (2-of-3)')
// The multisig parameters:
// - version: 1 (standard multisig version)
// - threshold: 2 (minimum signatures required)
// - addrs: list of participant addresses (order matters!)
const multisigParams: MultisigMetadata = {
version: 1,
threshold: 2,
addrs: [account1.addr, account2.addr, account3.addr],
}
// Create the MultisigAccount with 2 sub-signers (accounts 1 and 2)
// These are the accounts that will provide signatures
const multisigWith2Signers = new MultisigAccount(multisigParams, [account1, account2])
printInfo(`Multisig version: ${multisigParams.version}`)
printInfo(`Multisig threshold: ${multisigParams.threshold}`)
printInfo(`Number of participants: ${multisigParams.addrs.length}`)
// Step 4: Show the derived multisig address
printStep(4, 'Show Derived Multisig Address')
// The multisig address is deterministically derived from:
// Hash("MultisigAddr" || version || threshold || pk1 || pk2 || pk3)
const multisigAddress = multisigWith2Signers.addr
printInfo(`Multisig address: ${multisigAddress.toString()}`)
printInfo('')
printInfo('The multisig address is derived by hashing:')
printInfo(' "MultisigAddr" prefix + version + threshold + all public keys')
printInfo(' Order of public keys matters - different order = different address!')
// Step 5: Fund the multisig address
printStep(5, 'Fund the Multisig Address')
const dispenser = await getLocalNetFundedAccount(algorand)
const fundingAmount = 5_000_000n // 5 ALGO
const suggestedParams = await algod.suggestedParams()
const fundTx = new Transaction({
type: TransactionType.Payment,
sender: dispenser.addr,
firstValid: suggestedParams.firstValid,
lastValid: suggestedParams.lastValid,
genesisHash: suggestedParams.genesisHash,
genesisId: suggestedParams.genesisId,
payment: {
receiver: multisigAddress,
amount: fundingAmount,
},
})
const fundTxWithFee = assignFee(fundTx, {
feePerByte: suggestedParams.fee,
minFee: suggestedParams.minFee,
})
const signedFundTx = await dispenser.signer([fundTxWithFee], [0])
await algod.sendRawTransaction(signedFundTx)
await waitForConfirmation(algod, fundTxWithFee.txId())
const multisigBalance = await getAccountBalance(algorand, multisigAddress.toString())
printInfo(`Funded multisig with ${formatAlgo(fundingAmount)}`)
printInfo(`Multisig balance: ${formatAlgo(multisigBalance.microAlgo)}`)
// Step 6: Create a payment transaction from the multisig
printStep(6, 'Create Payment Transaction from Multisig')
const receiver = algorand.account.random()
const paymentAmount = 1_000_000n // 1 ALGO
const payParams = await algod.suggestedParams()
const paymentFields: PaymentTransactionFields = {
receiver: receiver.addr,
amount: paymentAmount,
}
const paymentTx = new Transaction({
type: TransactionType.Payment,
sender: multisigAddress, // The sender is the multisig address
firstValid: payParams.firstValid,
lastValid: payParams.lastValid,
genesisHash: payParams.genesisHash,
genesisId: payParams.genesisId,
payment: paymentFields,
})
const paymentTxWithFee = assignFee(paymentTx, {
feePerByte: payParams.fee,
minFee: payParams.minFee,
})
printInfo(`Payment amount: ${formatAlgo(paymentAmount)}`)
printInfo(`Sender (multisig): ${shortenAddress(multisigAddress.toString())}`)
printInfo(`Receiver: ${shortenAddress(receiver.addr.toString())}`)
printInfo(`Transaction ID: ${paymentTxWithFee.txId()}`)
// Step 7: Sign with 2 of the 3 accounts using MultisigAccount.signer
printStep(7, 'Sign with 2 of 3 Accounts')
printInfo('Signing with accounts 1 and 2 (meeting 2-of-3 threshold)...')
printInfo('')
printInfo('How multisig signing works:')
printInfo(' 1. Each sub-signer signs the transaction individually')
printInfo(' 2. Signatures are collected into a MultisigSignature structure')
printInfo(' 3. The structure includes version, threshold, and all subsigs')
printInfo(' 4. Subsigs contain public key + signature (or undefined if not signed)')
printInfo('')
// The MultisigAccount.signer automatically collects signatures from all sub-signers
const signedTxns = await multisigWith2Signers.signer([paymentTxWithFee], [0])
printInfo(`Signed transaction size: ${signedTxns[0].length} bytes`)
printSuccess('Transaction signed by accounts 1 and 2!')
// Step 8: Submit and verify the transaction succeeds
printStep(8, 'Submit and Verify Transaction')
await algod.sendRawTransaction(signedTxns)
printInfo('Transaction submitted to network...')
const pendingInfo = await waitForConfirmation(algod, paymentTxWithFee.txId())
printInfo(`Transaction confirmed in round: ${pendingInfo.confirmedRound}`)
// Verify balances
const multisigBalanceAfter = await getAccountBalance(algorand, multisigAddress.toString())
let receiverBalance: bigint
try {
const info = await getAccountBalance(algorand, receiver.addr.toString())
receiverBalance = info.microAlgo
} catch {
receiverBalance = 0n
}
printInfo(`Multisig balance after: ${formatAlgo(multisigBalanceAfter.microAlgo)}`)
printInfo(`Receiver balance: ${formatAlgo(receiverBalance)}`)
if (receiverBalance === paymentAmount) {
printSuccess('Receiver received the payment!')
}
// Step 9: Demonstrate that 1 signature is insufficient
printStep(9, 'Demonstrate Insufficient Signatures (1 of 3)')
printInfo('Creating a MultisigAccount with only 1 sub-signer (account 3)...')
printInfo('')
// Create a MultisigAccount with only 1 signer - below the threshold
const multisigWith1Signer = new MultisigAccount(multisigParams, [account3])
// Create another payment transaction
const insufficientParams = await algod.suggestedParams()
const insufficientTx = new Transaction({
type: TransactionType.Payment,
sender: multisigAddress,
firstValid: insufficientParams.firstValid,
lastValid: insufficientParams.lastValid,
genesisHash: insufficientParams.genesisHash,
genesisId: insufficientParams.genesisId,
payment: {
receiver: receiver.addr,
amount: 500_000n, // 0.5 ALGO
},
})
const insufficientTxWithFee = assignFee(insufficientTx, {
feePerByte: insufficientParams.fee,
minFee: insufficientParams.minFee,
})
printInfo('Signing with only account 3 (not meeting 2-of-3 threshold)...')
// Sign with only 1 account
const insufficientSignedTxns = await multisigWith1Signer.signer([insufficientTxWithFee], [0])
// Try to submit - this should fail
try {
await algod.sendRawTransaction(insufficientSignedTxns)
printInfo('ERROR: Transaction should have been rejected!')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
printInfo(`Transaction rejected as expected!`)
printInfo(`Reason: ${errorMessage.includes('multisig') ? 'Insufficient signatures for multisig' : errorMessage.slice(0, 100)}...`)
printSuccess('Demonstrated that 1 signature is insufficient for 2-of-3 multisig!')
}
// Summary
printInfo('')
printInfo('Summary - Multisig Key Points:')
printInfo(' - MultisigAccount wraps multiple signers with a threshold')
printInfo(' - version=1 is the standard multisig version')
printInfo(' - threshold specifies minimum signatures required')
printInfo(' - The multisig address is deterministically derived from params')
printInfo(' - Order of addresses matters for address derivation')
printInfo(' - Transactions require at least threshold signatures to succeed')
printInfo(` - This example used ${multisigParams.threshold}-of-${multisigParams.addrs.length} multisig`)
printSuccess('Multisig example completed!')
}
main().catch((error) => {
console.error('Error:', error)
process.exit(1)
})