Skip to content

Instantly share code, notes, and snippets.

@aymericb
Created April 13, 2014 17:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save aymericb/10593853 to your computer and use it in GitHub Desktop.
Save aymericb/10593853 to your computer and use it in GitHub Desktop.
A quick and dirty script written in TypeScript, using node.js and the Blockchain.info API, to compute the balance of a Bitcoin wallet file.
/// <reference path="../lib/node.d.ts"/>
/// <reference path="../lib/underscore.d.ts"/>
/// <reference path="../lib/node-fibers.d.ts"/>
/**
* A quick and dirty script written in TypeScript, using node.js and the Blockchain.info API,
* to compute the balance of a Bitcoin wallet file. It supports encrypted wallets as well.
* The script goes through all the addresses from the internal key pool, ask blochain.info
* for the balance, and sum it all up.
* @author Aymeric Barthe
*/
import fs = require("fs");
import _ = require("underscore");
import crypto = require("crypto");
var base58: any = require("bs58");
import http = require("http");
import Fiber = require("fibers");
import Future = require("fibers/future");
enum ParseState {
None,
C,
K,
E
};
/**
* Read a BitcoinQT wallet, and extract all the public keys from the key pool
*/
function readWallet(path: string, callback: (err: ErrnoException, keys: NodeBuffer[])=>void ) {
fs.readFile(path, (err: ErrnoException, data: NodeBuffer) => {
if (err) {
callback(err, null);
return;
}
var keys: NodeBuffer[] = [];
// Look for 'ckey' or 'key' in the BerkeleyDB key, but avoid 'mkey'
// See CWalletDB::ReadKeyValue()
// Unfortunately, node-bdb does not support Cursors, so we monkey parse the file.
var state: ParseState = ParseState.None;
for (var i=0; i<data.length; ++i) {
var b = data.readUInt8(i);
switch(state) {
case ParseState.None:
if (b === 0x63 /*c*/)
state = ParseState.C;
else if (b === 0x6b /*k*/ && i>1 && data.readUInt8(i-1) !== 0x6d /*m*/ )
state = ParseState.K; // avoid 'mkey'
break;
case ParseState.C:
if (b === 0x6b /*k*/)
state = ParseState.K;
else
state = ParseState.None;
break;
case ParseState.K:
if (b === 0x65 /*e*/)
state = ParseState.E;
else
state = ParseState.None;
break;
case ParseState.E:
if (b === 0x79 /*y*/) {
state = ParseState.None;
var length = data.readUInt8(++i);
if (length<65) {
// Max length is 32*2+1
// See CPubKey::Unserialize in key.h / Bitcoin
keys.push(data.slice(++i, length+i));
}
}
else
state = ParseState.None;
break;
}
}
callback(err, keys);
});
}
function RIPEMD160(buffer: NodeBuffer): NodeBuffer {
var hash = crypto.createHash("ripemd160");
hash.update(buffer);
return <any>(hash.digest()); // node.d.ts definition is wrong
}
function SHA256(buffer: NodeBuffer): NodeBuffer {
var hash = crypto.createHash("sha256");
hash.update(buffer);
return <any>(hash.digest()); // node.d.ts definition is wrong
}
/**
* Convert a public key to a Bitcoin address
* See https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses
*/
function convertToAddress(key:NodeBuffer): string {
// Compute Hash 160
// It does not matter if the key is compressed or not (checked source code)
var hash160 = RIPEMD160(SHA256(key));
// Prepend with "0x00"
var buffer = new Buffer(21);
buffer[0] = 0x00; // Main network flag
hash160.copy(buffer, 1); // hash160
// Compute checksum
var checksum = SHA256(SHA256(buffer));
// Compute address buffer
var address_buffer = new Buffer(25);
buffer.copy(address_buffer);
checksum.copy(address_buffer, 21);
// Convert to Base58
var address_array: number[] = [];
_.each(address_buffer, (b)=>address_array.push(b));
var address: string = base58.encode(address_array);
return address;
}
/**
* Compute the balance of "../../config/wallet.dat" using blockchain.info API
* and display it in the console.
*/
readWallet( "../../config/wallet.dat", (err: ErrnoException, keys: NodeBuffer[]) => {
if (err) throw err;
// Divide addresses in bulks of 200 for multiaddr API
var address_list = _.uniq(_.map(keys, convertToAddress));
var GROUP_LENGTH = 200; // How many URLs we query with a single API call
var address_bulks: string[][] = _.toArray(_.groupBy(address_list, (addr, idx)=>Math.floor(idx/GROUP_LENGTH)));
// Call blockchain.info multiaddr API
// There is a limit of 159 requests per 5 mins
var total = 0;
Fiber( () => {
var remaining = address_bulks.length;
var outer_future = new Future();
_.each(address_bulks, (address_list)=> {
var inner_future = new Future();
Fiber( () => {
var url = <string>_.reduce(address_list, (memo, addr) => memo+=addr+"|", "http://blockchain.info/multiaddr?active=");
url = url.substring(0, url.length-1);
http.get(url, (res) => {
var data = "";
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
var obj = JSON.parse(data);
total += _.reduce(obj.addresses, (memo, addr_json: any) => memo+=addr_json.final_balance, 0);
inner_future.return();
});
});
inner_future.wait();
if (--remaining == 0)
outer_future.return();
}).run();
});
outer_future.wait();
console.log("Final balance: "+(total/100000000));
}).run();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment