Matias Alejo Garcia
9 years ago
19 changed files with 1479 additions and 1012 deletions
@ -0,0 +1,22 @@ |
|||
'use strict'; |
|||
|
|||
var Constants = {}; |
|||
|
|||
Constants.SCRIPT_TYPES = { |
|||
P2SH: 'P2SH', |
|||
P2PKH: 'P2PKH', |
|||
}; |
|||
Constants.DERIVATION_STRATEGIES = { |
|||
BIP44: 'BIP44', |
|||
BIP45: 'BIP45', |
|||
}; |
|||
|
|||
Constants.PATHS = { |
|||
REQUEST_KEY: "m/1'/0", |
|||
TXPROPOSAL_KEY: "m/1'/1", |
|||
REQUEST_KEY_AUTH: "m/2", // relative to BASE
|
|||
}; |
|||
|
|||
Constants.BIP45_SHARED_INDEX = 0x80000000 - 1; |
|||
|
|||
module.exports = Constants; |
@ -0,0 +1,40 @@ |
|||
'use strict'; |
|||
|
|||
var Defaults = {}; |
|||
|
|||
Defaults.DEFAULT_FEE_PER_KB = 10000; |
|||
Defaults.MIN_FEE_PER_KB = 0; |
|||
Defaults.MAX_FEE_PER_KB = 1000000; |
|||
Defaults.MAX_TX_FEE = 1 * 1e8; |
|||
|
|||
Defaults.MAX_KEYS = 100; |
|||
|
|||
// Time after which a Tx proposal can be erased by any copayer. in seconds
|
|||
Defaults.DELETE_LOCKTIME = 24 * 3600; |
|||
|
|||
// Allowed consecutive txp rejections before backoff is applied.
|
|||
Defaults.BACKOFF_OFFSET = 3; |
|||
|
|||
// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in Minutes.
|
|||
Defaults.BACKOFF_TIME = 2; |
|||
|
|||
Defaults.MAX_MAIN_ADDRESS_GAP = 20; |
|||
|
|||
// TODO: should allow different gap sizes for external/internal chains
|
|||
Defaults.SCAN_ADDRESS_GAP = Defaults.MAX_MAIN_ADDRESS_GAP; |
|||
|
|||
Defaults.FEE_LEVELS = [{ |
|||
name: 'priority', |
|||
nbBlocks: 1, |
|||
defaultValue: 50000 |
|||
}, { |
|||
name: 'normal', |
|||
nbBlocks: 2, |
|||
defaultValue: 20000 |
|||
}, { |
|||
name: 'economy', |
|||
nbBlocks: 6, |
|||
defaultValue: 10000 |
|||
}]; |
|||
|
|||
module.exports = Defaults; |
@ -0,0 +1,7 @@ |
|||
var Common = {}; |
|||
|
|||
Common.Constants = require('./constants'); |
|||
Common.Defaults = require('./defaults'); |
|||
Common.Utils = require('./utils'); |
|||
|
|||
module.exports = Common; |
@ -0,0 +1,97 @@ |
|||
var $ = require('preconditions').singleton(); |
|||
var _ = require('lodash'); |
|||
|
|||
var Bitcore = require('bitcore-lib'); |
|||
var crypto = Bitcore.crypto; |
|||
var encoding = Bitcore.encoding; |
|||
|
|||
var Utils = {}; |
|||
|
|||
Utils.checkRequired = function(obj, args) { |
|||
args = [].concat(args); |
|||
if (!_.isObject(obj)) return false; |
|||
for (var i = 0; i < args.length; i++) { |
|||
if (!obj.hasOwnProperty(args[i])) return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
/** |
|||
* |
|||
* @desc rounds a JAvascript number |
|||
* @param number |
|||
* @return {number} |
|||
*/ |
|||
Utils.strip = function(number) { |
|||
return (parseFloat(number.toPrecision(12))); |
|||
} |
|||
|
|||
/* TODO: It would be nice to be compatible with bitcoind signmessage. How |
|||
* the hash is calculated there? */ |
|||
Utils.hashMessage = function(text) { |
|||
$.checkArgument(text); |
|||
var buf = new Buffer(text); |
|||
var ret = crypto.Hash.sha256sha256(buf); |
|||
ret = new Bitcore.encoding.BufferReader(ret).readReverse(); |
|||
return ret; |
|||
}; |
|||
|
|||
Utils.verifyMessage = function(text, signature, pubKey) { |
|||
$.checkArgument(text); |
|||
$.checkArgument(pubKey); |
|||
|
|||
if (!signature) |
|||
return false; |
|||
|
|||
var pub = new Bitcore.PublicKey(pubKey); |
|||
var hash = Utils.hashMessage(text); |
|||
|
|||
try { |
|||
var sig = new crypto.Signature.fromString(signature); |
|||
return crypto.ECDSA.verify(hash, sig, pub, 'little'); |
|||
} catch (e) { |
|||
return false; |
|||
} |
|||
}; |
|||
|
|||
Utils.formatAmount = function(satoshis, unit, opts) { |
|||
var UNITS = { |
|||
btc: { |
|||
toSatoshis: 100000000, |
|||
maxDecimals: 6, |
|||
minDecimals: 2, |
|||
}, |
|||
bit: { |
|||
toSatoshis: 100, |
|||
maxDecimals: 0, |
|||
minDecimals: 0, |
|||
}, |
|||
}; |
|||
|
|||
$.shouldBeNumber(satoshis); |
|||
$.checkArgument(_.contains(_.keys(UNITS), unit)); |
|||
|
|||
function addSeparators(nStr, thousands, decimal, minDecimals) { |
|||
nStr = nStr.replace('.', decimal); |
|||
var x = nStr.split(decimal); |
|||
var x0 = x[0]; |
|||
var x1 = x[1]; |
|||
|
|||
x1 = _.dropRightWhile(x1, function(n, i) { |
|||
return n == '0' && i >= minDecimals; |
|||
}).join(''); |
|||
var x2 = x.length > 1 ? decimal + x1 : ''; |
|||
|
|||
x0 = x0.replace(/\B(?=(\d{3})+(?!\d))/g, thousands); |
|||
return x0 + x2; |
|||
} |
|||
|
|||
opts = opts || {}; |
|||
|
|||
var u = UNITS[unit]; |
|||
var amount = (satoshis / u.toSatoshis).toFixed(u.maxDecimals); |
|||
return addSeparators(amount, opts.thousandsSeparator || ',', opts.decimalSeparator || '.', u.minDecimals); |
|||
}; |
|||
|
|||
|
|||
module.exports = Utils; |
@ -1,25 +0,0 @@ |
|||
var $ = require('preconditions').singleton(); |
|||
var _ = require('lodash'); |
|||
|
|||
var Utils = {}; |
|||
|
|||
Utils.checkRequired = function(obj, args) { |
|||
args = [].concat(args); |
|||
if (!_.isObject(obj)) return false; |
|||
for (var i = 0; i < args.length; i++) { |
|||
if (!obj.hasOwnProperty(args[i])) return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
/** |
|||
* |
|||
* @desc rounds a JAvascript number |
|||
* @param number |
|||
* @return {number} |
|||
*/ |
|||
Utils.strip = function(number) { |
|||
return (parseFloat(number.toPrecision(12))); |
|||
} |
|||
|
|||
module.exports = Utils; |
@ -0,0 +1,462 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var async = require('async'); |
|||
|
|||
var chai = require('chai'); |
|||
var sinon = require('sinon'); |
|||
var should = chai.should(); |
|||
var log = require('npmlog'); |
|||
log.debug = log.verbose; |
|||
log.level = 'info'; |
|||
|
|||
var WalletService = require('../../lib/server'); |
|||
var EmailService = require('../../lib/emailservice'); |
|||
|
|||
var TestData = require('../testdata'); |
|||
var helpers = require('./helpers'); |
|||
|
|||
describe('Email notifications', function() { |
|||
var server, wallet, mailerStub, emailService; |
|||
|
|||
before(function(done) { |
|||
helpers.before(done); |
|||
}); |
|||
after(function(done) { |
|||
helpers.after(done); |
|||
}); |
|||
describe('Shared wallet', function() { |
|||
beforeEach(function(done) { |
|||
helpers.beforeEach(function(res) { |
|||
helpers.createAndJoinWallet(2, 3, function(s, w) { |
|||
server = s; |
|||
wallet = w; |
|||
|
|||
var i = 0; |
|||
async.eachSeries(w.copayers, function(copayer, next) { |
|||
helpers.getAuthServer(copayer.id, function(server) { |
|||
server.savePreferences({ |
|||
email: 'copayer' + (++i) + '@domain.com', |
|||
unit: 'bit', |
|||
}, next); |
|||
}); |
|||
}, function(err) { |
|||
should.not.exist(err); |
|||
|
|||
mailerStub = sinon.stub(); |
|||
mailerStub.sendMail = sinon.stub(); |
|||
mailerStub.sendMail.yields(); |
|||
|
|||
emailService = new EmailService(); |
|||
emailService.start({ |
|||
lockOpts: {}, |
|||
messageBroker: server.messageBroker, |
|||
storage: helpers.getStorage(), |
|||
mailer: mailerStub, |
|||
emailOpts: { |
|||
from: 'bws@dummy.net', |
|||
subjectPrefix: '[test wallet]', |
|||
publicTxUrlTemplate: { |
|||
livenet: 'https://insight.bitpay.com/tx/{{txid}}', |
|||
testnet: 'https://test-insight.bitpay.com/tx/{{txid}}', |
|||
}, |
|||
}, |
|||
}, function(err) { |
|||
should.not.exist(err); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should notify copayers a new tx proposal has been created', function(done) { |
|||
var _readTemplateFile_old = emailService._readTemplateFile; |
|||
emailService._readTemplateFile = function(language, filename, cb) { |
|||
if (_.endsWith(filename, '.html')) { |
|||
return cb(null, '<html><body>{{walletName}}</body></html>'); |
|||
} else { |
|||
_readTemplateFile_old.call(emailService, language, filename, cb); |
|||
} |
|||
}; |
|||
helpers.stubUtxos(server, wallet, [1, 1], function() { |
|||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { |
|||
message: 'some message' |
|||
}); |
|||
server.createTx(txOpts, function(err, tx) { |
|||
should.not.exist(err); |
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
calls.length.should.equal(2); |
|||
var emails = _.map(calls, function(c) { |
|||
return c.args[0]; |
|||
}); |
|||
_.difference(['copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; |
|||
var one = emails[0]; |
|||
one.from.should.equal('bws@dummy.net'); |
|||
one.subject.should.contain('New payment proposal'); |
|||
one.text.should.contain(wallet.name); |
|||
one.text.should.contain(wallet.copayers[0].name); |
|||
should.exist(one.html); |
|||
one.html.indexOf('<html>').should.equal(0); |
|||
one.html.should.contain(wallet.name); |
|||
server.storage.fetchUnsentEmails(function(err, unsent) { |
|||
should.not.exist(err); |
|||
unsent.should.be.empty; |
|||
emailService._readTemplateFile = _readTemplateFile_old; |
|||
done(); |
|||
}); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should not send email if unable to apply template to notification', function(done) { |
|||
var _applyTemplate_old = emailService._applyTemplate; |
|||
emailService._applyTemplate = function(template, data, cb) { |
|||
_applyTemplate_old.call(emailService, template, undefined, cb); |
|||
}; |
|||
helpers.stubUtxos(server, wallet, [1, 1], function() { |
|||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { |
|||
message: 'some message' |
|||
}); |
|||
server.createTx(txOpts, function(err, tx) { |
|||
should.not.exist(err); |
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
calls.length.should.equal(0); |
|||
server.storage.fetchUnsentEmails(function(err, unsent) { |
|||
should.not.exist(err); |
|||
unsent.should.be.empty; |
|||
emailService._applyTemplate = _applyTemplate_old; |
|||
done(); |
|||
}); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should notify copayers a new outgoing tx has been created', function(done) { |
|||
var _readTemplateFile_old = emailService._readTemplateFile; |
|||
emailService._readTemplateFile = function(language, filename, cb) { |
|||
if (_.endsWith(filename, '.html')) { |
|||
return cb(null, '<html>{{&urlForTx}}<html>'); |
|||
} else { |
|||
_readTemplateFile_old.call(emailService, language, filename, cb); |
|||
} |
|||
}; |
|||
helpers.stubUtxos(server, wallet, [1, 1], function() { |
|||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { |
|||
message: 'some message' |
|||
}); |
|||
|
|||
var txp; |
|||
async.waterfall([ |
|||
|
|||
function(next) { |
|||
server.createTx(txOpts, next); |
|||
}, |
|||
function(t, next) { |
|||
txp = t; |
|||
async.eachSeries(_.range(2), function(i, next) { |
|||
var copayer = TestData.copayers[i]; |
|||
helpers.getAuthServer(copayer.id44, function(server) { |
|||
var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H); |
|||
server.signTx({ |
|||
txProposalId: txp.id, |
|||
signatures: signatures, |
|||
}, function(err, t) { |
|||
txp = t; |
|||
next(); |
|||
}); |
|||
}); |
|||
}, next); |
|||
}, |
|||
function(next) { |
|||
helpers.stubBroadcast(); |
|||
server.broadcastTx({ |
|||
txProposalId: txp.id, |
|||
}, next); |
|||
}, |
|||
], function(err) { |
|||
should.not.exist(err); |
|||
|
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
var emails = _.map(_.takeRight(calls, 3), function(c) { |
|||
return c.args[0]; |
|||
}); |
|||
_.difference(['copayer1@domain.com', 'copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; |
|||
var one = emails[0]; |
|||
one.from.should.equal('bws@dummy.net'); |
|||
one.subject.should.contain('Payment sent'); |
|||
one.text.should.contain(wallet.name); |
|||
one.text.should.contain('800,000'); |
|||
should.exist(one.html); |
|||
one.html.should.contain('https://insight.bitpay.com/tx/' + txp.txid); |
|||
server.storage.fetchUnsentEmails(function(err, unsent) { |
|||
should.not.exist(err); |
|||
unsent.should.be.empty; |
|||
emailService._readTemplateFile = _readTemplateFile_old; |
|||
done(); |
|||
}); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should notify copayers a tx has been finally rejected', function(done) { |
|||
helpers.stubUtxos(server, wallet, 1, function() { |
|||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { |
|||
message: 'some message' |
|||
}); |
|||
|
|||
var txpId; |
|||
async.waterfall([ |
|||
|
|||
function(next) { |
|||
server.createTx(txOpts, next); |
|||
}, |
|||
function(txp, next) { |
|||
txpId = txp.id; |
|||
async.eachSeries(_.range(1, 3), function(i, next) { |
|||
var copayer = TestData.copayers[i]; |
|||
helpers.getAuthServer(copayer.id44, function(server) { |
|||
server.rejectTx({ |
|||
txProposalId: txp.id, |
|||
}, next); |
|||
}); |
|||
}, next); |
|||
}, |
|||
], function(err) { |
|||
should.not.exist(err); |
|||
|
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
var emails = _.map(_.takeRight(calls, 2), function(c) { |
|||
return c.args[0]; |
|||
}); |
|||
_.difference(['copayer1@domain.com', 'copayer2@domain.com'], _.pluck(emails, 'to')).should.be.empty; |
|||
var one = emails[0]; |
|||
one.from.should.equal('bws@dummy.net'); |
|||
one.subject.should.contain('Payment proposal rejected'); |
|||
one.text.should.contain(wallet.name); |
|||
one.text.should.contain('copayer 2, copayer 3'); |
|||
one.text.should.not.contain('copayer 1'); |
|||
server.storage.fetchUnsentEmails(function(err, unsent) { |
|||
should.not.exist(err); |
|||
unsent.should.be.empty; |
|||
done(); |
|||
}); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should notify copayers of incoming txs', function(done) { |
|||
server.createAddress({}, function(err, address) { |
|||
should.not.exist(err); |
|||
|
|||
// Simulate incoming tx notification
|
|||
server._notify('NewIncomingTx', { |
|||
txid: '999', |
|||
address: address, |
|||
amount: 12300000, |
|||
}, function(err) { |
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
calls.length.should.equal(3); |
|||
var emails = _.map(calls, function(c) { |
|||
return c.args[0]; |
|||
}); |
|||
_.difference(['copayer1@domain.com', 'copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; |
|||
var one = emails[0]; |
|||
one.from.should.equal('bws@dummy.net'); |
|||
one.subject.should.contain('New payment received'); |
|||
one.text.should.contain(wallet.name); |
|||
one.text.should.contain('123,000'); |
|||
server.storage.fetchUnsentEmails(function(err, unsent) { |
|||
should.not.exist(err); |
|||
unsent.should.be.empty; |
|||
done(); |
|||
}); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should notify each email address only once', function(done) { |
|||
// Set same email address for copayer1 and copayer2
|
|||
server.savePreferences({ |
|||
email: 'copayer2@domain.com', |
|||
}, function(err) { |
|||
server.createAddress({}, function(err, address) { |
|||
should.not.exist(err); |
|||
|
|||
// Simulate incoming tx notification
|
|||
server._notify('NewIncomingTx', { |
|||
txid: '999', |
|||
address: address, |
|||
amount: 12300000, |
|||
}, function(err) { |
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
calls.length.should.equal(2); |
|||
var emails = _.map(calls, function(c) { |
|||
return c.args[0]; |
|||
}); |
|||
_.difference(['copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; |
|||
var one = emails[0]; |
|||
one.from.should.equal('bws@dummy.net'); |
|||
one.subject.should.contain('New payment received'); |
|||
one.text.should.contain(wallet.name); |
|||
one.text.should.contain('123,000'); |
|||
server.storage.fetchUnsentEmails(function(err, unsent) { |
|||
should.not.exist(err); |
|||
unsent.should.be.empty; |
|||
done(); |
|||
}); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should build each email using preferences of the copayers', function(done) { |
|||
// Set same email address for copayer1 and copayer2
|
|||
server.savePreferences({ |
|||
email: 'copayer1@domain.com', |
|||
language: 'es', |
|||
unit: 'btc', |
|||
}, function(err) { |
|||
server.createAddress({}, function(err, address) { |
|||
should.not.exist(err); |
|||
|
|||
// Simulate incoming tx notification
|
|||
server._notify('NewIncomingTx', { |
|||
txid: '999', |
|||
address: address, |
|||
amount: 12300000, |
|||
}, function(err) { |
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
calls.length.should.equal(3); |
|||
var emails = _.map(calls, function(c) { |
|||
return c.args[0]; |
|||
}); |
|||
var spanish = _.find(emails, { |
|||
to: 'copayer1@domain.com' |
|||
}); |
|||
spanish.from.should.equal('bws@dummy.net'); |
|||
spanish.subject.should.contain('Nuevo pago recibido'); |
|||
spanish.text.should.contain(wallet.name); |
|||
spanish.text.should.contain('0.123 BTC'); |
|||
var english = _.find(emails, { |
|||
to: 'copayer2@domain.com' |
|||
}); |
|||
english.from.should.equal('bws@dummy.net'); |
|||
english.subject.should.contain('New payment received'); |
|||
english.text.should.contain(wallet.name); |
|||
english.text.should.contain('123,000 bits'); |
|||
done(); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should support multiple emailservice instances running concurrently', function(done) { |
|||
var emailService2 = new EmailService(); |
|||
emailService2.start({ |
|||
lock: emailService.lock, // Use same locker service
|
|||
messageBroker: server.messageBroker, |
|||
storage: helpers.getStorage(), |
|||
mailer: mailerStub, |
|||
emailOpts: { |
|||
from: 'bws2@dummy.net', |
|||
subjectPrefix: '[test wallet 2]', |
|||
}, |
|||
}, function(err) { |
|||
helpers.stubUtxos(server, wallet, 1, function() { |
|||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { |
|||
message: 'some message' |
|||
}); |
|||
server.createTx(txOpts, function(err, tx) { |
|||
should.not.exist(err); |
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
calls.length.should.equal(2); |
|||
server.storage.fetchUnsentEmails(function(err, unsent) { |
|||
should.not.exist(err); |
|||
unsent.should.be.empty; |
|||
done(); |
|||
}); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('1-of-N wallet', function() { |
|||
beforeEach(function(done) { |
|||
helpers.beforeEach(function(res) { |
|||
helpers.createAndJoinWallet(1, 2, function(s, w) { |
|||
server = s; |
|||
wallet = w; |
|||
|
|||
var i = 0; |
|||
async.eachSeries(w.copayers, function(copayer, next) { |
|||
helpers.getAuthServer(copayer.id, function(server) { |
|||
server.savePreferences({ |
|||
email: 'copayer' + (++i) + '@domain.com', |
|||
unit: 'bit', |
|||
}, next); |
|||
}); |
|||
}, function(err) { |
|||
should.not.exist(err); |
|||
|
|||
mailerStub = sinon.stub(); |
|||
mailerStub.sendMail = sinon.stub(); |
|||
mailerStub.sendMail.yields(); |
|||
|
|||
emailService = new EmailService(); |
|||
emailService.start({ |
|||
lockOpts: {}, |
|||
messageBroker: server.messageBroker, |
|||
storage: helpers.getStorage(), |
|||
mailer: mailerStub, |
|||
emailOpts: { |
|||
from: 'bws@dummy.net', |
|||
subjectPrefix: '[test wallet]', |
|||
publicTxUrlTemplate: { |
|||
livenet: 'https://insight.bitpay.com/tx/{{txid}}', |
|||
testnet: 'https://test-insight.bitpay.com/tx/{{txid}}', |
|||
}, |
|||
}, |
|||
}, function(err) { |
|||
should.not.exist(err); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('should NOT notify copayers a new tx proposal has been created', function(done) { |
|||
helpers.stubUtxos(server, wallet, [1, 1], function() { |
|||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { |
|||
message: 'some message' |
|||
}); |
|||
server.createTx(txOpts, function(err, tx) { |
|||
should.not.exist(err); |
|||
setTimeout(function() { |
|||
var calls = mailerStub.sendMail.getCalls(); |
|||
calls.length.should.equal(0); |
|||
done(); |
|||
}, 100); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,419 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var async = require('async'); |
|||
|
|||
var chai = require('chai'); |
|||
var sinon = require('sinon'); |
|||
var should = chai.should(); |
|||
var log = require('npmlog'); |
|||
log.debug = log.verbose; |
|||
var tingodb = require('tingodb')({ |
|||
memStore: true |
|||
}); |
|||
|
|||
var Bitcore = require('bitcore-lib'); |
|||
|
|||
var Common = require('../../lib/common'); |
|||
var Utils = Common.Utils; |
|||
var Constants = Common.Constants; |
|||
var Defaults = Common.Defaults; |
|||
|
|||
var Storage = require('../../lib/storage'); |
|||
var Model = require('../../lib/model'); |
|||
var WalletService = require('../../lib/server'); |
|||
var TestData = require('../testdata'); |
|||
|
|||
var storage, blockchainExplorer; |
|||
|
|||
var helpers = {}; |
|||
|
|||
var useMongoDb = !!process.env.USE_MONGO_DB; |
|||
|
|||
helpers.before = function(cb) { |
|||
function getDb(cb) { |
|||
if (useMongoDb) { |
|||
var mongodb = require('mongodb'); |
|||
mongodb.MongoClient.connect('mongodb://localhost:27017/bws_test', function(err, db) { |
|||
if (err) throw err; |
|||
return cb(db); |
|||
}); |
|||
} else { |
|||
var db = new tingodb.Db('./db/test', {}); |
|||
return cb(db); |
|||
} |
|||
} |
|||
getDb(function(db) { |
|||
storage = new Storage({ |
|||
db: db |
|||
}); |
|||
return cb(); |
|||
}); |
|||
}; |
|||
|
|||
helpers.beforeEach = function(cb) { |
|||
if (!storage.db) return cb(); |
|||
storage.db.dropDatabase(function(err) { |
|||
if (err) return cb(err); |
|||
blockchainExplorer = sinon.stub(); |
|||
var opts = { |
|||
storage: storage, |
|||
blockchainExplorer: blockchainExplorer |
|||
}; |
|||
WalletService.initialize(opts, function() { |
|||
return cb(opts); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
helpers.after = function(cb) { |
|||
WalletService.shutDown(cb); |
|||
}; |
|||
|
|||
helpers.getBlockchainExplorer = function() { |
|||
return blockchainExplorer; |
|||
}; |
|||
|
|||
helpers.getStorage = function() { |
|||
return storage; |
|||
}; |
|||
|
|||
helpers.signMessage = function(text, privKey) { |
|||
var priv = new Bitcore.PrivateKey(privKey); |
|||
var hash = Utils.hashMessage(text); |
|||
return Bitcore.crypto.ECDSA.sign(hash, priv, 'little').toString(); |
|||
}; |
|||
|
|||
helpers.signRequestPubKey = function(requestPubKey, xPrivKey) { |
|||
var priv = new Bitcore.HDPrivateKey(xPrivKey).derive(Constants.PATHS.REQUEST_KEY_AUTH).privateKey; |
|||
return helpers.signMessage(requestPubKey, priv); |
|||
}; |
|||
|
|||
helpers.getAuthServer = function(copayerId, cb) { |
|||
var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); |
|||
verifyStub.returns(true); |
|||
WalletService.getInstanceWithAuth({ |
|||
copayerId: copayerId, |
|||
message: 'dummy', |
|||
signature: 'dummy', |
|||
clientVersion: 'bwc-0.1.0', |
|||
}, function(err, server) { |
|||
verifyStub.restore(); |
|||
if (err || !server) throw new Error('Could not login as copayerId ' + copayerId); |
|||
return cb(server); |
|||
}); |
|||
}; |
|||
|
|||
helpers._generateCopayersTestData = function(n) { |
|||
console.log('var copayers = ['); |
|||
_.each(_.range(n), function(c) { |
|||
var xpriv = new Bitcore.HDPrivateKey(); |
|||
var xpub = Bitcore.HDPublicKey(xpriv); |
|||
|
|||
var xpriv_45H = xpriv.derive(45, true); |
|||
var xpub_45H = Bitcore.HDPublicKey(xpriv_45H); |
|||
var id45 = Copayer._xPubToCopayerId(xpub_45H.toString()); |
|||
|
|||
var xpriv_44H_0H_0H = xpriv.derive(44, true).derive(0, true).derive(0, true); |
|||
var xpub_44H_0H_0H = Bitcore.HDPublicKey(xpriv_44H_0H_0H); |
|||
var id44 = Copayer._xPubToCopayerId(xpub_44H_0H_0H.toString()); |
|||
|
|||
var xpriv_1H = xpriv.derive(1, true); |
|||
var xpub_1H = Bitcore.HDPublicKey(xpriv_1H); |
|||
var priv = xpriv_1H.derive(0).privateKey; |
|||
var pub = xpub_1H.derive(0).publicKey; |
|||
|
|||
console.log('{id44: ', "'" + id44 + "',"); |
|||
console.log('id45: ', "'" + id45 + "',"); |
|||
console.log('xPrivKey: ', "'" + xpriv.toString() + "',"); |
|||
console.log('xPubKey: ', "'" + xpub.toString() + "',"); |
|||
console.log('xPrivKey_45H: ', "'" + xpriv_45H.toString() + "',"); |
|||
console.log('xPubKey_45H: ', "'" + xpub_45H.toString() + "',"); |
|||
console.log('xPrivKey_44H_0H_0H: ', "'" + xpriv_44H_0H_0H.toString() + "',"); |
|||
console.log('xPubKey_44H_0H_0H: ', "'" + xpub_44H_0H_0H.toString() + "',"); |
|||
console.log('xPrivKey_1H: ', "'" + xpriv_1H.toString() + "',"); |
|||
console.log('xPubKey_1H: ', "'" + xpub_1H.toString() + "',"); |
|||
console.log('privKey_1H_0: ', "'" + priv.toString() + "',"); |
|||
console.log('pubKey_1H_0: ', "'" + pub.toString() + "'},"); |
|||
}); |
|||
console.log('];'); |
|||
}; |
|||
|
|||
helpers.getSignedCopayerOpts = function(opts) { |
|||
var hash = WalletService._getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); |
|||
opts.copayerSignature = helpers.signMessage(hash, TestData.keyPair.priv); |
|||
return opts; |
|||
}; |
|||
|
|||
helpers.createAndJoinWallet = function(m, n, opts, cb) { |
|||
if (_.isFunction(opts)) { |
|||
cb = opts; |
|||
opts = {}; |
|||
} |
|||
opts = opts || {}; |
|||
|
|||
var server = new WalletService(); |
|||
var copayerIds = []; |
|||
var offset = opts.offset || 0; |
|||
|
|||
var walletOpts = { |
|||
name: 'a wallet', |
|||
m: m, |
|||
n: n, |
|||
pubKey: TestData.keyPair.pub, |
|||
}; |
|||
if (_.isBoolean(opts.supportBIP44AndP2PKH)) |
|||
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; |
|||
|
|||
server.createWallet(walletOpts, function(err, walletId) { |
|||
if (err) return cb(err); |
|||
|
|||
async.each(_.range(n), function(i, cb) { |
|||
var copayerData = TestData.copayers[i + offset]; |
|||
var copayerOpts = helpers.getSignedCopayerOpts({ |
|||
walletId: walletId, |
|||
name: 'copayer ' + (i + 1), |
|||
xPubKey: (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H, |
|||
requestPubKey: copayerData.pubKey_1H_0, |
|||
customData: 'custom data ' + (i + 1), |
|||
}); |
|||
if (_.isBoolean(opts.supportBIP44AndP2PKH)) |
|||
copayerOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; |
|||
|
|||
server.joinWallet(copayerOpts, function(err, result) { |
|||
should.not.exist(err); |
|||
copayerIds.push(result.copayerId); |
|||
return cb(err); |
|||
}); |
|||
}, function(err) { |
|||
if (err) return new Error('Could not generate wallet'); |
|||
helpers.getAuthServer(copayerIds[0], function(s) { |
|||
s.getWallet({}, function(err, w) { |
|||
cb(s, w); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
|
|||
helpers.randomTXID = function() { |
|||
return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');; |
|||
}; |
|||
|
|||
helpers.toSatoshi = function(btc) { |
|||
if (_.isArray(btc)) { |
|||
return _.map(btc, helpers.toSatoshi); |
|||
} else { |
|||
return Utils.strip(btc * 1e8); |
|||
} |
|||
}; |
|||
|
|||
helpers.stubUtxos = function(server, wallet, amounts, cb) { |
|||
async.mapSeries(_.range(0, amounts.length > 2 ? 2 : 1), function(i, next) { |
|||
server.createAddress({}, next); |
|||
}, function(err, addresses) { |
|||
should.not.exist(err); |
|||
addresses.should.not.be.empty; |
|||
var utxos = _.map([].concat(amounts), function(amount, i) { |
|||
var address = addresses[i % addresses.length]; |
|||
var confirmations; |
|||
if (_.isString(amount) && _.startsWith(amount, 'u')) { |
|||
amount = parseFloat(amount.substring(1)); |
|||
confirmations = 0; |
|||
} else { |
|||
confirmations = Math.floor(Math.random() * 100 + 1); |
|||
} |
|||
|
|||
var scriptPubKey; |
|||
switch (wallet.addressType) { |
|||
case Constants.SCRIPT_TYPES.P2SH: |
|||
scriptPubKey = Bitcore.Script.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut(); |
|||
break; |
|||
case Constants.SCRIPT_TYPES.P2PKH: |
|||
scriptPubKey = Bitcore.Script.buildPublicKeyHashOut(address.address); |
|||
break; |
|||
} |
|||
should.exist(scriptPubKey); |
|||
|
|||
return { |
|||
txid: helpers.randomTXID(), |
|||
vout: Math.floor(Math.random() * 10 + 1), |
|||
satoshis: helpers.toSatoshi(amount).toString(), |
|||
scriptPubKey: scriptPubKey.toBuffer().toString('hex'), |
|||
address: address.address, |
|||
confirmations: confirmations, |
|||
}; |
|||
}); |
|||
blockchainExplorer.getUnspentUtxos = function(addresses, cb) { |
|||
var selected = _.filter(utxos, function(utxo) { |
|||
return _.contains(addresses, utxo.address); |
|||
}); |
|||
return cb(null, selected); |
|||
}; |
|||
|
|||
return cb(utxos); |
|||
}); |
|||
}; |
|||
|
|||
helpers.stubBroadcast = function(thirdPartyBroadcast) { |
|||
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, null, '112233'); |
|||
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null); |
|||
}; |
|||
|
|||
helpers.stubHistory = function(txs) { |
|||
blockchainExplorer.getTransactions = function(addresses, from, to, cb) { |
|||
var MAX_BATCH_SIZE = 100; |
|||
var nbTxs = txs.length; |
|||
|
|||
if (_.isUndefined(from) && _.isUndefined(to)) { |
|||
from = 0; |
|||
to = MAX_BATCH_SIZE; |
|||
} |
|||
if (!_.isUndefined(from) && _.isUndefined(to)) |
|||
to = from + MAX_BATCH_SIZE; |
|||
|
|||
if (!_.isUndefined(from) && !_.isUndefined(to) && to - from > MAX_BATCH_SIZE) |
|||
to = from + MAX_BATCH_SIZE; |
|||
|
|||
if (from < 0) from = 0; |
|||
if (to < 0) to = 0; |
|||
if (from > nbTxs) from = nbTxs; |
|||
if (to > nbTxs) to = nbTxs; |
|||
|
|||
var page = txs.slice(from, to); |
|||
return cb(null, page); |
|||
}; |
|||
}; |
|||
|
|||
helpers.stubFeeLevels = function(levels) { |
|||
blockchainExplorer.estimateFee = function(nbBlocks, cb) { |
|||
var result = _.zipObject(_.map(_.pick(levels, nbBlocks), function(fee, n) { |
|||
return [+n, fee > 0 ? fee / 1e8 : fee]; |
|||
})); |
|||
return cb(null, result); |
|||
}; |
|||
}; |
|||
|
|||
helpers.stubAddressActivity = function(activeAddresses) { |
|||
blockchainExplorer.getAddressActivity = function(address, cb) { |
|||
return cb(null, _.contains(activeAddresses, address)); |
|||
}; |
|||
}; |
|||
|
|||
helpers.clientSign = function(txp, derivedXPrivKey) { |
|||
var self = this; |
|||
|
|||
//Derive proper key to sign, for each input
|
|||
var privs = []; |
|||
var derived = {}; |
|||
|
|||
var xpriv = new Bitcore.HDPrivateKey(derivedXPrivKey, txp.network); |
|||
|
|||
_.each(txp.inputs, function(i) { |
|||
if (!derived[i.path]) { |
|||
derived[i.path] = xpriv.derive(i.path).privateKey; |
|||
privs.push(derived[i.path]); |
|||
} |
|||
}); |
|||
|
|||
var t = txp.getBitcoreTx(); |
|||
|
|||
var signatures = _.map(privs, function(priv, i) { |
|||
return t.getSignatures(priv); |
|||
}); |
|||
|
|||
signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { |
|||
return s.signature.toDER().toString('hex'); |
|||
}); |
|||
|
|||
return signatures; |
|||
}; |
|||
|
|||
|
|||
helpers.createProposalOptsLegacy = function(toAddress, amount, message, signingKey, feePerKb) { |
|||
var opts = { |
|||
toAddress: toAddress, |
|||
amount: helpers.toSatoshi(amount), |
|||
message: message, |
|||
proposalSignature: null, |
|||
}; |
|||
if (feePerKb) opts.feePerKb = feePerKb; |
|||
|
|||
var hash = WalletService._getProposalHash(toAddress, opts.amount, message); |
|||
|
|||
try { |
|||
opts.proposalSignature = helpers.signMessage(hash, signingKey); |
|||
} catch (ex) {} |
|||
|
|||
return opts; |
|||
}; |
|||
|
|||
helpers.createSimpleProposalOpts = function(toAddress, amount, signingKey, opts) { |
|||
var outputs = [{ |
|||
toAddress: toAddress, |
|||
amount: amount, |
|||
}]; |
|||
return helpers.createProposalOpts(Model.TxProposal.Types.SIMPLE, outputs, signingKey, opts); |
|||
}; |
|||
|
|||
helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts) { |
|||
_.each(outputs, function(output) { |
|||
output.amount = helpers.toSatoshi(output.amount); |
|||
}); |
|||
|
|||
var opts = { |
|||
type: type, |
|||
proposalSignature: null |
|||
}; |
|||
|
|||
if (moreOpts) { |
|||
moreOpts = _.chain(moreOpts) |
|||
.pick(['feePerKb', 'customData', 'message']) |
|||
.value(); |
|||
opts = _.assign(opts, moreOpts); |
|||
} |
|||
|
|||
opts = _.defaults(opts, { |
|||
message: null |
|||
}); |
|||
|
|||
var hash; |
|||
if (type == Model.TxProposal.Types.SIMPLE) { |
|||
opts.toAddress = outputs[0].toAddress; |
|||
opts.amount = outputs[0].amount; |
|||
hash = WalletService._getProposalHash(opts.toAddress, opts.amount, |
|||
opts.message, opts.payProUrl); |
|||
} else if (type == Model.TxProposal.Types.MULTIPLEOUTPUTS) { |
|||
opts.outputs = outputs; |
|||
var header = { |
|||
outputs: outputs, |
|||
message: opts.message, |
|||
payProUrl: opts.payProUrl |
|||
}; |
|||
hash = WalletService._getProposalHash(header); |
|||
} |
|||
|
|||
try { |
|||
opts.proposalSignature = helpers.signMessage(hash, signingKey); |
|||
} catch (ex) {} |
|||
|
|||
return opts; |
|||
}; |
|||
|
|||
helpers.createAddresses = function(server, wallet, main, change, cb) { |
|||
var clock = sinon.useFakeTimers(Date.now(), 'Date'); |
|||
async.map(_.range(main + change), function(i, next) { |
|||
clock.tick(1000); |
|||
var address = wallet.createAddress(i >= main); |
|||
server.storage.storeAddressAndWallet(wallet, address, function(err) { |
|||
next(err, address); |
|||
}); |
|||
}, function(err, addresses) { |
|||
if (err) throw new Error('Could not generate addresses'); |
|||
clock.restore(); |
|||
return cb(_.take(addresses, main), _.takeRight(addresses, change)); |
|||
}); |
|||
}; |
|||
|
|||
module.exports = helpers; |
File diff suppressed because it is too large
Loading…
Reference in new issue