Skip to content

Multisig Account Management

← Back to KMD Client

This example demonstrates how to manage multisig accounts using KMD:

  • listMultisig() - List all multisig accounts in a wallet
  • exportMultisig() - Get the multisig preimage information
  • deleteMultisig() - Remove a multisig account from the wallet Key concepts:
  • Multisig accounts can be listed to see all multisigs in a wallet
  • The multisig preimage contains the original parameters: publicKeys, threshold, version
  • Deleting a multisig only removes it from the wallet, not from the blockchain
  • Funds in a deleted multisig address remain on the blockchain
  • LocalNet running (via algokit localnet start)
  • Covered operations:
  • generateKey() - Generate keys to use as multisig participants
  • importMultisig() - Create a multisig account from public keys
  • listMultisig() - List all multisig accounts in the wallet
  • exportMultisig() - Export the multisig preimage (configuration)
  • deleteMultisig() - Delete a multisig account from the wallet

From the repository root:

Terminal window
cd examples
npm run example kmd_client/09-multisig-management.ts

View source on GitHub

09-multisig-management.ts
/**
* Example: Multisig Account Management
*
* This example demonstrates how to manage multisig accounts using KMD:
* - listMultisig() - List all multisig accounts in a wallet
* - exportMultisig() - Get the multisig preimage information
* - deleteMultisig() - Remove a multisig account from the wallet
*
* Key concepts:
* - Multisig accounts can be listed to see all multisigs in a wallet
* - The multisig preimage contains the original parameters: publicKeys, threshold, version
* - Deleting a multisig only removes it from the wallet, not from the blockchain
* - Funds in a deleted multisig address remain on the blockchain
*
* 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
* - listMultisig() - List all multisig accounts in the wallet
* - exportMultisig() - Export the multisig preimage (configuration)
* - deleteMultisig() - Delete a multisig account from the wallet
*/
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 Management 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`)
// =========================================================================
// Step 3: Create a 2-of-3 Multisig Account
// =========================================================================
printStep(3, 'Creating a 2-of-3 multisig account')
const publicKeys: Uint8Array[] = participantAddresses.map((addr) => {
const decoded = decodeAddress(addr)
return decoded.publicKey
})
const threshold = 2
const multisigVersion = 1
const multisigResult = await kmd.importMultisig({
walletHandleToken,
publicKeys,
threshold,
multisigVersion,
})
const multisigAddress = multisigResult.address.toString()
printSuccess('Multisig account created!')
printInfo(`Multisig address: ${multisigAddress}`)
printInfo(`Configuration: ${threshold}-of-${numParticipants}`)
// =========================================================================
// Step 4: List All Multisig Accounts with listMultisig()
// =========================================================================
printStep(4, 'Listing all multisig accounts with listMultisig()')
const listResult = await kmd.listMultisig({ walletHandleToken })
printSuccess(`Found ${listResult.addresses.length} multisig address(es) in wallet`)
printInfo('')
printInfo('ListMultisigResponse fields:')
printInfo(` addresses: Array of ${listResult.addresses.length} Address(es)`)
printInfo('')
printInfo('Multisig addresses in wallet:')
listResult.addresses.forEach((addr, i) => {
printInfo(` ${i + 1}. ${addr}`)
})
printInfo('')
printInfo('Note: listMultisig() returns all multisig addresses currently')
printInfo('imported in the wallet. Each address represents a unique multisig')
printInfo('configuration (different participants, threshold, or version).')
// =========================================================================
// Step 5: Export Multisig Preimage with exportMultisig()
// =========================================================================
printStep(5, 'Exporting multisig preimage with exportMultisig()')
const exportResult = await kmd.exportMultisig({
walletHandleToken,
address: multisigResult.address,
})
printSuccess('Multisig preimage exported successfully!')
printInfo('')
printInfo('ExportMultisigResponse fields:')
printInfo(` multisigVersion: ${exportResult.multisigVersion}`)
printInfo(` threshold: ${exportResult.threshold}`)
printInfo(` publicKeys: Array of ${exportResult.publicKeys.length} Uint8Array(s)`)
printInfo('')
printInfo('Exported multisig configuration:')
printInfo(` Version: ${exportResult.multisigVersion}`)
printInfo(` Threshold: ${exportResult.threshold} (minimum signatures required)`)
printInfo(` Public Keys:`)
exportResult.publicKeys.forEach((pk, i) => {
printInfo(` ${i + 1}. ${formatBytesForDisplay(pk)} (${pk.length} bytes)`)
})
printInfo('')
printInfo('What is the multisig preimage?')
printInfo('-'.repeat(40))
printInfo('The preimage contains the original parameters used to create')
printInfo('the multisig address:')
printInfo(' - multisigVersion: The format version (always 1)')
printInfo(' - threshold: Minimum signatures required')
printInfo(' - publicKeys: The ordered list of participant public keys')
printInfo('')
printInfo('This information is needed to:')
printInfo(' - Reconstruct the multisig address')
printInfo(' - Import the multisig into another wallet')
printInfo(' - Verify the configuration of an existing multisig')
// =========================================================================
// Step 6: Verify Exported Info Matches Original
// =========================================================================
printStep(6, 'Verifying exported info matches original parameters')
const versionMatches = exportResult.multisigVersion === multisigVersion
const thresholdMatches = exportResult.threshold === threshold
const keyCountMatches = exportResult.publicKeys.length === publicKeys.length
// Check if all public keys match
let allKeysMatch = keyCountMatches
if (keyCountMatches) {
for (let i = 0; i < publicKeys.length; i++) {
const originalKey = publicKeys[i]
const exportedKey = exportResult.publicKeys[i]
if (originalKey.length !== exportedKey.length) {
allKeysMatch = false
break
}
for (let j = 0; j < originalKey.length; j++) {
if (originalKey[j] !== exportedKey[j]) {
allKeysMatch = false
break
}
}
}
}
printInfo('Verification results:')
printInfo(` Version matches: ${versionMatches ? 'Yes' : 'No'} (expected: ${multisigVersion}, got: ${exportResult.multisigVersion})`)
printInfo(` Threshold matches: ${thresholdMatches ? 'Yes' : 'No'} (expected: ${threshold}, got: ${exportResult.threshold})`)
printInfo(
` Key count matches: ${keyCountMatches ? 'Yes' : 'No'} (expected: ${publicKeys.length}, got: ${exportResult.publicKeys.length})`,
)
printInfo(` All keys match: ${allKeysMatch ? 'Yes' : 'No'}`)
if (versionMatches && thresholdMatches && allKeysMatch) {
printSuccess('All exported information matches the original parameters!')
}
// =========================================================================
// Step 7: Delete the Multisig Account with deleteMultisig()
// =========================================================================
printStep(7, 'Deleting the multisig account with deleteMultisig()')
printInfo(`Deleting multisig: ${multisigAddress}`)
await kmd.deleteMultisig({
walletHandleToken,
address: multisigResult.address,
walletPassword,
})
printSuccess('Multisig account deleted from wallet!')
printInfo('')
printInfo('deleteMultisig() parameters:')
printInfo(' - walletHandleToken: Session token for the wallet')
printInfo(' - address: The multisig address to delete')
printInfo(' - walletPassword: Wallet password (required for security)')
printInfo('')
printInfo('Important notes about deleteMultisig():')
printInfo(' - Only removes the multisig from the local KMD wallet')
printInfo(' - Does NOT affect the blockchain account')
printInfo(' - Any funds at the multisig address remain accessible')
printInfo(' - To spend funds, re-import the multisig with the same parameters')
// =========================================================================
// Step 8: Verify Deletion by Listing Multisig Accounts Again
// =========================================================================
printStep(8, 'Verifying deletion by listing multisig accounts')
const listAfterDelete = await kmd.listMultisig({ walletHandleToken })
printInfo('Multisig accounts after deletion:')
if (listAfterDelete.addresses.length === 0) {
printSuccess('No multisig accounts remaining in wallet')
} else {
printInfo(`Found ${listAfterDelete.addresses.length} multisig address(es):`)
listAfterDelete.addresses.forEach((addr, i) => {
printInfo(` ${i + 1}. ${addr}`)
})
}
// Check if the deleted address is still present
const deletedAddressStillPresent = listAfterDelete.addresses.some((addr) => addr.toString() === multisigAddress)
if (deletedAddressStillPresent) {
printError('The deleted multisig address is still present (unexpected)')
} else {
printSuccess(`Confirmed: ${multisigAddress.slice(0, 8)}... is no longer in the wallet`)
}
// =========================================================================
// Cleanup
// =========================================================================
printStep(9, '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 management in KMD:')
printInfo('')
printInfo(' listMultisig()')
printInfo(' Parameters:')
printInfo(' - walletHandleToken: Session token for the wallet')
printInfo(' Returns:')
printInfo(' - addresses: Array of multisig Address objects')
printInfo('')
printInfo(' exportMultisig()')
printInfo(' Parameters:')
printInfo(' - walletHandleToken: Session token for the wallet')
printInfo(' - address: The multisig address to export')
printInfo(' Returns:')
printInfo(' - multisigVersion: Multisig format version (1)')
printInfo(' - threshold: Minimum signatures required')
printInfo(' - publicKeys: Array of participant public keys')
printInfo('')
printInfo(' deleteMultisig()')
printInfo(' Parameters:')
printInfo(' - walletHandleToken: Session token for the wallet')
printInfo(' - address: The multisig address to delete')
printInfo(' - walletPassword: Wallet password (required)')
printInfo(' Returns:')
printInfo(' - (void)')
printInfo('')
printInfo('Key takeaways:')
printInfo(' - listMultisig() shows all multisig accounts in the wallet')
printInfo(' - exportMultisig() retrieves the original configuration (preimage)')
printInfo(' - deleteMultisig() removes from wallet only, not blockchain')
printInfo(' - Wallet password is required for deleteMultisig() for security')
printInfo(' - Deleted multisigs can be re-imported with the same parameters')
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)
})