commit
1bce66246c
8 changed files with 182 additions and 262 deletions
|
@ -1,6 +1,14 @@
|
||||||
var assert = require('assert')
|
var assert = require('assert')
|
||||||
var opcodes = require('./opcodes')
|
var opcodes = require('./opcodes')
|
||||||
|
|
||||||
|
// https://github.com/feross/buffer/blob/master/index.js#L1127
|
||||||
|
function verifuint(value, max) {
|
||||||
|
assert(typeof value === 'number', 'cannot write a non-number as a number')
|
||||||
|
assert(value >= 0, 'specified a negative value for writing an unsigned value')
|
||||||
|
assert(value <= max, 'value is larger than maximum value for type')
|
||||||
|
assert(Math.floor(value) === value, 'value has a fractional component')
|
||||||
|
}
|
||||||
|
|
||||||
function pushDataSize(i) {
|
function pushDataSize(i) {
|
||||||
return i < opcodes.OP_PUSHDATA1 ? 1
|
return i < opcodes.OP_PUSHDATA1 ? 1
|
||||||
: i < 0xff ? 2
|
: i < 0xff ? 2
|
||||||
|
@ -47,9 +55,7 @@ function readUInt64LE(buffer, offset) {
|
||||||
var b = buffer.readUInt32LE(offset + 4)
|
var b = buffer.readUInt32LE(offset + 4)
|
||||||
b *= 0x100000000
|
b *= 0x100000000
|
||||||
|
|
||||||
// Javascript Safe Integer limitation
|
verifuint(b + a, 0x001fffffffffffff)
|
||||||
// assert(Number.isSafeInteger(value), 'value must be < 2^53')
|
|
||||||
assert(b + a < 0x0020000000000000, 'value must be < 2^53')
|
|
||||||
|
|
||||||
return b + a
|
return b + a
|
||||||
}
|
}
|
||||||
|
@ -104,10 +110,6 @@ function writePushDataInt(buffer, number, offset) {
|
||||||
|
|
||||||
// 32 bit
|
// 32 bit
|
||||||
} else {
|
} else {
|
||||||
// Javascript Safe Integer limitation
|
|
||||||
// assert(Number.isSafeInteger(value), 'value must be < 2^53')
|
|
||||||
assert(number < 0x0020000000000000, 'value must be < 2^53')
|
|
||||||
|
|
||||||
buffer.writeUInt8(opcodes.OP_PUSHDATA4, offset)
|
buffer.writeUInt8(opcodes.OP_PUSHDATA4, offset)
|
||||||
buffer.writeUInt32LE(number, offset + 1)
|
buffer.writeUInt32LE(number, offset + 1)
|
||||||
|
|
||||||
|
@ -117,9 +119,7 @@ function writePushDataInt(buffer, number, offset) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeUInt64LE(buffer, value, offset) {
|
function writeUInt64LE(buffer, value, offset) {
|
||||||
// Javascript Safe Integer limitation
|
verifuint(value, 0x001fffffffffffff)
|
||||||
// assert(Number.isSafeInteger(value), 'value must be < 2^53')
|
|
||||||
assert(value < 0x0020000000000000, 'value must be < 2^53')
|
|
||||||
|
|
||||||
buffer.writeInt32LE(value & -1, offset)
|
buffer.writeInt32LE(value & -1, offset)
|
||||||
buffer.writeUInt32LE(Math.floor(value / 0x100000000), offset + 4)
|
buffer.writeUInt32LE(Math.floor(value / 0x100000000), offset + 4)
|
||||||
|
|
|
@ -12,14 +12,11 @@ var networks = require('./networks')
|
||||||
var sec = require('./sec')
|
var sec = require('./sec')
|
||||||
var ecparams = sec("secp256k1")
|
var ecparams = sec("secp256k1")
|
||||||
|
|
||||||
function HDWallet(seed, networkString) {
|
function HDWallet(seed, network) {
|
||||||
if (seed == undefined) return; // FIXME: Boo, should be stricter
|
if (seed == undefined) return; // FIXME: Boo, should be stricter
|
||||||
|
|
||||||
this.network = networkString || 'bitcoin'
|
network = network || networks.bitcoin
|
||||||
|
assert(network.bip32, 'Unknown BIP32 constants for network')
|
||||||
if(!networks.hasOwnProperty(this.network)) {
|
|
||||||
throw new Error("Unknown network: " + this.network)
|
|
||||||
}
|
|
||||||
|
|
||||||
var I = crypto.HmacSHA512(seed, HDWallet.MASTER_SECRET)
|
var I = crypto.HmacSHA512(seed, HDWallet.MASTER_SECRET)
|
||||||
var IL = I.slice(0, 32)
|
var IL = I.slice(0, 32)
|
||||||
|
@ -28,6 +25,7 @@ function HDWallet(seed, networkString) {
|
||||||
// In case IL is 0 or >= n, the master key is invalid (handled by ECKey.fromBuffer)
|
// In case IL is 0 or >= n, the master key is invalid (handled by ECKey.fromBuffer)
|
||||||
var pIL = BigInteger.fromBuffer(IL)
|
var pIL = BigInteger.fromBuffer(IL)
|
||||||
|
|
||||||
|
this.network = network
|
||||||
this.priv = new ECKey(pIL, true)
|
this.priv = new ECKey(pIL, true)
|
||||||
this.pub = this.priv.pub
|
this.pub = this.priv.pub
|
||||||
|
|
||||||
|
@ -40,8 +38,8 @@ HDWallet.MASTER_SECRET = new Buffer('Bitcoin seed')
|
||||||
HDWallet.HIGHEST_BIT = 0x80000000
|
HDWallet.HIGHEST_BIT = 0x80000000
|
||||||
HDWallet.LENGTH = 78
|
HDWallet.LENGTH = 78
|
||||||
|
|
||||||
HDWallet.fromSeedHex = function(hex, networkString) {
|
HDWallet.fromSeedHex = function(hex, network) {
|
||||||
return new HDWallet(new Buffer(hex, 'hex'), networkString)
|
return new HDWallet(new Buffer(hex, 'hex'), network)
|
||||||
}
|
}
|
||||||
|
|
||||||
HDWallet.fromBase58 = function(string) {
|
HDWallet.fromBase58 = function(string) {
|
||||||
|
@ -51,8 +49,8 @@ HDWallet.fromBase58 = function(string) {
|
||||||
var checksum = buffer.slice(-4)
|
var checksum = buffer.slice(-4)
|
||||||
var newChecksum = crypto.hash256(payload).slice(0, 4)
|
var newChecksum = crypto.hash256(payload).slice(0, 4)
|
||||||
|
|
||||||
assert.deepEqual(newChecksum, checksum)
|
assert.deepEqual(newChecksum, checksum, 'Invalid checksum')
|
||||||
assert.equal(payload.length, HDWallet.LENGTH)
|
assert.equal(payload.length, HDWallet.LENGTH, 'Invalid BIP32 string')
|
||||||
|
|
||||||
return HDWallet.fromBuffer(payload)
|
return HDWallet.fromBuffer(payload)
|
||||||
}
|
}
|
||||||
|
@ -73,7 +71,7 @@ HDWallet.fromBuffer = function(input) {
|
||||||
if (version != network.bip32[t]) continue
|
if (version != network.bip32[t]) continue
|
||||||
|
|
||||||
type = t
|
type = t
|
||||||
hd.network = name
|
hd.network = network
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +125,7 @@ HDWallet.prototype.getAddress = function() {
|
||||||
|
|
||||||
HDWallet.prototype.toBuffer = function(priv) {
|
HDWallet.prototype.toBuffer = function(priv) {
|
||||||
// Version
|
// Version
|
||||||
var version = networks[this.network].bip32[priv ? 'priv' : 'pub']
|
var version = this.network.bip32[priv ? 'priv' : 'pub']
|
||||||
var buffer = new Buffer(HDWallet.LENGTH)
|
var buffer = new Buffer(HDWallet.LENGTH)
|
||||||
|
|
||||||
// 4 bytes: version bytes
|
// 4 bytes: version bytes
|
||||||
|
@ -254,7 +252,7 @@ HDWallet.prototype.derivePrivate = function(index) {
|
||||||
}
|
}
|
||||||
|
|
||||||
HDWallet.prototype.getKeyVersion = function() {
|
HDWallet.prototype.getKeyVersion = function() {
|
||||||
return networks[this.network].pubKeyHash
|
return this.network.pubKeyHash
|
||||||
}
|
}
|
||||||
|
|
||||||
HDWallet.prototype.toString = HDWallet.prototype.toBase58
|
HDWallet.prototype.toString = HDWallet.prototype.toBase58
|
||||||
|
|
110
src/wallet.js
110
src/wallet.js
|
@ -1,15 +1,13 @@
|
||||||
var Address = require('./address')
|
var assert = require('assert')
|
||||||
var convert = require('./convert')
|
|
||||||
var HDNode = require('./hdwallet.js')
|
|
||||||
var networks = require('./networks')
|
var networks = require('./networks')
|
||||||
var rng = require('secure-random')
|
var rng = require('secure-random')
|
||||||
|
|
||||||
|
var Address = require('./address')
|
||||||
|
var HDNode = require('./hdwallet')
|
||||||
var Transaction = require('./transaction').Transaction
|
var Transaction = require('./transaction').Transaction
|
||||||
|
|
||||||
function Wallet(seed, options) {
|
function Wallet(seed, network) {
|
||||||
if (!(this instanceof Wallet)) { return new Wallet(seed, options); }
|
network = network || networks.bitcoin
|
||||||
|
|
||||||
var options = options || {}
|
|
||||||
var network = options.network || 'bitcoin'
|
|
||||||
|
|
||||||
// Stored in a closure to make accidental serialization less likely
|
// Stored in a closure to make accidental serialization less likely
|
||||||
var masterkey = null
|
var masterkey = null
|
||||||
|
@ -22,11 +20,14 @@ function Wallet(seed, options) {
|
||||||
this.addresses = []
|
this.addresses = []
|
||||||
this.changeAddresses = []
|
this.changeAddresses = []
|
||||||
|
|
||||||
|
// Dust value
|
||||||
|
this.dustThreshold = 5430
|
||||||
|
|
||||||
// Transaction output data
|
// Transaction output data
|
||||||
this.outputs = {}
|
this.outputs = {}
|
||||||
|
|
||||||
// Make a new master key
|
// Make a new master key
|
||||||
this.newMasterKey = function(seed, network) {
|
this.newMasterKey = function(seed) {
|
||||||
seed = seed || new Buffer(rng(32))
|
seed = seed || new Buffer(rng(32))
|
||||||
masterkey = new HDNode(seed, network)
|
masterkey = new HDNode(seed, network)
|
||||||
|
|
||||||
|
@ -41,7 +42,8 @@ function Wallet(seed, options) {
|
||||||
|
|
||||||
me.outputs = {}
|
me.outputs = {}
|
||||||
}
|
}
|
||||||
this.newMasterKey(seed, network)
|
|
||||||
|
this.newMasterKey(seed)
|
||||||
|
|
||||||
this.generateAddress = function() {
|
this.generateAddress = function() {
|
||||||
var key = externalAccount.derive(this.addresses.length)
|
var key = externalAccount.derive(this.addresses.length)
|
||||||
|
@ -84,23 +86,11 @@ function Wallet(seed, options) {
|
||||||
this.outputs = outputs
|
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){
|
function outputToUnspentOutput(output){
|
||||||
var hashAndIndex = output.receive.split(":")
|
var hashAndIndex = output.receive.split(":")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hash: hashAndIndex[0],
|
hash: hashAndIndex[0],
|
||||||
hashLittleEndian: convert.reverseEndian(hashAndIndex[0]),
|
|
||||||
outputIndex: parseInt(hashAndIndex[1]),
|
outputIndex: parseInt(hashAndIndex[1]),
|
||||||
address: output.address,
|
address: output.address,
|
||||||
value: output.value
|
value: output.value
|
||||||
|
@ -108,7 +98,7 @@ function Wallet(seed, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function unspentOutputToOutput(o) {
|
function unspentOutputToOutput(o) {
|
||||||
var hash = o.hash || convert.reverseEndian(o.hashLittleEndian)
|
var hash = o.hash
|
||||||
var key = hash + ":" + o.outputIndex
|
var key = hash + ":" + o.outputIndex
|
||||||
return {
|
return {
|
||||||
receive: key,
|
receive: key,
|
||||||
|
@ -120,8 +110,8 @@ function Wallet(seed, options) {
|
||||||
function validateUnspentOutput(uo) {
|
function validateUnspentOutput(uo) {
|
||||||
var missingField
|
var missingField
|
||||||
|
|
||||||
if (isNullOrUndefined(uo.hash) && isNullOrUndefined(uo.hashLittleEndian)) {
|
if (isNullOrUndefined(uo.hash)) {
|
||||||
missingField = "hash(or hashLittleEndian)"
|
missingField = "hash"
|
||||||
}
|
}
|
||||||
|
|
||||||
var requiredKeys = ['outputIndex', 'address', 'value']
|
var requiredKeys = ['outputIndex', 'address', 'value']
|
||||||
|
@ -137,7 +127,7 @@ function Wallet(seed, options) {
|
||||||
'A valid unspent output must contain'
|
'A valid unspent output must contain'
|
||||||
]
|
]
|
||||||
message.push(requiredKeys.join(', '))
|
message.push(requiredKeys.join(', '))
|
||||||
message.push("and hash(or hashLittleEndian)")
|
message.push("and hash")
|
||||||
throw new Error(message.join(' '))
|
throw new Error(message.join(' '))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,7 +143,7 @@ function Wallet(seed, options) {
|
||||||
var address
|
var address
|
||||||
|
|
||||||
try {
|
try {
|
||||||
address = Address.fromScriptPubKey(txOut.script, networks[network]).toString()
|
address = Address.fromScriptPubKey(txOut.script, network).toString()
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (!(e.message.match(/has no matching Address/))) throw e
|
if (!(e.message.match(/has no matching Address/))) throw e
|
||||||
}
|
}
|
||||||
|
@ -171,6 +161,7 @@ function Wallet(seed, options) {
|
||||||
|
|
||||||
tx.ins.forEach(function(txIn, i){
|
tx.ins.forEach(function(txIn, i){
|
||||||
var op = txIn.outpoint
|
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
|
||||||
|
@ -179,65 +170,41 @@ function Wallet(seed, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.createTx = function(to, value, fixedFee, changeAddress) {
|
this.createTx = function(to, value, fixedFee, changeAddress) {
|
||||||
checkDust(value)
|
assert(value > this.dustThreshold, value + ' must be above dust threshold (' + this.dustThreshold + ' Satoshis)')
|
||||||
|
|
||||||
|
var utxos = getCandidateOutputs(value)
|
||||||
|
var accum = 0
|
||||||
|
var subTotal = value
|
||||||
|
|
||||||
var tx = new Transaction()
|
var tx = new Transaction()
|
||||||
tx.addOutput(to, value)
|
tx.addOutput(to, value)
|
||||||
|
|
||||||
var utxo = getCandidateOutputs(value)
|
for (var i = 0; i < utxos.length; ++i) {
|
||||||
var totalInValue = 0
|
var utxo = utxos[i]
|
||||||
for(var i=0; i<utxo.length; i++){
|
|
||||||
var output = utxo[i]
|
|
||||||
tx.addInput(output.receive)
|
|
||||||
|
|
||||||
totalInValue += output.value
|
tx.addInput(utxo.receive)
|
||||||
if(totalInValue < value) continue
|
accum += utxo.value
|
||||||
|
|
||||||
var fee = fixedFee == undefined ? estimateFeePadChangeOutput(tx) : fixedFee
|
var fee = fixedFee == undefined ? estimateFeePadChangeOutput(tx) : fixedFee
|
||||||
if(totalInValue < value + fee) continue
|
|
||||||
|
|
||||||
var change = totalInValue - value - fee
|
subTotal = value + fee
|
||||||
if(change > 0 && !isDust(change)) {
|
if (accum >= subTotal) {
|
||||||
|
var change = accum - subTotal
|
||||||
|
|
||||||
|
if (change > this.dustThreshold) {
|
||||||
tx.addOutput(changeAddress || getChangeAddress(), change)
|
tx.addOutput(changeAddress || getChangeAddress(), change)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkInsufficientFund(totalInValue, value, fee)
|
assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal)
|
||||||
|
|
||||||
this.sign(tx)
|
this.sign(tx)
|
||||||
|
|
||||||
return 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){
|
function getCandidateOutputs(value){
|
||||||
var unspent = []
|
var unspent = []
|
||||||
for (var key in me.outputs){
|
for (var key in me.outputs){
|
||||||
|
@ -263,13 +230,6 @@ function Wallet(seed, options) {
|
||||||
return me.changeAddresses[me.changeAddresses.length - 1]
|
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) {
|
||||||
tx.ins.forEach(function(inp,i) {
|
tx.ins.forEach(function(inp,i) {
|
||||||
var output = me.outputs[inp.outpoint.hash + ':' + inp.outpoint.index]
|
var output = me.outputs[inp.outpoint.hash + ':' + inp.outpoint.index]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
var assert = require('assert')
|
var assert = require('assert')
|
||||||
var bufferutils = require('../src/bufferutils')
|
var bufferutils = require('../src/bufferutils')
|
||||||
|
|
||||||
var fixtures = require('./fixtures/buffer.json')
|
var fixtures = require('./fixtures/bufferutils.json')
|
||||||
|
|
||||||
describe('bufferutils', function() {
|
describe('bufferutils', function() {
|
||||||
describe('pushDataSize', function() {
|
describe('pushDataSize', function() {
|
||||||
|
@ -39,6 +39,16 @@ describe('bufferutils', function() {
|
||||||
assert.equal(number, f.dec)
|
assert.equal(number, f.dec)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fixtures.invalid.forEach(function(f) {
|
||||||
|
it('throws on ' + f.description, function() {
|
||||||
|
var buffer = new Buffer(f.hex64, 'hex')
|
||||||
|
|
||||||
|
assert.throws(function() {
|
||||||
|
bufferutils.readUInt64LE(buffer, 0)
|
||||||
|
}, new RegExp(f.exception))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('readVarInt', function() {
|
describe('readVarInt', function() {
|
||||||
|
@ -51,6 +61,16 @@ describe('bufferutils', function() {
|
||||||
assert.equal(d.size, buffer.length)
|
assert.equal(d.size, buffer.length)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fixtures.invalid.forEach(function(f) {
|
||||||
|
it('throws on ' + f.description, function() {
|
||||||
|
var buffer = new Buffer(f.hexVI, 'hex')
|
||||||
|
|
||||||
|
assert.throws(function() {
|
||||||
|
bufferutils.readVarInt(buffer, 0)
|
||||||
|
}, new RegExp(f.exception))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('varIntSize', function() {
|
describe('varIntSize', function() {
|
||||||
|
@ -75,17 +95,6 @@ describe('bufferutils', function() {
|
||||||
assert.equal(buffer.slice(0, n).toString('hex'), f.hexPD)
|
assert.equal(buffer.slice(0, n).toString('hex'), f.hexPD)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
fixtures.invalid.forEach(function(f) {
|
|
||||||
it('throws on ' + f.description, function() {
|
|
||||||
var buffer = new Buffer(5)
|
|
||||||
buffer.fill(0)
|
|
||||||
|
|
||||||
assert.throws(function() {
|
|
||||||
bufferutils.writePushDataInt(buffer, f.dec, 0)
|
|
||||||
}, /value must be < 2\^53/)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('writeUInt64LE', function() {
|
describe('writeUInt64LE', function() {
|
||||||
|
@ -106,7 +115,7 @@ describe('bufferutils', function() {
|
||||||
|
|
||||||
assert.throws(function() {
|
assert.throws(function() {
|
||||||
bufferutils.writeUInt64LE(buffer, f.dec, 0)
|
bufferutils.writeUInt64LE(buffer, f.dec, 0)
|
||||||
}, /value must be < 2\^53/)
|
}, new RegExp(f.exception))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -129,7 +138,7 @@ describe('bufferutils', function() {
|
||||||
|
|
||||||
assert.throws(function() {
|
assert.throws(function() {
|
||||||
bufferutils.writeVarInt(buffer, f.dec, 0)
|
bufferutils.writeVarInt(buffer, f.dec, 0)
|
||||||
}, /value must be < 2\^53/)
|
}, new RegExp(f.exception))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -85,11 +85,17 @@
|
||||||
"invalid": [
|
"invalid": [
|
||||||
{
|
{
|
||||||
"description": "n === 2^53",
|
"description": "n === 2^53",
|
||||||
"value": 9007199254740992
|
"exception": "value is larger than maximum value for type",
|
||||||
|
"hex64": "0000000000002000",
|
||||||
|
"hexVI": "ff0000000000000020",
|
||||||
|
"dec": 9007199254740992
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "n > 2^53",
|
"description": "n > 2^53",
|
||||||
"value": 18374686479671624000
|
"exception": "value is larger than maximum value for type",
|
||||||
|
"hex64": "0100000000002000",
|
||||||
|
"hexVI": "ff0100000000000020",
|
||||||
|
"dec": 9007199254740993
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
17
test/fixtures/hdwallet.json
vendored
17
test/fixtures/hdwallet.json
vendored
|
@ -176,5 +176,20 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"invalid": []
|
"invalid": {
|
||||||
|
"fromBase58": [
|
||||||
|
{
|
||||||
|
"exception": "Invalid checksum",
|
||||||
|
"string": "xprvQQQQQQQQQQQQQQQQCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exception": "Invalid BIP32 string",
|
||||||
|
"string": "SQ8nQ2jWarXqLo9oHGKKP6iQDsQbPRftq7rjtYY3hqJRPQRgrmeunFnDKbH7B15yGPLZBrhhkKXx3pwD6LcBooJRGq6x7matAXpMsgn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exception": "Invalid BIP32 string",
|
||||||
|
"string": "37hdAfw3aMiWcBGPP2ywmY5jizTeSSP5GXayKY3RxkEZ7f3SBnRE1pN6eY3VzGkgx6vbdNtuKfrHgEaYvW9KkFZCycaPvWiA9TtfmeVB592Sf9RfSzQzXo72"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
var assert = require('assert')
|
var assert = require('assert')
|
||||||
|
var networks = require('../src/networks')
|
||||||
|
|
||||||
var HDWallet = require('../src/hdwallet')
|
var HDWallet = require('../src/hdwallet')
|
||||||
var fixtures = require('./fixtures/hdwallet.json')
|
var fixtures = require('./fixtures/hdwallet.json')
|
||||||
|
@ -32,6 +33,16 @@ describe('HDWallet', function() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('fromBase58', function() {
|
||||||
|
fixtures.invalid.fromBase58.forEach(function(f) {
|
||||||
|
it('throws on ' + f.string, function() {
|
||||||
|
assert.throws(function() {
|
||||||
|
HDWallet.fromBase58(f.string)
|
||||||
|
}, new RegExp(f.exception))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('constructor & seed deserialization', function() {
|
describe('constructor & seed deserialization', function() {
|
||||||
var expectedPrivateKey = '0fd71c652e847ba7ea7956e3cf3fc0a0985871846b1b2c23b9c6a29a38cee860'
|
var expectedPrivateKey = '0fd71c652e847ba7ea7956e3cf3fc0a0985871846b1b2c23b9c6a29a38cee860'
|
||||||
var seed = new Buffer([
|
var seed = new Buffer([
|
||||||
|
@ -68,6 +79,15 @@ describe('HDWallet', function() {
|
||||||
buffer.writeUInt32BE(0xFFFFFFFF, 9)
|
buffer.writeUInt32BE(0xFFFFFFFF, 9)
|
||||||
assert.throws(function() { HDWallet.fromBuffer(buffer) }, /Invalid index/)
|
assert.throws(function() { HDWallet.fromBuffer(buffer) }, /Invalid index/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('fails for an invalid network type', function() {
|
||||||
|
var network = { bip32: { priv: 0x11111111, pub: 0x22222222 } }
|
||||||
|
var buffer = new HDWallet(seed, network).toBuffer()
|
||||||
|
|
||||||
|
assert.throws(function() {
|
||||||
|
HDWallet.fromBuffer(buffer)
|
||||||
|
}, /Could not find version 22222222/)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -119,24 +139,36 @@ describe('HDWallet', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('network types', function() {
|
describe('network types', function() {
|
||||||
|
var seed
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
seed = new Buffer('foobar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ensure that a bitcoin wallet is the default', function() {
|
||||||
|
var wallet = new HDWallet(seed)
|
||||||
|
|
||||||
|
assert.equal(wallet.network, networks.bitcoin)
|
||||||
|
})
|
||||||
|
|
||||||
it('ensures that a bitcoin Wallet generates bitcoin addresses', function() {
|
it('ensures that a bitcoin Wallet generates bitcoin addresses', function() {
|
||||||
var wallet = new HDWallet(new Buffer('foobar'), 'bitcoin')
|
var wallet = new HDWallet(seed)
|
||||||
assert.equal(wallet.getAddress().toString(), '17SnB9hyGwJPoKpLb9eVPHjsujyEuBpMAA')
|
var address = wallet.getAddress().toString()
|
||||||
|
|
||||||
|
assert.equal(address, '17SnB9hyGwJPoKpLb9eVPHjsujyEuBpMAA')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ensures that a testnet Wallet generates testnet addresses', function() {
|
it('ensures that a testnet Wallet generates testnet addresses', function() {
|
||||||
var wallet = new HDWallet(new Buffer('foobar'), 'testnet')
|
var wallet = new HDWallet(seed, networks.testnet)
|
||||||
assert.equal(wallet.getAddress().toString(), 'mmxjUCnx5xjeaSHxJicsDCxCmjZwq8KTbv')
|
var address = wallet.getAddress().toString()
|
||||||
|
|
||||||
|
assert.equal(address, 'mmxjUCnx5xjeaSHxJicsDCxCmjZwq8KTbv')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an exception when unknown network type is passed in', function() {
|
it('throws an exception when unknown network type is passed in', function() {
|
||||||
assert.throws(function() { new HDWallet(new Buffer('foobar'), 'doge') }, /Unknown network: doge/)
|
assert.throws(function() {
|
||||||
})
|
new HDWallet(seed, {})
|
||||||
|
}, /Unknown BIP32 constants for network/)
|
||||||
it('throws an exception with bad network type using fromBuffer', function() {
|
|
||||||
var buffer = new HDWallet(new Buffer('foobar'), 'bitcoin').toBuffer()
|
|
||||||
buffer.writeUInt32BE(0x00000000, 0)
|
|
||||||
assert.throws(function() { HDWallet.fromBuffer(buffer) }, /Could not find version 0/)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
160
test/wallet.js
160
test/wallet.js
|
@ -1,5 +1,6 @@
|
||||||
var assert = require('assert')
|
var assert = require('assert')
|
||||||
var crypto = require('../src/crypto')
|
var crypto = require('../src/crypto')
|
||||||
|
var networks = require('../src/networks')
|
||||||
var sinon = require('sinon')
|
var sinon = require('sinon')
|
||||||
|
|
||||||
var Address = require('../src/address')
|
var Address = require('../src/address')
|
||||||
|
@ -24,12 +25,8 @@ describe('Wallet', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('constructor', function() {
|
describe('constructor', function() {
|
||||||
it('should be ok to call without new', function() {
|
|
||||||
assert.ok(Wallet(seed) instanceof Wallet)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('defaults to Bitcoin network', function() {
|
it('defaults to Bitcoin network', function() {
|
||||||
assert.equal(wallet.getMasterKey().network, 'bitcoin')
|
assert.equal(wallet.getMasterKey().network, networks.bitcoin)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("generates m/0' as the main account", function() {
|
it("generates m/0' as the main account", function() {
|
||||||
|
@ -59,11 +56,11 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
describe('constructor options', function() {
|
describe('constructor options', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
wallet = new Wallet(seed, {network: 'testnet'})
|
wallet = new Wallet(seed, networks.testnet)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the network if specified', function() {
|
it('uses the network if specified', function() {
|
||||||
assert.equal(wallet.getMasterKey().network, 'testnet')
|
assert.equal(wallet.getMasterKey().network, networks.testnet)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -98,7 +95,7 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
describe('generateAddress', function(){
|
describe('generateAddress', function(){
|
||||||
it('generate receiving addresses', function(){
|
it('generate receiving addresses', function(){
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
var expectedAddresses = [
|
var expectedAddresses = [
|
||||||
"n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa",
|
"n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa",
|
||||||
"n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X"
|
"n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X"
|
||||||
|
@ -112,7 +109,7 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
describe('generateChangeAddress', function(){
|
describe('generateChangeAddress', function(){
|
||||||
it('generates change addresses', function(){
|
it('generates change addresses', function(){
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
var expectedAddresses = ["mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"]
|
var expectedAddresses = ["mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"]
|
||||||
|
|
||||||
assert.equal(wallet.generateChangeAddress(), expectedAddresses[0])
|
assert.equal(wallet.generateChangeAddress(), expectedAddresses[0])
|
||||||
|
@ -122,7 +119,7 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
describe('getPrivateKey', function(){
|
describe('getPrivateKey', function(){
|
||||||
it('returns the private key at the given index of external account', function(){
|
it('returns the private key at the given index of external account', function(){
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
|
|
||||||
assertEqual(wallet.getPrivateKey(0), wallet.getExternalAccount().derive(0).priv)
|
assertEqual(wallet.getPrivateKey(0), wallet.getExternalAccount().derive(0).priv)
|
||||||
assertEqual(wallet.getPrivateKey(1), wallet.getExternalAccount().derive(1).priv)
|
assertEqual(wallet.getPrivateKey(1), wallet.getExternalAccount().derive(1).priv)
|
||||||
|
@ -131,7 +128,7 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
describe('getInternalPrivateKey', function(){
|
describe('getInternalPrivateKey', function(){
|
||||||
it('returns the private key at the given index of internal account', function(){
|
it('returns the private key at the given index of internal account', function(){
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
|
|
||||||
assertEqual(wallet.getInternalPrivateKey(0), wallet.getInternalAccount().derive(0).priv)
|
assertEqual(wallet.getInternalPrivateKey(0), wallet.getInternalAccount().derive(0).priv)
|
||||||
assertEqual(wallet.getInternalPrivateKey(1), wallet.getInternalAccount().derive(1).priv)
|
assertEqual(wallet.getInternalPrivateKey(1), wallet.getInternalAccount().derive(1).priv)
|
||||||
|
@ -140,7 +137,7 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
describe('getPrivateKeyForAddress', function(){
|
describe('getPrivateKeyForAddress', function(){
|
||||||
it('returns the private key for the given address', function(){
|
it('returns the private key for the given address', function(){
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
wallet.generateChangeAddress()
|
wallet.generateChangeAddress()
|
||||||
wallet.generateAddress()
|
wallet.generateAddress()
|
||||||
wallet.generateAddress()
|
wallet.generateAddress()
|
||||||
|
@ -156,7 +153,7 @@ describe('Wallet', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('raises an error when address is not found', function(){
|
it('raises an error when address is not found', function(){
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
assert.throws(function() {
|
assert.throws(function() {
|
||||||
wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X")
|
wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X")
|
||||||
}, /Unknown address. Make sure the address is from the keychain and has been generated./)
|
}, /Unknown address. Make sure the address is from the keychain and has been generated./)
|
||||||
|
@ -168,7 +165,6 @@ describe('Wallet', function() {
|
||||||
beforeEach(function(){
|
beforeEach(function(){
|
||||||
expectedUtxo = {
|
expectedUtxo = {
|
||||||
"hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7",
|
"hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7",
|
||||||
"hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a",
|
|
||||||
"outputIndex": 0,
|
"outputIndex": 0,
|
||||||
"address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv",
|
"address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv",
|
||||||
"value": 20000
|
"value": 20000
|
||||||
|
@ -230,36 +226,13 @@ describe('Wallet', function() {
|
||||||
utxo = cloneObject([expectedUtxo])
|
utxo = cloneObject([expectedUtxo])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses hashLittleEndian when hash is not present', function(){
|
it('matches the expected behaviour', 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)
|
wallet.setUnspentOutputs(utxo)
|
||||||
verifyOutputs()
|
verifyOutputs()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('required fields', function(){
|
describe('required fields', function(){
|
||||||
it("throws an error when hash and hashLittleEndian are both missing", function(){
|
['outputIndex', 'address', 'hash', 'value'].forEach(function(field){
|
||||||
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(){
|
it("throws an error when " + field + " is missing", function(){
|
||||||
delete utxo[0][field]
|
delete utxo[0][field]
|
||||||
|
|
||||||
|
@ -277,38 +250,6 @@ describe('Wallet', function() {
|
||||||
assert.equal(output.address, utxo[0].address)
|
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(){
|
describe('processTx', function(){
|
||||||
|
@ -490,21 +431,31 @@ describe('Wallet', function() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testnet', function(){
|
describe(networks.testnet, function(){
|
||||||
it('should create transaction', function(){
|
it('should create transaction', function(){
|
||||||
var to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue'
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var address = wallet.generateAddress()
|
||||||
var tx = wallet.createTx(to, value)
|
|
||||||
|
|
||||||
|
wallet.setUnspentOutputs([{
|
||||||
|
hash: fakeTxHash(0),
|
||||||
|
outputIndex: 0,
|
||||||
|
address: address,
|
||||||
|
value: value
|
||||||
|
}])
|
||||||
|
|
||||||
|
var to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue'
|
||||||
|
var toValue = value - 20000
|
||||||
|
|
||||||
|
var tx = wallet.createTx(to, toValue)
|
||||||
assert.equal(tx.outs.length, 1)
|
assert.equal(tx.outs.length, 1)
|
||||||
assert.equal(tx.outs[0].address.toString(), to)
|
assert.equal(tx.outs[0].address.toString(), to)
|
||||||
assert.equal(tx.outs[0].value, value)
|
assert.equal(tx.outs[0].value, toValue)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('changeAddress', function(){
|
describe('changeAddress', function(){
|
||||||
it('should allow custom changeAddress', function(){
|
it('should allow custom changeAddress', function(){
|
||||||
var wallet = new Wallet(seed, {network: 'testnet'})
|
var wallet = new Wallet(seed, networks.testnet)
|
||||||
var address = wallet.generateAddress()
|
var address = wallet.generateAddress()
|
||||||
|
|
||||||
wallet.setUnspentOutputs([{
|
wallet.setUnspentOutputs([{
|
||||||
|
@ -595,7 +546,7 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
assert.throws(function() {
|
assert.throws(function() {
|
||||||
wallet.createTx(to, value)
|
wallet.createTx(to, value)
|
||||||
}, /Value must be above dust threshold/)
|
}, /5430 must be above dust threshold \(5430 Satoshis\)/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -605,62 +556,11 @@ describe('Wallet', function() {
|
||||||
|
|
||||||
assert.throws(function() {
|
assert.throws(function() {
|
||||||
wallet.createTx(to, value)
|
wallet.createTx(to, value)
|
||||||
}, /Not enough money to send funds including transaction fee. Have: 1420000, needed: 1420001/)
|
}, /Not enough funds \(incl. fee\): 1420000 < 1420001/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
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){
|
function assertEqual(obj1, obj2){
|
||||||
assert.equal(obj1.toString(), obj2.toString())
|
assert.equal(obj1.toString(), obj2.toString())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue