Skip to content

Multisig Account Setup

← Back to KMD Client

This example demonstrates how to create multisig accounts using the KMD importMultisig() method. Key concepts:

  • A multisig account requires M-of-N signatures to authorize transactions
  • The threshold (M) is the minimum number of signatures required
  • The public keys (N) are the participants who can sign
  • The multisig version parameter (currently always 1) defines the format
  • The resulting multisig address is deterministically derived from the public keys, threshold, and version
  • LocalNet running (via algokit localnet start)
  • Covered operations:
  • generateKey() - Generate keys to use as multisig participants
  • importMultisig() - Create a multisig account from public keys

From the repository root:

Terminal window
cd examples
npm run example kmd_client/08-multisig-setup.ts

View source on GitHub

08-multisig-setup.ts
/**
* Example: Multisig Account Setup
*
* This example demonstrates how to create multisig accounts using the KMD
* `importMultisig()` method.
*
* Key concepts:
* - A multisig account requires M-of-N signatures to authorize transactions
* - The threshold (M) is the minimum number of signatures required
* - The public keys (N) are the participants who can sign
* - The multisig version parameter (currently always 1) defines the format
* - The resulting multisig address is deterministically derived from the
* public keys, threshold, and version
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*
* Covered operations:
* - generateKey() - Generate keys to use as multisig participants
* - importMultisig() - Create a multisig account from public keys
*/
import { decodeAddress } from '@algorandfoundation/algokit-utils'
import {
cleanupTestWallet,
createKmdClient,
createTestWallet,
printError,
printHeader,
printInfo,
printStep,
printSuccess,
} from '../shared/utils.js'
/**
* Format a byte array for display, showing first and last few bytes
*/
function formatBytesForDisplay(bytes: Uint8Array, showFirst = 4, showLast = 4): string {
const hex = Buffer.from(bytes).toString('hex')
if (bytes.length <= showFirst + showLast) {
return hex
}
const firstBytes = hex.slice(0, showFirst * 2)
const lastBytes = hex.slice(-(showLast * 2))
return `${firstBytes}...${lastBytes}`
}
async function main() {
printHeader('KMD Multisig Account Setup Example')
const kmd = createKmdClient()
let walletHandleToken = ''
const walletPassword = 'test-password'
try {
// =========================================================================
// Step 1: Create a Test Wallet
// =========================================================================
printStep(1, 'Creating a test wallet')
const testWallet = await createTestWallet(kmd, walletPassword)
walletHandleToken = testWallet.walletHandleToken
printSuccess(`Test wallet created: ${testWallet.walletName}`)
printInfo(`Wallet ID: ${testWallet.walletId}`)
// =========================================================================
// Step 2: Generate 3 Keys for Multisig Participants
// =========================================================================
printStep(2, 'Generating 3 keys to use as multisig participants')
const participantAddresses: string[] = []
const numParticipants = 3
for (let i = 1; i <= numParticipants; i++) {
const result = await kmd.generateKey({ walletHandleToken })
participantAddresses.push(result.address.toString())
printInfo(`Participant ${i}: ${result.address}`)
}
printSuccess(`Generated ${numParticipants} participant keys`)
printInfo('')
printInfo('These addresses will be used to create a 2-of-3 multisig account.')
// =========================================================================
// Step 3: Convert Addresses to Public Keys
// =========================================================================
printStep(3, 'Converting addresses to public keys')
const publicKeys: Uint8Array[] = participantAddresses.map((addr) => {
const decoded = decodeAddress(addr)
return decoded.publicKey
})
printInfo('Public keys extracted from addresses:')
publicKeys.forEach((pk, i) => {
printInfo(` Participant ${i + 1}: ${formatBytesForDisplay(pk)} (${pk.length} bytes)`)
})
printInfo('')
printInfo('Note: Each Algorand address encodes a 32-byte public key.')
printInfo('The address also includes a 4-byte checksum for error detection.')
// =========================================================================
// Step 4: Create the Multisig Account with importMultisig()
// =========================================================================
printStep(4, 'Creating a 2-of-3 multisig account with importMultisig()')
const threshold = 2 // Minimum signatures required
const multisigVersion = 1 // Multisig format version
const multisigResult = await kmd.importMultisig({
walletHandleToken,
publicKeys,
threshold,
multisigVersion,
})
const multisigAddress = multisigResult.address.toString()
printSuccess('Multisig account created successfully!')
printInfo('')
printInfo('ImportMultisigResponse fields:')
printInfo(` address: ${multisigAddress}`)
printInfo('')
printInfo('Parameters used:')
printInfo(` publicKeys: ${numParticipants} participant keys`)
printInfo(` threshold: ${threshold} (minimum signatures required)`)
printInfo(` multisigVersion: ${multisigVersion}`)
// =========================================================================
// Step 5: Explain the Threshold Parameter
// =========================================================================
printStep(5, 'Understanding the threshold parameter')
printInfo('')
printInfo('What is the threshold?')
printInfo('-'.repeat(40))
printInfo('')
printInfo(`The threshold (${threshold}) is the minimum number of signatures required`)
printInfo('to authorize any transaction from this multisig account.')
printInfo('')
printInfo(`With a ${threshold}-of-${numParticipants} configuration:`)
printInfo(` - ${numParticipants} participants can potentially sign`)
printInfo(` - At least ${threshold} signatures are required`)
printInfo(` - Any ${threshold} of the ${numParticipants} participants can authorize a transaction`)
printInfo('')
printInfo('Common use cases:')
printInfo(' - 2-of-3: Standard security (recover if one key is lost)')
printInfo(' - 2-of-2: Joint control (both parties must agree)')
printInfo(' - 3-of-5: Committee/board decisions')
printInfo(' - 1-of-N: Any participant can act alone (hot wallet backup)')
// =========================================================================
// Step 6: Explain the Multisig Version Parameter
// =========================================================================
printStep(6, 'Understanding the multisig version parameter')
printInfo('')
printInfo('What is the multisig version?')
printInfo('-'.repeat(40))
printInfo('')
printInfo(`The multisig version (${multisigVersion}) specifies the format of the multisig account.`)
printInfo('')
printInfo('Currently, version 1 is the only supported version on Algorand.')
printInfo('This parameter exists for future compatibility if the multisig')
printInfo('format is ever updated.')
printInfo('')
printInfo('Always use version 1 unless Algorand documentation specifies otherwise.')
// =========================================================================
// Step 7: Show Relationship Between Keys and Address
// =========================================================================
printStep(7, 'Relationship between public keys and multisig address')
printInfo('')
printInfo('How is the multisig address derived?')
printInfo('-'.repeat(40))
printInfo('')
printInfo('The multisig address is deterministically computed from:')
printInfo(' 1. The multisig version')
printInfo(' 2. The threshold value')
printInfo(' 3. The ordered list of public keys')
printInfo('')
printInfo('Important properties:')
printInfo(' - Same inputs always produce the same multisig address')
printInfo(' - Changing the order of public keys changes the address')
printInfo(' - Changing the threshold changes the address')
printInfo(' - The address encodes the complete multisig configuration')
printInfo('')
printInfo('Multisig address structure:')
printInfo(` ${multisigAddress}`)
printInfo('')
printInfo('Participant addresses (order matters!):')
participantAddresses.forEach((addr, i) => {
printInfo(` ${i + 1}. ${addr}`)
})
// =========================================================================
// Step 8: Verify Multisig is Listed
// =========================================================================
printStep(8, 'Verifying the multisig account is in the wallet')
const listResult = await kmd.listMultisig({ walletHandleToken })
printSuccess(`Wallet contains ${listResult.addresses.length} multisig address(es)`)
printInfo('')
printInfo('Multisig addresses in wallet:')
listResult.addresses.forEach((addr, i) => {
const marker = addr.toString() === multisigAddress ? ' (our new multisig)' : ''
printInfo(` ${i + 1}. ${addr}${marker}`)
})
// =========================================================================
// Step 9: Summary of Multisig Operations
// =========================================================================
printStep(9, 'What you can do with the multisig account')
printInfo('')
printInfo('Now that the multisig is imported, you can:')
printInfo('')
printInfo(' 1. RECEIVE FUNDS: Send Algo or ASAs to the multisig address')
printInfo(` Address: ${multisigAddress}`)
printInfo('')
printInfo(' 2. SIGN TRANSACTIONS: Use signMultisigTransaction() to add')
printInfo(' signatures from participants whose keys are in this wallet')
printInfo('')
printInfo(' 3. EXPORT CONFIGURATION: Use exportMultisig() to get the')
printInfo(' full multisig parameters (keys, threshold, version)')
printInfo('')
printInfo(' 4. DELETE: Use deleteMultisig() to remove from the wallet')
printInfo(' (does not affect the blockchain account or funds)')
printInfo('')
printInfo('Note: To fully authorize a transaction, collect signatures from')
printInfo(`at least ${threshold} participants, then combine and submit.`)
// =========================================================================
// Cleanup
// =========================================================================
printStep(10, 'Cleaning up test wallet')
await cleanupTestWallet(kmd, walletHandleToken)
walletHandleToken = '' // Mark as cleaned up
printSuccess('Test wallet handle released')
// =========================================================================
// Summary
// =========================================================================
printHeader('Summary')
printInfo('This example demonstrated multisig account setup in KMD:')
printInfo('')
printInfo(' importMultisig()')
printInfo(' Parameters:')
printInfo(' - walletHandleToken: Session token for the wallet')
printInfo(' - publicKeys: Array of Uint8Array participant keys')
printInfo(' - threshold: Minimum signatures required (M in M-of-N)')
printInfo(' - multisigVersion: Format version (always 1)')
printInfo(' Returns:')
printInfo(' - address: The generated multisig Address')
printInfo('')
printInfo('Key takeaways:')
printInfo(' - Multisig requires M-of-N signatures to authorize transactions')
printInfo(' - The address is derived from version + threshold + ordered keys')
printInfo(' - Same configuration always produces the same address')
printInfo(' - Public keys are extracted from addresses using decodeAddress()')
printInfo(' - multisigVersion should always be 1 (current Algorand standard)')
printInfo('')
printInfo('Note: The test wallet remains in KMD (wallets cannot be deleted via API).')
} catch (error) {
printError(`Error: ${error instanceof Error ? error.message : String(error)}`)
printInfo('')
printInfo('Troubleshooting:')
printInfo(' - Ensure LocalNet is running: algokit localnet start')
printInfo(' - If LocalNet issues occur: algokit localnet reset')
printInfo(' - Check that KMD is accessible on port 4002')
// Cleanup on error
if (walletHandleToken) {
await cleanupTestWallet(kmd, walletHandleToken)
}
process.exit(1)
}
}
main().catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
})