Browse Source

Merge pull request #83 from bitcoinjs/wallet

Wallet cleanup & rewrite
hk-custom-address
Wei Lu 11 years ago
parent
commit
7820ea7ea0
  1. 3
      package.json
  2. 7
      src/convert.js
  3. 8
      src/script.js
  4. 21
      src/transaction.js
  5. 302
      src/wallet.js
  6. 9
      test/convert.js
  7. 5
      test/fixtures/mainnet_tx.json
  8. 45
      test/script.js
  9. 48
      test/transaction.js
  10. 450
      test/wallet.js

3
package.json

@ -18,7 +18,8 @@
"mocha": "1.18.2", "mocha": "1.18.2",
"istanbul": "0.1.30", "istanbul": "0.1.30",
"uglify-js": "2.4.13", "uglify-js": "2.4.13",
"node-browserify": "https://github.com/substack/node-browserify/tarball/master" "node-browserify": "https://github.com/substack/node-browserify/tarball/master",
"sinon": "1.9.0"
}, },
"testling": { "testling": {
"browsers": [ "browsers": [

7
src/convert.js

@ -158,6 +158,10 @@ function wordArrayToBytes(wordArray) {
return wordsToBytes(wordArray.words) return wordsToBytes(wordArray.words)
} }
function reverseEndian (hex) {
return bytesToHex(hexToBytes(hex).reverse())
}
module.exports = { module.exports = {
lpad: lpad, lpad: lpad,
bytesToHex: bytesToHex, bytesToHex: bytesToHex,
@ -175,5 +179,6 @@ module.exports = {
bytesToWords: bytesToWords, bytesToWords: bytesToWords,
wordsToBytes: wordsToBytes, wordsToBytes: wordsToBytes,
bytesToWordArray: bytesToWordArray, bytesToWordArray: bytesToWordArray,
wordArrayToBytes: wordArrayToBytes wordArrayToBytes: wordArrayToBytes,
reverseEndian: reverseEndian
} }

8
src/script.js

@ -150,7 +150,8 @@ Script.prototype.toScriptHash = function() {
return util.sha256ripe160(this.buffer) return util.sha256ripe160(this.buffer)
} }
Script.prototype.toAddress = function() { //TODO: support testnet
Script.prototype.getToAddress = function() {
var outType = this.getOutType(); var outType = this.getOutType();
if (outType == 'Pubkey') { if (outType == 'Pubkey') {
@ -164,6 +165,11 @@ Script.prototype.toAddress = function() {
return new Address(this.chunks[1], 5) return new Address(this.chunks[1], 5)
} }
//TODO: support testnet
Script.prototype.getFromAddress = function(){
return new Address(this.simpleInHash());
}
/** /**
* Compare the script to known templates of scriptSig. * Compare the script to known templates of scriptSig.
* *

21
src/transaction.js

@ -377,6 +377,18 @@ Transaction.prototype.validateSig = function(index, script, sig, pub) {
convert.coerceToBytes(pub)); convert.coerceToBytes(pub));
} }
Transaction.feePerKb = 20000
Transaction.prototype.estimateFee = function(feePerKb){
var uncompressedInSize = 180
var outSize = 34
var fixedPadding = 34
if(feePerKb == undefined) feePerKb = Transaction.feePerKb
var size = this.ins.length * uncompressedInSize + this.outs.length * outSize + fixedPadding
return feePerKb * Math.ceil(size / 1000)
}
var TransactionIn = function (data) { var TransactionIn = function (data) {
if (typeof data == "string") if (typeof data == "string")
this.outpoint = { hash: data.split(':')[0], index: data.split(':')[1] } this.outpoint = { hash: data.split(':')[0], index: data.split(':')[1] }
@ -415,7 +427,7 @@ var TransactionOut = function (data) {
: data.address ? Script.createOutputScript(data.address) : data.address ? Script.createOutputScript(data.address)
: new Script(); : new Script();
if (this.script.buffer.length > 0) this.address = this.script.toAddress(); if (this.script.buffer.length > 0) this.address = this.script.getToAddress();
this.value = this.value =
Array.isArray(data.value) ? convert.bytesToNum(data.value) Array.isArray(data.value) ? convert.bytesToNum(data.value)
@ -424,8 +436,7 @@ var TransactionOut = function (data) {
: data.value; : data.value;
}; };
TransactionOut.prototype.clone = function () TransactionOut.prototype.clone = function() {
{
var newTxout = new TransactionOut({ var newTxout = new TransactionOut({
script: this.script.clone(), script: this.script.clone(),
value: this.value value: this.value
@ -433,6 +444,10 @@ TransactionOut.prototype.clone = function ()
return newTxout; return newTxout;
}; };
TransactionOut.prototype.scriptPubKey = function() {
return convert.bytesToHex(this.script.buffer)
}
module.exports = { module.exports = {
Transaction: Transaction, Transaction: Transaction,
TransactionIn: TransactionIn, TransactionIn: TransactionIn,

302
src/wallet.js

@ -1,11 +1,5 @@
var Script = require('./script');
var ECKey = require('./eckey').ECKey;
var convert = require('./convert'); var convert = require('./convert');
var assert = require('assert');
var BigInteger = require('./jsbn/jsbn');
var Transaction = require('./transaction').Transaction; var Transaction = require('./transaction').Transaction;
var TransactionIn = require('./transaction').TransactionIn;
var TransactionOut = require('./transaction').TransactionOut;
var HDNode = require('./hdwallet.js') var HDNode = require('./hdwallet.js')
var rng = require('secure-random'); var rng = require('secure-random');
@ -60,111 +54,209 @@ var Wallet = function (seed, options) {
return this.changeAddresses[this.changeAddresses.length - 1] return this.changeAddresses[this.changeAddresses.length - 1]
} }
// Processes a transaction object this.getBalance = function() {
// If "verified" is true, then we trust the transaction as "final" return this.getUnspentOutputs().reduce(function(memo, output){
this.processTx = function(tx, verified) { return memo + output.value
var txhash = convert.bytesToHex(tx.getHash()) }, 0)
for (var i = 0; i < tx.outs.length; i++) { }
if (this.addresses.indexOf(tx.outs[i].address.toString()) >= 0) {
me.outputs[txhash+':'+i] = { this.getUnspentOutputs = function() {
output: txhash+':'+i, var utxo = []
value: tx.outs[i].value,
address: tx.outs[i].address.toString(), for(var key in this.outputs){
timestamp: new Date().getTime() / 1000, var output = this.outputs[key]
pending: true 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) {
try {
this.setUnspentOutputs(utxo)
} catch(err) {
return callback(err)
}
return callback()
}
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', field, '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 = convert.bytesToHex(tx.getHash())
tx.outs.forEach(function(txOut, i){
var address = txOut.address.toString()
if (isMyAddress(address)) {
var output = txhash+':'+i
me.outputs[output] = {
receive: output,
value: txOut.value,
address: address,
} }
for (var i = 0; i < tx.ins.length; i++) { }
var op = tx.ins[i].outpoint })
tx.ins.forEach(function(txIn, i){
var op = txIn.outpoint
var o = me.outputs[op.hash+':'+op.index] var o = me.outputs[op.hash+':'+op.index]
if (o) { if (o) {
o.spend = txhash+':'+i o.spend = txhash+':'+i
o.spendpending = true }
o.timestamp = new Date().getTime() / 1000
}
}
}
// Processes an output from an external source of the form
// { output: txhash:index, value: integer, address: address }
// Excellent compatibility with SX and pybitcointools
this.processOutput = function(o) {
if (!this.outputs[o.output] || this.outputs[o.output].pending)
this.outputs[o.output] = o;
}
this.processExistingOutputs = function() {
var t = new Date().getTime() / 1000
for (var o in this.outputs) {
if (o.pending && t > o.timestamp + 1200)
delete this.outputs[o]
if (o.spendpending && t > o.timestamp + 1200) {
o.spendpending = false
o.spend = false
delete o.timestamp
}
}
}
var peoInterval = setInterval(this.processExistingOutputs, 10000)
this.getUtxoToPay = function(value) {
var h = []
for (var out in this.outputs) h.push(this.outputs[out])
var utxo = h.filter(function(x) { return !x.spend });
var valuecompare = function(a,b) { return a.value > b.value; }
var high = utxo.filter(function(o) { return o.value >= value; })
.sort(valuecompare);
if (high.length > 0) return [high[0]];
utxo.sort(valuecompare);
var totalval = 0;
for (var i = 0; i < utxo.length; i++) {
totalval += utxo[i].value;
if (totalval >= value) return utxo.slice(0,i+1);
}
throw ("Not enough money to send funds including transaction fee. Have: "
+ (totalval / 100000000) + ", needed: " + (value / 100000000));
}
this.mkSend = function(to, value, fee) {
var utxo = this.getUtxoToPay(value + fee)
var sum = utxo.reduce(function(t,o) { return t + o.value },0),
remainder = sum - value - fee
if (value < 5430) throw new Error("Amount below dust threshold!")
var unspentOuts = 0;
for (var o in this.outputs) {
if (!this.outputs[o].spend) unspentOuts += 1
if (unspentOuts >= 5) return
}
var change = this.addresses[this.addresses.length - 1]
var toOut = { address: to, value: value },
changeOut = { address: change, value: remainder }
halfChangeOut = { address: change, value: Math.floor(remainder/2) };
var outs =
remainder < 5430 ? [toOut]
: remainder < 10860 ? [toOut, changeOut]
: unspentOuts == 5 ? [toOut, changeOut]
: [toOut, halfChangeOut, halfChangeOut]
var tx = new Bitcoin.Transaction({
ins: utxo.map(function(x) { return x.output }),
outs: outs
}) })
}
this.createTx = function(to, value, fixedFee) {
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 || estimateFeePadChangeOutput(tx)
if(totalInValue < value + fee) continue;
var change = totalInValue - value - fee
if(change > 0 && !isDust(change)) {
tx.addOutput(getChangeAddress(), change)
}
break;
}
checkInsufficientFund(totalInValue, value, fee)
this.sign(tx) this.sign(tx)
return tx return tx
} }
this.mkSendToOutputs = function(outputs, changeIndex, fee) { this.createTxAsync = function(to, value, fixedFee, callback){
var value = outputs.reduce(function(t,o) { return t + o.value },0), if(fixedFee instanceof Function) {
utxo = this.getUtxoToPay(value + fee), callback = fixedFee
sum = utxo.reduce(function(t,p) { return t + o.value },0); fixedFee = undefined
utxo[changeIndex].value += sum - value - fee; }
var tx = new Bitcoin.Transaction({ var tx = null
ins: utxo.map(function(x) { return x.output }),
outs: outputs try {
tx = this.createTx(to, value, fixedFee)
} catch(err) {
return callback(err)
}
callback(null, 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
}) })
this.sign(tx)
return tx 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) { this.sign = function(tx) {
@ -201,6 +293,18 @@ var Wallet = function (seed, options) {
throw new Error('Unknown address. Make sure the address is from the keychain and has been generated.') 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; module.exports = Wallet;

9
test/convert.js

@ -93,4 +93,13 @@ describe('convert', function() {
} }
}) })
}) })
describe('reverseEndian', function() {
it('works', function() {
var bigEndian = "6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7"
var littleEdian = "c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a"
assert.deepEqual(convert.reverseEndian(bigEndian), littleEdian)
assert.deepEqual(convert.reverseEndian(littleEdian), bigEndian)
})
})
}) })

5
test/fixtures/mainnet_tx.json

File diff suppressed because one or more lines are too long

45
test/script.js

@ -10,6 +10,14 @@ var bytesToHex = Convert.bytesToHex;
var hexToBytes = Convert.hexToBytes; var hexToBytes = Convert.hexToBytes;
describe('Script', function() { describe('Script', function() {
var p2shScriptPubKey, pubkeyScriptPubkey, addressScriptSig
beforeEach(function(){
p2shScriptPubKey = "a914e8c300c87986efa84c37c0519929019ef86eb5b487"
pubkeyScriptPubKey = "76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac"
addressScriptSig = "48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8"
})
describe('constructor', function() { describe('constructor', function() {
it('works for a byte array', function() { it('works for a byte array', function() {
assert.ok(new Script([])) assert.ok(new Script([]))
@ -49,6 +57,43 @@ describe('Script', function() {
assert.equal(Address(sha256ripe160(hexToBytes(redeemScript)),network).toString(), assert.equal(Address(sha256ripe160(hexToBytes(redeemScript)),network).toString(),
multiSigAddress) multiSigAddress)
}) })
})
describe('getOutType', function() {
it('works for p2sh', function() {
var script = Script.fromHex(p2shScriptPubKey)
assert.equal(script.getOutType(), 'P2SH')
})
it('works for pubkey', function() {
var script = Script.fromHex(pubkeyScriptPubKey)
assert.equal(script.getOutType(), 'Pubkey')
})
})
describe('getInType', function() {
it('works for address', function() {
var script = Script.fromHex(addressScriptSig)
assert.equal(script.getInType(), 'Address')
})
})
describe('getToAddress', function() {
it('works for p2sh type output', function() {
var script = Script.fromHex(p2shScriptPubKey)
assert.equal(script.getToAddress().toString(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8')
})
it('works for pubkey type output', function() {
var script = Script.fromHex(pubkeyScriptPubKey)
assert.equal(script.getToAddress().toString(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu')
})
})
describe('getFromAddress', function() {
it('works for address type input', function() {
var script = Script.fromHex(addressScriptSig)
assert.equal(script.getFromAddress().toString(), '1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u')
})
}) })
}) })

48
test/transaction.js

@ -1,8 +1,17 @@
var Transaction = require('../src/transaction').Transaction var T = require('../src/transaction')
var Transaction = T.Transaction
var TransactionOut = T.TransactionOut
var convert = require('../src/convert') var convert = require('../src/convert')
var ECKey = require('../src/eckey').ECKey var ECKey = require('../src/eckey').ECKey
var Script = require('../src/script')
var assert = require('assert') var assert = require('assert')
var fixtureTxes = require('./fixtures/mainnet_tx')
var fixtureTx1Hex = fixtureTxes.prevTx
var fixtureTx2Hex = fixtureTxes.tx
var fixtureTxBigHex = fixtureTxes.bigTx
describe('Transaction', function() { describe('Transaction', function() {
describe('deserialize', function() { describe('deserialize', function() {
var tx, serializedTx var tx, serializedTx
@ -65,7 +74,7 @@ describe('Transaction', function() {
describe('creating a transaction', function() { describe('creating a transaction', function() {
var tx, prevTx var tx, prevTx
beforeEach(function() { beforeEach(function() {
prevTx = Transaction.deserialize('0100000001e0214ebebb0fd3414d3fdc0dbf3b0f4b247a296cafc984558622c3041b0fcc9b010000008b48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8ffffffff03a0860100000000001976a91400ea3576c8fcb0bc8392f10e23a3425ae24efea888ac40420f00000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188acf07cd210000000001976a9146fb93c557ee62b109370fd9003e456917401cbfa88ac00000000') prevTx = Transaction.deserialize(fixtureTx1Hex)
tx = new Transaction() tx = new Transaction()
}) })
@ -157,7 +166,7 @@ describe('Transaction', function() {
var validTx var validTx
beforeEach(function() { beforeEach(function() {
validTx = Transaction.deserialize('0100000001576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c000000006b483045022100a82a31607b837c1ae510ae3338d1d3c7cbd57c15e322ab6e5dc927d49bffa66302205f0db6c90f1fae3c8db4ebfa753d7da1b2343d653ce0331aa94ed375e6ba366c0121020497bfc87c3e97e801414fed6a0db4b8c2e01c46e2cf9dff59b406b52224a76bffffffff02409c0000000000001976a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac50c30000000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188ac00000000') validTx = Transaction.deserialize(fixtureTx2Hex)
}) })
it('returns true for valid signature', function(){ it('returns true for valid signature', function(){
@ -170,7 +179,40 @@ describe('Transaction', function() {
}) })
}) })
describe('estimateFee', function(){
it('works for fixture tx 1', function(){
var tx = Transaction.deserialize(fixtureTx1Hex)
assert.equal(tx.estimateFee(), 20000)
})
it('works for fixture big tx', function(){
var tx = Transaction.deserialize(fixtureTxBigHex)
assert.equal(tx.estimateFee(), 60000)
})
it('allow feePerKb to be passed in as an argument', function(){
var tx = Transaction.deserialize(fixtureTx2Hex)
assert.equal(tx.estimateFee(10000), 10000)
}) })
it('allow feePerKb to be set to 0', function(){
var tx = Transaction.deserialize(fixtureTx2Hex)
assert.equal(tx.estimateFee(0), 0)
})
})
})
describe('TransactionOut', function() {
describe('scriptPubKey', function() {
it('returns hex string', function() {
var txOut = new TransactionOut({
value: 50000,
script: Script.createOutputScript("1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv")
})
assert.equal(txOut.scriptPubKey(), "76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac")
})
})
})
}) })

450
test/wallet.js

@ -1,22 +1,28 @@
var Wallet = require('../src/wallet.js') var Wallet = require('../src/wallet.js')
var HDNode = require('../src/hdwallet.js') var HDNode = require('../src/hdwallet.js')
var T = require('../src/transaction.js')
var Transaction = T.Transaction
var TransactionOut = T.TransactionOut
var Script = require('../src/script.js')
var convert = require('../src/convert.js') var convert = require('../src/convert.js')
var assert = require('assert') var assert = require('assert')
var SHA256 = require('crypto-js/sha256') var SHA256 = require('crypto-js/sha256')
var Crypto = require('crypto-js') var Crypto = require('crypto-js')
var fixtureTxes = require('./fixtures/mainnet_tx')
var fixtureTx1Hex = fixtureTxes.prevTx
var fixtureTx2Hex = fixtureTxes.tx
var sinon = require('sinon')
describe('Wallet', function() { describe('Wallet', function() {
var seed; var seed, wallet;
beforeEach(function(){ beforeEach(function(){
seed = convert.wordArrayToBytes(SHA256("don't use a string seed like this in real life")) seed = convert.wordArrayToBytes(SHA256("don't use a string seed like this in real life"))
})
describe('constructor', function() {
var wallet;
beforeEach(function() {
wallet = new Wallet(seed) wallet = new Wallet(seed)
}) })
describe('constructor', function() {
it('defaults to Bitcoin mainnet', function() { it('defaults to Bitcoin mainnet', function() {
assert.equal(wallet.getMasterKey().network, 'mainnet') assert.equal(wallet.getMasterKey().network, 'mainnet')
}) })
@ -47,7 +53,6 @@ describe('Wallet', function() {
}) })
describe('constructor options', function() { describe('constructor options', function() {
var wallet;
beforeEach(function() { beforeEach(function() {
wallet = new Wallet(seed, {network: 'testnet'}) wallet = new Wallet(seed, {network: 'testnet'})
}) })
@ -149,6 +154,432 @@ describe('Wallet', function() {
}) })
}) })
describe('Unspent Outputs', function(){
var expectedUtxo, expectedOutputKey;
beforeEach(function(){
expectedUtxo = {
"hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7",
"hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a",
"outputIndex": 0,
"address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv",
"value": 20000
}
expectedOutputKey = expectedUtxo.hash + ":" + expectedUtxo.outputIndex
})
function addUtxoToOutput(utxo){
var key = utxo.hash + ":" + utxo.outputIndex
wallet.outputs[key] = {
receive: key,
address: utxo.address,
value: utxo.value
}
}
describe('getBalance', function(){
var utxo1
beforeEach(function(){
utxo1 = cloneObject(expectedUtxo)
utxo1.hash = utxo1.hash.replace('7', 'l')
})
it('sums over utxo values', function(){
addUtxoToOutput(expectedUtxo)
addUtxoToOutput(utxo1)
assert.deepEqual(wallet.getBalance(), 40000)
})
it('excludes spent outputs', function(){
addUtxoToOutput(expectedUtxo)
addUtxoToOutput(utxo1)
wallet.outputs[utxo1.hash + ':' + utxo1.outputIndex].spend = "sometxn:m"
assert.deepEqual(wallet.getBalance(), 20000)
})
})
describe('getUnspentOutputs', function(){
beforeEach(function(){
addUtxoToOutput(expectedUtxo)
})
it('parses wallet outputs to the expect format', function(){
assert.deepEqual(wallet.getUnspentOutputs(), [expectedUtxo])
})
it('excludes spent outputs', function(){
wallet.outputs[expectedOutputKey].spend = "sometxn:m"
assert.deepEqual(wallet.getUnspentOutputs(), [])
})
})
describe('setUnspentOutputs', function(){
var utxo;
beforeEach(function(){
utxo = cloneObject([expectedUtxo])
})
it('uses hashLittleEndian when hash is not present', function(){
delete utxo[0]['hash']
wallet.setUnspentOutputs(utxo)
verifyOutputs()
})
it('uses hash when hashLittleEndian is not present', function(){
delete utxo[0]['hashLittleEndian']
wallet.setUnspentOutputs(utxo)
verifyOutputs()
})
it('uses hash when both hash and hashLittleEndian are present', function(){
wallet.setUnspentOutputs(utxo)
verifyOutputs()
})
describe('required fields', function(){
it("throws an error when hash and hashLittleEndian are both missing", function(){
delete utxo[0]['hash']
delete utxo[0]['hashLittleEndian']
var errorMessage = 'Invalid unspent output: key hash(or hashLittleEndian) is missing. ' +
'A valid unspent output must contain outputIndex, address, value and hash(or hashLittleEndian)'
assert.throws(function() {
wallet.setUnspentOutputs(utxo)
}, Error, errorMessage)
});
['outputIndex', 'address', 'value'].forEach(function(field){
it("throws an error when " + field + " is missing", function(){
delete utxo[0][field]
var errorMessage = 'Invalid unspent output: key ' + field +
' is missing. A valid unspent output must contain outputIndex, address, value and hash(or hashLittleEndian)'
assert.throws(function() {
wallet.setUnspentOutputs(utxo)
}, Error, errorMessage)
})
})
})
function verifyOutputs() {
var output = wallet.outputs[expectedOutputKey]
assert(output)
assert.equal(output.value, utxo[0].value)
assert.equal(output.address, utxo[0].address)
}
})
describe('setUnspentOutputsAsync', function(){
var utxo;
beforeEach(function(){
utxo = cloneObject([expectedUtxo])
})
afterEach(function(){
wallet.setUnspentOutputs.restore()
})
it('calls setUnspentOutputs', function(){
sinon.stub(wallet, "setUnspentOutputs")
var callback = sinon.spy()
var tx = wallet.setUnspentOutputsAsync(utxo, callback)
assert(wallet.setUnspentOutputs.calledWith(utxo))
assert(callback.called)
})
it('when setUnspentOutputs throws an error, it invokes callback with error', function(){
sinon.stub(wallet, "setUnspentOutputs").throws()
var callback = sinon.spy()
var tx = wallet.setUnspentOutputsAsync(utxo, callback)
assert(callback.called)
assert(callback.args[0][0] instanceof Error)
})
})
})
describe('processTx', function(){
var tx;
beforeEach(function(){
tx = Transaction.deserialize(fixtureTx1Hex)
})
describe("when tx outs contains an address owned by the wallet, an 'output' gets added to wallet.outputs", function(){
it("works for receive address", function(){
var totalOuts = outputCount()
wallet.addresses = [tx.outs[0].address.toString()]
wallet.processTx(tx)
assert.equal(outputCount(), totalOuts + 1)
verifyOutputAdded(0)
})
it("works for change address", function(){
var totalOuts = outputCount()
wallet.changeAddresses = [tx.outs[1].address.toString()]
wallet.processTx(tx)
assert.equal(outputCount(), totalOuts + 1)
verifyOutputAdded(1)
})
function outputCount(){
return Object.keys(wallet.outputs).length
}
function verifyOutputAdded(index) {
var txOut = tx.outs[index]
var key = convert.bytesToHex(tx.getHash()) + ":" + index
var output = wallet.outputs[key]
assert.equal(output.receive, key)
assert.equal(output.value, txOut.value)
assert.equal(output.address, txOut.address)
}
})
describe("when tx ins outpoint contains a known txhash:i, the corresponding 'output' gets updated", function(){
beforeEach(function(){
wallet.addresses = [tx.outs[0].address.toString()] // the address fixtureTx2 used as input
wallet.processTx(tx)
tx = Transaction.deserialize(fixtureTx2Hex)
})
it("does not add to wallet.outputs", function(){
var outputs = wallet.outputs
wallet.processTx(tx)
assert.deepEqual(wallet.outputs, outputs)
})
it("sets spend with the transaction hash and input index", function(){
wallet.processTx(tx)
var txIn = tx.ins[0]
var key = txIn.outpoint.hash + ":" + txIn.outpoint.index
var output = wallet.outputs[key]
assert.equal(output.spend, convert.bytesToHex(tx.getHash()) + ':' + 0)
})
})
it("does nothing when none of the involved addresses belong to the wallet", function(){
var outputs = wallet.outputs
wallet.processTx(tx)
assert.deepEqual(wallet.outputs, outputs)
})
})
describe('createTx', function(){
var to, value;
var address1, address2;
beforeEach(function(){
to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3'
value = 500000
// generate 2 addresses
address1 = wallet.generateAddress()
address2 = wallet.generateAddress()
// set up 3 utxo
utxo = [
{
"hash": fakeTxHash(1),
"outputIndex": 0,
"address" : address1,
"value": 400000 // not enough for value
},
{
"hash": fakeTxHash(2),
"outputIndex": 1,
"address" : address1,
"value": 500000 // enough for only value
},
{
"hash": fakeTxHash(3),
"outputIndex": 0,
"address" : address2,
"value": 520000 // enough for value and fee
}
]
wallet.setUnspentOutputs(utxo)
})
describe('choosing utxo', function(){
it('calculates fees', function(){
var tx = wallet.createTx(to, value)
assert.equal(tx.ins.length, 1)
assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 })
})
it('allows fee to be specified', function(){
var fee = 30000
var tx = wallet.createTx(to, value, fee)
assert.equal(tx.ins.length, 2)
assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 })
assert.deepEqual(tx.ins[1].outpoint, { hash: fakeTxHash(2), index: 1 })
})
it('ignores spent outputs', function(){
utxo.push(
{
"hash": fakeTxHash(4),
"outputIndex": 0,
"address" : address2,
"value": 530000 // enough but spent before createTx
}
)
wallet.setUnspentOutputs(utxo)
wallet.outputs[fakeTxHash(4) + ":" + 0].spend = fakeTxHash(5) + ":" + 0
var tx = wallet.createTx(to, value)
assert.equal(tx.ins.length, 1)
assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 })
})
})
describe('transaction outputs', function(){
it('includes the specified address and amount', function(){
var tx = wallet.createTx(to, value)
assert.equal(tx.outs.length, 1)
var out = tx.outs[0]
assert.equal(out.address, to)
assert.equal(out.value, value)
})
describe('change', function(){
it('uses the last change address if there is any', function(){
var fee = 5000
wallet.generateChangeAddress()
wallet.generateChangeAddress()
var tx = wallet.createTx(to, value, fee)
assert.equal(tx.outs.length, 2)
var out = tx.outs[1]
assert.equal(out.address, wallet.changeAddresses[1])
assert.equal(out.value, 15000)
})
it('generates a change address if there is not any', function(){
var fee = 5000
assert.equal(wallet.changeAddresses.length, 0)
var tx = wallet.createTx(to, value, fee)
assert.equal(wallet.changeAddresses.length, 1)
var out = tx.outs[1]
assert.equal(out.address, wallet.changeAddresses[0])
assert.equal(out.value, 15000)
})
it('skips change if it is not above dust threshold', function(){
var fee = 14570
var tx = wallet.createTx(to, value)
assert.equal(tx.outs.length, 1)
})
})
})
describe('signing', function(){
afterEach(function(){
Transaction.prototype.sign.restore()
})
it('signes the inputs with respective keys', function(){
var fee = 30000
sinon.stub(Transaction.prototype, "sign")
var tx = wallet.createTx(to, value, fee)
assert(Transaction.prototype.sign.calledWith(0, wallet.getPrivateKeyForAddress(address2)))
assert(Transaction.prototype.sign.calledWith(1, wallet.getPrivateKeyForAddress(address1)))
})
})
describe('when value is below dust threshold', function(){
it('throws an error', function(){
var value = 5430
assert.throws(function() {
wallet.createTx(to, value)
}, /Value must be above dust threshold/)
})
})
describe('when there is not enough money', function(){
it('throws an error', function(){
var value = 1400001
assert.throws(function() {
wallet.createTx(to, value)
}, Error, 'Not enough money to send funds including transaction fee. Have: 1420000, needed: 1420001')
})
})
function fakeTxHash(i) {
return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i
}
})
describe('createTxAsync', function(){
var to, value, fee;
beforeEach(function(){
to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3'
value = 500000
fee = 10000
})
afterEach(function(){
wallet.createTx.restore()
})
it('calls createTx', function(){
sinon.stub(wallet, "createTx").returns("fakeTx")
var callback = sinon.spy()
var tx = wallet.createTxAsync(to, value, callback)
assert(wallet.createTx.calledWith(to, value))
assert(callback.calledWith(null, "fakeTx"))
})
it('calls createTx correctly when fee is specified', function(){
sinon.stub(wallet, "createTx").returns("fakeTx")
var callback = sinon.spy()
var tx = wallet.createTxAsync(to, value, fee, callback)
assert(wallet.createTx.calledWith(to, value, fee))
assert(callback.calledWith(null, "fakeTx"))
})
it('when createTx throws an error, it invokes callback with error', function(){
sinon.stub(wallet, "createTx").throws()
var callback = sinon.spy()
var tx = wallet.createTxAsync(to, value, callback)
assert(callback.called)
assert(callback.args[0][0] instanceof Error)
})
})
function assertEqual(obj1, obj2){ function assertEqual(obj1, obj2){
assert.equal(obj1.toString(), obj2.toString()) assert.equal(obj1.toString(), obj2.toString())
} }
@ -156,4 +587,9 @@ describe('Wallet', function() {
function assertNotEqual(obj1, obj2){ function assertNotEqual(obj1, obj2){
assert.notEqual(obj1.toString(), obj2.toString()) assert.notEqual(obj1.toString(), obj2.toString())
} }
// quick and dirty: does not deal with functions on object
function cloneObject(obj){
return JSON.parse(JSON.stringify(obj))
}
}) })

Loading…
Cancel
Save