Multisig Transaction Signing with KMD
Description
Section titled “Description”This example demonstrates how to sign multisig transactions using the KMD signMultisigTransaction() method. It shows:
- Creating a multisig account with 2-of-3 threshold
- Funding the multisig account via the dispenser
- Creating a payment transaction from the multisig account
- Signing with the first participant (partial signature)
- Signing with the second participant (completing the multisig)
- Submitting the fully signed transaction to the network
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start) - Covered operations:
- signMultisigTransaction() - Sign a multisig transaction with a participant key
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example kmd_client/11-multisig-signing.ts/** * Example: Multisig Transaction Signing with KMD * * This example demonstrates how to sign multisig transactions using the KMD * `signMultisigTransaction()` method. It shows: * - Creating a multisig account with 2-of-3 threshold * - Funding the multisig account via the dispenser * - Creating a payment transaction from the multisig account * - Signing with the first participant (partial signature) * - Signing with the second participant (completing the multisig) * - Submitting the fully signed transaction to the network * * Prerequisites: * - LocalNet running (via `algokit localnet start`) * * Covered operations: * - signMultisigTransaction() - Sign a multisig transaction with a participant key */
import { Address, algo, encodeAddress } from '@algorandfoundation/algokit-utils';import type { MultisigSig } from '@algorandfoundation/algokit-utils/kmd-client';import { Transaction, TransactionType, assignFee, encodeSignedTransaction, type MultisigSignature, type SignedTransaction,} from '@algorandfoundation/algokit-utils/transact';import { cleanupTestWallet, createAlgodClient, createAlgorandClient, createKmdClient, createTestWallet, printError, printHeader, printInfo, printStep, printSuccess, shortenAddress,} from '../shared/utils.js';// Use algorand-msgpack for decoding the multisig responseimport { IntMode, decode as msgpackDecode } from 'algorand-msgpack';
/** * Format microAlgos to a human-readable string */function formatMicroAlgo(microAlgos: bigint): string { const algoValue = Number(microAlgos) / 1_000_000; return `${microAlgos.toLocaleString('en-US')} µALGO (${algoValue.toFixed(6)} ALGO)`;}
/** * Decode the KMD multisig response bytes into a MultisigSig structure. * The KMD API returns msgpack-encoded MultisigSig with wire keys: * - 'subsig' -> subsignatures array * - 'thr' -> threshold * - 'v' -> version * Each subsig has: * - 'pk' -> publicKey * - 's' -> signature (optional) */function decodeKmdMultisigResponse(multisigBytes: Uint8Array): MultisigSig { const decoded = msgpackDecode(multisigBytes, { intMode: IntMode.AS_ENCODED, useMap: true, rawBinaryStringKeys: true, rawBinaryStringValues: true, }) as Map<Uint8Array, unknown>;
// Helper to find value by string key in Map with Uint8Array keys const findByKey = (map: Map<Uint8Array, unknown>, key: string): unknown => { const keyBytes = new TextEncoder().encode(key); for (const [k, v] of map) { if (k.length === keyBytes.length && k.every((b, i) => b === keyBytes[i])) { return v; } } return undefined; };
const subsigArray = findByKey(decoded, 'subsig') as Array<Map<Uint8Array, unknown>>; const threshold = findByKey(decoded, 'thr') as number; const version = findByKey(decoded, 'v') as number;
const subsignatures = subsigArray.map(subsigMap => { const publicKey = findByKey(subsigMap, 'pk') as Uint8Array; const signature = findByKey(subsigMap, 's') as Uint8Array | undefined; return { publicKey, signature }; });
return { subsignatures, threshold, version };}
/** * Convert a KMD MultisigSig to the transact MultisigSignature format */function kmdMultisigToTransactMultisig(kmdMsig: MultisigSig): MultisigSignature { return { version: kmdMsig.version, threshold: kmdMsig.threshold, subsigs: kmdMsig.subsignatures.map(subsig => ({ publicKey: subsig.publicKey, sig: subsig.signature, })), };}
/** * Count the number of signatures in a KMD MultisigSig */function countSignatures(msig: MultisigSig): number { return msig.subsignatures.filter(subsig => subsig.signature !== undefined).length;}
async function main() { printHeader('KMD Multisig Transaction Signing Example');
const kmd = createKmdClient(); const algod = createAlgodClient(); const algorand = createAlgorandClient(); let walletHandleToken = ''; const walletPassword = 'test-password';
try { // ========================================================================= // Step 1: Create a Test Wallet // ========================================================================= printStep(1, 'Creating a test wallet for multisig signing');
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: Address[] = []; const publicKeys: Uint8Array[] = []; const numParticipants = 3;
for (let i = 1; i <= numParticipants; i++) { const result = await kmd.generateKey({ walletHandleToken }); participantAddresses.push(result.address); publicKeys.push(result.address.publicKey); printInfo(`Participant ${i}: ${shortenAddress(result.address.toString())}`); }
printSuccess(`Generated ${numParticipants} participant keys`);
// ========================================================================= // Step 3: Create a 2-of-3 Multisig Account // ========================================================================= printStep(3, 'Creating a 2-of-3 multisig account');
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;
printSuccess('Multisig account created!'); printInfo(`Multisig Address: ${multisigAddress}`); printInfo(`Threshold: ${threshold}-of-${numParticipants}`);
// ========================================================================= // Step 4: Fund the Multisig Account Using the Dispenser // ========================================================================= printStep(4, 'Funding the multisig account using the dispenser');
const dispenser = await algorand.account.dispenserFromEnvironment(); printInfo(`Dispenser address: ${shortenAddress(dispenser.addr.toString())}`);
// Fund the multisig account with 1 ALGO const fundAmount = algo(1); await algorand.send.payment({ sender: dispenser.addr, receiver: multisigAddress, amount: fundAmount, });
// Verify funding const accountInfo = await algod.accountInformation(multisigAddress.toString()); printSuccess(`Multisig funded: ${formatMicroAlgo(accountInfo.amount)}`);
// ========================================================================= // Step 5: Create a Payment Transaction from the Multisig Account // ========================================================================= printStep(5, 'Creating a payment transaction from the multisig account');
const suggestedParams = await algod.suggestedParams();
printInfo('Suggested Parameters:'); printInfo(` First Valid Round: ${suggestedParams.firstValid.toLocaleString('en-US')}`); printInfo(` Last Valid Round: ${suggestedParams.lastValid.toLocaleString('en-US')}`); printInfo(` Genesis ID: ${suggestedParams.genesisId}`); printInfo('');
// Create a payment back to the dispenser const receiverAddress = dispenser.addr; const paymentAmount = 100_000n; // 0.1 ALGO
const transactionWithoutFee = new Transaction({ type: TransactionType.Payment, sender: multisigAddress, firstValid: suggestedParams.firstValid, lastValid: suggestedParams.lastValid, genesisHash: suggestedParams.genesisHash, genesisId: suggestedParams.genesisId, payment: { receiver: receiverAddress, amount: paymentAmount, }, });
// Assign the fee const transaction = assignFee(transactionWithoutFee, { feePerByte: suggestedParams.fee, minFee: suggestedParams.minFee, });
const txId = transaction.txId();
printSuccess('Transaction created!'); printInfo(`Transaction ID: ${txId}`); printInfo(`Sender: ${shortenAddress(multisigAddress.toString())} (multisig)`); printInfo(`Receiver: ${shortenAddress(receiverAddress.toString())}`); printInfo(`Amount: ${formatMicroAlgo(paymentAmount)}`); printInfo(`Fee: ${formatMicroAlgo(transaction.fee ?? 0n)}`);
// ========================================================================= // Step 6: Sign with the First Participant (Partial Signature) // ========================================================================= printStep(6, 'Signing with the first participant (partial signature)');
printInfo(`First signer: ${shortenAddress(participantAddresses[0].toString())}`); printInfo('');
const firstSignResult = await kmd.signMultisigTransaction({ walletHandleToken, transaction, publicKey: publicKeys[0], walletPassword, });
printSuccess('First signature obtained!'); printInfo(''); printInfo('SignMultisigResponse fields:'); printInfo(` multisig: Uint8Array (${firstSignResult.multisig.length} bytes)`); printInfo('');
// Decode and display the partial multisig signature const partialKmdMultisig = decodeKmdMultisigResponse(firstSignResult.multisig); const sigCount1 = countSignatures(partialKmdMultisig);
printInfo('Partial Multisig Signature:'); printInfo(` version: ${partialKmdMultisig.version}`); printInfo(` threshold: ${partialKmdMultisig.threshold}`); printInfo(` subsigs: ${partialKmdMultisig.subsignatures.length} participants`); printInfo(` Signatures collected: ${sigCount1} of ${threshold} required`); printInfo(''); printInfo('Subsignature details:'); partialKmdMultisig.subsignatures.forEach((subsig, i) => { const hasSig = subsig.signature !== undefined; const status = hasSig ? '✓ SIGNED' : '○ pending'; const addr = encodeAddress(subsig.publicKey); printInfo(` ${i + 1}. ${shortenAddress(addr)} - ${status}`); });
printInfo(''); printInfo('Note: With only 1 signature, the transaction cannot yet be submitted.'); printInfo(` We need ${threshold} signatures (${threshold - sigCount1} more required).`);
// ========================================================================= // Step 7: Sign with the Second Participant (Complete the Multisig) // ========================================================================= printStep(7, 'Signing with the second participant (completing the signature)');
printInfo(`Second signer: ${shortenAddress(participantAddresses[1].toString())}`); printInfo(''); printInfo('Passing the partial multisig from Step 6 to collect the second signature...'); printInfo('');
const secondSignResult = await kmd.signMultisigTransaction({ walletHandleToken, transaction, publicKey: publicKeys[1], walletPassword, partialMultisig: partialKmdMultisig, // Pass the partial signature from first signer });
printSuccess('Second signature obtained!'); printInfo('');
// Decode and display the completed multisig signature const completedKmdMultisig = decodeKmdMultisigResponse(secondSignResult.multisig); const sigCount2 = countSignatures(completedKmdMultisig);
printInfo('Completed Multisig Signature:'); printInfo(` version: ${completedKmdMultisig.version}`); printInfo(` threshold: ${completedKmdMultisig.threshold}`); printInfo(` Signatures collected: ${sigCount2} of ${threshold} required`); printInfo(''); printInfo('Subsignature details:'); completedKmdMultisig.subsignatures.forEach((subsig, i) => { const hasSig = subsig.signature !== undefined; const status = hasSig ? '✓ SIGNED' : '○ pending'; const addr = encodeAddress(subsig.publicKey); printInfo(` ${i + 1}. ${shortenAddress(addr)} - ${status}`); });
printInfo(''); printSuccess(`Threshold met! ${sigCount2} >= ${threshold} signatures collected.`); printInfo('The transaction is now fully authorized and ready for submission.');
// ========================================================================= // Step 8: Construct and Submit the Signed Transaction // ========================================================================= printStep(8, 'Constructing and submitting the multisig-signed transaction');
// Convert KMD MultisigSig to transact's MultisigSignature for the SignedTransaction const completedMultisig = kmdMultisigToTransactMultisig(completedKmdMultisig);
// Build the signed transaction with the multisig signature const signedTxn: SignedTransaction = { txn: transaction, msig: completedMultisig, };
// Encode and submit const encodedSignedTxn = encodeSignedTransaction(signedTxn); printInfo(`Encoded signed transaction: ${encodedSignedTxn.length} bytes`); printInfo('');
const submitResponse = await algod.sendRawTransaction(encodedSignedTxn);
printSuccess('Transaction submitted!'); printInfo(`Transaction ID: ${submitResponse.txId}`);
// ========================================================================= // Step 9: Wait for Confirmation // ========================================================================= printStep(9, 'Waiting for confirmation');
let confirmedRound: bigint | undefined; const maxWaitRounds = 10; let currentRound = (await algod.status()).lastRound;
for (let i = 0; i < maxWaitRounds; i++) { const pendingInfo = await algod.pendingTransactionInformation(txId);
if (pendingInfo.confirmedRound && pendingInfo.confirmedRound > 0n) { confirmedRound = pendingInfo.confirmedRound; break; }
if (pendingInfo.poolError && pendingInfo.poolError.length > 0) { throw new Error(`Transaction rejected: ${pendingInfo.poolError}`); }
// Wait for next block await algod.statusAfterBlock(currentRound); currentRound++; }
if (confirmedRound) { printSuccess(`Transaction confirmed in round ${confirmedRound.toLocaleString('en-US')}`); } else { printError('Transaction not confirmed within expected rounds'); }
// ========================================================================= // Step 10: Verify the Transaction // ========================================================================= printStep(10, 'Verifying the transaction was successful');
// Check multisig account balance const multisigInfo = await algod.accountInformation(multisigAddress.toString()); const expectedDeduction = paymentAmount + (transaction.fee ?? suggestedParams.minFee); const expectedBalance = fundAmount.microAlgo - expectedDeduction;
printInfo(`Multisig balance before: ${formatMicroAlgo(fundAmount.microAlgo)}`); printInfo(`Multisig balance after: ${formatMicroAlgo(multisigInfo.amount)}`); printInfo(`Expected balance: ~${formatMicroAlgo(expectedBalance)}`); printInfo('');
if (multisigInfo.amount <= fundAmount.microAlgo - paymentAmount) { printSuccess('Transaction verified! Balance reduced as expected.'); }
// ========================================================================= // Cleanup // ========================================================================= printStep(11, '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 transaction signing with KMD:'); printInfo(''); printInfo(' signMultisigTransaction() - Sign a multisig transaction'); printInfo(' Parameters:'); printInfo(' - walletHandleToken: The wallet session token'); printInfo(' - transaction: The Transaction object to sign'); printInfo(' - publicKey: The public key of the signer (must be in wallet)'); printInfo(' - walletPassword: The wallet password'); printInfo(' - partialMultisig: (optional) Existing partial signature to add to'); printInfo(' Returns:'); printInfo(' - multisig: Uint8Array of the multisig signature (msgpack encoded)'); printInfo(''); printInfo('Multisig signing workflow:'); printInfo(' 1. Create a multisig account with importMultisig()'); printInfo(' 2. Fund the multisig account'); printInfo(' 3. Create a transaction with the multisig address as sender'); printInfo(' 4. Sign with first participant (returns partial multisig signature)'); printInfo(' 5. Sign with additional participants, passing the partial signature'); printInfo(' 6. Once threshold is met, construct SignedTransaction with msig field'); printInfo(' 7. Encode and submit the signed transaction'); printInfo(''); printInfo('Key points:'); printInfo(' - Each signer adds their signature to the multisig structure'); printInfo(' - The partialMultisig parameter chains signatures together'); printInfo(' - The response contains a msgpack-encoded MultisigSignature'); printInfo(' - Transaction is valid once threshold signatures are collected'); printInfo(' - Any 2 of the 3 participants could have signed (2-of-3 threshold)'); 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'); printInfo(' - Check that Algod is accessible on port 4001');
// Cleanup on error if (walletHandleToken) { await cleanupTestWallet(kmd, walletHandleToken); }
process.exit(1); }}
main().catch(error => { console.error('Fatal error:', error); process.exit(1);});Other examples in KMD Client
Section titled “Other examples in KMD Client”- KMD Version Information
- Wallet Creation and Listing
- Wallet Session Management
- Key Generation
- Key Import and Export
- Key Listing and Deletion
- Master Key Export
- Multisig Account Setup
- Multisig Account Management
- Transaction Signing with KMD
- Multisig Transaction Signing with KMD
- Program Signing (Delegated Logic Signatures) with KMD
- Multisig Program Signing (Delegated Multisig Logic Signatures) with KMD