bitcoinjs-lib/test/wallet.js
Daniel Cousens 708aa03390 Transaction/Script: bitcoin network no longer implied
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)
2014-05-08 10:59:58 +10:00

664 lines
20 KiB
JavaScript

var assert = require('assert')
var crypto = require('../').crypto
var sinon = require('sinon')
var Address = require('..').Address
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 Wallet = require('../src/wallet.js')
var fixtureTxes = require('./fixtures/mainnet_tx')
var fixtureTx1Hex = fixtureTxes.prevTx
var fixtureTx2Hex = fixtureTxes.tx
describe('Wallet', function() {
var seed, wallet
beforeEach(function(){
seed = crypto.sha256("don't use a string seed like this in real life")
wallet = new Wallet(seed)
})
describe('constructor', function() {
it('should be ok to call without new', function() {
assert.ok(Wallet(seed) instanceof Wallet)
})
it('defaults to Bitcoin network', function() {
assert.equal(wallet.getMasterKey().network, 'bitcoin')
})
it("generates m/0' as the main account", function() {
var mainAccount = wallet.getAccountZero()
assert.equal(mainAccount.index, 0 + HDNode.HIGHEST_BIT)
assert.equal(mainAccount.depth, 1)
})
it("generates m/0'/0 as the external account", function() {
var account = wallet.getExternalAccount()
assert.equal(account.index, 0)
assert.equal(account.depth, 2)
})
it("generates m/0'/1 as the internal account", function() {
var account = wallet.getInternalAccount()
assert.equal(account.index, 1)
assert.equal(account.depth, 2)
})
describe('when seed is not specified', function(){
it('generates a seed', function(){
var wallet = new Wallet()
assert.ok(wallet.getMasterKey())
})
})
describe('constructor options', function() {
beforeEach(function() {
wallet = new Wallet(seed, {network: 'testnet'})
})
it('uses the network if specified', function() {
assert.equal(wallet.getMasterKey().network, 'testnet')
})
})
})
describe('newMasterKey', function(){
it('resets accounts', function(){
var wallet = new Wallet()
var oldAccountZero = wallet.getAccountZero()
var oldExternalAccount = wallet.getExternalAccount()
var oldInternalAccount = wallet.getInternalAccount()
wallet.newMasterKey(seed)
assertNotEqual(wallet.getAccountZero(), oldAccountZero)
assertNotEqual(wallet.getExternalAccount(), oldExternalAccount)
assertNotEqual(wallet.getInternalAccount(), oldInternalAccount)
})
it('resets addresses', function(){
var wallet = new Wallet()
wallet.generateAddress()
wallet.generateChangeAddress()
var oldAddresses = wallet.addresses
var oldChangeAddresses = wallet.changeAddresses
assert.notDeepEqual(oldAddresses, [])
assert.notDeepEqual(oldChangeAddresses, [])
wallet.newMasterKey(seed)
assert.deepEqual(wallet.addresses, [])
assert.deepEqual(wallet.changeAddresses, [])
})
})
describe('generateAddress', function(){
it('generate receiving addresses', function(){
var wallet = new Wallet(seed, {network: 'testnet'})
var expectedAddresses = [
"n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa",
"n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X"
]
assert.equal(wallet.generateAddress(), expectedAddresses[0])
assert.equal(wallet.generateAddress(), expectedAddresses[1])
assert.deepEqual(wallet.addresses, expectedAddresses)
})
})
describe('generateChangeAddress', function(){
it('generates change addresses', function(){
var wallet = new Wallet(seed, {network: 'testnet'})
var expectedAddresses = ["mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"]
assert.equal(wallet.generateChangeAddress(), expectedAddresses[0])
assert.deepEqual(wallet.changeAddresses, expectedAddresses)
})
})
describe('getPrivateKey', function(){
it('returns the private key at the given index of external account', function(){
var wallet = new Wallet(seed, {network: 'testnet'})
assertEqual(wallet.getPrivateKey(0), wallet.getExternalAccount().derive(0).priv)
assertEqual(wallet.getPrivateKey(1), wallet.getExternalAccount().derive(1).priv)
})
})
describe('getInternalPrivateKey', function(){
it('returns the private key at the given index of internal account', function(){
var wallet = new Wallet(seed, {network: 'testnet'})
assertEqual(wallet.getInternalPrivateKey(0), wallet.getInternalAccount().derive(0).priv)
assertEqual(wallet.getInternalPrivateKey(1), wallet.getInternalAccount().derive(1).priv)
})
})
describe('getPrivateKeyForAddress', function(){
it('returns the private key for the given address', function(){
var wallet = new Wallet(seed, {network: 'testnet'})
wallet.generateChangeAddress()
wallet.generateAddress()
wallet.generateAddress()
assertEqual(
wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X"),
wallet.getExternalAccount().derive(1).priv
)
assertEqual(
wallet.getPrivateKeyForAddress("mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"),
wallet.getInternalAccount().derive(0).priv
)
})
it('raises an error when address is not found', function(){
var wallet = new Wallet(seed, {network: 'testnet'})
assert.throws(function() {
wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X")
}, /Unknown address. Make sure the address is from the keychain and has been generated./)
})
})
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.equal(wallet.getBalance(), 40000)
})
it('excludes spent outputs', function(){
addUtxoToOutput(expectedUtxo)
addUtxoToOutput(utxo1)
wallet.outputs[utxo1.hash + ':' + utxo1.outputIndex].spend = "sometxn:m"
assert.equal(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']
assert.throws(function() {
wallet.setUnspentOutputs(utxo)
}, /Invalid unspent output: key hash\(or hashLittleEndian\) is missing/)
});
['outputIndex', 'address', 'value'].forEach(function(field){
it("throws an error when " + field + " is missing", function(){
delete utxo[0][field]
assert.throws(function() {
wallet.setUnspentOutputs(utxo)
}, new RegExp('Invalid unspent output: key ' + field + ' is missing'))
})
})
})
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(done){
sinon.stub(wallet, "setUnspentOutputs")
var callback = function(){
assert(wallet.setUnspentOutputs.calledWith(utxo))
done()
}
wallet.setUnspentOutputsAsync(utxo, callback)
})
it('when setUnspentOutputs throws an error, it invokes callback with error', function(done){
sinon.stub(wallet, "setUnspentOutputs").throws()
var callback = function(err){
assert(err instanceof Error)
done()
}
wallet.setUnspentOutputsAsync(utxo, callback)
})
})
})
describe('processTx', function(){
var addresses
var tx
beforeEach(function(){
addresses = [
'115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi',
'1Bu3bhwRmevHLAy1JrRB6AfcxfgDG2vXRd',
'1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u'
]
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 = [addresses[0]]
wallet.processTx(tx)
assert.equal(outputCount(), totalOuts + 1)
verifyOutputAdded(0)
})
it("works for change address", function(){
var totalOuts = outputCount()
wallet.changeAddresses = [addresses[1]]
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 = tx.getHash() + ":" + index
var output = wallet.outputs[key]
assert.equal(output.receive, key)
assert.equal(output.value, txOut.value)
var txOutAddress = Address.fromScriptPubKey(txOut.script).toString()
assert.equal(output.address, txOutAddress)
}
})
describe("when tx ins outpoint contains a known txhash:i, the corresponding 'output' gets updated", function(){
beforeEach(function(){
wallet.addresses = [addresses[0]] // 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, 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('allows fee to be set to zero', function(){
value = 520000
var fee = 0
var tx = wallet.createTx(to, value, fee)
assert.equal(tx.ins.length, 1)
assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 })
})
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('testnet', function(){
it('should create transaction', function(){
var to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue'
var wallet = new Wallet(seed, {network: 'testnet'})
var tx = wallet.createTx(to, value)
assert.equal(tx.outs.length, 1)
assert.equal(tx.outs[0].address.toString(), to)
assert.equal(tx.outs[0].value, value)
})
})
describe('changeAddress', function(){
it('should allow custom changeAddress', function(){
var wallet = new Wallet(seed, {network: 'testnet'})
var address = wallet.generateAddress()
wallet.setUnspentOutputs([{
hash: fakeTxHash(0),
outputIndex: 0,
address: address,
value: value
}])
assert.equal(wallet.getBalance(), value)
var changeAddress = 'mfrFjnKZUvTcvdAK2fUX5D8v1Epu5H8JCk'
var to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue'
var toValue = value / 2
var fee = 1e3
var tx = wallet.createTx(to, toValue, fee, changeAddress)
assert.equal(tx.outs.length, 2)
assert.equal(tx.outs[0].address.toString(), to)
assert.equal(tx.outs[0].value, toValue)
assert.equal(tx.outs[1].address.toString(), changeAddress)
assert.equal(tx.outs[1].value, value - (toValue + fee))
})
})
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)
}, /Not enough money to send funds including transaction fee. Have: 1420000, needed: 1420001/)
})
})
function fakeTxHash(i) {
return "efefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + 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(done){
sinon.stub(wallet, "createTx").returns("fakeTx")
var callback = function(err, tx){
assert(wallet.createTx.calledWith(to, value))
assert.equal(err, null)
assert.equal(tx, "fakeTx")
done()
}
wallet.createTxAsync(to, value, callback)
})
it('calls createTx correctly when fee is specified', function(done){
sinon.stub(wallet, "createTx").returns("fakeTx")
var callback = function(err, tx){
assert(wallet.createTx.calledWith(to, value, fee))
assert.equal(err, null)
assert.equal(tx, "fakeTx")
done()
}
wallet.createTxAsync(to, value, fee, callback)
})
it('when createTx throws an error, it invokes callback with error', function(done){
sinon.stub(wallet, "createTx").throws()
var callback = function(err, tx){
assert(err instanceof Error)
done()
}
wallet.createTxAsync(to, value, callback)
})
})
function assertEqual(obj1, obj2){
assert.equal(obj1.toString(), obj2.toString())
}
function assertNotEqual(obj1, obj2){
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))
}
})