510 lines
16 KiB
JavaScript
510 lines
16 KiB
JavaScript
/*
|
|
tx.js - Bitcoin transactions for JavaScript (public domain)
|
|
|
|
Obtaining inputs:
|
|
1) http://blockchain.info/unspent?address=<address>
|
|
2) http://blockexplorer.com/q/mytransactions/<address>
|
|
|
|
Sending transactions:
|
|
1) http://bitsend.rowit.co.uk
|
|
2) http://www.blockchain.info/pushtx
|
|
*/
|
|
|
|
var TX = new function () {
|
|
|
|
var inputs = [];
|
|
var outputs = [];
|
|
var eckeys = null;
|
|
var balance = 0;
|
|
var redemption_script = null;
|
|
|
|
this.init = function(_eckeys, _redemption_script) {
|
|
outputs = [];
|
|
eckeys = _eckeys;
|
|
redemption_script = _redemption_script;
|
|
}
|
|
|
|
this.addOutput = function(addr, fval) {
|
|
outputs.push({address: addr, value: fval});
|
|
}
|
|
|
|
this.removeOutputs = function() {
|
|
outputs = [];
|
|
}
|
|
|
|
this.getBalance = function() {
|
|
return balance;
|
|
}
|
|
|
|
this.getFee = function(sendTx) {
|
|
var out = BigInteger.ZERO;
|
|
for (var i in outputs) {
|
|
var fval = outputs[i].value;
|
|
value = new BigInteger('' + Math.round(fval*1e8), 10);
|
|
out = out.add(value);
|
|
}
|
|
return balance.subtract(out);
|
|
}
|
|
|
|
this.parseInputs = function(text, address) {
|
|
try {
|
|
var res = tx_parseBCI(text, address);
|
|
} catch(err) {
|
|
var res = parseTxs(text, address);
|
|
}
|
|
|
|
balance = res.balance;
|
|
inputs = res.unspenttxs;
|
|
}
|
|
|
|
this.rebuild = function(sendTx, resign) {
|
|
if (!resign)
|
|
sendTx = new Bitcoin.Transaction();
|
|
|
|
var selectedOuts = [];
|
|
for (var hash in inputs) {
|
|
if (!inputs.hasOwnProperty(hash))
|
|
continue;
|
|
for (var index in inputs[hash]) {
|
|
if (!inputs[hash].hasOwnProperty(index))
|
|
continue;
|
|
var script = parseScript(inputs[hash][index].script);
|
|
var b64hash = Crypto.util.bytesToBase64(Crypto.util.hexToBytes(hash));
|
|
var txin = new Bitcoin.TransactionIn({outpoint: {hash: b64hash, index: index}, script: script, sequence: 4294967295});
|
|
selectedOuts.push(txin);
|
|
if (!resign)
|
|
sendTx.addInput(txin);
|
|
}
|
|
}
|
|
|
|
for (var i in outputs) {
|
|
var address = outputs[i].address;
|
|
var fval = outputs[i].value;
|
|
var value = new BigInteger('' + Math.round(fval * 1e8), 10);
|
|
if (!resign)
|
|
sendTx.addOutput(new Bitcoin.Address(address), value);
|
|
}
|
|
|
|
var hashType = 1; // SIGHASH_ALL
|
|
for (var i = 0; i < sendTx.ins.length; i++) {
|
|
var connectedScript = selectedOuts[i].script;
|
|
var hash = sendTx.hashTransactionForSignature(redemption_script, i, hashType);
|
|
var script = new Bitcoin.Script();
|
|
|
|
// No idea why this remains in Bitcoin code...
|
|
script.writeOp(0);
|
|
|
|
for (var j = 0; j < eckeys.length; j++ ) {
|
|
var signature = eckeys[j].sign(hash);
|
|
signature.push(parseInt(hashType, 10));
|
|
script.writeBytes(signature);
|
|
}
|
|
|
|
script.writeBytes(redemption_script.buffer);
|
|
sendTx.ins[i].script = script;
|
|
}
|
|
return sendTx;
|
|
};
|
|
|
|
this.construct = function() {
|
|
return this.rebuild(null, false);
|
|
}
|
|
|
|
this.resign = function(sendTx) {
|
|
return this.rebuild(sendTx, true);
|
|
}
|
|
|
|
function uint(f, size) {
|
|
if (f.length < size)
|
|
return 0;
|
|
var bytes = f.slice(0, size);
|
|
var pos = 1;
|
|
var n = 0;
|
|
for (var i = 0; i < size; i++) {
|
|
var b = f.shift();
|
|
n += b * pos;
|
|
pos *= 256;
|
|
}
|
|
return size <= 4 ? n : bytes;
|
|
}
|
|
|
|
function u8(f) { return uint(f,1); }
|
|
function u16(f) { return uint(f,2); }
|
|
function u32(f) { return uint(f,4); }
|
|
function u64(f) { return uint(f,8); }
|
|
|
|
function errv(val) {
|
|
return (val instanceof BigInteger || val > 0xffff);
|
|
}
|
|
|
|
function readBuffer(f, size) {
|
|
var res = f.slice(0, size);
|
|
for (var i = 0; i < size; i++) f.shift();
|
|
return res;
|
|
}
|
|
|
|
function readString(f) {
|
|
var len = readVarInt(f);
|
|
if (errv(len)) return [];
|
|
return readBuffer(f, len);
|
|
}
|
|
|
|
function readVarInt(f) {
|
|
var t = u8(f);
|
|
if (t == 0xfd) return u16(f); else
|
|
if (t == 0xfe) return u32(f); else
|
|
if (t == 0xff) return u64(f); else
|
|
return t;
|
|
}
|
|
|
|
this.deserialize = function(bytes) {
|
|
var sendTx = new Bitcoin.Transaction();
|
|
|
|
var f = bytes.slice(0);
|
|
var tx_ver = u32(f);
|
|
var vin_sz = readVarInt(f);
|
|
if (errv(vin_sz))
|
|
return null;
|
|
|
|
for (var i = 0; i < vin_sz; i++) {
|
|
var op = readBuffer(f, 32);
|
|
var n = u32(f);
|
|
var script = readString(f);
|
|
var seq = u32(f);
|
|
var txin = new Bitcoin.TransactionIn({
|
|
outpoint: {
|
|
hash: Crypto.util.bytesToBase64(op),
|
|
index: n
|
|
},
|
|
script: new Bitcoin.Script(script),
|
|
sequence: seq
|
|
});
|
|
sendTx.addInput(txin);
|
|
}
|
|
|
|
var vout_sz = readVarInt(f);
|
|
|
|
if (errv(vout_sz))
|
|
return null;
|
|
|
|
for (var i = 0; i < vout_sz; i++) {
|
|
var value = u64(f);
|
|
var script = readString(f);
|
|
|
|
var txout = new Bitcoin.TransactionOut({
|
|
value: value,
|
|
script: new Bitcoin.Script(script)
|
|
});
|
|
|
|
sendTx.addOutput(txout);
|
|
}
|
|
var lock_time = u32(f);
|
|
sendTx.lock_time = lock_time;
|
|
return sendTx;
|
|
};
|
|
|
|
this.toBBE = function(sendTx) {
|
|
//serialize to Bitcoin Block Explorer format
|
|
var buf = sendTx.serialize();
|
|
var hash = Crypto.SHA256(Crypto.SHA256(buf, {asBytes: true}), {asBytes: true});
|
|
|
|
var r = {};
|
|
r['hash'] = Crypto.util.bytesToHex(hash.reverse());
|
|
r['ver'] = sendTx.version;
|
|
r['vin_sz'] = sendTx.ins.length;
|
|
r['vout_sz'] = sendTx.outs.length;
|
|
r['lock_time'] = sendTx.lock_time;
|
|
r['size'] = buf.length;
|
|
r['in'] = []
|
|
r['out'] = []
|
|
|
|
for (var i = 0; i < sendTx.ins.length; i++) {
|
|
var txin = sendTx.ins[i];
|
|
var hash = Crypto.util.base64ToBytes(txin.outpoint.hash);
|
|
var n = txin.outpoint.index;
|
|
var prev_out = {'hash': Crypto.util.bytesToHex(hash.reverse()), 'n': n};
|
|
var seq = txin.sequence;
|
|
|
|
if (n == 4294967295) {
|
|
var cb = Crypto.util.bytesToHex(txin.script.buffer);
|
|
r['in'].push({'prev_out': prev_out, 'coinbase' : cb, 'sequence':seq});
|
|
} else {
|
|
var ss = dumpScript(txin.script);
|
|
r['in'].push({'prev_out': prev_out, 'scriptSig' : ss, 'sequence':seq});
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < sendTx.outs.length; i++) {
|
|
var txout = sendTx.outs[i];
|
|
var bytes = txout.value.slice(0);
|
|
var fval = parseFloat(Bitcoin.Util.formatValue(bytes.reverse()));
|
|
var value = fval.toFixed(8);
|
|
var spk = dumpScript(txout.script);
|
|
r['out'].push({'value' : value, 'scriptPubKey': spk});
|
|
}
|
|
|
|
return JSON.stringify(r, null, 4);
|
|
};
|
|
|
|
this.fromBBE = function(text) {
|
|
//deserialize from Bitcoin Block Explorer format
|
|
var sendTx = new Bitcoin.Transaction();
|
|
var r = JSON.parse(text);
|
|
if (!r)
|
|
return sendTx;
|
|
var tx_ver = r['ver'];
|
|
var vin_sz = r['vin_sz'];
|
|
|
|
for (var i = 0; i < vin_sz; i++) {
|
|
var txi = r['in'][i];
|
|
var hash = Crypto.util.hexToBytes(txi['prev_out']['hash']);
|
|
var n = txi['prev_out']['n'];
|
|
|
|
if (txi['coinbase'])
|
|
var script = Crypto.util.hexToBytes(txi['coinbase']);
|
|
else
|
|
var script = parseScript(txi['scriptSig']);
|
|
|
|
var seq = txi['sequence'] === undefined ? 4294967295 : txi['sequence'];
|
|
|
|
var txin = new Bitcoin.TransactionIn({
|
|
outpoint: {
|
|
hash: Crypto.util.bytesToBase64(hash.reverse()),
|
|
index: n
|
|
},
|
|
script: new Bitcoin.Script(script),
|
|
sequence: seq
|
|
});
|
|
sendTx.addInput(txin);
|
|
}
|
|
|
|
var vout_sz = r['vout_sz'];
|
|
|
|
TX.removeOutputs();
|
|
for (var i = 0; i < vout_sz; i++) {
|
|
var txo = r['out'][i];
|
|
var fval = parseFloat(txo['value']);
|
|
var value = new BigInteger('' + Math.round(fval * 1e8), 10);
|
|
var script = parseScript(txo['scriptPubKey']);
|
|
|
|
if (value instanceof BigInteger) {
|
|
value = value.toByteArrayUnsigned().reverse();
|
|
while (value.length < 8) value.push(0);
|
|
}
|
|
|
|
var txout = new Bitcoin.TransactionOut({
|
|
value: value,
|
|
script: new Bitcoin.Script(script)
|
|
});
|
|
|
|
sendTx.addOutput(txout);
|
|
TX.addOutput(txo,fval);
|
|
}
|
|
sendTx.lock_time = r['lock_time'];
|
|
return sendTx;
|
|
};
|
|
return this;
|
|
};
|
|
|
|
function dumpScript(script) {
|
|
var out = [];
|
|
for (var i = 0; i < script.chunks.length; i++) {
|
|
var chunk = script.chunks[i];
|
|
var op = new Bitcoin.Opcode(chunk);
|
|
typeof chunk == 'number' ? out.push(op.toString()) :
|
|
out.push(Crypto.util.bytesToHex(chunk));
|
|
}
|
|
return out.join(' ');
|
|
}
|
|
|
|
// blockchain.info parser (adapted)
|
|
// uses http://blockchain.info/unspent?address=<address>
|
|
function tx_parseBCI(data, address) {
|
|
var r = JSON.parse(data);
|
|
var txs = r.unspent_outputs;
|
|
|
|
if (!txs)
|
|
throw 'Not a BCI format';
|
|
|
|
delete unspenttxs;
|
|
var unspenttxs = {};
|
|
var balance = BigInteger.ZERO;
|
|
for (var i in txs) {
|
|
var o = txs[i];
|
|
var lilendHash = o.tx_hash;
|
|
|
|
//convert script back to BBE-compatible text
|
|
var script = dumpScript( new Bitcoin.Script(Crypto.util.hexToBytes(o.script)) );
|
|
|
|
var value = new BigInteger('' + o.value, 10);
|
|
if (!(lilendHash in unspenttxs))
|
|
unspenttxs[lilendHash] = {};
|
|
unspenttxs[lilendHash][o.tx_output_n] = {amount: value, script: script};
|
|
balance = balance.add(value);
|
|
}
|
|
return {balance:balance, unspenttxs:unspenttxs};
|
|
}
|
|
|
|
// blockexplorer parser (by BTCurious)
|
|
// uses http://blockexplorer.com/q/mytransactions/<address>
|
|
// --->8---
|
|
function parseTxs(data, address) {
|
|
|
|
var address = address.toString();
|
|
var tmp = JSON.parse(data);
|
|
var txs = [];
|
|
for (var a in tmp) {
|
|
if (!tmp.hasOwnProperty(a))
|
|
continue;
|
|
txs.push(tmp[a]);
|
|
}
|
|
|
|
// Sort chronologically
|
|
txs.sort(function(a,b) {
|
|
if (a.time > b.time) return 1;
|
|
else if (a.time < b.time) return -1;
|
|
return 0;
|
|
})
|
|
|
|
delete unspenttxs;
|
|
var unspenttxs = {}; // { "<hash>": { <output index>: { amount:<amount>, script:<script> }}}
|
|
|
|
var balance = BigInteger.ZERO;
|
|
|
|
// Enumerate the transactions
|
|
for (var a in txs) {
|
|
|
|
if (!txs.hasOwnProperty(a))
|
|
continue;
|
|
var tx = txs[a];
|
|
if (tx.ver != 1) throw "Unknown version found. Expected version 1, found version " + tx.ver;
|
|
|
|
// Enumerate inputs
|
|
for (var b in tx.in ) {
|
|
if (!tx.in.hasOwnProperty(b))
|
|
continue;
|
|
var input = tx.in[b];
|
|
var p = input.prev_out;
|
|
var lilendHash = endian(p.hash)
|
|
// if this came from a transaction to our address...
|
|
if (lilendHash in unspenttxs) {
|
|
unspenttx = unspenttxs[lilendHash];
|
|
|
|
// remove from unspent transactions, and deduce the amount from the balance
|
|
balance = balance.subtract(unspenttx[p.n].amount);
|
|
delete unspenttx[p.n]
|
|
if (isEmpty(unspenttx)) {
|
|
delete unspenttxs[lilendHash]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enumerate outputs
|
|
var i = 0;
|
|
for (var b in tx.out) {
|
|
if (!tx.out.hasOwnProperty(b))
|
|
continue;
|
|
|
|
var output = tx.out[b];
|
|
|
|
// if this was sent to our address...
|
|
if (output.address == address) {
|
|
// remember the transaction, index, amount, and script, and add the amount to the wallet balance
|
|
var value = btcstr2bignum(output.value);
|
|
var lilendHash = endian(tx.hash)
|
|
if (!(lilendHash in unspenttxs))
|
|
unspenttxs[lilendHash] = {};
|
|
unspenttxs[lilendHash][i] = {amount: value, script: output.scriptPubKey};
|
|
balance = balance.add(value);
|
|
}
|
|
i = i + 1;
|
|
}
|
|
}
|
|
|
|
return {balance:balance, unspenttxs:unspenttxs};
|
|
}
|
|
|
|
function isEmpty(ob) {
|
|
for(var i in ob){ if(ob.hasOwnProperty(i)){return false;}}
|
|
return true;
|
|
}
|
|
|
|
function endian(string) {
|
|
var out = []
|
|
for(var i = string.length; i > 0; i-=2) {
|
|
out.push(string.substring(i-2,i));
|
|
}
|
|
return out.join("");
|
|
}
|
|
|
|
function btcstr2bignum(btc) {
|
|
var i = btc.indexOf('.');
|
|
var value = new BigInteger(btc.replace(/\./,''));
|
|
var diff = 9 - (btc.length - i);
|
|
if (i == -1) {
|
|
var mul = "100000000";
|
|
} else if (diff < 0) {
|
|
return value.divide(new BigInteger(Math.pow(10,-1*diff).toString()));
|
|
} else {
|
|
var mul = Math.pow(10,diff).toString();
|
|
}
|
|
return value.multiply(new BigInteger(mul));
|
|
}
|
|
|
|
function parseScript(script) {
|
|
var newScript = new Bitcoin.Script();
|
|
var s = script.split(" ");
|
|
for (var i in s) {
|
|
if (Bitcoin.Opcode.map.hasOwnProperty(s[i])){
|
|
newScript.writeOp(Bitcoin.Opcode.map[s[i]]);
|
|
} else {
|
|
newScript.writeBytes(Crypto.util.hexToBytes(s[i]));
|
|
}
|
|
}
|
|
return newScript;
|
|
}
|
|
// --->8---
|
|
|
|
// Some cross-domain magic (to bypass Access-Control-Allow-Origin)
|
|
function tx_fetch(url, onSuccess, onError, postdata) {
|
|
var useYQL = true;
|
|
|
|
if (useYQL) {
|
|
var q = 'select * from html where url="'+url+'"';
|
|
if (postdata) {
|
|
q = 'use "http://brainwallet.github.com/js/htmlpost.xml" as htmlpost; ';
|
|
q += 'select * from htmlpost where url="' + url + '" ';
|
|
q += 'and postdata="' + postdata + '" and xpath="//p"';
|
|
}
|
|
url = 'https://query.yahooapis.com/v1/public/yql?q=' + encodeURIComponent(q);
|
|
}
|
|
|
|
$.ajax({
|
|
url: url,
|
|
success: function(res) {
|
|
onSuccess(useYQL ? $(res).find('results').text() : res.responseText);
|
|
},
|
|
error:function (xhr, opt, err) {
|
|
if (onError)
|
|
onError(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
var tx_dest = '1Ce8WxgwjarzLtV6zkUGgdwmAe5yjHoPXX';
|
|
var tx_sec = '5KdttCmkLPPLN4oDet53FBdPxp4N1DWoGCiigd3ES9Wuknhm8uT';
|
|
var tx_addr = '32c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
|
|
var tx_unspent = '{"unspent_outputs":[{"tx_hash":"7a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf","tx_index":5,"tx_output_n": 0,"script":"4104184f32b212815c6e522e66686324030ff7e5bf08efb21f8b00614fb7690e19131dd31304c54f37baa40db231c918106bb9fd43373e37ae31a0befc6ecaefb867ac","value": 5000000000,"value_hex": "012a05f200","confirmations":177254}]}';
|
|
|
|
function tx_test() {
|
|
//TODO
|
|
//var secret = Bitcoin.Base58.decode(tx_sec).slice(1, 33);
|
|
//var eckey = new Bitcoin.ECKey(secret);
|
|
//TX.init(eckey);
|
|
//TX.parseInputs(tx_unspent, tx_addr);
|
|
//TX.addOutput(tx_dest, 50.0);
|
|
//var sendTx = TX.construct();
|
|
//console.log(TX.toBBE(sendTx));
|
|
//console.log(Crypto.util.bytesToHex(sendTx.serialize()));
|
|
}
|