Skip to content

Instantly share code, notes, and snippets.

@ottosch
Created March 29, 2024 21:49
Show Gist options
  • Save ottosch/aae9110afc7fcb856c6e06a716d4ae71 to your computer and use it in GitHub Desktop.
Save ottosch/aae9110afc7fcb856c6e06a716d4ae71 to your computer and use it in GitHub Desktop.
Extract outpoints (utxos) from addresses or xpubs
// 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