diff --git a/RpcClient.js b/RpcClient.js index 5add426..5e3b78e 100644 --- a/RpcClient.js +++ b/RpcClient.js @@ -35,7 +35,7 @@ function ClassSpec(b) { dumpPrivKey: '', encryptWallet: '', getAccount: '', - getAccountAddress: '', + getAccountAddress: 'str', getAddedNodeInfo: '', getAddressesByAccount: '', getBalance: 'str int', diff --git a/Sign.js b/Sign.js new file mode 100644 index 0000000..24889dc --- /dev/null +++ b/Sign.js @@ -0,0 +1,133 @@ + +function signOne(hash, addrStr, keys) +{ + var keyObj = keys[addrStr]; + var rawPrivKey = new Buffer(keyObj.priv, 'hex'); + var key = new KeyModule.Key(); + key.private = rawPrivKey; + var signature = key.signSync(hash); + + return signature; +} + +function signTxIn(nIn, tx, txInputs, network, keys, scripts) +{ + // locate TX input needing a signature + var txin = tx.ins[nIn]; + var scriptSig = txin.getScript(); + + // locate TX output, within txInputs + var txoutHash = txin.getOutpointHash(); + if (!(txoutHash in txInputs)) + throw new Error("signTxIn missing input hash"); + var txFrom = txInputs[txoutHash]; + var txoutIndex = txin.getOutpointIndex(); + if (txFrom.outs.length >= txoutIndex) + throw new Error("signTxIn missing input index"); + var txout = txFrom.outs[txoutIndex]; + var scriptPubKey = txout.getScript(); + + // detect type of transaction, and extract useful elements + var txType = scriptPubKey.classify(); + if (txType == TX_UNKNOWN) + throw new Error("unknown TX type"); + var scriptData = scriptPubKey.capture(); + + // if P2SH, lookup the script + var subscriptRaw = undefined; + var subscript = undefined; + var subType = undefined; + var subData = undefined; + if (txType == TX_SCRIPTHASH) { + var addr = new Address(network.addressScript, scriptData[0]); + var addrStr = addr.toString(); + if (!(addrStr in scripts)) + throw new Error("unknown script hash address"); + + subscriptRaw = new Buffer(scripts[addrStr], 'hex'); + subscript = new Script(subscriptRaw); + subType = subscript.classify(); + if (subType == TX_UNKNOWN) + throw new Error("unknown subscript TX type"); + subData = subscript.capture(); + } + + var hash = tx.hashForSignature(scriptPubKey, i, 0); + + switch (txType) { + case TX_PUBKEY: + // already signed + if (scriptSig.chunks.length > 0) + return; + + var pubkeyhash = util.sha256ripe160(scriptData[0]); + var addr = new Address(network.addressPubkey, pubkeyhash); + var addrStr = addr.toString(); + if (!(addrStr in keys)) + throw new Error("unknown pubkey"); + + var signature = signOne(hash, addrStr, keys); + scriptSig.writeBytes(signature); + break; + + case TX_PUBKEYHASH: + // already signed + if (scriptSig.chunks.length > 0) + return; + + var addr = new Address(network.addressPubkey, scriptData[0]); + var addrStr = addr.toString(); + if (!(addrStr in keys)) + throw new Error("unknown pubkey hash address"); + + var signature = signOne(hash, addrStr, keys); + scriptSig.writeBytes(signature); + scriptSig.writeBytes(key.public); + break; + + case TX_SCRIPTHASH: + // already signed + if (scriptSig.chunks.length > 0) + return; + + var addr = new Address(network.addressPubkey, subData[0]); + var addrStr = addr.toString(); + if (!(addrStr in keys)) + throw new Error("unknown script(pubkey hash) address"); + + var signature = signOne(hash, addrStr, keys); + scriptSig.writeBytes(signature); + scriptSig.writeBytes(key.public); + break; + + case TX_MULTISIG: + while (scriptSig.chunks.length < scriptData.length) { + scriptSig.writeBytes(util.EMPTY_BUFFER); + } + for (var i = 0; i < scriptData.length; i++) { + // skip already signed + if (scriptSig.chunks[i].length > 0) + continue; + + var pubkeyhash = util.sha256ripe160(scriptSig.chunks[i]); + var addr = new Address(network.addressPubkey, pubkeyhash); + var addrStr = addr.toString(); + if (!(addrStr in keys)) + continue; + + var signature = signOne(hash, addrStr, keys); + scriptSig.chunks[i] = signature; + } + break; + } + + if (txtype == TX_SCRIPTHASH) + scriptSig.writeBytes(subscriptRaw); +} + +exports.Transaction = function Transaction(tx, txInputs, network, keys, scripts) +{ + for (var i = 0; i < tx.ins.length; i++) + signTxIn(i, tx, txInputs, network, keys, scripts); +}; + diff --git a/Transaction.js b/Transaction.js index 0e20b6c..92c90db 100644 --- a/Transaction.js +++ b/Transaction.js @@ -45,7 +45,7 @@ function spec(b) { return Buffer.concat([this.o, slen, this.s, qbuf]); }; - TransactionIn.prototype.getOutpointHash = function getOutpointIndex() { + TransactionIn.prototype.getOutpointHash = function getOutpointHash() { if ("undefined" !== typeof this.o.outHashCache) { return this.o.outHashCache; } @@ -181,6 +181,19 @@ function spec(b) { return this.hash; }; + // convert encoded list of inputs to easy-to-use JS list-of-lists + Transaction.prototype.inputs = function inputs() { + var res = []; + for (var i = 0; i < this.ins.length; i++) { + var txin = this.ins[i]; + var outHash = txin.getOutpointHash(); + var outIndex = txin.getOutpointIndex(); + res.push([outHash, outIndex]); + } + + return res; + } + /** * Load and cache transaction inputs. * diff --git a/WalletKey.js b/WalletKey.js index ec78153..1fb8c23 100644 --- a/WalletKey.js +++ b/WalletKey.js @@ -31,6 +31,12 @@ function ClassSpec(b) { return obj; }; + WalletKey.prototype.fromObj = function(obj) { + this.created = obj.created; + this.privKey = new KeyModule.Key(); + this.privKey.private = new Buffer(obj.priv, 'hex'); + }; + return WalletKey; }; module.defineClass(ClassSpec); diff --git a/package.json b/package.json index cd150f6..3fa7852 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libcoin", "description": "Bitcoin Library", - "version": "0.1.3", + "version": "0.1.4", "author": { "name": "Stephen Pair", "email": "stephen@bitpay.com" @@ -10,7 +10,6 @@ {"name": "Stefan Thomas", "email": "moon@justmoon.net"}, {"name": "Jeff Garzik", "email": "jgarzik@bitpay.com"} ], - "main": "./index", "keywords": [ "bitcoin", "btc", @@ -25,15 +24,15 @@ }, "scripts": {}, "dependencies": { - "classtool": ">=1.0.0", - "base58-native": ">=0.1.1", - "bindings": ">=1.1.0", - "bufferput": ">=0.1.1", - "bignum": "0.6.1", - "binary": "0.3.0", - "step": "0.0.4", - "buffers": ">=0.1.1", - "buffertools": "1.1.1" + "classtool": "=1.0.0", + "base58-native": "=0.1.1", + "bindings": "=1.1.0", + "bufferput": "=0.1.1", + "bignum": "=0.6.1", + "binary": "=0.3.0", + "step": "=0.0.4", + "buffers": "=0.1.1", + "buffertools": "=1.1.1" }, "devDependencies": {}, "license": "MIT" diff --git a/test/basic.js b/test/basic.js index cdf1e71..f34a2a7 100644 --- a/test/basic.js +++ b/test/basic.js @@ -5,6 +5,7 @@ var Address = require('../Address').class(); var PrivateKey = require('../PrivateKey').class(); var networks = require('../networks'); var KeyModule = require('../Key'); +var coinUtil = require('../util/util'); suite('basic'); @@ -110,8 +111,22 @@ function is_invalid(datum) assert.equal(valid, false); } +function test_value(datum) +{ + if (datum.length != 2) + throw new Error("Bad test"); + + var decimal = datum[0]; + var intStr = datum[1]; + + var bn = coinUtil.parseValue(decimal); + assert.notEqual(bn, undefined); + assert.equal(bn.toString(), intStr); +} + var dataValid = JSON.parse(fs.readFileSync('test/base58_keys_valid.json')); var dataInvalid = JSON.parse(fs.readFileSync('test/base58_keys_invalid.json')); +var dataValues = JSON.parse(fs.readFileSync('test/values.json')); test('valid', function() { dataValid.forEach(function(datum) { is_valid(datum); }); @@ -121,3 +136,7 @@ test('invalid', function() { dataInvalid.forEach(function(datum) { is_invalid(datum); }); }); +test('values', function() { + dataValues.forEach(function(datum) { test_value(datum); }); +}); + diff --git a/test/values.json b/test/values.json new file mode 100644 index 0000000..517acbc --- /dev/null +++ b/test/values.json @@ -0,0 +1,7 @@ +[ + [ "0", "0" ], + [ "1.0", "100000000" ], + [ "0.1", "10000000" ], + [ ".1", "10000000" ], + [ "0.0005", "50000" ] +] diff --git a/util/util.js b/util/util.js index b57dfab..973e261 100644 --- a/util/util.js +++ b/util/util.js @@ -108,6 +108,49 @@ var formatValue = exports.formatValue = function (valueBuffer) { return integerPart+"."+decimalPart; }; +var reFullVal = /^\s*(\d+)\.(\d+)/; +var reFracVal = /^\s*\.(\d+)/; +var reWholeVal = /^\s*(\d+)/; + +function padFrac(frac) +{ + while (frac.length < 8) + frac = frac + '0'; + return frac; +} + +function parseFullValue(res) +{ + return bignum(res[1]).mul('100000000').add(padFrac(res[2])); +} + +function parseFracValue(res) +{ + return bignum(padFrac(res[1])); +} + +function parseWholeValue(res) +{ + return bignum(res[1]).mul('100000000'); +} + +exports.parseValue = function parseValue(valueStr) +{ + var res = valueStr.match(reFullVal); + if (res) + return parseFullValue(res); + + res = valueStr.match(reFracVal); + if (res) + return parseFracValue(res); + + res = valueStr.match(reWholeVal); + if (res) + return parseWholeValue(res); + + return undefined; +}; + // Utility that synchronizes function calls based on a key var createSynchrotron = exports.createSynchrotron = function (fn) { var table = {}; @@ -280,15 +323,6 @@ var varStrBuf = exports.varStrBuf = function varStrBuf(s) { return Buffer.concat(varIntBuf(s.length), s); }; -var buf64 = exports.buf64 = function buf64(n) { - var lo = n & 0xffffffff; - var hi = (n >>> 32); - var buf = new Buffer(4 + 4); - buf.writeUInt32LE(lo, 0); - buf.writeUInt32LE(hi, 4); - return buf; -}; - // Initializations exports.NULL_HASH = new Buffer(32).fill(0); exports.EMPTY_BUFFER = new Buffer(0);