UNPKG

@ducatus/ducatus-wallet-client-rev

Version:

Client for @ducatus/ducatus-wallet-service-rev

1,562 lines (1,394 loc) 239 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 sjcl = require('sjcl'); var log = require('../ts_build/lib/log'); var mongodb = require('mongodb'); var config = require('./test-config'); var oldCredentials = require('./legacyCredentialsExports'); var CWC = require('../../ducatus-crypto-wallet-core-rev'); var Bitcore = CWC.BitcoreLib; var Bitcore_ = { btc: Bitcore, bch: CWC.BitcoreLibCash, }; var BWS = require('@ducatus/ducatus-wallet-service-rev'); var { Constants } = require('../ts_build/lib/common'); var Client = require('../ts_build').default; var Key = Client.Key; var { Request } = require('../ts_build/lib/request.js'); var { Utils } = require('../ts_build/lib/common'); var ExpressApp = BWS.ExpressApp; var Storage = BWS.Storage; var TestData = require('./testdata'); var Errors = require('../ts_build/lib/errors'); var helpers = {}; helpers.toSatoshi = (btc) => { if (_.isArray(btc)) { return _.map(btc, helpers.toSatoshi); } else { return parseFloat((btc * 1e8).toPrecision(12)); } }; helpers.newClient = (app) => { $.checkArgument(app); return new Client({ baseUrl: '/bws/api', request: request(app), bp_partner: 'xxx', bp_partner_version: 'yyy', // logLevel: 'debug', }); }; helpers.stubRequest = (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'], (mem, verb) => { mem[verb] = (url) => { return request; }; return mem; }, {}); return reqFactory; }; helpers.generateUtxos = (scriptType, publicKeyRing, path, requiredSignatures, amounts) => { var amounts = [].concat(amounts); var utxos = _.map(amounts, (amount, i) => { var address = Utils.deriveAddress(scriptType, publicKeyRing, path, requiredSignatures, 'testnet'); var scriptPubKey; switch (scriptType) { case Constants.SCRIPT_TYPES.P2WSH: case Constants.SCRIPT_TYPES.P2SH: scriptPubKey = new Bitcore.Script.buildMultisigOut(address.publicKeys, requiredSignatures).toScriptHashOut(); break; case Constants.SCRIPT_TYPES.P2WPKH: case Constants.SCRIPT_TYPES.P2PKH: scriptPubKey = new Bitcore.Script.buildPublicKeyHashOut(address.address); break; } should.exist(scriptPubKey); var obj = { txid: new Bitcore.crypto.Hash.sha256(Buffer.alloc(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 = (clients, keys, m, n, opts, cb) => { opts = opts || {}; var coin = opts.coin || 'btc'; var network = opts.network || 'testnet'; let keyOpts = { useLegacyCoinType: opts.useLegacyCoinType, useLegacyPurpose: opts.useLegacyPurpose, passphrase: opts.passphrase, }; keys[0] = opts.key || Key.create(keyOpts); let cred = keys[0].createCredentials(null, { coin: coin, network: network, account: 0, n: n, addressType: opts.addressType }); clients[0].fromObj(cred); clients[0].createWallet('mywallet', 'creator', m, n, { coin: coin, network: network, singleAddress: !!opts.singleAddress, doNotCheck: true, useNativeSegwit: !!opts.useNativeSegwit }, (err, secret) => { if (err) console.log(err); should.not.exist(err); if (n > 1) { should.exist(secret); } async.series([ (next) => { async.each(_.range(1, n), (i, cb) => { keys[i] = Key.create(keyOpts); clients[i].fromString( keys[i].createCredentials(null, { coin: coin, network: network, account: 0, n: n, addressType: opts.addressType }) ); clients[i].joinWallet(secret, 'copayer ' + i, { coin: coin }, cb); }, next); }, (next) => { async.each(_.range(n), (i, cb) => { clients[i].openWallet(cb); }, next); }, ], (err) => { should.not.exist(err); return cb({ m: m, n: n, secret: secret, }); }); }); }; helpers.tamperResponse = (clients, method, url, args, tamper, cb) => { clients = [].concat(clients); // Use first client to get a clean response from server clients[0].request.doRequest(method, url, args, false, (err, result) => { should.not.exist(err); tamper(result); // Return tampered data for every client in the list _.each(clients, (client) => { client.request.doRequest = sinon.stub().withArgs(method, url).yields(null, result); }); return cb(); }); }; helpers.createAndPublishTxProposal = (client, opts, cb) => { if (!opts.outputs) { opts.outputs = [{ toAddress: opts.toAddress, amount: opts.amount, }]; } client.createTxProposal(opts, (err, txp) => { if (err) return cb(err); client.publishTxProposal({ txp: txp }, cb); }); }; var blockchainExplorerMock = { register: sinon.stub().callsArgWith(1, null, null), getCheckData: sinon.stub().callsArgWith(1, null, { sum: 100 }), addAddresses: sinon.stub().callsArgWith(2, null, null), }; blockchainExplorerMock.getUtxos = (wallet, height, cb) => { return cb(null, _.cloneDeep(blockchainExplorerMock.utxos)); }; // v8 blockchainExplorerMock.getAddressUtxos = (address, height, cb) => { var selected = _.filter(blockchainExplorerMock.utxos, (utxo) => { return _.includes(address, utxo.address); }); return cb(null, _.cloneDeep(selected)); }; blockchainExplorerMock.setUtxo = (address, amount, m, confirmations) => { var B = Bitcore_[address.coin]; var scriptPubKey; switch (address.type) { case Constants.SCRIPT_TYPES.P2WSH: case Constants.SCRIPT_TYPES.P2SH: scriptPubKey = address.publicKeys ? B.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut() : ''; break; case Constants.SCRIPT_TYPES.P2WPKH: case Constants.SCRIPT_TYPES.P2PKH: scriptPubKey = B.Script.buildPublicKeyHashOut(address.address); break; } should.exist(scriptPubKey); blockchainExplorerMock.utxos.push({ txid: new Bitcore.crypto.Hash.sha256(Buffer.alloc(Math.random() * 100000)).toString('hex'), outputIndex: 0, amount: amount, satoshis: amount * 1e8, address: address.address, scriptPubKey: scriptPubKey.toBuffer().toString('hex'), confirmations: _.isUndefined(confirmations) ? Math.floor((Math.random() * 100) + 1) : +confirmations, }); }; blockchainExplorerMock.supportsGrouping = () => { return false; } blockchainExplorerMock.getBlockchainHeight = (cb) => { return cb(null, 1000); } blockchainExplorerMock.broadcast = (raw, cb) => { blockchainExplorerMock.lastBroadcasted = raw; let hash; try { let tx = new Bitcore.Transaction(raw); if (_.isEmpty(tx.outputs)) { throw 'no bitcoin'; } hash = tx.id; // btc/bch return cb(null, hash); } catch (e) { // try eth hash = CWC.Transactions.getHash({ tx: raw[0], chain: 'ETH', }); return cb(null, hash); }; }; blockchainExplorerMock.setHistory = (txs) => { blockchainExplorerMock.txHistory = txs; }; blockchainExplorerMock.getTransaction = (txid, cb) => { return cb(); }; var createTxsV8 = (nr, bcHeight, txs) => { txs = txs || []; // Will generate // order / confirmations / height / txid // 0. => -1 / -1 / txid0 / id0 <= LAST ONE! // 1. => 1 / bcHeight / txid1 // 2. => 2 / bcHeight - 1 / txid2 // 3. => 3... / bcHeight - 2 / txid3 var i = 0; if (_.isEmpty(txs)) { while (i < nr) { txs.push({ id: 'id' + i, txid: 'txid' + i, size: 226, category: 'receive', satoshis: 30001, // this is translated on V8.prototype.getTransactions amount: 30001 / 1e8, height: (i == 0) ? -1 : bcHeight - i + 1, address: 'muFJi3ZPfR5nhxyD7dfpx2nYZA8Wmwzgck', blockTime: '2018-09-21T18:08:31.000Z', }); i++; } } return txs; }; blockchainExplorerMock.getTransactions = (wallet, startBlock, cb) => { var list = [].concat(blockchainExplorerMock.txHistory); // -1 = mempool, always included in server' s v8.js list = _.filter(list, (x) => { return x.height >= startBlock || x.height == -1; }); return cb(null, list); }; blockchainExplorerMock.getAddressActivity = (address, cb) => { var activeAddresses = _.map(blockchainExplorerMock.utxos || [], 'address'); return cb(null, _.includes(activeAddresses, address)); }; blockchainExplorerMock.setFeeLevels = (levels) => { blockchainExplorerMock.feeLevels = levels; }; blockchainExplorerMock.estimateFee = (nbBlocks, cb) => { var levels = {}; _.each(nbBlocks, (nb) => { var feePerKb = blockchainExplorerMock.feeLevels[nb]; levels[nb] = _.isNumber(feePerKb) ? feePerKb / 1e8 : -1; }); return cb(null, levels); }; blockchainExplorerMock.estimateGas = (nbBlocks, cb) => { return cb(null, '20000000000'); }; blockchainExplorerMock.getBalance = (nbBlocks, cb) => { return cb(null, { unconfirmed: 0, confirmed: 20000000000 * 5, balance: 20000000000 * 5, }); }; blockchainExplorerMock.getTransactionCount = (addr, cb) => { return cb(null, 0); }; blockchainExplorerMock.reset = () => { blockchainExplorerMock.utxos = []; blockchainExplorerMock.txHistory = []; blockchainExplorerMock.feeLevels = []; }; helpers.newDb = (extra, cb) => { extra = extra || ''; mongodb.MongoClient.connect(config.mongoDb.uri + extra, (err, in_db) => { if (err) return cb(err); in_db.dropDatabase((err) => { return cb(err, in_db); }); }); } var db; describe('client API', function() { // DONT USE LAMBAS HERE!!! https://stackoverflow.com/questions/23492043/change-default-timeout-for-mocha, or this.timeout() will BREAK! // var clients, app, sandbox, storage, keys, i; this.timeout(8000); before((done) => { i = 0; clients = []; keys = []; helpers.newDb('', (err, in_db) => { db = in_db; storage = new Storage({ db: db, }); Storage.createIndexes(db); return done(err); }); }); beforeEach((done) => { var expressApp = new ExpressApp(); expressApp.start({ ignoreRateLimiter: true, storage: storage, blockchainExplorer: blockchainExplorerMock, disableLogs: true, doNotCheckV8: true, }, () => { app = expressApp.app; // Generates 5 clients clients = _.map(_.range(5), (i) => { return helpers.newClient(app); }); blockchainExplorerMock.reset(); sandbox = sinon.createSandbox(); if (!process.env.BWC_SHOW_LOGS) { sandbox.stub(log, 'warn'); sandbox.stub(log, 'info'); sandbox.stub(log, 'error'); } done(); }); }); afterEach((done) => { sandbox.restore(); done(); }); describe('constructor', () => { it('should set the log level based on the logLevel option', () => { 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', () => { 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', () => { it('should expose bitcore', () => { should.exist(Bitcore); should.exist(Bitcore.HDPublicKey); }); }); // todo describe('Server internals', () => { var k; before(() => { k = Key.create(); }); it('should allow cors', (done) => { clients[0].credentials = {}; clients[0].request.doRequest('options', '/', {}, false, (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 request set credentials before creating/joining', (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, }, () => { var client = helpers.newClient(app); client.createWallet('1', '2', 1, 1, { network: 'testnet' }, (err) => { should.exist(err); err.toString().should.contain('credentials'); done(); }) }); }); it('should handle critical errors', (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, }, () => { var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); var client = helpers.newClient(app); client.storage = s2; client.fromString( k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 }) ); client.createWallet('1', '2', 1, 1, { network: 'testnet' }, (err) => { err.should.be.an.instanceOf(Error); err.message.should.equal('bigerror'); done(); }); }); }); it('should handle critical errors (Case2)', (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, }, () => { var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); var client = helpers.newClient(app); client.storage = s2; client.fromString( k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 }) ); client.createWallet('1', '2', 1, 1, { network: 'testnet' }, (err) => { err.should.be.an.instanceOf(Error); err.message.should.equal('wow'); done(); }); }); }); it('should handle critical errors (Case3)', (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, }, () => { var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); var client = helpers.newClient(app); client.storage = s2; client.fromString( k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 }) ); client.createWallet('1', '2', 1, 1, { network: 'testnet' }, (err) => { err.should.be.an.instanceOf(Errors.NOT_FOUND); done(); }); }); }); it('should handle critical errors (Case4)', (done) => { var body = { code: 999, message: 'unexpected body' }; var ret = Request._parseError(body); ret.should.be.an.instanceOf(Error); ret.message.should.equal('999: unexpected body'); done(); }); it('should handle critical errors (Case5)', (done) => { clients[0].request.r = helpers.stubRequest('some error'); clients[0].fromString( k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 }) ); clients[0].createWallet('mywallet', 'creator', 1, 2, { network: 'testnet' }, (err, secret) => { should.exist(err); err.should.be.an.instanceOf(Errors.CONNECTION_ERROR); done(); }); }); it('should correctly use remote message', (done) => { var body = { code: 'INSUFFICIENT_FUNDS', }; var ret = Request._parseError(body); ret.should.be.an.instanceOf(Error); ret.message.should.equal('Insufficient funds.'); var body = { code: 'INSUFFICIENT_FUNDS', message: 'remote message', }; var ret2 = Request._parseError(body); ret2.should.be.an.instanceOf(Error); ret2.message.should.equal('remote message'); var body = { code: 'MADE_UP_ERROR', message: 'remote message', }; var ret3 = Request._parseError(body); ret3.should.be.an.instanceOf(Error); ret3.message.should.equal('MADE_UP_ERROR: remote message'); done(); }); }); describe('Build & sign txs', () => { 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', () => { it('Raw tx roundtrip', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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: 1200, 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(1200); }); it('should build a tx correctly (BIP44)', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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: 1200, 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 P2WPKH tx correctly (BIP44)', () => { var publicKeyRing = [{ xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']), }]; const toAddress = Utils.deriveAddress('P2WPKH', publicKeyRing, 'm/0/0', 1, 'livenet', 'btc'); const changeAddress = Utils.deriveAddress('P2WPKH', publicKeyRing, 'm/0/1', 1, 'livenet', 'btc'); toAddress.address.should.equal('bc1qrshu7r9z9y22y3wrrghfmjrvn0xxasfl7qrmvf'); changeAddress.address.should.equal('bc1quhzpvcmllzm3kkf7jwsxdemgaec3dz2j0uuan0'); var utxos = helpers.generateUtxos('P2WPKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]); var txp = { version: '2.0.0', inputs: utxos, toAddress: toAddress.address, amount: 1200, changeAddress: { address: changeAddress.address }, requiredSignatures: 1, outputOrder: [0, 1], fee: 10050, derivationStrategy: 'BIP44', addressType: 'P2WPKH', }; 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 P2WSH tx correctly (BIP48)', () => { var publicKeyRing = [{ xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP48']), }]; const toAddress = Utils.deriveAddress('P2WSH', publicKeyRing, 'm/0/0', 1, 'livenet', 'btc'); const changeAddress = Utils.deriveAddress('P2WSH', publicKeyRing, 'm/0/1', 1, 'livenet', 'btc'); toAddress.address.should.equal('bc1qxq4tyr7uhwprj4w8ayc8manv4t64g0hc74ka9w4qka0uygr7gplqqnlu24'); changeAddress.address.should.equal('bc1qk8q74mfp7mcldhvfu4azjyqnu7rnd0d9ghdnxkxye34utvp0fgvq50jl0v'); var utxos = helpers.generateUtxos('P2WSH', publicKeyRing, 'm/1/0', 1, [1000, 2000]); var txp = { version: '2.0.0', inputs: utxos, toAddress: toAddress.address, amount: 1200, changeAddress: { address: changeAddress.address }, requiredSignatures: 1, outputOrder: [0, 1], fee: 10050, derivationStrategy: 'BIP44', addressType: 'P2WSH', }; 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)', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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: 1200, 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 build an eth txp correctly', () => { const toAddress = '0xa062a07a0a56beb2872b12f388f511d694626730'; const key = Key.fromExtendedPrivateKey(masterPrivateKey); const path = 'm/44\'/60\'/0\''; const publicKeyRing = [{ xPubKey: new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild(path).toString(), }]; const from = Utils.deriveAddress('P2PKH', publicKeyRing, 'm/0/0', 1, 'livenet', 'eth'); const txp = { version: 3, from: from.address, coin: 'eth', outputs: [{ toAddress: toAddress, amount: 3896000000000000, gasLimit: 21000, message: 'first output' } ], requiredSignatures: 1, outputOrder: [0, 1, 2], fee: 420000000000000, nonce: 6, gasPrice: 20000000000, derivationStrategy: 'BIP44', addressType: 'P2PKH', amount: 3896000000000000 }; var t = Utils.buildTx(txp); const rawTxp = t.uncheckedSerialize(); rawTxp.should.deep.equal(['0xeb068504a817c80082520894a062a07a0a56beb2872b12f388f511d694626730870dd764300b800080018080']); }); it('should protect from creating excessive fee', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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; x.newBitcoreTransaction = () => { return { from: sinon.stub(), to: sinon.stub(), change: sinon.stub(), outputs: [{ satoshis: 1000, }], fee: sinon.stub(), } }; (() => { var t = x.buildTx(txp); }).should.throw('Illegal State'); x.newBitcoreTransaction = x; }); it('should build a tx with multiple outputs', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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: 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 t = Utils.buildTx(txp); var bitcoreError = t.getSerializationError({ disableIsFullySigned: true, }); should.not.exist(bitcoreError); }); it('should build a tx with provided output scripts', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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": "18433T2TSgajt9jWhcTBw4GoNREA6LpX3E", "amount": 700, "script": "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae" }, { "amount": 600, "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', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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": 700, }, { "amount": 600, "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); }).should.throw('Output should have either toAddress or script specified'); txp.outputs[0].toAddress = "18433T2TSgajt9jWhcTBw4GoNREA6LpX3E"; 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', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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: 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 t = Utils.buildTx(txp); var bitcoreError = t.getSerializationError({ disableIsFullySigned: true, }); should.not.exist(bitcoreError); }); it('should build a v4 tx proposal', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var publicKeyRing = [{ xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']), }]; var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]); var txp = { version: 4, 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 t = Utils.buildTx(txp); var bitcoreError = t.getSerializationError({ disableIsFullySigned: true, }); should.not.exist(bitcoreError); }); }); describe('#pushSignatures', () => { it('should sign BIP45 P2SH correctly', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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, coin: 'btc', toAddress: toAddress, amount: 1200, changeAddress: { address: changeAddress }, requiredSignatures: 1, outputOrder: [0, 1], fee: 10000, derivationStrategy: 'BIP45', addressType: 'P2SH', }; var key = Key.fromExtendedPrivateKey(masterPrivateKey); var path = 'm/45\''; var signatures = key.sign(path, txp); // This is a GOOD tests, since bitcore ONLY accept VALID signatures signatures.length.should.be.equal(utxos.length); }); it('should sign BIP44 P2PKH correctly', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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, coin: 'btc', amount: 1200, changeAddress: { address: changeAddress }, requiredSignatures: 1, outputOrder: [0, 1], fee: 10000, derivationStrategy: 'BIP44', addressType: 'P2PKH', }; var path = 'm/44\'/1\'/0\''; var key = Key.fromExtendedPrivateKey(masterPrivateKey); var signatures = key.sign(path, txp); // This is a GOOD test, since bitcore ONLY accept VALID signatures signatures.length.should.be.equal(utxos.length); }); it('should sign multiple-outputs proposal correctly', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var publicKeyRing = [{ xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']), }]; var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]); var txp = { inputs: utxos, coin: 'btc', 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 path = 'm/44\'/1\'/0\''; var key = Key.fromExtendedPrivateKey(masterPrivateKey); var signatures = key.sign(path, txp); signatures.length.should.be.equal(utxos.length); }); it('should sign proposal with provided output scripts correctly', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; 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', coin: 'btc', outputs: [{ "amount": 700, "script": "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae" }, { "amount": 600, "script": "76a9144d5bd54809f846dc6b1a14cbdd0ac87a3c66f76688ac" }, { "amount": 0, "script": "6a1e43430102fa9213bc243af03857d0f9165e971153586d3915201201201210" }], changeAddress: { address: changeAddress }, requiredSignatures: 1, outputOrder: [0, 1, 2, 3], fee: 10000, derivationStrategy: 'BIP44', addressType: 'P2PKH', }; var path = 'm/44\'/1\'/0\''; var key = Key.fromExtendedPrivateKey(masterPrivateKey); var signatures = key.sign(path, txp); signatures.length.should.be.equal(utxos.length); }); it('should sign btc proposal correctly', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var publicKeyRing = [{ xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']), }]; var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]); var txp = { version: 3, coin: 'btc', 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 path = 'm/44\'/1\'/0\''; var key = Key.fromExtendedPrivateKey(masterPrivateKey); var signatures = key.sign(path, txp); signatures.length.should.be.equal(utxos.length); signatures[0].should.equal('3045022100cfacaf8e4c9782f33f717eba3162d44cf9f34d9768a3bcd66b7052eb0868a0880220015e930e1f7d9a8b6b9e54d1450556bf4ba95c2cf8ef5c55d97de7df270cc6fd'); signatures[1].should.equal('3044022069cf6e5d8700ff117f754e4183e81690d99d6a6443e86c9589efa072ecb7d82c02204c254506ac38774a2176f9ef56cc239ef7867fbd24da2bef795128c75a063301'); }); it('should sign btc proposal correctly (tx V2)', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var publicKeyRing = [{ xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']), }]; var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]); var txp = { version: 4, coin: 'btc', 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 path = 'm/44\'/1\'/0\''; var key = Key.fromExtendedPrivateKey(masterPrivateKey); var signatures = key.sign(path, txp); signatures.length.should.be.equal(utxos.length); signatures[0].should.equal('3045022100da83ffb02ce0c5c7f2b30d0eb2fd62d1177d282fff5ce7deb9d3a8fd6e002c9d022030f0f0b29dd1fb9b602c50e8916568aa0dd68054523989291decfdbf36d70299'); signatures[1].should.equal('3045022100951f980ad2fcd764a7824575e18aa4f28309b7160c353a0e3d239bff83050184022039c4ab5be5c40d19cd2c8bfcbf42a6262df851454a494ad78668be7d35519f05'); }); it('should sign eth proposal correctly', () => { const toAddress = '0xa062a07a0a56beb2872b12f388f511d694626730'; const key = Key.fromExtendedPrivateKey(masterPrivateKey); const path = 'm/44\'/60\'/0\''; const publicKeyRing = [{ xPubKey: new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild(path).toString(), }]; const from = Utils.deriveAddress('P2PKH', publicKeyRing, 'm/0/0', 1, 'livenet', 'eth'); const txp = { version: 3, from: from.address, coin: 'eth', outputs: [{ toAddress: toAddress, amount: 3896000000000000, gasLimit: 21000, message: 'first output' } ], requiredSignatures: 1, outputOrder: [0, 1, 2], fee: 420000000000000, nonce: 6, gasPrice: 20000000000, derivationStrategy: 'BIP44', addressType: 'P2PKH', amount: 3896000000000000 }; const signatures = key.sign(path, txp); const expectedSignatures = [ '0x4f761cd5f1cf1008d398c854ee338f82b457dc67ae794a987083b36b83fc6c917247fe72fe1880c0ee914c6e1b608625d8ab4e735520c33b2f7f76e0dcaf59801c', ]; signatures.should.deep.equal(expectedSignatures); }); it('should sign BCH proposal correctly', () => { var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx'; var publicKeyRing = [{ xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']), }]; var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]); var txp = { version: 3, coin: 'bch', 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 path = 'm/44\'/1\'/0\''; var key = Key.fromExtendedPrivateKey(masterPrivateKey); var signatures = key.sign(path, txp); signatures.length.should.be.equal(utxos.length); signatures[0].should.equal('304402200aa70dfe99e25792c4a7edf773477100b6659f1ba906e551e6e5218ec32d273402202e31c575edb55b2da824e8cafd02b4769017ef63d3c888718cf6f0243c570d41'); signatures[1].should.equal('3045022100afde45e125f654453493b40d288cd66e8a011c66484509ae730a2686c9dff30502201bf34a6672c5848dd010b89ea1a5f040731acf78fec062f61b305e9ce32798a5'); }); }); }); describe('Wallet secret round trip', () => { it('should create secret and parse secret', () => { 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', () => { (() => { Client.parseSecret('invalidSecret'); }).should.throw('Invalid secret'); }); it('should create secret and parse secret from string', () => { 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', () => { var result = Client.parseSecret('5ZN64RxKWCJXcy1pZwgrAzL1NnN5FQic5M2tLJVG5bEHaGXNRQs2uzJjMa9pMAbP5rz9Vu2xSaT'); result.coin.should.equal('btc'); }); }); describe('Notification polling', () => { var clock, interval; beforeEach(() => { clock = sinon.useFakeTimers({ now: 1234000, toFake: ['Date'] }); }); afterEach(() => { clock.restore(); }); it('should fetch notifications at intervals', (done) => { helpers.createAndJoinWallet(clients, keys, 2, 2, {}, () => { clients[0].on('notification', (data) => { notifications.push(data); }); var notifications = []; clients[0]._fetchLatestNotifications(5, () => { _.map(notifications, 'type').should.deep.equal(['NewCopayer', 'WalletComplete']); clock.tick(2000); notifications = []; clients[0]._fetchLatestNotifications(5, () => { notifications.length.should.equal(0); clock.tick(2000); clients[1].createAddress((err, x) => { should.not.exist(err); clients[0]._fetchLatestNotifications(5, () => { _.map(notifications, 'type').should.deep.equal(['NewAddress']); clock.tick(2000); notifications = []; clients[0]._fetchLatestNotifications(5, () => { notifications.length.should.equal(0); clients[1].createAddress((err, x) => { should.not.exist(err); clock.tick(60 * 1000); clients[0]._fetchLatestNotifications(5, () => { notifications.length.should.equal(0); done(); }); }); }); }); }); }); }); }); }); }); describe('Wallet Creation', () => { var k; beforeEach((done) => { k = Key.create(); db.dropDatabase((err) => { return done(err); }); }); it('should fail to create wallet in bogus device', (done) => { clients[0].fromString( k.createCredentials(null, { coin: 'btc', network: 'livenet', account: 0, n: 1, }) ); clients[0].keyDerivationOk = false; clients[0].createWallet('mywallet', 'pepe', 1, 1, {}, (err, secret) => { should.exist(err); err.toString().should.contain('Cannot'); should.not.exist(secret); done(); }); }); it('should encrypt wallet name', (done) => { clients[0].fromString( k.createCredentials(null, { coin: 'btc', network: 'livenet', account: 0, n: 1, }) ); var spy = sinon.spy(clients[0].request, 'post'); clients[0].createWallet('mywallet', 'pepe', 1, 1, {}, (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({}, (err, status) => { should.not.exist(err); status.wallet.name.should.equal('mywallet'); done(); }) }); }); it('should encrypt copayer name in wallet creation', (done) => { clients[0].