UNPKG

bitcore-wallet-client-dash

Version:
1,472 lines (1,343 loc) 199 kB
'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