|
|
@ -5,32 +5,35 @@ var networks = require('./networks') |
|
|
|
var Address = require('./address') |
|
|
|
var HDNode = require('./hdnode') |
|
|
|
var Transaction = require('./transaction') |
|
|
|
var Script = require('./script') |
|
|
|
|
|
|
|
function Wallet(seed, network) { |
|
|
|
function Wallet(seed, network, unspents) { |
|
|
|
seed = seed || crypto.randomBytes(32) |
|
|
|
network = network || networks.bitcoin |
|
|
|
|
|
|
|
// Stored in a closure to make accidental serialization less likely
|
|
|
|
var masterkey = null |
|
|
|
var me = this |
|
|
|
var accountZero = null |
|
|
|
var internalAccount = null |
|
|
|
var externalAccount = null |
|
|
|
var masterKey = HDNode.fromSeedBuffer(seed, network) |
|
|
|
|
|
|
|
// HD first-level child derivation method should be hardened
|
|
|
|
// See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254
|
|
|
|
var accountZero = masterKey.deriveHardened(0) |
|
|
|
var externalAccount = accountZero.derive(0) |
|
|
|
var internalAccount = accountZero.derive(1) |
|
|
|
|
|
|
|
// Addresses
|
|
|
|
this.addresses = [] |
|
|
|
this.changeAddresses = [] |
|
|
|
this.network = network |
|
|
|
this.outputs = unspents ? processUnspentOutputs(unspents) : {} |
|
|
|
|
|
|
|
// Transaction output data
|
|
|
|
this.outputs = {} |
|
|
|
|
|
|
|
// Make a new master key
|
|
|
|
// FIXME: remove in 2.x.y
|
|
|
|
var me = this |
|
|
|
this.newMasterKey = function(seed) { |
|
|
|
console.warn('newMasterKey is deprecated, please make a new Wallet instance instead') |
|
|
|
|
|
|
|
seed = seed || crypto.randomBytes(32) |
|
|
|
masterkey = HDNode.fromSeedBuffer(seed, network) |
|
|
|
masterKey = HDNode.fromSeedBuffer(seed, network) |
|
|
|
|
|
|
|
// HD first-level child derivation method should be hardened
|
|
|
|
// See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254
|
|
|
|
accountZero = masterkey.deriveHardened(0) |
|
|
|
accountZero = masterKey.deriveHardened(0) |
|
|
|
externalAccount = accountZero.derive(0) |
|
|
|
internalAccount = accountZero.derive(1) |
|
|
|
|
|
|
@ -40,196 +43,206 @@ function Wallet(seed, network) { |
|
|
|
me.outputs = {} |
|
|
|
} |
|
|
|
|
|
|
|
this.newMasterKey(seed) |
|
|
|
this.getMasterKey = function() { return masterKey } |
|
|
|
this.getAccountZero = function() { return accountZero } |
|
|
|
this.getExternalAccount = function() { return externalAccount } |
|
|
|
this.getInternalAccount = function() { return internalAccount } |
|
|
|
} |
|
|
|
|
|
|
|
this.generateAddress = function() { |
|
|
|
var key = externalAccount.derive(this.addresses.length) |
|
|
|
this.addresses.push(key.getAddress().toString()) |
|
|
|
return this.addresses[this.addresses.length - 1] |
|
|
|
} |
|
|
|
Wallet.prototype.createTx = function(to, value, fixedFee, changeAddress) { |
|
|
|
assert(value > this.network.dustThreshold, value + ' must be above dust threshold (' + this.network.dustThreshold + ' Satoshis)') |
|
|
|
|
|
|
|
this.generateChangeAddress = function() { |
|
|
|
var key = internalAccount.derive(this.changeAddresses.length) |
|
|
|
this.changeAddresses.push(key.getAddress().toString()) |
|
|
|
return this.changeAddresses[this.changeAddresses.length - 1] |
|
|
|
} |
|
|
|
var utxos = getCandidateOutputs(this.outputs, value) |
|
|
|
var accum = 0 |
|
|
|
var subTotal = value |
|
|
|
var addresses = [] |
|
|
|
|
|
|
|
this.getBalance = function() { |
|
|
|
return this.getUnspentOutputs().reduce(function(memo, output){ |
|
|
|
return memo + output.value |
|
|
|
}, 0) |
|
|
|
} |
|
|
|
var tx = new Transaction() |
|
|
|
tx.addOutput(to, value) |
|
|
|
|
|
|
|
this.getUnspentOutputs = function() { |
|
|
|
var utxo = [] |
|
|
|
for (var i = 0; i < utxos.length; ++i) { |
|
|
|
var utxo = utxos[i] |
|
|
|
addresses.push(utxo.address) |
|
|
|
|
|
|
|
for(var key in this.outputs){ |
|
|
|
var output = this.outputs[key] |
|
|
|
if(!output.to) utxo.push(outputToUnspentOutput(output)) |
|
|
|
var outpoint = utxo.from.split(':') |
|
|
|
tx.addInput(outpoint[0], parseInt(outpoint[1])) |
|
|
|
|
|
|
|
var fee = fixedFee == undefined ? estimatePaddedFee(tx, this.network) : fixedFee |
|
|
|
|
|
|
|
accum += utxo.value |
|
|
|
subTotal = value + fee |
|
|
|
if (accum >= subTotal) { |
|
|
|
var change = accum - subTotal |
|
|
|
|
|
|
|
if (change > this.network.dustThreshold) { |
|
|
|
tx.addOutput(changeAddress || this.getChangeAddress(), change) |
|
|
|
} |
|
|
|
|
|
|
|
return utxo |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.setUnspentOutputs = function(utxo) { |
|
|
|
var outputs = {} |
|
|
|
assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal) |
|
|
|
|
|
|
|
utxo.forEach(function(uo){ |
|
|
|
validateUnspentOutput(uo) |
|
|
|
var o = unspentOutputToOutput(uo) |
|
|
|
outputs[o.from] = o |
|
|
|
}) |
|
|
|
this.signWith(tx, addresses) |
|
|
|
return tx |
|
|
|
} |
|
|
|
|
|
|
|
this.outputs = outputs |
|
|
|
} |
|
|
|
Wallet.prototype.processPendingTx = function(tx){ |
|
|
|
processTx.bind(this)(tx, true) |
|
|
|
} |
|
|
|
|
|
|
|
function outputToUnspentOutput(output){ |
|
|
|
var hashAndIndex = output.from.split(":") |
|
|
|
Wallet.prototype.processConfirmedTx = function(tx){ |
|
|
|
processTx.bind(this)(tx, false) |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
hash: hashAndIndex[0], |
|
|
|
outputIndex: parseInt(hashAndIndex[1]), |
|
|
|
address: output.address, |
|
|
|
value: output.value, |
|
|
|
pending: output.pending |
|
|
|
} |
|
|
|
} |
|
|
|
Wallet.prototype.generateAddress = function() { |
|
|
|
var k = this.addresses.length |
|
|
|
var address = this.getExternalAccount().derive(k).getAddress() |
|
|
|
|
|
|
|
function unspentOutputToOutput(o) { |
|
|
|
var hash = o.hash |
|
|
|
var key = hash + ":" + o.outputIndex |
|
|
|
return { |
|
|
|
from: key, |
|
|
|
address: o.address, |
|
|
|
value: o.value, |
|
|
|
pending: o.pending |
|
|
|
} |
|
|
|
} |
|
|
|
this.addresses.push(address.toString()) |
|
|
|
|
|
|
|
function validateUnspentOutput(uo) { |
|
|
|
var missingField |
|
|
|
return this.getReceiveAddress() |
|
|
|
} |
|
|
|
|
|
|
|
if (isNullOrUndefined(uo.hash)) { |
|
|
|
missingField = "hash" |
|
|
|
} |
|
|
|
Wallet.prototype.generateChangeAddress = function() { |
|
|
|
var k = this.changeAddresses.length |
|
|
|
var address = this.getInternalAccount().derive(k).getAddress() |
|
|
|
|
|
|
|
var requiredKeys = ['outputIndex', 'address', 'value'] |
|
|
|
requiredKeys.forEach(function (key) { |
|
|
|
if (isNullOrUndefined(uo[key])){ |
|
|
|
missingField = key |
|
|
|
} |
|
|
|
}) |
|
|
|
this.changeAddresses.push(address.toString()) |
|
|
|
|
|
|
|
if (missingField) { |
|
|
|
var message = [ |
|
|
|
'Invalid unspent output: key', missingField, 'is missing.', |
|
|
|
'A valid unspent output must contain' |
|
|
|
] |
|
|
|
message.push(requiredKeys.join(', ')) |
|
|
|
message.push("and hash") |
|
|
|
throw new Error(message.join(' ')) |
|
|
|
} |
|
|
|
} |
|
|
|
return this.getChangeAddress() |
|
|
|
} |
|
|
|
|
|
|
|
function isNullOrUndefined(value) { |
|
|
|
return value == undefined |
|
|
|
} |
|
|
|
Wallet.prototype.getBalance = function() { |
|
|
|
return this.getUnspentOutputs().reduce(function(accum, output) { |
|
|
|
return accum + output.value |
|
|
|
}, 0) |
|
|
|
} |
|
|
|
|
|
|
|
this.processPendingTx = function(tx){ |
|
|
|
processTx(tx, true) |
|
|
|
Wallet.prototype.getChangeAddress = function() { |
|
|
|
if (this.changeAddresses.length === 0) { |
|
|
|
this.generateChangeAddress() |
|
|
|
} |
|
|
|
|
|
|
|
this.processConfirmedTx = function(tx){ |
|
|
|
processTx(tx, false) |
|
|
|
} |
|
|
|
return this.changeAddresses[this.changeAddresses.length - 1] |
|
|
|
} |
|
|
|
|
|
|
|
function processTx(tx, isPending) { |
|
|
|
var txid = tx.getId() |
|
|
|
Wallet.prototype.getInternalPrivateKey = function(index) { |
|
|
|
return this.getInternalAccount().derive(index).privKey |
|
|
|
} |
|
|
|
|
|
|
|
tx.outs.forEach(function(txOut, i) { |
|
|
|
var address |
|
|
|
Wallet.prototype.getPrivateKey = function(index) { |
|
|
|
return this.getExternalAccount().derive(index).privKey |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
address = Address.fromOutputScript(txOut.script, network).toString() |
|
|
|
} catch(e) { |
|
|
|
if (!(e.message.match(/has no matching Address/))) throw e |
|
|
|
Wallet.prototype.getPrivateKeyForAddress = function(address) { |
|
|
|
if (includeAddress(this.addresses, address)) { |
|
|
|
var index = this.addresses.indexOf(address) |
|
|
|
|
|
|
|
return this.getPrivateKey(index) |
|
|
|
} |
|
|
|
|
|
|
|
if (isMyAddress(address)) { |
|
|
|
var output = txid + ':' + i |
|
|
|
if (includeAddress(this.changeAddresses, address)) { |
|
|
|
var index = this.changeAddresses.indexOf(address) |
|
|
|
|
|
|
|
me.outputs[output] = { |
|
|
|
from: output, |
|
|
|
value: txOut.value, |
|
|
|
address: address, |
|
|
|
pending: isPending |
|
|
|
} |
|
|
|
return this.getInternalPrivateKey(index) |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
tx.ins.forEach(function(txIn, i) { |
|
|
|
// copy and convert to big-endian hex
|
|
|
|
var txinId = new Buffer(txIn.hash) |
|
|
|
Array.prototype.reverse.call(txinId) |
|
|
|
txinId = txinId.toString('hex') |
|
|
|
assert(false, 'Unknown address. Make sure the address is from the keychain and has been generated') |
|
|
|
} |
|
|
|
|
|
|
|
var output = txinId + ':' + txIn.index |
|
|
|
Wallet.prototype.getReceiveAddress = function() { |
|
|
|
if (this.addresses.length === 0) { |
|
|
|
this.generateAddress() |
|
|
|
} |
|
|
|
|
|
|
|
return this.addresses[this.addresses.length - 1] |
|
|
|
} |
|
|
|
|
|
|
|
if (!(output in me.outputs)) return |
|
|
|
Wallet.prototype.getUnspentOutputs = function() { |
|
|
|
var utxo = [] |
|
|
|
|
|
|
|
if (isPending) { |
|
|
|
me.outputs[output].to = txid + ':' + i |
|
|
|
me.outputs[output].pending = true |
|
|
|
} else { |
|
|
|
delete me.outputs[output] |
|
|
|
} |
|
|
|
}) |
|
|
|
for(var key in this.outputs){ |
|
|
|
var output = this.outputs[key] |
|
|
|
if(!output.to) utxo.push(outputToUnspentOutput(output)) |
|
|
|
} |
|
|
|
|
|
|
|
this.createTx = function(to, value, fixedFee, changeAddress) { |
|
|
|
assert(value > network.dustThreshold, value + ' must be above dust threshold (' + network.dustThreshold + ' Satoshis)') |
|
|
|
return utxo |
|
|
|
} |
|
|
|
|
|
|
|
var utxos = getCandidateOutputs(value) |
|
|
|
var accum = 0 |
|
|
|
var subTotal = value |
|
|
|
var addresses = [] |
|
|
|
Wallet.prototype.setUnspentOutputs = function(utxo) { |
|
|
|
console.warn('setUnspentOutputs is deprecated, please use the constructor option instead') |
|
|
|
|
|
|
|
var tx = new Transaction() |
|
|
|
tx.addOutput(to, value) |
|
|
|
this.outputs = processUnspentOutputs(utxo) |
|
|
|
} |
|
|
|
|
|
|
|
for (var i = 0; i < utxos.length; ++i) { |
|
|
|
var utxo = utxos[i] |
|
|
|
addresses.push(utxo.address) |
|
|
|
Wallet.prototype.signWith = function(tx, addresses) { |
|
|
|
assert.equal(tx.ins.length, addresses.length, 'Number of addresses must match number of transaction inputs') |
|
|
|
|
|
|
|
var outpoint = utxo.from.split(':') |
|
|
|
tx.addInput(outpoint[0], parseInt(outpoint[1])) |
|
|
|
addresses.forEach(function(address, i) { |
|
|
|
var key = this.getPrivateKeyForAddress(address) |
|
|
|
|
|
|
|
var fee = fixedFee == undefined ? estimateFeePadChangeOutput(tx) : fixedFee |
|
|
|
tx.sign(i, key) |
|
|
|
}, this) |
|
|
|
|
|
|
|
accum += utxo.value |
|
|
|
subTotal = value + fee |
|
|
|
if (accum >= subTotal) { |
|
|
|
var change = accum - subTotal |
|
|
|
return tx |
|
|
|
} |
|
|
|
|
|
|
|
if (change > network.dustThreshold) { |
|
|
|
tx.addOutput(changeAddress || getChangeAddress(), change) |
|
|
|
} |
|
|
|
function outputToUnspentOutput(output){ |
|
|
|
var hashAndIndex = output.from.split(":") |
|
|
|
|
|
|
|
break |
|
|
|
} |
|
|
|
return { |
|
|
|
hash: hashAndIndex[0], |
|
|
|
index: parseInt(hashAndIndex[1]), |
|
|
|
address: output.address, |
|
|
|
value: output.value, |
|
|
|
pending: output.pending |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal) |
|
|
|
function estimatePaddedFee(tx, network) { |
|
|
|
var tmpTx = tx.clone() |
|
|
|
tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0) |
|
|
|
|
|
|
|
this.signWith(tx, addresses) |
|
|
|
return tx |
|
|
|
return network.estimateFee(tmpTx) |
|
|
|
} |
|
|
|
|
|
|
|
function processUnspentOutputs(utxos) { |
|
|
|
var outputs = {} |
|
|
|
|
|
|
|
utxos.forEach(function(utxo){ |
|
|
|
var hash = new Buffer(utxo.hash, 'hex') |
|
|
|
var index = utxo.index |
|
|
|
var address = utxo.address |
|
|
|
var value = utxo.value |
|
|
|
|
|
|
|
// FIXME: remove alternative in 2.x.y
|
|
|
|
if (index === undefined) index = utxo.outputIndex |
|
|
|
|
|
|
|
assert.equal(hash.length, 32, 'Expected hash length of 32, got ' + hash.length) |
|
|
|
assert.equal(typeof index, 'number', 'Expected number index, got ' + index) |
|
|
|
assert.doesNotThrow(function() { Address.fromBase58Check(address) }, 'Expected Base58 Address, got ' + address) |
|
|
|
assert.equal(typeof value, 'number', 'Expected number value, got ' + value) |
|
|
|
|
|
|
|
var key = utxo.hash + ':' + utxo.index |
|
|
|
|
|
|
|
outputs[key] = { |
|
|
|
from: key, |
|
|
|
address: address, |
|
|
|
value: value, |
|
|
|
pending: utxo.pending |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
return outputs |
|
|
|
} |
|
|
|
|
|
|
|
function getCandidateOutputs() { |
|
|
|
function getCandidateOutputs(outputs/*, value*/) { |
|
|
|
var unspent = [] |
|
|
|
|
|
|
|
for (var key in me.outputs) { |
|
|
|
var output = me.outputs[key] |
|
|
|
for (var key in outputs) { |
|
|
|
var output = outputs[key] |
|
|
|
if (!output.pending) unspent.push(output) |
|
|
|
} |
|
|
|
|
|
|
@ -238,67 +251,54 @@ function Wallet(seed, network) { |
|
|
|
}) |
|
|
|
|
|
|
|
return sortByValueDesc |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function estimateFeePadChangeOutput(tx) { |
|
|
|
var tmpTx = tx.clone() |
|
|
|
tmpTx.addOutput(getChangeAddress(), network.dustSoftThreshold || 0) |
|
|
|
function processTx(tx, isPending) { |
|
|
|
var txid = tx.getId() |
|
|
|
|
|
|
|
return network.estimateFee(tmpTx) |
|
|
|
} |
|
|
|
tx.outs.forEach(function(txOut, i) { |
|
|
|
var address |
|
|
|
|
|
|
|
function getChangeAddress() { |
|
|
|
if(me.changeAddresses.length === 0) me.generateChangeAddress(); |
|
|
|
return me.changeAddresses[me.changeAddresses.length - 1] |
|
|
|
try { |
|
|
|
address = Address.fromOutputScript(txOut.script, this.network).toString() |
|
|
|
} catch(e) { |
|
|
|
if (!(e.message.match(/has no matching Address/))) throw e |
|
|
|
} |
|
|
|
|
|
|
|
this.signWith = function(tx, addresses) { |
|
|
|
assert.equal(tx.ins.length, addresses.length, 'Number of addresses must match number of transaction inputs') |
|
|
|
|
|
|
|
addresses.forEach(function(address, i) { |
|
|
|
var key = me.getPrivateKeyForAddress(address) |
|
|
|
|
|
|
|
tx.sign(i, key) |
|
|
|
}) |
|
|
|
var myAddresses = this.addresses.concat(this.changeAddresses) |
|
|
|
if (includeAddress(myAddresses, address)) { |
|
|
|
var output = txid + ':' + i |
|
|
|
|
|
|
|
return tx |
|
|
|
this.outputs[output] = { |
|
|
|
from: output, |
|
|
|
value: txOut.value, |
|
|
|
address: address, |
|
|
|
pending: isPending |
|
|
|
} |
|
|
|
|
|
|
|
this.getMasterKey = function() { return masterkey } |
|
|
|
this.getAccountZero = function() { return accountZero } |
|
|
|
this.getInternalAccount = function() { return internalAccount } |
|
|
|
this.getExternalAccount = function() { return externalAccount } |
|
|
|
|
|
|
|
this.getPrivateKey = function(index) { |
|
|
|
return externalAccount.derive(index).privKey |
|
|
|
} |
|
|
|
}, this) |
|
|
|
|
|
|
|
this.getInternalPrivateKey = function(index) { |
|
|
|
return internalAccount.derive(index).privKey |
|
|
|
} |
|
|
|
tx.ins.forEach(function(txIn, i) { |
|
|
|
// copy and convert to big-endian hex
|
|
|
|
var txinId = new Buffer(txIn.hash) |
|
|
|
Array.prototype.reverse.call(txinId) |
|
|
|
txinId = txinId.toString('hex') |
|
|
|
|
|
|
|
this.getPrivateKeyForAddress = function(address) { |
|
|
|
var index |
|
|
|
if((index = this.addresses.indexOf(address)) > -1) { |
|
|
|
return this.getPrivateKey(index) |
|
|
|
} else if((index = this.changeAddresses.indexOf(address)) > -1) { |
|
|
|
return this.getInternalPrivateKey(index) |
|
|
|
} else { |
|
|
|
throw new Error('Unknown address. Make sure the address is from the keychain and has been generated.') |
|
|
|
} |
|
|
|
} |
|
|
|
var output = txinId + ':' + txIn.index |
|
|
|
|
|
|
|
function isReceiveAddress(address){ |
|
|
|
return me.addresses.indexOf(address) > -1 |
|
|
|
} |
|
|
|
if (!(output in this.outputs)) return |
|
|
|
|
|
|
|
function isChangeAddress(address){ |
|
|
|
return me.changeAddresses.indexOf(address) > -1 |
|
|
|
if (isPending) { |
|
|
|
this.outputs[output].to = txid + ':' + i |
|
|
|
this.outputs[output].pending = true |
|
|
|
} else { |
|
|
|
delete this.outputs[output] |
|
|
|
} |
|
|
|
}, this) |
|
|
|
} |
|
|
|
|
|
|
|
function isMyAddress(address) { |
|
|
|
return isReceiveAddress(address) || isChangeAddress(address) |
|
|
|
} |
|
|
|
function includeAddress(addresses, address) { |
|
|
|
return addresses.indexOf(address) > -1 |
|
|
|
} |
|
|
|
|
|
|
|
module.exports = Wallet |
|
|
|