Daniel Cousens
10 years ago
12 changed files with 12 additions and 1301 deletions
@ -1,371 +0,0 @@ |
|||
var assert = require('assert') |
|||
var bufferutils = require('./bufferutils') |
|||
var crypto = require('crypto') |
|||
var typeForce = require('typeforce') |
|||
var networks = require('./networks') |
|||
|
|||
var Address = require('./address') |
|||
var HDNode = require('./hdnode') |
|||
var TransactionBuilder = require('./transaction_builder') |
|||
var Script = require('./script') |
|||
|
|||
function Wallet (seed, network) { |
|||
console.warn('Wallet is deprecated and will be removed in 2.0.0, see #296') |
|||
|
|||
seed = seed || crypto.randomBytes(32) |
|||
network = network || networks.bitcoin |
|||
|
|||
// Stored in a closure to make accidental serialization less likely
|
|||
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) |
|||
|
|||
this.addresses = [] |
|||
this.changeAddresses = [] |
|||
this.network = network |
|||
this.unspents = [] |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
this.unspentMap = {} |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
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) |
|||
|
|||
accountZero = masterKey.deriveHardened(0) |
|||
externalAccount = accountZero.derive(0) |
|||
internalAccount = accountZero.derive(1) |
|||
|
|||
me.addresses = [] |
|||
me.changeAddresses = [] |
|||
|
|||
me.unspents = [] |
|||
me.unspentMap = {} |
|||
} |
|||
|
|||
this.getMasterKey = function () { |
|||
return masterKey |
|||
} |
|||
this.getAccountZero = function () { |
|||
return accountZero |
|||
} |
|||
this.getExternalAccount = function () { |
|||
return externalAccount |
|||
} |
|||
this.getInternalAccount = function () { |
|||
return internalAccount |
|||
} |
|||
} |
|||
|
|||
Wallet.prototype.createTransaction = function (to, value, options) { |
|||
// FIXME: remove in 2.0.0
|
|||
if (typeof options !== 'object') { |
|||
if (options !== undefined) { |
|||
console.warn('Non options object parameters are deprecated, use options object instead') |
|||
|
|||
options = { |
|||
fixedFee: arguments[2], |
|||
changeAddress: arguments[3] |
|||
} |
|||
} |
|||
} |
|||
|
|||
options = options || {} |
|||
|
|||
assert(value > this.network.dustThreshold, value + ' must be above dust threshold (' + this.network.dustThreshold + ' Satoshis)') |
|||
|
|||
var changeAddress = options.changeAddress |
|||
var fixedFee = options.fixedFee |
|||
var minConf = options.minConf === undefined ? 0 : options.minConf // FIXME: change minConf:1 by default in 2.0.0
|
|||
|
|||
// filter by minConf, then pending and sort by descending value
|
|||
var unspents = this.unspents.filter(function (unspent) { |
|||
return unspent.confirmations >= minConf |
|||
}).filter(function (unspent) { |
|||
return !unspent.pending |
|||
}).sort(function (o1, o2) { |
|||
return o2.value - o1.value |
|||
}) |
|||
|
|||
var accum = 0 |
|||
var addresses = [] |
|||
var subTotal = value |
|||
|
|||
var txb = new TransactionBuilder() |
|||
txb.addOutput(to, value) |
|||
|
|||
for (var i = 0; i < unspents.length; ++i) { |
|||
var unspent = unspents[i] |
|||
addresses.push(unspent.address) |
|||
|
|||
txb.addInput(unspent.txHash, unspent.index) |
|||
|
|||
var fee = fixedFee === undefined ? estimatePaddedFee(txb.buildIncomplete(), this.network) : fixedFee |
|||
|
|||
accum += unspent.value |
|||
subTotal = value + fee |
|||
|
|||
if (accum >= subTotal) { |
|||
var change = accum - subTotal |
|||
|
|||
if (change > this.network.dustThreshold) { |
|||
txb.addOutput(changeAddress || this.getChangeAddress(), change) |
|||
} |
|||
|
|||
break |
|||
} |
|||
} |
|||
|
|||
assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal) |
|||
|
|||
return this.signWith(txb, addresses).build() |
|||
} |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
Wallet.prototype.processPendingTx = function (tx) { |
|||
this.__processTx(tx, true) |
|||
} |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
Wallet.prototype.processConfirmedTx = function (tx) { |
|||
this.__processTx(tx, false) |
|||
} |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
Wallet.prototype.__processTx = function (tx, isPending) { |
|||
console.warn('processTransaction is considered harmful, see issue #260 for more information') |
|||
|
|||
var txId = tx.getId() |
|||
var txHash = tx.getHash() |
|||
|
|||
tx.outs.forEach(function (txOut, i) { |
|||
var address |
|||
|
|||
try { |
|||
address = Address.fromOutputScript(txOut.script, this.network).toString() |
|||
} catch (e) { |
|||
if (!(e.message.match(/has no matching Address/))) |
|||
throw e |
|||
} |
|||
|
|||
var myAddresses = this.addresses.concat(this.changeAddresses) |
|||
if (myAddresses.indexOf(address) > -1) { |
|||
var lookup = txId + ':' + i |
|||
if (lookup in this.unspentMap) return |
|||
|
|||
// its unique, add it
|
|||
var unspent = { |
|||
address: address, |
|||
confirmations: 0, // no way to determine this without more information
|
|||
index: i, |
|||
txHash: txHash, |
|||
txId: txId, |
|||
value: txOut.value, |
|||
pending: isPending |
|||
} |
|||
|
|||
this.unspentMap[lookup] = unspent |
|||
this.unspents.push(unspent) |
|||
} |
|||
}, this) |
|||
|
|||
tx.ins.forEach(function (txIn) { |
|||
// copy and convert to big-endian hex
|
|||
var txInId = bufferutils.reverse(txIn.hash).toString('hex') |
|||
|
|||
var lookup = txInId + ':' + txIn.index |
|||
if (!(lookup in this.unspentMap)) return |
|||
|
|||
var unspent = this.unspentMap[lookup] |
|||
|
|||
if (isPending) { |
|||
unspent.pending = true |
|||
unspent.spent = true |
|||
} else { |
|||
delete this.unspentMap[lookup] |
|||
|
|||
this.unspents = this.unspents.filter(function (unspent2) { |
|||
return unspent !== unspent2 |
|||
}) |
|||
} |
|||
}, this) |
|||
} |
|||
|
|||
Wallet.prototype.generateAddress = function () { |
|||
var k = this.addresses.length |
|||
var address = this.getExternalAccount().derive(k).getAddress() |
|||
|
|||
this.addresses.push(address.toString()) |
|||
|
|||
return this.getReceiveAddress() |
|||
} |
|||
|
|||
Wallet.prototype.generateChangeAddress = function () { |
|||
var k = this.changeAddresses.length |
|||
var address = this.getInternalAccount().derive(k).getAddress() |
|||
|
|||
this.changeAddresses.push(address.toString()) |
|||
|
|||
return this.getChangeAddress() |
|||
} |
|||
|
|||
Wallet.prototype.getAddress = function () { |
|||
if (this.addresses.length === 0) { |
|||
this.generateAddress() |
|||
} |
|||
|
|||
return this.addresses[this.addresses.length - 1] |
|||
} |
|||
|
|||
Wallet.prototype.getBalance = function (minConf) { |
|||
minConf = minConf || 0 |
|||
|
|||
return this.unspents.filter(function (unspent) { |
|||
return unspent.confirmations >= minConf |
|||
|
|||
// FIXME: remove spent filter in 2.0.0
|
|||
}).filter(function (unspent) { |
|||
return !unspent.spent |
|||
}).reduce(function (accum, unspent) { |
|||
return accum + unspent.value |
|||
}, 0) |
|||
} |
|||
|
|||
Wallet.prototype.getChangeAddress = function () { |
|||
if (this.changeAddresses.length === 0) { |
|||
this.generateChangeAddress() |
|||
} |
|||
|
|||
return this.changeAddresses[this.changeAddresses.length - 1] |
|||
} |
|||
|
|||
Wallet.prototype.getInternalPrivateKey = function (index) { |
|||
return this.getInternalAccount().derive(index).privKey |
|||
} |
|||
|
|||
Wallet.prototype.getPrivateKey = function (index) { |
|||
return this.getExternalAccount().derive(index).privKey |
|||
} |
|||
|
|||
Wallet.prototype.getPrivateKeyForAddress = function (address) { |
|||
var index |
|||
|
|||
if ((index = this.addresses.indexOf(address)) > -1) { |
|||
return this.getPrivateKey(index) |
|||
} |
|||
|
|||
if ((index = this.changeAddresses.indexOf(address)) > -1) { |
|||
return this.getInternalPrivateKey(index) |
|||
} |
|||
|
|||
assert(false, 'Unknown address. Make sure the address is from the keychain and has been generated') |
|||
} |
|||
|
|||
Wallet.prototype.getUnspentOutputs = function (minConf) { |
|||
minConf = minConf || 0 |
|||
|
|||
return this.unspents.filter(function (unspent) { |
|||
return unspent.confirmations >= minConf |
|||
|
|||
// FIXME: remove spent filter in 2.0.0
|
|||
}).filter(function (unspent) { |
|||
return !unspent.spent |
|||
}).map(function (unspent) { |
|||
return { |
|||
address: unspent.address, |
|||
confirmations: unspent.confirmations, |
|||
index: unspent.index, |
|||
txId: unspent.txId, |
|||
value: unspent.value, |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
hash: unspent.txId, |
|||
pending: unspent.pending |
|||
} |
|||
}) |
|||
} |
|||
|
|||
Wallet.prototype.setUnspentOutputs = function (unspents) { |
|||
this.unspentMap = {} |
|||
this.unspents = unspents.map(function (unspent) { |
|||
// FIXME: remove unspent.hash in 2.0.0
|
|||
var txId = unspent.txId || unspent.hash |
|||
var index = unspent.index |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
if (unspent.hash !== undefined) { |
|||
console.warn('unspent.hash is deprecated, use unspent.txId instead') |
|||
} |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
if (index === undefined) { |
|||
console.warn('unspent.outputIndex is deprecated, use unspent.index instead') |
|||
index = unspent.outputIndex |
|||
} |
|||
|
|||
typeForce('String', txId) |
|||
typeForce('Number', index) |
|||
typeForce('Number', unspent.value) |
|||
|
|||
assert.equal(txId.length, 64, 'Expected valid txId, got ' + txId) |
|||
assert.doesNotThrow(function () { |
|||
Address.fromBase58Check(unspent.address) |
|||
}, 'Expected Base58 Address, got ' + unspent.address) |
|||
assert(isFinite(index), 'Expected finite index, got ' + index) |
|||
|
|||
// FIXME: remove branch in 2.0.0
|
|||
if (unspent.confirmations !== undefined) { |
|||
typeForce('Number', unspent.confirmations) |
|||
} |
|||
|
|||
var txHash = bufferutils.reverse(new Buffer(txId, 'hex')) |
|||
|
|||
unspent = { |
|||
address: unspent.address, |
|||
confirmations: unspent.confirmations || 0, |
|||
index: index, |
|||
txHash: txHash, |
|||
txId: txId, |
|||
value: unspent.value, |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
pending: unspent.pending || false |
|||
} |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
this.unspentMap[txId + ':' + index] = unspent |
|||
|
|||
return unspent |
|||
}, this) |
|||
} |
|||
|
|||
Wallet.prototype.signWith = function (tx, addresses) { |
|||
addresses.forEach(function (address, i) { |
|||
var privKey = this.getPrivateKeyForAddress(address) |
|||
|
|||
tx.sign(i, privKey) |
|||
}, this) |
|||
|
|||
return tx |
|||
} |
|||
|
|||
function estimatePaddedFee (tx, network) { |
|||
var tmpTx = tx.clone() |
|||
tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0) |
|||
|
|||
return network.estimateFee(tmpTx) |
|||
} |
|||
|
|||
// FIXME: 1.0.0 shims, remove in 2.0.0
|
|||
Wallet.prototype.getReceiveAddress = Wallet.prototype.getAddress |
|||
Wallet.prototype.createTx = Wallet.prototype.createTransaction |
|||
|
|||
module.exports = Wallet |
File diff suppressed because one or more lines are too long
@ -1,691 +0,0 @@ |
|||
/* global describe, it, beforeEach, afterEach */ |
|||
|
|||
var assert = require('assert') |
|||
var bufferutils = require('../src/bufferutils') |
|||
var crypto = require('../src/crypto') |
|||
var networks = require('../src/networks') |
|||
var sinon = require('sinon') |
|||
var scripts = require('../src/scripts') |
|||
|
|||
var Address = require('../src/address') |
|||
var HDNode = require('../src/hdnode') |
|||
var Transaction = require('../src/transaction') |
|||
var TransactionBuilder = require('../src/transaction_builder') |
|||
var Wallet = require('../src/wallet') |
|||
|
|||
var fixtureTxes = require('./fixtures/mainnet_tx') |
|||
var fixtureTx1Hex = fixtureTxes.prevTx |
|||
var fixtureTx2Hex = fixtureTxes.tx |
|||
|
|||
function fakeTxHash (i) { |
|||
var hash = new Buffer(32) |
|||
hash.fill(i) |
|||
return hash |
|||
} |
|||
|
|||
function fakeTxId (i) { |
|||
var hash = fakeTxHash(i) |
|||
Array.prototype.reverse.call(hash) |
|||
return hash.toString('hex') |
|||
} |
|||
|
|||
describe('Wallet', function () { |
|||
var seed |
|||
beforeEach(function () { |
|||
seed = crypto.sha256("don't use a string seed like this in real life") |
|||
}) |
|||
|
|||
describe('constructor', function () { |
|||
var wallet |
|||
beforeEach(function () { |
|||
wallet = new Wallet(seed) |
|||
}) |
|||
|
|||
it('defaults to Bitcoin network', function () { |
|||
assert.equal(wallet.getMasterKey().network, networks.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(wallet.getMasterKey()) |
|||
}) |
|||
}) |
|||
|
|||
describe('constructor options', function () { |
|||
beforeEach(function () { |
|||
wallet = new Wallet(seed, networks.testnet) |
|||
}) |
|||
|
|||
it('uses the network if specified', function () { |
|||
assert.equal(wallet.getMasterKey().network, networks.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, networks.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, networks.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, networks.testnet) |
|||
|
|||
assertEqual(wallet.getPrivateKey(0), wallet.getExternalAccount().derive(0).privKey) |
|||
assertEqual(wallet.getPrivateKey(1), wallet.getExternalAccount().derive(1).privKey) |
|||
}) |
|||
}) |
|||
|
|||
describe('getInternalPrivateKey', function () { |
|||
it('returns the private key at the given index of internal account', function () { |
|||
var wallet = new Wallet(seed, networks.testnet) |
|||
|
|||
assertEqual(wallet.getInternalPrivateKey(0), wallet.getInternalAccount().derive(0).privKey) |
|||
assertEqual(wallet.getInternalPrivateKey(1), wallet.getInternalAccount().derive(1).privKey) |
|||
}) |
|||
}) |
|||
|
|||
describe('getPrivateKeyForAddress', function () { |
|||
it('returns the private key for the given address', function () { |
|||
var wallet = new Wallet(seed, networks.testnet) |
|||
wallet.generateChangeAddress() |
|||
wallet.generateAddress() |
|||
wallet.generateAddress() |
|||
|
|||
assertEqual( |
|||
wallet.getPrivateKeyForAddress('n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X'), |
|||
wallet.getExternalAccount().derive(1).privKey |
|||
) |
|||
assertEqual( |
|||
wallet.getPrivateKeyForAddress('mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn'), |
|||
wallet.getInternalAccount().derive(0).privKey |
|||
) |
|||
}) |
|||
|
|||
it('raises an error when address is not found', function () { |
|||
var wallet = new Wallet(seed, networks.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 utxo |
|||
var wallet |
|||
|
|||
beforeEach(function () { |
|||
utxo = { |
|||
'address': '1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv', |
|||
'confirmations': 1, |
|||
'index': 0, |
|||
'txId': fakeTxId(6), |
|||
'value': 20000, |
|||
'pending': false |
|||
} |
|||
}) |
|||
|
|||
describe('on construction', function () { |
|||
beforeEach(function () { |
|||
wallet = new Wallet(seed, networks.bitcoin) |
|||
wallet.setUnspentOutputs([utxo]) |
|||
}) |
|||
|
|||
it('matches the expected behaviour', function () { |
|||
var output = wallet.unspents[0] |
|||
|
|||
assert.equal(output.address, utxo.address) |
|||
assert.equal(output.value, utxo.value) |
|||
}) |
|||
}) |
|||
|
|||
describe('getBalance', function () { |
|||
beforeEach(function () { |
|||
var utxo1 = cloneObject(utxo) |
|||
utxo1.hash = fakeTxId(5) |
|||
|
|||
wallet = new Wallet(seed, networks.bitcoin) |
|||
wallet.setUnspentOutputs([utxo, utxo1]) |
|||
}) |
|||
|
|||
it('sums over utxo values', function () { |
|||
assert.equal(wallet.getBalance(), 40000) |
|||
}) |
|||
}) |
|||
|
|||
describe('getUnspentOutputs', function () { |
|||
beforeEach(function () { |
|||
wallet = new Wallet(seed, networks.bitcoin) |
|||
wallet.setUnspentOutputs([utxo]) |
|||
}) |
|||
|
|||
it('parses wallet unspents to the expected format', function () { |
|||
var outputs = wallet.getUnspentOutputs() |
|||
var output = outputs[0] |
|||
|
|||
assert.equal(utxo.address, output.address) |
|||
assert.equal(utxo.index, output.index) |
|||
assert.equal(utxo.value, output.value) |
|||
|
|||
// FIXME: remove in 2.0.0
|
|||
assert.equal(utxo.txId, output.hash) |
|||
assert.equal(utxo.pending, output.pending) |
|||
|
|||
// new in 2.0.0
|
|||
assert.equal(utxo.txId, output.txId) |
|||
assert.equal(utxo.confirmations, output.confirmations) |
|||
}) |
|||
|
|||
it("ignores spent unspents (outputs with 'spent' property)", function () { |
|||
var unspent = wallet.unspents[0] |
|||
unspent.pending = true |
|||
unspent.spent = true |
|||
assert.deepEqual(wallet.getUnspentOutputs(), []) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe('setUnspentOutputs', function () { |
|||
var utxo |
|||
var wallet |
|||
|
|||
beforeEach(function () { |
|||
utxo = { |
|||
hash: fakeTxId(0), |
|||
index: 0, |
|||
address: '115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi', |
|||
value: 500000 |
|||
} |
|||
|
|||
wallet = new Wallet(seed, networks.bitcoin) |
|||
}) |
|||
|
|||
it('matches the expected behaviour', function () { |
|||
wallet.setUnspentOutputs([utxo]) |
|||
|
|||
var output = wallet.unspents[0] |
|||
assert.equal(output.value, utxo.value) |
|||
assert.equal(output.address, utxo.address) |
|||
}) |
|||
|
|||
describe('required fields', function () { |
|||
['index', 'address', 'hash', 'value'].forEach(function (field) { |
|||
it('throws an error when ' + field + ' is missing', function () { |
|||
delete utxo[field] |
|||
|
|||
assert.throws(function () { |
|||
wallet.setUnspentOutputs([utxo]) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe('Process transaction', function () { |
|||
var wallet |
|||
beforeEach(function () { |
|||
wallet = new Wallet(seed) |
|||
}) |
|||
|
|||
var addresses |
|||
var tx |
|||
|
|||
beforeEach(function () { |
|||
addresses = [ |
|||
'115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi', |
|||
'1Bu3bhwRmevHLAy1JrRB6AfcxfgDG2vXRd', |
|||
'1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u' |
|||
] |
|||
|
|||
tx = Transaction.fromHex(fixtureTx1Hex) |
|||
}) |
|||
|
|||
describe('processPendingTx', function () { |
|||
it('incoming: sets the pending flag on output', function () { |
|||
wallet.addresses = [addresses[0]] |
|||
wallet.processPendingTx(tx) |
|||
|
|||
verifyOutputAdded(0, true) |
|||
}) |
|||
|
|||
describe('when tx ins outpoint contains a known txhash:i', function () { |
|||
var spendTx |
|||
beforeEach(function () { |
|||
wallet.addresses = [addresses[0]] |
|||
wallet.processConfirmedTx(tx) |
|||
|
|||
spendTx = Transaction.fromHex(fixtureTx2Hex) |
|||
}) |
|||
|
|||
it("outgoing: sets the pending flag and 'spent' on output", function () { |
|||
var txIn = spendTx.ins[0] |
|||
var txInId = new Buffer(txIn.hash) |
|||
Array.prototype.reverse.call(txInId) |
|||
txInId = txInId.toString('hex') |
|||
|
|||
var unspent = wallet.unspents[0] |
|||
assert(!unspent.pending) |
|||
|
|||
wallet.processPendingTx(spendTx) |
|||
assert(unspent.pending) |
|||
assert(unspent.spent, true) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe('processConfirmedTx', function () { |
|||
it('does not throw on scripts with no corresponding Address', function () { |
|||
var pubKey = wallet.getPrivateKey(0).pub |
|||
var script = scripts.pubKeyOutput(pubKey) |
|||
var tx2 = new Transaction() |
|||
|
|||
tx2.addInput(fakeTxHash(1), 0) |
|||
tx2.addOutput(script, 10000) |
|||
|
|||
wallet.processConfirmedTx(tx2) |
|||
}) |
|||
|
|||
describe("when tx outs contains an address owned by the wallet, an 'output' gets added to wallet.unspentMap", function () { |
|||
it('works for receive address', function () { |
|||
var totalOuts = outputCount() |
|||
|
|||
wallet.addresses = [addresses[0]] |
|||
wallet.processConfirmedTx(tx) |
|||
|
|||
assert.equal(outputCount(), totalOuts + 1) |
|||
verifyOutputAdded(0, false) |
|||
}) |
|||
|
|||
it('works for change address', function () { |
|||
var totalOuts = outputCount() |
|||
wallet.changeAddresses = [addresses[1]] |
|||
|
|||
wallet.processConfirmedTx(tx) |
|||
|
|||
assert.equal(outputCount(), totalOuts + 1) |
|||
verifyOutputAdded(1, false) |
|||
}) |
|||
|
|||
function outputCount () { |
|||
return Object.keys(wallet.unspentMap).length |
|||
} |
|||
}) |
|||
|
|||
describe('when tx ins contains a known txhash:i', function () { |
|||
var spendTx |
|||
beforeEach(function () { |
|||
wallet.addresses = [addresses[0]] // the address fixtureTx2 used as input
|
|||
wallet.processConfirmedTx(tx) |
|||
|
|||
spendTx = Transaction.fromHex(fixtureTx2Hex) |
|||
}) |
|||
|
|||
it('does not add to wallet.unspentMap', function () { |
|||
wallet.processConfirmedTx(spendTx) |
|||
assert.deepEqual(wallet.unspentMap, {}) |
|||
}) |
|||
|
|||
it("deletes corresponding 'unspent'", function () { |
|||
var txIn = spendTx.ins[0] |
|||
var txInId = bufferutils.reverse(txIn.hash).toString('hex') |
|||
|
|||
var expected = txInId + ':' + txIn.index |
|||
assert(expected in wallet.unspentMap) |
|||
|
|||
wallet.processConfirmedTx(spendTx) |
|||
assert(!(expected in wallet.unspentMap)) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
it('does nothing when none of the involved addresses belong to the wallet', function () { |
|||
wallet.processConfirmedTx(tx) |
|||
assert.deepEqual(wallet.unspentMap, {}) |
|||
}) |
|||
|
|||
function verifyOutputAdded (index, pending) { |
|||
var txOut = tx.outs[index] |
|||
|
|||
var key = tx.getId() + ':' + index |
|||
var output = wallet.unspentMap[key] |
|||
assert.deepEqual(output.txHash, tx.getHash()) |
|||
assert.equal(output.value, txOut.value) |
|||
assert.equal(output.pending, pending) |
|||
|
|||
var txOutAddress = Address.fromOutputScript(txOut.script).toString() |
|||
assert.equal(output.address, txOutAddress) |
|||
} |
|||
}) |
|||
|
|||
describe('createTx', function () { |
|||
var wallet |
|||
var address1, address2 |
|||
var to, value |
|||
|
|||
beforeEach(function () { |
|||
to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue' |
|||
value = 500000 |
|||
|
|||
address1 = 'n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa' |
|||
address2 = 'n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X' |
|||
|
|||
// set up 3 utxos
|
|||
var utxos = [ |
|||
{ |
|||
'txId': fakeTxId(1), |
|||
'index': 0, |
|||
'address': address1, |
|||
'value': 400000 // not enough for value
|
|||
}, |
|||
{ |
|||
'txId': fakeTxId(2), |
|||
'index': 1, |
|||
'address': address1, |
|||
'value': 500000 // enough for only value
|
|||
}, |
|||
{ |
|||
'txId': fakeTxId(3), |
|||
'index': 0, |
|||
'address': address2, |
|||
'value': 510000 // enough for value and fee
|
|||
} |
|||
] |
|||
|
|||
wallet = new Wallet(seed, networks.testnet) |
|||
wallet.setUnspentOutputs(utxos) |
|||
wallet.generateAddress() |
|||
wallet.generateAddress() |
|||
}) |
|||
|
|||
describe('transaction fee', function () { |
|||
it('allows fee to be specified', function () { |
|||
var fee = 30000 |
|||
var tx = wallet.createTx(to, value, { |
|||
fixedFee: fee |
|||
}) |
|||
|
|||
assert.equal(getFee(wallet, tx), fee) |
|||
}) |
|||
|
|||
it('allows fee to be set to zero', function () { |
|||
value = 510000 |
|||
var fee = 0 |
|||
var tx = wallet.createTx(to, value, { |
|||
fixedFee: fee |
|||
}) |
|||
|
|||
assert.equal(getFee(wallet, tx), fee) |
|||
}) |
|||
|
|||
it('does not overestimate fees when network has dustSoftThreshold', function () { |
|||
var utxo = { |
|||
txId: fakeTxId(0), |
|||
index: 0, |
|||
address: 'LeyySKbQrRRwodKEj1W4a8y3YQupPLw5os', |
|||
value: 500000 |
|||
} |
|||
|
|||
var wallet = new Wallet(seed, networks.litecoin) |
|||
wallet.setUnspentOutputs([utxo]) |
|||
wallet.generateAddress() |
|||
|
|||
value = 200000 |
|||
var tx = wallet.createTx(utxo.address, value) |
|||
|
|||
assert.equal(getFee(wallet, tx), 100000) |
|||
}) |
|||
|
|||
function getFee (wallet, tx) { |
|||
var inputValue = tx.ins.reduce(function (accum, input) { |
|||
var txId = bufferutils.reverse(input.hash).toString('hex') |
|||
|
|||
return accum + wallet.unspentMap[txId + ':' + input.index].value |
|||
}, 0) |
|||
|
|||
return tx.outs.reduce(function (accum, output) { |
|||
return accum - output.value |
|||
}, inputValue) |
|||
} |
|||
}) |
|||
|
|||
describe('choosing utxo', function () { |
|||
it('takes fees into account', function () { |
|||
var tx = wallet.createTx(to, value) |
|||
|
|||
assert.equal(tx.ins.length, 1) |
|||
assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) |
|||
assert.equal(tx.ins[0].index, 0) |
|||
}) |
|||
|
|||
it('uses confirmed outputs', function () { |
|||
var tx2 = new Transaction() |
|||
tx2.addInput(fakeTxId(4), 0) |
|||
tx2.addOutput(address2, 530000) |
|||
|
|||
wallet.processConfirmedTx(tx2) |
|||
var tx = wallet.createTx(to, value) |
|||
|
|||
assert.equal(tx.ins.length, 1) |
|||
assert.deepEqual(tx.ins[0].hash, tx2.getHash()) |
|||
assert.equal(tx.ins[0].index, 0) |
|||
}) |
|||
|
|||
it('ignores pending outputs', function () { |
|||
var tx2 = new Transaction() |
|||
tx2.addInput(fakeTxId(4), 0) |
|||
tx2.addOutput(address2, 530000) |
|||
|
|||
wallet.processPendingTx(tx2) |
|||
var tx = wallet.createTx(to, value) |
|||
|
|||
assert.equal(tx.ins.length, 1) |
|||
assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) |
|||
assert.equal(tx.ins[0].index, 0) |
|||
}) |
|||
}) |
|||
|
|||
describe('changeAddress', function () { |
|||
it('should allow custom changeAddress', function () { |
|||
var changeAddress = 'mfrFjnKZUvTcvdAK2fUX5D8v1Epu5H8JCk' |
|||
var fromValue = 510000 |
|||
var toValue = fromValue / 2 |
|||
var fee = 1e3 |
|||
|
|||
var tx = wallet.createTx(to, toValue, { |
|||
fixedFee: fee, |
|||
changeAddress: changeAddress |
|||
}) |
|||
assert.equal(tx.outs.length, 2) |
|||
|
|||
var outAddress0 = Address.fromOutputScript(tx.outs[0].script, networks.testnet) |
|||
var outAddress1 = Address.fromOutputScript(tx.outs[1].script, networks.testnet) |
|||
|
|||
assert.equal(outAddress0.toString(), to) |
|||
assert.equal(tx.outs[0].value, toValue) |
|||
|
|||
assert.equal(outAddress1.toString(), changeAddress) |
|||
assert.equal(tx.outs[1].value, fromValue - (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] |
|||
var outAddress = Address.fromOutputScript(out.script, networks.testnet) |
|||
|
|||
assert.equal(outAddress.toString(), to) |
|||
assert.equal(out.value, value) |
|||
}) |
|||
|
|||
describe('change', function () { |
|||
it('uses the last change address if there is any', function () { |
|||
var fee = 0 |
|||
wallet.generateChangeAddress() |
|||
wallet.generateChangeAddress() |
|||
var tx = wallet.createTx(to, value, { |
|||
fixedFee: fee |
|||
}) |
|||
|
|||
assert.equal(tx.outs.length, 2) |
|||
var out = tx.outs[1] |
|||
var outAddress = Address.fromOutputScript(out.script, networks.testnet) |
|||
|
|||
assert.equal(outAddress.toString(), wallet.changeAddresses[1]) |
|||
assert.equal(out.value, 10000) |
|||
}) |
|||
|
|||
it('generates a change address if there is not any', function () { |
|||
var fee = 0 |
|||
assert.equal(wallet.changeAddresses.length, 0) |
|||
|
|||
var tx = wallet.createTx(to, value, { |
|||
fixedFee: fee |
|||
}) |
|||
|
|||
assert.equal(wallet.changeAddresses.length, 1) |
|||
var out = tx.outs[1] |
|||
var outAddress = Address.fromOutputScript(out.script, networks.testnet) |
|||
|
|||
assert.equal(outAddress.toString(), wallet.changeAddresses[0]) |
|||
assert.equal(out.value, 10000) |
|||
}) |
|||
|
|||
it('skips change if it is not above dust threshold', function () { |
|||
var tx1 = wallet.createTx(to, value - 546) |
|||
assert.equal(tx1.outs.length, 1) |
|||
|
|||
var tx2 = wallet.createTx(to, value - 547) |
|||
assert.equal(tx2.outs.length, 2) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe('signing', function () { |
|||
afterEach(function () { |
|||
TransactionBuilder.prototype.sign.restore() |
|||
}) |
|||
|
|||
it('signs the inputs with respective keys', function () { |
|||
var fee = 30000 |
|||
sinon.spy(TransactionBuilder.prototype, 'sign') |
|||
|
|||
wallet.createTx(to, value, { |
|||
fixedFee: fee |
|||
}) |
|||
|
|||
var priv1 = wallet.getPrivateKeyForAddress(address1) |
|||
var priv2 = wallet.getPrivateKeyForAddress(address2) |
|||
|
|||
// FIXME: boo, toString invokes reqiuired affine coordinate side effects
|
|||
priv1.pub.Q.toString() |
|||
priv2.pub.Q.toString() |
|||
|
|||
assert(TransactionBuilder.prototype.sign.calledWith(0, priv2)) |
|||
assert(TransactionBuilder.prototype.sign.calledWith(1, priv1)) |
|||
}) |
|||
}) |
|||
|
|||
describe('when value is below dust threshold', function () { |
|||
it('throws an error', function () { |
|||
var value = 546 |
|||
|
|||
assert.throws(function () { |
|||
wallet.createTx(to, value) |
|||
}, /546 must be above dust threshold \(546 Satoshis\)/) |
|||
}) |
|||
}) |
|||
|
|||
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 funds \(incl. fee\): 1410000 < 1410001/) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
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)) |
|||
} |
|||
}) |
Loading…
Reference in new issue