|
| 1 | +/** |
| 2 | + * Verify a BIP322 proof of address ownership from a BitGo multi-sig wallet. |
| 3 | + * |
| 4 | + * This example demonstrates how to: |
| 5 | + * 1. Get a wallet by ID |
| 6 | + * 2. Read address/message pairs from messages.json |
| 7 | + * 3. Get address information to obtain chain and index for pubkey derivation |
| 8 | + * 4. Create a BIP322 proof using sendMany with type 'bip322' |
| 9 | + * 5. Verify the proof using bip322.assertBip322TxProof |
| 10 | + * |
| 11 | + * IMPORTANT: This example does NOT work with descriptor wallets. |
| 12 | + * Only use this with traditional BitGo multi-sig wallets. |
| 13 | + * |
| 14 | + * This works for Hot wallets only. |
| 15 | + * |
| 16 | + * Supports all address types except for Taproot Musig2. |
| 17 | + * |
| 18 | + * Copyright 2025, BitGo, Inc. All Rights Reserved. |
| 19 | + */ |
| 20 | + |
| 21 | +import * as fs from 'fs'; |
| 22 | +import * as path from 'path'; |
| 23 | +import { BitGo } from 'bitgo'; |
| 24 | +import { AbstractUtxoCoin } from '@bitgo/abstract-utxo'; |
| 25 | +import * as utxolib from '@bitgo/utxo-lib'; |
| 26 | +import { bip322 } from '@bitgo/utxo-core'; |
| 27 | +import { BIP32Factory, ecc } from '@bitgo/secp256k1'; |
| 28 | + |
| 29 | +// ============================================================================ |
| 30 | +// CONFIGURATION - Set these values before running |
| 31 | +// ============================================================================ |
| 32 | + |
| 33 | +// Set your environment: 'prod' for mainnet, 'test' for testnet |
| 34 | +const environment: 'prod' | 'test' = 'test'; |
| 35 | + |
| 36 | +// Set the coin: 'btc' for mainnet, 'tbtc4' for testnet |
| 37 | +const coin = 'tbtc4'; |
| 38 | + |
| 39 | +// Set your BitGo access token |
| 40 | +const accessToken = ''; |
| 41 | + |
| 42 | +// Set your wallet ID |
| 43 | +const walletId = ''; |
| 44 | + |
| 45 | +// Set your wallet passphrase for signing |
| 46 | +const walletPassphrase = ''; |
| 47 | + |
| 48 | +// Set the OTP code. If you dont need one, set it to undefined. |
| 49 | +const otp: string | undefined = undefined; |
| 50 | + |
| 51 | +// ============================================================================ |
| 52 | +// TYPES |
| 53 | +// ============================================================================ |
| 54 | + |
| 55 | +interface MessageEntry { |
| 56 | + address: string; |
| 57 | + message: string; |
| 58 | +} |
| 59 | + |
| 60 | +async function main(): Promise<void> { |
| 61 | + // Validate configuration |
| 62 | + if (!accessToken) { |
| 63 | + throw new Error('Please set your accessToken in the configuration section'); |
| 64 | + } |
| 65 | + if (!walletId) { |
| 66 | + throw new Error('Please set your walletId in the configuration section'); |
| 67 | + } |
| 68 | + if (!walletPassphrase) { |
| 69 | + throw new Error('Please set your walletPassphrase in the configuration section'); |
| 70 | + } |
| 71 | + |
| 72 | + // Initialize BitGo SDK |
| 73 | + const bitgo = new BitGo({ env: environment }); |
| 74 | + bitgo.authenticateWithAccessToken({ accessToken }); |
| 75 | + if (otp) { |
| 76 | + const unlock = await bitgo.unlock({ otp, duration: 3600 }); |
| 77 | + if (!unlock) { |
| 78 | + console.log('We did not unlock.'); |
| 79 | + throw new Error(); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + const baseCoin = bitgo.coin(coin); |
| 84 | + |
| 85 | + console.log(`Environment: ${environment}`); |
| 86 | + console.log(`Coin: ${coin}`); |
| 87 | + console.log(`Wallet ID: ${walletId}`); |
| 88 | + |
| 89 | + // Read messages from JSON file |
| 90 | + const messagesPath = path.join(__dirname, 'messages.json'); |
| 91 | + const messagesContent = fs.readFileSync(messagesPath, 'utf-8'); |
| 92 | + const messages: MessageEntry[] = JSON.parse(messagesContent); |
| 93 | + |
| 94 | + if (!Array.isArray(messages) || messages.length === 0) { |
| 95 | + throw new Error('messages.json must contain an array of {address, message} objects'); |
| 96 | + } |
| 97 | + |
| 98 | + console.log(`\nLoaded ${messages.length} message(s) to prove:`); |
| 99 | + messages.forEach((m, i) => { |
| 100 | + console.log(` ${i + 1}. Address: ${m.address}`); |
| 101 | + console.log(` Message: ${m.message}`); |
| 102 | + }); |
| 103 | + |
| 104 | + // Get the wallet |
| 105 | + console.log('\nFetching wallet...'); |
| 106 | + const wallet = await baseCoin.wallets().get({ id: walletId }); |
| 107 | + console.log(`Wallet label: ${wallet.label()}`); |
| 108 | + |
| 109 | + // Get keychains for the wallet (needed for deriving pubkeys) |
| 110 | + console.log('\nFetching keychains...'); |
| 111 | + const keychains = await baseCoin.keychains().getKeysForSigning({ wallet }); |
| 112 | + const xpubs = keychains.map((k) => { |
| 113 | + if (!k.pub) { |
| 114 | + throw new Error('Keychain missing public key'); |
| 115 | + } |
| 116 | + return k.pub; |
| 117 | + }); |
| 118 | + console.log('Retrieved wallet public keys'); |
| 119 | + |
| 120 | + // Create RootWalletKeys from xpubs for derivation |
| 121 | + const bip32 = BIP32Factory(ecc); |
| 122 | + const rootWalletKeys = new utxolib.bitgo.RootWalletKeys( |
| 123 | + xpubs.map((xpub) => bip32.fromBase58(xpub)) as utxolib.bitgo.Triple<utxolib.BIP32Interface> |
| 124 | + ); |
| 125 | + |
| 126 | + // Build messageInfo array by getting address details for each message |
| 127 | + console.log('\nBuilding message info from address data...'); |
| 128 | + const messageInfo: bip322.MessageInfo[] = []; |
| 129 | + |
| 130 | + for (const entry of messages) { |
| 131 | + // Get address information from wallet to obtain chain and index |
| 132 | + console.log(` Getting address info for: ${entry.address}`); |
| 133 | + const addressInfo = await wallet.getAddress({ address: entry.address }); |
| 134 | + |
| 135 | + if (addressInfo.chain === undefined || addressInfo.index === undefined) { |
| 136 | + throw new Error(`Address ${entry.address} is missing chain or index information`); |
| 137 | + } |
| 138 | + |
| 139 | + const chain = addressInfo.chain as utxolib.bitgo.ChainCode; |
| 140 | + const index = addressInfo.index; |
| 141 | + |
| 142 | + // Derive scriptType from chain |
| 143 | + const scriptType = utxolib.bitgo.scriptTypeForChain(chain); |
| 144 | + |
| 145 | + // Derive pubkeys for this address using chain and index |
| 146 | + const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chain, index); |
| 147 | + const pubkeys = derivedKeys.publicKeys.map((pk) => pk.toString('hex')); |
| 148 | + |
| 149 | + console.log(` Chain: ${chain}, Index: ${index}, ScriptType: ${scriptType}`); |
| 150 | + |
| 151 | + messageInfo.push({ |
| 152 | + address: entry.address, |
| 153 | + message: entry.message, |
| 154 | + pubkeys, |
| 155 | + scriptType, |
| 156 | + }); |
| 157 | + } |
| 158 | + |
| 159 | + console.log('\nCreating BIP322 proof via sendMany...'); |
| 160 | + const sendManyResult = await wallet.sendMany({ |
| 161 | + recipients: [], |
| 162 | + messages: messages, |
| 163 | + walletPassphrase, |
| 164 | + }); |
| 165 | + |
| 166 | + console.log('BIP322 proof created successfully'); |
| 167 | + |
| 168 | + // Extract the signed transaction from the result |
| 169 | + // The result should contain the fully signed PSBT or transaction hex |
| 170 | + const txHex = sendManyResult.txHex || sendManyResult.tx; |
| 171 | + if (!txHex) { |
| 172 | + throw new Error('No transaction hex found in sendMany result'); |
| 173 | + } |
| 174 | + |
| 175 | + console.log('\nVerifying BIP322 proof...'); |
| 176 | + |
| 177 | + // Parse the transaction and verify |
| 178 | + const network = (baseCoin as AbstractUtxoCoin).network; |
| 179 | + |
| 180 | + // Check if it's a PSBT or raw transaction |
| 181 | + if (utxolib.bitgo.isPsbt(txHex)) { |
| 182 | + // Parse as PSBT |
| 183 | + const psbt = utxolib.bitgo.createPsbtFromHex(txHex, network); |
| 184 | + bip322.assertBip322PsbtProof(psbt, messageInfo); |
| 185 | + console.log('PSBT proof verified successfully!'); |
| 186 | + } else { |
| 187 | + // Parse as raw transaction |
| 188 | + const tx = utxolib.bitgo.createTransactionFromHex<bigint>(txHex, network, { amountType: 'bigint' }); |
| 189 | + bip322.assertBip322TxProof(tx, messageInfo); |
| 190 | + console.log('Transaction proof verified successfully!'); |
| 191 | + } |
| 192 | + |
| 193 | + // Display summary |
| 194 | + console.log('\n============================================'); |
| 195 | + console.log('BIP322 PROOF VERIFICATION COMPLETE'); |
| 196 | + console.log('============================================'); |
| 197 | + console.log(`Verified ${messageInfo.length} address/message pair(s):`); |
| 198 | + messageInfo.forEach((info, i) => { |
| 199 | + console.log(`\n${i + 1}. Address: ${info.address}`); |
| 200 | + console.log(` Message: "${info.message}"`); |
| 201 | + console.log(` Script Type: ${info.scriptType}`); |
| 202 | + }); |
| 203 | + console.log('\nAll proofs are valid. The wallet controls the specified addresses.'); |
| 204 | +} |
| 205 | + |
| 206 | +// Run the example |
| 207 | +main() |
| 208 | + .then(() => { |
| 209 | + console.log('\nExample completed successfully.'); |
| 210 | + process.exit(0); |
| 211 | + }) |
| 212 | + .catch((e) => { |
| 213 | + console.error('\nExample failed with error:', e.message); |
| 214 | + if (e.result) { |
| 215 | + console.error('API Error details:', JSON.stringify(e.result, null, 2)); |
| 216 | + } |
| 217 | + process.exit(1); |
| 218 | + }); |
0 commit comments