bitcore-wallet-client-dash
Version:
Client for bitcore-wallet-service-dash
1,472 lines (1,343 loc) • 199 kB
JavaScript
'use strict';
var _ = require('lodash');
var $ = require('preconditions').singleton();
var chai = require('chai');
chai.config.includeStack = true;
var sinon = require('sinon');
var should = chai.should();
var async = require('async');
var request = require('supertest');
var Uuid = require('uuid');
var tingodb = require('tingodb')({
memStore: true
});
var log = require('../lib/log');
var Bitcore = require('@dashevo/dashcore-lib');
var Bitcore_ = {
btc: Bitcore,
bch: require('bitcore-lib-cash'),
};
var BitcorePayPro = require('@dashevo/dashcore-payment-protocol');
var BWS = require('bitcore-wallet-service-dash');
var Common = require('../lib/common');
var Constants = Common.Constants;
var Utils = Common.Utils;
var Client = require('../lib');
var ExpressApp = BWS.ExpressApp;
var Storage = BWS.Storage;
var TestData = require('./testdata');
var ImportData = require('./legacyImportData.js');
var Errors = require('../lib/errors');
var helpers = {};
helpers.toSatoshi = function(btc) {
if (_.isArray(btc)) {
return _.map(btc, helpers.toSatoshi);
} else {
return parseFloat((btc * 1e8).toPrecision(12));
}
};
helpers.newClient = function(app) {
$.checkArgument(app);
return new Client({
baseUrl: '/bws/api',
request: request(app),
});
};
helpers.stubRequest = function(err, res) {
var request = {
accept: sinon.stub(),
set: sinon.stub(),
query: sinon.stub(),
send: sinon.stub(),
timeout: sinon.stub(),
end: sinon.stub().yields(err, res),
};
var reqFactory = _.reduce(['get', 'post', 'put', 'delete'], function(mem, verb) {
mem[verb] = function(url) {
return request;
};
return mem;
}, {});
return reqFactory;
};
helpers.newDb = function() {
this.dbCounter = (this.dbCounter || 0) + 1;
return new tingodb.Db('./db/test' + this.dbCounter, {});
};
helpers.generateUtxos = function(scriptType, publicKeyRing, path, requiredSignatures, amounts, coin) {
var amounts = [].concat(amounts);
var coin = coin || 'btc';
if (coin == 'bch') {
var bitcore = Bitcore_.bch;
} else {
var bitcore = Bitcore;
}
var utxos = _.map(amounts, function(amount, i) {
var address = Utils.deriveAddress(scriptType, publicKeyRing, path, requiredSignatures, 'testnet', coin);
var scriptPubKey;
switch (scriptType) {
case Constants.SCRIPT_TYPES.P2SH:
scriptPubKey = bitcore.Script.buildMultisigOut(address.publicKeys, requiredSignatures).toScriptHashOut();
break;
case Constants.SCRIPT_TYPES.P2PKH:
scriptPubKey = bitcore.Script.buildPublicKeyHashOut(address.address);
break;
}
should.exist(scriptPubKey);
var obj = {
txid: bitcore.crypto.Hash.sha256(new Buffer(i)).toString('hex'),
vout: 100,
satoshis: helpers.toSatoshi(amount),
scriptPubKey: scriptPubKey.toBuffer().toString('hex'),
address: address.address,
path: path,
publicKeys: address.publicKeys,
};
return obj;
});
return utxos;
};
helpers.createAndJoinWallet = function(clients, m, n, opts, cb) {
if (_.isFunction(opts)) {
cb = opts;
opts = null;
}
opts = opts || {};
var coin = opts.coin || 'btc';
var network = opts.network || 'testnet';
clients[0].seedFromRandomWithMnemonic({
coin: coin,
network: network,
});
clients[0].createWallet('mywallet', 'creator', m, n, {
coin: coin,
network: network,
singleAddress: !!opts.singleAddress,
}, function(err, secret) {
should.not.exist(err);
if (n > 1) {
should.exist(secret);
}
async.series([
function(next) {
async.each(_.range(1, n), function(i, cb) {
clients[i].seedFromRandomWithMnemonic({
coin: coin,
network: network
});
clients[i].joinWallet(secret, 'copayer ' + i, {
coin: coin
}, cb);
}, next);
},
function(next) {
async.each(_.range(n), function(i, cb) {
clients[i].openWallet(cb);
}, next);
},
],
function(err) {
should.not.exist(err);
return cb({
m: m,
n: n,
secret: secret,
});
});
});
};
helpers.tamperResponse = function(clients, method, url, args, tamper, cb) {
clients = [].concat(clients);
// Use first client to get a clean response from server
clients[0]._doRequest(method, url, args, false, function(err, result) {
should.not.exist(err);
tamper(result);
// Return tampered data for every client in the list
_.each(clients, function(client) {
client._doRequest = sinon.stub().withArgs(method, url).yields(null, result);
});
return cb();
});
};
helpers.createAndPublishTxProposal = function(client, opts, cb) {
if (!opts.outputs) {
opts.outputs = [{
toAddress: opts.toAddress,
amount: opts.amount,
}];
}
client.createTxProposal(opts, function(err, txp) {
if (err) return cb(err);
client.publishTxProposal({
txp: txp
}, cb);
});
};
var blockchainExplorerMock = {};
blockchainExplorerMock.getUtxos = function(addresses, cb) {
var selected = _.filter(blockchainExplorerMock.utxos, function(utxo) {
return _.includes(addresses, utxo.address);
});
return cb(null, selected);
};
blockchainExplorerMock.setUtxo = function(address, amount, m, confirmations) {
var B = Bitcore_[address.coin];
var scriptPubKey;
switch (address.type) {
case Constants.SCRIPT_TYPES.P2SH:
scriptPubKey = address.publicKeys ? B.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut() : '';
break;
case Constants.SCRIPT_TYPES.P2PKH:
scriptPubKey = B.Script.buildPublicKeyHashOut(address.address);
break;
}
should.exist(scriptPubKey);
blockchainExplorerMock.utxos.push({
txid: Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex'),
vout: Math.floor((Math.random() * 10) + 1),
amount: amount,
address: address.address,
scriptPubKey: scriptPubKey.toBuffer().toString('hex'),
confirmations: _.isUndefined(confirmations) ? Math.floor((Math.random() * 100) + 1) : +confirmations,
});
};
blockchainExplorerMock.broadcast = function(raw, cb) {
blockchainExplorerMock.lastBroadcasted = raw;
return cb(null, (new Bitcore.Transaction(raw)).id);
};
blockchainExplorerMock.setHistory = function(txs) {
blockchainExplorerMock.txHistory = txs;
};
blockchainExplorerMock.getTransaction = function(txid, cb) {
return cb();
};
blockchainExplorerMock.getTransactions = function(addresses, from, to, cb) {
var list = [].concat(blockchainExplorerMock.txHistory);
list = _.slice(list, from, to);
return cb(null, list);
};
blockchainExplorerMock.getAddressActivity = function(address, cb) {
var activeAddresses = _.map(blockchainExplorerMock.utxos || [], 'address');
return cb(null, _.includes(activeAddresses, address));
};
blockchainExplorerMock.setFeeLevels = function(levels) {
blockchainExplorerMock.feeLevels = levels;
};
blockchainExplorerMock.estimateFee = function(nbBlocks, cb) {
var levels = {};
_.each(nbBlocks, function(nb) {
var feePerKb = blockchainExplorerMock.feeLevels[nb];
levels[nb] = _.isNumber(feePerKb) ? feePerKb / 1e8 : -1;
});
return cb(null, levels);
};
blockchainExplorerMock.reset = function() {
blockchainExplorerMock.utxos = [];
blockchainExplorerMock.txHistory = [];
blockchainExplorerMock.feeLevels = [];
};
describe('client API', function() {
var clients, app, sandbox;
var i = 0;
beforeEach(function(done) {
var storage = new Storage({
db: helpers.newDb(),
});
var expressApp = new ExpressApp();
expressApp.start({
ignoreRateLimiter: true,
storage: storage,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
},
function() {
app = expressApp.app;
// Generates 5 clients
clients = _.map(_.range(5), function(i) {
return helpers.newClient(app);
});
blockchainExplorerMock.reset();
sandbox = sinon.sandbox.create();
if (!process.env.BWC_SHOW_LOGS) {
sandbox.stub(log, 'warn');
sandbox.stub(log, 'info');
sandbox.stub(log, 'error');
}
done();
});
});
afterEach(function(done) {
sandbox.restore();
done();
});
describe('constructor', function() {
it('should set the log level based on the logLevel option', function() {
var originalLogLevel = log.level;
var client = new Client({
logLevel: 'info'
});
client.logLevel.should.equal('info');
log.level.should.equal('info');
var client = new Client({
logLevel: 'debug'
});
client.logLevel.should.equal('debug');
log.level.should.equal('debug');
log.level = originalLogLevel; //restore since log is a singleton
});
it('should use silent for the log level if no logLevel is specified', function() {
var originalLogLevel = log.level;
log.level = 'foo;'
var client = new Client();
client.logLevel.should.equal('silent');
log.level.should.equal('silent');
log.level = originalLogLevel; //restore since log is a singleton
});
});
describe('Client Internals', function() {
it('should expose bitcore', function() {
should.exist(Client.Bitcore);
should.exist(Client.Bitcore.HDPublicKey);
});
});
describe('Server internals', function() {
it('should allow cors', function(done) {
clients[0].credentials = {};
clients[0]._doRequest('options', '/', {}, false, function(err, x, headers) {
headers['access-control-allow-origin'].should.equal('*');
should.exist(headers['access-control-allow-methods']);
should.exist(headers['access-control-allow-headers']);
done();
});
});
it('should handle critical errors', function(done) {
var s = sinon.stub();
s.storeWallet = sinon.stub().yields('bigerror');
s.fetchWallet = sinon.stub().yields(null);
var expressApp = new ExpressApp();
expressApp.start({
storage: s,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
}, function() {
var s2 = sinon.stub();
s2.load = sinon.stub().yields(null);
var client = helpers.newClient(app);
client.storage = s2;
client.createWallet('1', '2', 1, 1, {
network: 'testnet'
},
function(err) {
err.should.be.an.instanceOf(Error);
err.message.should.equal('bigerror');
done();
});
});
});
it('should handle critical errors (Case2)', function(done) {
var s = sinon.stub();
s.storeWallet = sinon.stub().yields({
code: 501,
message: 'wow'
});
s.fetchWallet = sinon.stub().yields(null);
var expressApp = new ExpressApp();
expressApp.start({
storage: s,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
}, function() {
var s2 = sinon.stub();
s2.load = sinon.stub().yields(null);
var client = helpers.newClient(app);
client.storage = s2;
client.createWallet('1', '2', 1, 1, {
network: 'testnet'
},
function(err) {
err.should.be.an.instanceOf(Error);
err.message.should.equal('wow');
done();
});
});
});
it('should handle critical errors (Case3)', function(done) {
var s = sinon.stub();
s.storeWallet = sinon.stub().yields({
code: 404,
message: 'wow'
});
s.fetchWallet = sinon.stub().yields(null);
var expressApp = new ExpressApp();
expressApp.start({
storage: s,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
}, function() {
var s2 = sinon.stub();
s2.load = sinon.stub().yields(null);
var client = helpers.newClient(app);
client.storage = s2;
client.createWallet('1', '2', 1, 1, {
network: 'testnet'
},
function(err) {
err.should.be.an.instanceOf(Errors.NOT_FOUND);
done();
});
});
});
it('should handle critical errors (Case4)', function(done) {
var body = {
code: 999,
message: 'unexpected body'
};
var ret = Client._parseError(body);
ret.should.be.an.instanceOf(Error);
ret.message.should.equal('999: unexpected body');
done();
});
it('should handle critical errors (Case5)', function(done) {
clients[0].request = helpers.stubRequest('some error');
clients[0].createWallet('mywallet', 'creator', 1, 2, {
network: 'testnet'
}, function(err, secret) {
should.exist(err);
err.should.be.an.instanceOf(Errors.CONNECTION_ERROR);
done();
});
});
it('should correctly use remote message', function(done) {
var body = {
code: 'INSUFFICIENT_FUNDS',
};
var ret = Client._parseError(body);
ret.should.be.an.instanceOf(Error);
ret.message.should.equal('Insufficient funds.');
var body = {
code: 'INSUFFICIENT_FUNDS',
message: 'remote message',
};
var ret = Client._parseError(body);
ret.should.be.an.instanceOf(Error);
ret.message.should.equal('remote message');
var body = {
code: 'MADE_UP_ERROR',
message: 'remote message',
};
var ret = Client._parseError(body);
ret.should.be.an.instanceOf(Error);
ret.message.should.equal('MADE_UP_ERROR: remote message');
done();
});
});
describe('Build & sign txs', function() {
var masterPrivateKey = 'tprv8ZgxMBicQKsPd8U9aBBJ5J2v8XMwKwZvf8qcu2gLK5FRrsrPeSgkEcNHqKx4zwv6cP536m68q2UD7wVM24zdSCpaJRmpowaeJTeVMXL5v5k';
var derivedPrivateKey = {
'BIP44': new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild("m/44'/1'/0'").toString(),
'BIP45': new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild("m/45'").toString(),
'BIP48': new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild("m/48'/1'/0'").toString(),
};
describe('#buildTx', function() {
it('Raw tx roundtrip', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress,
"amount": 5460,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Client.getRawTx(txp);
should.exist(t);
_.isString(t).should.be.true;
/^[\da-f]+$/.test(t).should.be.true;
var t2 = new Bitcore.Transaction(t);
t2.inputs.length.should.equal(2);
t2.outputs.length.should.equal(2);
t2.outputs[0].satoshis.should.equal(5460);
});
it('should build a tx correctly (BIP44)', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress,
"amount": 5460,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
disableSmallFees: true,
disableLargeFees: true,
});
should.not.exist(bitcoreError);
t.getFee().should.equal(10050);
});
it('should build a tx correctly (BIP48)', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP48']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress,
"amount": 5460,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP48',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
disableSmallFees: true,
disableLargeFees: true,
});
should.not.exist(bitcoreError);
t.getFee().should.equal(10050);
});
it('should protect from creating excessive fee', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1, 2]);
var txp = {
inputs: utxos,
toAddress: toAddress,
amount: 1.5e8,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 1.2e8,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var x = Utils.newBitcoreTransaction;
Utils.newBitcoreTransaction = function() {
return {
from: sinon.stub(),
to: sinon.stub(),
change: sinon.stub(),
outputs: [{
satoshis: 1000,
}],
fee: sinon.stub(),
}
};
(function() {
var t = Utils.buildTx(txp);
}).should.throw('Illegal State');
Utils.newBitcoreTransaction = x;
});
it('should build a tx with multiple outputs', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 8000,
message: 'first output'
}, {
toAddress: toAddress,
amount: 9000,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
});
it('should build a tx with provided output scripts', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [0.001]);
var txp = {
inputs: utxos,
type: 'external',
outputs: [{
"toAddress": "yVnNAcuJ2qJHA1X2femwK1uWb4En2v9Qx7",
"amount": 7000,
"script": "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae"
}, {
"amount": 6000,
"script": "76a9144d5bd54809f846dc6b1a14cbdd0ac87a3c66f76688ac"
}, {
"amount": 0,
"script": "6a1e43430102fa9213bc243af03857d0f9165e971153586d3915201201201210"
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2, 3],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
t.outputs.length.should.equal(4);
t.outputs[0].script.toHex().should.equal(txp.outputs[0].script);
t.outputs[0].satoshis.should.equal(txp.outputs[0].amount);
t.outputs[1].script.toHex().should.equal(txp.outputs[1].script);
t.outputs[1].satoshis.should.equal(txp.outputs[1].amount);
t.outputs[2].script.toHex().should.equal(txp.outputs[2].script);
t.outputs[2].satoshis.should.equal(txp.outputs[2].amount);
var changeScript = Bitcore.Script.fromAddress(txp.changeAddress.address).toHex();
t.outputs[3].script.toHex().should.equal(changeScript);
});
it('should fail if provided output has no either toAddress or script', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [0.001]);
var txp = {
inputs: utxos,
type: 'external',
outputs: [{
"amount": 7000,
}, {
"amount": 6000,
"script": "76a9144d5bd54809f846dc6b1a14cbdd0ac87a3c66f76688ac"
}, {
"amount": 0,
"script": "6a1e43430102fa9213bc243af03857d0f9165e971153586d3915201201201210"
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2, 3],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
(function() {
var t = Utils.buildTx(txp);
}).should.throw('Output should have either toAddress or script specified');
txp.outputs[0].toAddress = "yVnNAcuJ2qJHA1X2femwK1uWb4En2v9Qx7";
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
delete txp.outputs[0].toAddress;
txp.outputs[0].script = "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae";
t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
});
it('should build a v3 tx proposal', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: 3,
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 8000,
message: 'first output'
}, {
toAddress: toAddress,
amount: 9000,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
});
});
describe('#signTxp', function() {
it('should sign BIP45 P2SH correctly', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP45']),
}];
var utxos = helpers.generateUtxos('P2SH', publicKeyRing, 'm/2147483647/0/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
toAddress: toAddress,
"amount": 5460,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10000,
derivationStrategy: 'BIP45',
addressType: 'P2SH',
};
var signatures = Client.signTxp(txp, derivedPrivateKey['BIP45']);
signatures.length.should.be.equal(utxos.length);
});
it('should sign BIP44 P2PKH correctly', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
toAddress: toAddress,
"amount": 5460,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var signatures = Client.signTxp(txp, derivedPrivateKey['BIP44']);
signatures.length.should.be.equal(utxos.length);
});
it('should sign multiple-outputs proposal correctly', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 8000,
message: 'first output'
}, {
toAddress: toAddress,
amount: 9000,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var signatures = Client.signTxp(txp, derivedPrivateKey['BIP44']);
signatures.length.should.be.equal(utxos.length);
});
it('should sign proposal with provided output scripts correctly', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [0.001]);
var txp = {
inputs: utxos,
type: 'external',
outputs: [{
"amount": 7000,
"script": "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae"
}, {
"amount": 6000,
"script": "76a9144d5bd54809f846dc6b1a14cbdd0ac87a3c66f76688ac"
}, {
"amount": 0,
"script": "6a1e43430102fa9213bc243af03857d0f9165e971153586d3915201201201210"
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2, 3],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var signatures = Client.signTxp(txp, derivedPrivateKey['BIP44']);
signatures.length.should.be.equal(utxos.length);
});
it('should sign btc proposal correctly', function() {
var toAddress = 'yie4Ubd2ieCdzqwNyAc8QRutfri3E9ChTm';
var changeAddress = 'yd1ctBBVpugG8EThogVdRm1mpHAJkRETD9';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [2000]);
var txp = {
version: 3,
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var signatures = Client.signTxp(txp, derivedPrivateKey['BIP44']);
signatures.length.should.be.equal(utxos.length);
signatures[0].should.equal('304502210087b754a017f424cbffc83decdac1b944ba249bdb26be1c790f752186a59a896e02204ce981f3cb2d7491ebe868a6dc5b93a64d6b0a72de3d4288af1f1701c20f2217');
// signatures[1].should.equal('3044022069cf6e5d8700ff117f754e4183e81690d99d6a6443e86c9589efa072ecb7d82c02204c254506ac38774a2176f9ef56cc239ef7867fbd24da2bef795128c75a063301'); // this generates a unique TXID every time which changes the signature
});
it('should sign BCH proposal correctly', function() {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore_.bch.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [2000], 'bch');
var txp = {
version: 3,
coin: 'bch',
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 8000,
message: 'first output'
}, {
toAddress: toAddress,
amount: 9000,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var signatures = Client.signTxp(txp, derivedPrivateKey['BIP44']);
signatures.length.should.be.equal(utxos.length);
signatures[0].should.equal('304402200ad97576c300cb40cda8af0eb55872c271a55767d6ba416a8e49907c87705151022065a9e2321aca809b17f1c4845e7c14bec9fde7533e2eee2c4f178ec792d074ee');
// signatures[1].should.equal('3045022100afde45e125f654453493b40d288cd66e8a011c66484509ae730a2686c9dff30502201bf34a6672c5848dd010b89ea1a5f040731acf78fec062f61b305e9ce32798a5'); // this generates a unique TXID every time which changes the signature
});
});
});
describe('Wallet secret round trip', function() {
it('should create secret and parse secret', function() {
var i = 0;
while (i++ < 100) {
var walletId = Uuid.v4();
var walletPrivKey = new Bitcore.PrivateKey();
var network = i % 2 == 0 ? 'testnet' : 'livenet';
var coin = i % 3 == 0 ? 'bch' : 'btc';
var secret = Client._buildSecret(walletId, walletPrivKey, coin, network);
var result = Client.parseSecret(secret);
result.walletId.should.equal(walletId);
result.walletPrivKey.toString().should.equal(walletPrivKey.toString());
result.coin.should.equal(coin);
result.network.should.equal(network);
};
});
it('should fail on invalid secret', function() {
(function() {
Client.parseSecret('invalidSecret');
}).should.throw('Invalid secret');
});
it('should create secret and parse secret from string', function() {
var walletId = Uuid.v4();
var walletPrivKey = new Bitcore.PrivateKey();
var coin = 'btc';
var network = 'testnet';
var secret = Client._buildSecret(walletId, walletPrivKey.toString(), coin, network);
var result = Client.parseSecret(secret);
result.walletId.should.equal(walletId);
result.walletPrivKey.toString().should.equal(walletPrivKey.toString());
result.coin.should.equal(coin);
result.network.should.equal(network);
});
it('should default to btc for secrets not specifying coin', function() {
var result = Client.parseSecret('nv5RGTN5C8gxnZFD4bHBK0XFeY7Jvakn9SBZtrFz83Yn1HdKDfRyHAhGwaFKFw2HHuTBo1Vm78Tbtc');
result.coin.should.equal('btc');
});
});
describe('Notification polling', function() {
var clock, interval;
beforeEach(function() {
clock = sinon.useFakeTimers(1234000, 'Date');
});
afterEach(function() {
clock.restore();
});
it('should fetch notifications at intervals', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function() {
clients[0].on('notification', function(data) {
notifications.push(data);
});
var notifications = [];
clients[0]._fetchLatestNotifications(5, function() {
_.map(notifications, 'type').should.deep.equal(['NewCopayer', 'WalletComplete']);
clock.tick(2000);
notifications = [];
clients[0]._fetchLatestNotifications(5, function() {
notifications.length.should.equal(0);
clock.tick(2000);
clients[1].createAddress(function(err, x) {
should.not.exist(err);
clients[0]._fetchLatestNotifications(5, function() {
_.map(notifications, 'type').should.deep.equal(['NewAddress']);
clock.tick(2000);
notifications = [];
clients[0]._fetchLatestNotifications(5, function() {
notifications.length.should.equal(0);
clients[1].createAddress(function(err, x) {
should.not.exist(err);
clock.tick(60 * 1000);
clients[0]._fetchLatestNotifications(5, function() {
notifications.length.should.equal(0);
done();
});
});
});
});
});
});
});
});
});
});
describe('Wallet Creation', function() {
it('should fail to create wallet in bogus device', function(done) {
clients[0].seedFromRandomWithMnemonic();
clients[0].keyDerivationOk = false;
clients[0].createWallet('mywallet', 'pepe', 1, 1, {}, function(err, secret) {
should.exist(err);
should.not.exist(secret);
done();
});
});
it('should encrypt wallet name', function(done) {
var spy = sinon.spy(clients[0], '_doPostRequest');
clients[0].seedFromRandomWithMnemonic();
clients[0].createWallet('mywallet', 'pepe', 1, 1, {}, function(err, secret) {
should.not.exist(err);
var url = spy.getCall(0).args[0];
var body = JSON.stringify(spy.getCall(0).args[1]);
url.should.contain('/wallets');
body.should.not.contain('mywallet');
clients[0].getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.name.should.equal('mywallet');
done();
})
});
});
it('should encrypt copayer name in wallet creation', function(done) {
var spy = sinon.spy(clients[0], '_doPostRequest');
clients[0].seedFromRandomWithMnemonic();
clients[0].createWallet('mywallet', 'pepe', 1, 1, {}, function(err, secret) {
should.not.exist(err);
var url = spy.getCall(1).args[0];
var body = JSON.stringify(spy.getCall(1).args[1]);
url.should.contain('/copayers');
body.should.not.contain('pepe');
clients[0].getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.copayers[0].name.should.equal('pepe');
done();
})
});
});
it('should be able to access wallet name in non-encrypted wallet (legacy)', function(done) {
clients[0].seedFromRandomWithMnemonic();
var wpk = new Bitcore.PrivateKey();
var args = {
name: 'mywallet',
m: 1,
n: 1,
pubKey: wpk.toPublicKey().toString(),
network: 'livenet',
id: '123',
};
clients[0]._doPostRequest('/v2/wallets/', args, function(err, wallet) {
should.not.exist(err);
var c = clients[0].credentials;
var args = {
walletId: '123',
name: 'pepe',
xPubKey: c.xPubKey,
requestPubKey: c.requestPubKey,
customData: Utils.encryptMessage(JSON.stringify({
walletPrivKey: wpk.toString(),
}), c.personalEncryptingKey),
};
var hash = Utils.getCopayerHash(args.name, args.xPubKey, args.requestPubKey);
args.copayerSignature = Utils.signMessage(hash, wpk);
clients[0]._doPostRequest('/v2/wallets/123/copayers', args, function(err, wallet) {
should.not.exist(err);
clients[0].openWallet(function(err) {
should.not.exist(err);
clients[0].getStatus({}, function(err, status) {
should.not.exist(err);
var wallet = status.wallet;
wallet.name.should.equal('mywallet');
should.not.exist(wallet.encryptedName);
wallet.copayers[0].name.should.equal('pepe');
should.not.exist(wallet.copayers[0].encryptedName);
done();
});
});
});
});
});
it('should create Bitcoin Cash wallet', function(done) {
clients[0].seedFromRandomWithMnemonic({
coin: 'bch'
});
clients[0].createWallet('mycashwallet', 'pepe', 1, 1, {
coin: 'bch'
}, function(err, secret) {
should.not.exist(err);
clients[0].getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.coin.should.equal('bch');
done();
})
});
});
it('should create a BCH address correctly', function(done) {
var xPriv = 'xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu';
clients[0].seedFromExtendedPrivateKey(xPriv, {
'coin': 'bch',
});
clients[0].createWallet('mycashwallet', 'pepe', 1, 1, {
coin: 'bch'
}, function(err, secret) {
should.not.exist(err);
clients[0].createAddress(function(err, x) {
should.not.exist(err);
should.not.exist(err);
x.coin.should.equal('bch');
x.network.should.equal('livenet');
x.address.should.equal('CV5CscvDHNHzUYe5iR4dLLzGdfHGvyoBRX');
done();
})
});
});
it('should check balance in a 1-1 ', function(done) {
helpers.createAndJoinWallet(clients, 1, 1, function() {
clients[0].getBalance({}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(0);
balance.availableAmount.should.equal(0);
balance.lockedAmount.should.equal(0);
done();
})
});
});
it('should be able to complete wallet in copayer that joined later', function(done) {
helpers.createAndJoinWallet(clients, 2, 3, function() {
clients[0].getBalance({}, function(err, x) {
should.not.exist(err);
clients[1].getBalance({}, function(err, x) {
should.not.exist(err);
clients[2].getBalance({}, function(err, x) {
should.not.exist(err);
done();
})
})
})
});
});
it('should fire event when wallet is complete', function(done) {
var checks = 0;
clients[0].on('walletCompleted', function(wallet) {
wallet.name.should.equal('mywallet');
wallet.status.should.equal('complete');
clients[0].isComplete().should.equal(true);
clients[0].credentials.isComplete().should.equal(true);
if (++checks == 2) done();
});
clients[0].createWallet('mywallet', 'creator', 2, 2, {
network: 'testnet'
}, function(err, secret) {
should.not.exist(err);
clients[0].isComplete().should.equal(false);
clients[0].credentials.isComplete().should.equal(false);
clients[1].joinWallet(secret, 'guest', {}, function(err, wallet) {
should.not.exist(err);
wallet.name.should.equal('mywallet');
clients[0].openWallet(function(err, walletStatus) {
should.not.exist(err);
should.exist(walletStatus);
_.difference(_.map(walletStatus.copayers, 'name'), ['creator', 'guest']).length.should.equal(0);
if (++checks == 2) done();
});
});
});
});
it('should fill wallet info in an incomplete wallet', function(done) {
clients[0].seedFromRandomWithMnemonic();
clients[0].createWallet('XXX', 'creator', 2, 3, {}, function(err, secret) {
should.not.exist(err);
clients[1].seedFromMnemonic(clients[0].getMnemonic());
clients[1].openWallet(function(err) {
clients[1].credentials.walletName.should.equal('XXX');
clients[1].credentials.m.should.equal(2);
clients[1].credentials.n.should.equal(3);
should.not.exist(err);
done();
});
});
});
it('should return wallet on successful join', function(done) {
clients[0].createWallet('mywallet', 'creator', 2, 2, {
network: 'testnet'
}, function(err, secret) {
should.not.exist(err);
clients[1].joinWallet(secret, 'guest', {}, function(err, wallet) {
should.not.exist(err);
wallet.name.should.equal('mywallet');
wallet.copayers[0].name.should.equal('creator');
wallet.copayers[1].name.should.equal('guest');
done();
});
});
});
it('should not allow to join wallet on bogus device', function(done) {
clients[0].createWallet('mywallet', 'creator', 2, 2, {
network: 'testnet'
}, function(err, secret) {
should.not.exist(err);
clients[1].keyDerivationOk = false;
clients[1].joinWallet(secret, 'guest', {}, function(err, wallet) {
should.exist(err);
done();
});
});
});
it('should not allow to join a full wallet ', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function(w) {
should.exist(w.secret);
clients[4].joinWallet(w.secret, 'copayer', {}, function(err, result) {
err.should.be.an.instanceOf(Errors.WALLET_FULL);
done();
});
});
});
it('should fail with an invalid secret', function(done) {
// Invalid
clients[0].joinWallet('dummy', 'copayer', {}, function(err, result) {
err.message.should.contain('Invalid secret');
// Right length, invalid char for base 58
clients[0].joinWallet('DsZbqNQQ9LrTKU8EknR7gFKyCQMPg2UUHNPZ1BzM5EbJwjRZaUNBfNtdWLluuFc0f7f7sTCkh7T', 'copayer', {}, function(err, result) {
err.message.should.contain('Invalid secret');
done();
});
});
});
it('should fail with an unknown secret', function(done) {
// Unknown walletId
var oldSecret = 'Y47GFabvPXw4mUWK3itsVUXFxkxtspZDeBeP6eNp5LFk56XADoHKrPBEjUxgA7Diocq55foyF9T';
clients[0].joinWallet(oldSecret, 'copayer', {}, function(err, result) {
err.should.be.an.instanceOf(Errors.WALLET_NOT_FOUND);
done();
});
});
it('should detect wallets with bad signatures', function(done) {
// Do not complete clients[1] pkr
var openWalletStub = sinon.stub(clients[1], 'openWallet').yields();
helpers.createAndJoinWallet(clients, 2, 3, function() {
helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) {
status.wallet.copayers[0].xPubKey = status.wallet.copayers[1].xPubKey;
}, function() {
openWalletStub.restore();
clients[1].openWallet(function(err, x) {
err.should.be.an.instanceOf(Errors.SERVER_COMPROMISED);
done();
});
});
});
});
it('should detect wallets with missing signatures', function(done) {
// Do not complete clients[1] pkr
var openWalletStub = sinon.stub(clients[1], 'openWallet').yields();
helpers.createAndJoinWallet(clients, 2, 3, function() {
helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) {
delete status.wallet.copayers[1].xPubKey;
}, function() {
openWalletStub.restore();
clients[1].openWallet(function(err, x) {
err.should.be.an.instanceOf(Errors.SERVER_COMPROMISED);
done();
});
});
});
});
it('should detect wallets missing callers pubkey', function(done) {
// Do not complete clients[1] pkr
var openWalletStub = sinon.stub(clients[1], 'openWallet').yields();
helpers.createAndJoinWallet(clients, 2, 3, function() {
helpers.tamperResponse([clients[0], clients[1]], 'get', '/v1/wallets/', {}, function(status) {
// Replace caller's pubkey
status.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey;
// Add a correct signature
status.wallet.copayers[1].xPubKeySignature = Utils.signMessage(
status.wallet.copayers[1].xPubKey.toString(),
clients[0].credentials.walletPrivKey
);
}, function() {
openWalletStub.restore();
clients[1].openWallet(function(err, x) {
err.should.be.an.instanceOf(Errors.SERVER_COMPROMISED);
done();
});
});
});
});
it('should perform a dry join without actually joining', function(done) {
clients[0].createWallet('mywallet', 'creator', 1, 2, {}, function(err, secret) {
should.not.exist(err);
should.exist(secret);
clients[1].joinWallet(secret, 'dummy', {
dryRun: true
}, function(err, wallet) {
should.not.exist(err);
should.exist(wallet);
wallet.status.should.equal('pending');
wallet.copayers.length.should.equal(1);
done();
});
});
});
it('should return wallet status even if wallet is not yet complete', function(done) {
clients[0].createWallet('mywallet', 'creator', 1, 2, {
network: 'testnet'
}, function(err, secret) {
should.not.exist(err);
should.exist(secret);
clients[0].getStatus({}, function(err, status) {
should.not.exist(err);
should.exist(status);
status.wallet.status.should.equal('pending');
should.exist(status.wallet.secret);
status.wallet.secret.should.equal(secret);
done();
});
});
});
it('should return status using v2 version', function(done) {
clients[0].createWallet('mywallet', 'creator', 1, 1, {
network: 'testnet'
}, function(err, secret) {
should.not.exist(err);
clients[0].getStatus({}, function(err, status) {
should.not.exist(err);
should.not.exist(status.wallet.publicKeyRing);
status.wallet.status.should.equal('complete');
done();
});
});
});
it('should return extended status using v2 version', function(done) {
clients[0].createWallet('mywallet', 'creator', 1, 1, {
network: 'testnet'
}, function(err, secret) {
should.not.exist(err);
clients[0].getStatus({
includeExtendedInfo: true
}, function(err, status) {
should.not.exist(err);
status.wallet.publicKeyRing.length.should.equal(1);
status.wallet.status.should.equal('complete');
done();
});
});
});
it('should store walletPrivKey', function(done) {
clients[0].createWallet('mywallet', 'creator', 1, 1, {
network: 'testnet'
}, function(err) {
var key = clients[0].credentials.walletPrivKey;
should.not.exist(err);
clients[0].getStatus({
includeExtendedInfo: true