Created
March 29, 2024 21:49
-
-
Save ottosch/aae9110afc7fcb856c6e06a716d4ae71 to your computer and use it in GitHub Desktop.
Extract outpoints (utxos) from addresses or xpubs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// package.json | |
{ | |
"dependencies": { | |
"bip32": "^4.0.0", | |
"bitcoinjs-lib": "^6.1.1", | |
"electrum-client": "^0.0.6", | |
"tiny-secp256k1": "^2.2.1", | |
"xpub-converter": "^1.0.2" | |
}, | |
"name": "get-utxos", | |
"version": "1.0.0", | |
"main": "index.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "", | |
"license": "ISC", | |
"description": "" | |
} | |
// =========================================================== | |
// addresses.txt | |
# add addresses and/or xpubs below | |
xpubXXXXXX | |
zpubXXXXXX | |
bc1qxxxx | |
etc | |
// =========================================================== | |
// index.js | |
#! /usr/bin/env node | |
"use strict" | |
const crypto = require("crypto"); | |
const fs = require("fs"); | |
const ElectrumCli = require('electrum-client'); | |
const bitcoin = require("bitcoinjs-lib"); | |
const ecc = require("tiny-secp256k1"); | |
const { BIP32Factory } = require("bip32"); | |
bitcoin.initEccLib(ecc); | |
const bip32 = BIP32Factory(ecc); | |
const network = bitcoin.networks.bitcoin; | |
const xpubConverter = require('xpub-converter'); | |
const inputFile = "addresses.txt"; | |
const outputFile = "output.txt"; | |
const gapLimit = 20; | |
const AddressType = { p2pkh: 1, p2sh: 2, p2wpkh: 3, p2tr: 4 }; | |
const typeList = [AddressType.p2pkh, AddressType.p2sh, AddressType.p2wpkh, AddressType.p2tr]; | |
let ecl; | |
let allUtxos = []; | |
let unmatchedKeys = []; | |
async function main() { | |
startElectrum(); | |
let data = fs.readFileSync(inputFile).toString().trim().split("\n"); | |
for (let key of data) { | |
if (key && key[0] === "#") { | |
continue; | |
} | |
if (/^(x|y|z)pub\w+$/.test(key)) { | |
let xpubStr = xpubConverter(key, "xpub"); | |
let xpub = bip32.fromBase58(xpubStr); | |
await processXpub(xpub.derive(0)); // receive | |
await processXpub(xpub.derive(1)); // change | |
} else if (/^(1|3|bc1)\w+$/i.test(key)) { | |
let utxo = await getUTXOs(key); | |
if (utxo.length) { | |
allUtxos.push(utxo); | |
} | |
} else { | |
unmatchedKeys.push(key); | |
} | |
} | |
console.log("\n=============================================="); | |
if (outputFile) { | |
fs.writeFileSync(outputFile, ""); | |
} | |
for (let arr of allUtxos) { | |
for (let u of arr) { | |
let outpoint = `${u["tx_hash"]}:${u["tx_pos"]}`; | |
console.log(outpoint); | |
if (outputFile) { | |
fs.appendFileSync(outputFile, `${outpoint}\n`); | |
} | |
} | |
} | |
console.log("Unmatched keys: ", unmatchedKeys); | |
stopElectrum(); | |
} | |
async function processXpub(xpub) { | |
for (let type of typeList) { | |
let gap = 0; | |
let index = 0; | |
while (gap <= gapLimit) { | |
let key = xpub.derive(index); | |
let address = pubkeyToAddress(key.publicKey, type); | |
let history = await getHistory(address); | |
if (history.length) { | |
gap = 0; | |
let utxo = await getUTXOs(address); | |
if (utxo.length) { | |
allUtxos.push(utxo); | |
} | |
} else { | |
gap++; | |
} | |
index++; | |
} | |
} | |
} | |
async function getUTXOs(address) { | |
let scriptHash = getScriptHash(address); | |
try { | |
let utxos = await ecl.blockchainScripthash_listunspent(scriptHash); | |
return utxos; | |
} catch (e) { | |
console.error(e); | |
process.exit(1); | |
} | |
} | |
function pubkeyToAddress(pubkey, type) { | |
switch (type) { | |
case AddressType.p2pkh: | |
return bitcoin.payments.p2pkh({ pubkey, network }).address; | |
case AddressType.p2sh: | |
return bitcoin.payments.p2sh({ redeem: bitcoin.payments.p2wpkh({ pubkey, network }) }).address; | |
case AddressType.p2wpkh: | |
return bitcoin.payments.p2wpkh({ pubkey, network }).address; | |
case AddressType.p2tr: | |
return bitcoin.payments.p2tr({ internalPubkey: pubkey.subarray(1), network }).address; | |
default: | |
console.error(`Invalid script type: ${type}`); | |
process.exit(1); | |
} | |
} | |
function getScriptHash(address) { | |
let func; | |
if (address[0] === "1") { | |
func = bitcoin.payments.p2pkh; | |
} else if (address[0] === "3") { | |
func = bitcoin.payments.p2sh; | |
} else if (address.startsWith("bc1q")) { | |
func = bitcoin.payments.p2wpkh; | |
} else if (address.startsWith("bc1p")) { | |
func = bitcoin.payments.p2tr; | |
} else { | |
console.error(`Invalid address: ${address}`); | |
process.exit(1); | |
} | |
let { output } = func({ address: address, network: network }); | |
let bufferHash = crypto.createHash("sha256").update(output).digest(); | |
return bufferHash.reverse().toString("hex"); | |
} | |
async function getHistory(address) { | |
let scriptHash = getScriptHash(address); | |
try { | |
let utxos = await ecl.blockchainScripthash_getHistory(scriptHash); | |
return utxos; | |
} catch (e) { | |
console.error(e); | |
console.error(address); | |
process.exit(1); | |
} | |
} | |
async function startElectrum() { | |
ecl = new ElectrumCli( | |
"50001", | |
"localhost", | |
"tcp" | |
); | |
await ecl.connect(); | |
} | |
async function stopElectrum() { | |
try { | |
await ecl.close(); | |
} catch (e) { | |
console.error(e); | |
process.exit(1); | |
} | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment