Created
April 13, 2014 17:32
-
-
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.
This file contains hidden or 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
/// <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