A Transaction (and its subsequent scripts) do not carry any network
specific information in the Bitcoin protocol.
Therefore they can not (without further context) produce the network
specific constants for the generation of the base58 Addresses.
As TransactionOut.address is used heavily throughout Wallet and other
areas of the library, this could not be entirely removed without a large
number of changes.
For now, TransactionOut.address is only defined in the case of
Tx.addOutput being used directly:
Transaction.addOutput(address, value)
321 lines
8.1 KiB
JavaScript
321 lines
8.1 KiB
JavaScript
var Address = require('./address')
|
|
var convert = require('./convert')
|
|
var HDNode = require('./hdwallet.js')
|
|
var networks = require('./networks')
|
|
var rng = require('secure-random')
|
|
var Transaction = require('./transaction').Transaction
|
|
|
|
function Wallet(seed, options) {
|
|
if (!(this instanceof Wallet)) { return new Wallet(seed, options); }
|
|
|
|
var options = options || {}
|
|
var network = options.network || '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
|
|
|
|
// Addresses
|
|
this.addresses = []
|
|
this.changeAddresses = []
|
|
|
|
// Transaction output data
|
|
this.outputs = {}
|
|
|
|
// Make a new master key
|
|
this.newMasterKey = function(seed, network) {
|
|
if (!seed) seed = rng(32, { array: true });
|
|
masterkey = new HDNode(seed, network)
|
|
|
|
// HD first-level child derivation method should be private
|
|
// See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254
|
|
accountZero = masterkey.derivePrivate(0)
|
|
externalAccount = accountZero.derive(0)
|
|
internalAccount = accountZero.derive(1)
|
|
|
|
me.addresses = []
|
|
me.changeAddresses = []
|
|
|
|
me.outputs = {}
|
|
}
|
|
this.newMasterKey(seed, network)
|
|
|
|
this.generateAddress = function() {
|
|
var key = externalAccount.derive(this.addresses.length)
|
|
this.addresses.push(key.getAddress().toString())
|
|
return this.addresses[this.addresses.length - 1]
|
|
}
|
|
|
|
this.generateChangeAddress = function() {
|
|
var key = internalAccount.derive(this.changeAddresses.length)
|
|
this.changeAddresses.push(key.getAddress().toString())
|
|
return this.changeAddresses[this.changeAddresses.length - 1]
|
|
}
|
|
|
|
this.getBalance = function() {
|
|
return this.getUnspentOutputs().reduce(function(memo, output){
|
|
return memo + output.value
|
|
}, 0)
|
|
}
|
|
|
|
this.getUnspentOutputs = function() {
|
|
var utxo = []
|
|
|
|
for(var key in this.outputs){
|
|
var output = this.outputs[key]
|
|
if(!output.spend) utxo.push(outputToUnspentOutput(output))
|
|
}
|
|
|
|
return utxo
|
|
}
|
|
|
|
this.setUnspentOutputs = function(utxo) {
|
|
var outputs = {}
|
|
|
|
utxo.forEach(function(uo){
|
|
validateUnspentOutput(uo)
|
|
var o = unspentOutputToOutput(uo)
|
|
outputs[o.receive] = o
|
|
})
|
|
|
|
this.outputs = outputs
|
|
}
|
|
|
|
this.setUnspentOutputsAsync = function(utxo, callback) {
|
|
var error = null
|
|
try {
|
|
this.setUnspentOutputs(utxo)
|
|
} catch(err) {
|
|
error = err
|
|
} finally {
|
|
process.nextTick(function(){ callback(error) })
|
|
}
|
|
}
|
|
|
|
function outputToUnspentOutput(output){
|
|
var hashAndIndex = output.receive.split(":")
|
|
|
|
return {
|
|
hash: hashAndIndex[0],
|
|
hashLittleEndian: convert.reverseEndian(hashAndIndex[0]),
|
|
outputIndex: parseInt(hashAndIndex[1]),
|
|
address: output.address,
|
|
value: output.value
|
|
}
|
|
}
|
|
|
|
function unspentOutputToOutput(o) {
|
|
var hash = o.hash || convert.reverseEndian(o.hashLittleEndian)
|
|
var key = hash + ":" + o.outputIndex
|
|
return {
|
|
receive: key,
|
|
address: o.address,
|
|
value: o.value
|
|
}
|
|
}
|
|
|
|
function validateUnspentOutput(uo) {
|
|
var missingField
|
|
|
|
if (isNullOrUndefined(uo.hash) && isNullOrUndefined(uo.hashLittleEndian)) {
|
|
missingField = "hash(or hashLittleEndian)"
|
|
}
|
|
|
|
var requiredKeys = ['outputIndex', 'address', 'value']
|
|
requiredKeys.forEach(function (key) {
|
|
if (isNullOrUndefined(uo[key])){
|
|
missingField = key
|
|
}
|
|
})
|
|
|
|
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(or hashLittleEndian)")
|
|
throw new Error(message.join(' '))
|
|
}
|
|
}
|
|
|
|
function isNullOrUndefined(value) {
|
|
return value == undefined
|
|
}
|
|
|
|
this.processTx = function(tx) {
|
|
var txhash = tx.getHash()
|
|
|
|
tx.outs.forEach(function(txOut, i){
|
|
var address
|
|
|
|
try {
|
|
address = Address.fromScriptPubKey(txOut.script, networks[network]).toString()
|
|
} catch(e) {
|
|
if (!(e instanceof Address.Error)) throw e
|
|
}
|
|
|
|
if (isMyAddress(address)) {
|
|
var output = txhash + ':' + i
|
|
|
|
me.outputs[output] = {
|
|
receive: output,
|
|
value: txOut.value,
|
|
address: address,
|
|
}
|
|
}
|
|
})
|
|
|
|
tx.ins.forEach(function(txIn, i){
|
|
var op = txIn.outpoint
|
|
var o = me.outputs[op.hash+':'+op.index]
|
|
if (o) {
|
|
o.spend = txhash + ':' +i
|
|
}
|
|
})
|
|
}
|
|
|
|
this.createTx = function(to, value, fixedFee, changeAddress) {
|
|
checkDust(value)
|
|
|
|
var tx = new Transaction()
|
|
tx.addOutput(to, value)
|
|
|
|
var utxo = getCandidateOutputs(value)
|
|
var totalInValue = 0
|
|
for(var i=0; i<utxo.length; i++){
|
|
var output = utxo[i]
|
|
tx.addInput(output.receive)
|
|
|
|
totalInValue += output.value
|
|
if(totalInValue < value) continue
|
|
|
|
var fee = fixedFee == undefined ? estimateFeePadChangeOutput(tx) : fixedFee
|
|
if(totalInValue < value + fee) continue
|
|
|
|
var change = totalInValue - value - fee
|
|
if(change > 0 && !isDust(change)) {
|
|
tx.addOutput(changeAddress || getChangeAddress(), change)
|
|
}
|
|
break
|
|
}
|
|
|
|
checkInsufficientFund(totalInValue, value, fee)
|
|
|
|
this.sign(tx)
|
|
|
|
return tx
|
|
}
|
|
|
|
this.createTxAsync = function(to, value, fixedFee, callback){
|
|
if(fixedFee instanceof Function) {
|
|
callback = fixedFee
|
|
fixedFee = undefined
|
|
}
|
|
var tx = null
|
|
var error = null
|
|
|
|
try {
|
|
tx = this.createTx(to, value, fixedFee)
|
|
} catch(err) {
|
|
error = err
|
|
} finally {
|
|
process.nextTick(function(){ callback(error, tx) })
|
|
}
|
|
}
|
|
|
|
this.dustThreshold = 5430
|
|
function isDust(amount) {
|
|
return amount <= me.dustThreshold
|
|
}
|
|
|
|
function checkDust(value){
|
|
if (isNullOrUndefined(value) || isDust(value)) {
|
|
throw new Error("Value must be above dust threshold")
|
|
}
|
|
}
|
|
|
|
function getCandidateOutputs(value){
|
|
var unspent = []
|
|
for (var key in me.outputs){
|
|
var output = me.outputs[key]
|
|
if(!output.spend) unspent.push(output)
|
|
}
|
|
|
|
var sortByValueDesc = unspent.sort(function(o1, o2){
|
|
return o2.value - o1.value
|
|
})
|
|
|
|
return sortByValueDesc
|
|
}
|
|
|
|
function estimateFeePadChangeOutput(tx){
|
|
var tmpTx = tx.clone()
|
|
tmpTx.addOutput(getChangeAddress(), 0)
|
|
return tmpTx.estimateFee()
|
|
}
|
|
|
|
function getChangeAddress() {
|
|
if(me.changeAddresses.length === 0) me.generateChangeAddress();
|
|
return me.changeAddresses[me.changeAddresses.length - 1]
|
|
}
|
|
|
|
function checkInsufficientFund(totalInValue, value, fee) {
|
|
if(totalInValue < value + fee) {
|
|
throw new Error('Not enough money to send funds including transaction fee. Have: ' +
|
|
totalInValue + ', needed: ' + (value + fee))
|
|
}
|
|
}
|
|
|
|
this.sign = function(tx) {
|
|
tx.ins.forEach(function(inp,i) {
|
|
var output = me.outputs[inp.outpoint.hash + ':' + inp.outpoint.index]
|
|
if (output) {
|
|
tx.sign(i, me.getPrivateKeyForAddress(output.address), false)
|
|
}
|
|
})
|
|
return tx
|
|
}
|
|
|
|
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).priv
|
|
}
|
|
|
|
this.getInternalPrivateKey = function(index) {
|
|
return internalAccount.derive(index).priv
|
|
}
|
|
|
|
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.')
|
|
}
|
|
}
|
|
|
|
function isReceiveAddress(address){
|
|
return me.addresses.indexOf(address) > -1
|
|
}
|
|
|
|
function isChangeAddress(address){
|
|
return me.changeAddresses.indexOf(address) > -1
|
|
}
|
|
|
|
function isMyAddress(address) {
|
|
return isReceiveAddress(address) || isChangeAddress(address)
|
|
}
|
|
}
|
|
|
|
module.exports = Wallet
|