Multisig
Description
Section titled “Description”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
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example transact/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)})