UNPKG

@abcpros/bitcore-wallet-service

Version:
1,621 lines (1,478 loc) 369 kB
'use strict'; const _ = require('lodash'); const async = require('async'); const chai = require('chai'); const sinon = require('sinon'); const CWC = require('@abcpros/crypto-wallet-core'); const LOG_LEVEL = 'info'; //const LOG_LEVEL = 'debug'; const should = chai.should(); const { logger, transport } = require('../../ts_build/lib/logger.js'); const { ChainService } = require('../../ts_build/lib/chain/index'); var config = require('../../ts_build/config.js'); const Bitcore = require('@abcpros/bitcore-lib'); const Bitcore_ = { btc: Bitcore, bch: require('@abcpros/bitcore-lib-cash'), xec: require('@abcpros/bitcore-lib-xec'), eth: Bitcore, xrp: Bitcore, doge: require('@abcpros/bitcore-lib-doge'), xpi: require('@abcpros/bitcore-lib-xpi'), ltc: require('@abcpros/bitcore-lib-ltc') }; const { WalletService } = require('../../ts_build/lib/server'); const { Storage } = require('../../ts_build/lib/storage') const Common = require('../../ts_build/lib/common'); const Utils = Common.Utils; const Constants = Common.Constants; const Defaults = Common.Defaults; const VanillaDefaults = _.cloneDeep(Defaults); const Model = require('../../ts_build/lib/model'); const BCHAddressTranslator = require('../../ts_build/lib/bchaddresstranslator'); var HugeTxs = require('./hugetx'); var TestData = require('../testdata'); var helpers = require('./helpers'); var storage, blockchainExplorer, request; const TO_SAT = { 'bch': 1e8, 'btc': 1e8, 'eth': 1e18, 'usdc': 1e6, 'xrp': 1e6, 'doge': 1e8, 'ltc': 1e8 }; const TOKENS = ['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', '0x056fd409e1d7a124bd7017459dfea2f387b6d5cd']; describe('Wallet service', function() { before(function(done) { helpers.before(function(res) { storage = res.storage; blockchainExplorer = res.blockchainExplorer; request = res.request; done(); }); }); beforeEach(function(done) { transport.level= LOG_LEVEL; config.suspendedChains = []; // restore defaults, cp values _.each(_.keys(VanillaDefaults), (x) => { Defaults[x] = VanillaDefaults[x]; }); helpers.beforeEach(function(res) { done(); }); }); after(function(done) { helpers.after(done); }); describe('#getServiceVersion', function() { it('should get version from package', function() { WalletService.getServiceVersion().should.equal('bws-' + require('../../package').version); }); }); describe('#getInstance', function() { it('should get server instance', function() { var server = WalletService.getInstance({ clientVersion: 'bwc-2.9.0', }); server.clientVersion.should.equal('bwc-2.9.0'); }); it('should not get server instance for BWC lower than v1.2', function() { var err; try { var server = WalletService.getInstance({ clientVersion: 'bwc-1.1.99', }); } catch(ex) { err = ex; } should.exist(err); err.code.should.equal('UPGRADE_NEEDED'); }); it('should get server instance for non-BWC clients', function() { var server = WalletService.getInstance({ clientVersion: 'dummy-1.0.0', }); server.clientVersion.should.equal('dummy-1.0.0'); server = WalletService.getInstance({}); (server.clientVersion == null).should.be.true; }); }); describe('#getInstanceWithAuth', function() { it('should not get server instance for BWC lower than v1.2', function(done) { var server = WalletService.getInstanceWithAuth({ copayerId: '1234', message: 'hello world', signature: 'xxx', clientVersion: 'bwc-1.1.99', }, function(err, server) { should.exist(err); should.not.exist(server); err.code.should.equal('UPGRADE_NEEDED'); done(); }); }); it('should get server instance for existing copayer', function(done) { helpers.createAndJoinWallet(1, 2, function(s, wallet) { // using copayer 0 data. var xpriv = TestData.copayers[0].xPrivKey; var priv = TestData.copayers[0].privKey_1H_0; var sig = helpers.signMessage('hello world', priv); WalletService.getInstanceWithAuth({ // test assumes wallet's copayer[0] is TestData's copayer[0] copayerId: wallet.copayers[0].id, message: 'hello world', signature: sig, clientVersion: 'bwc-2.0.0', walletId: '123', }, function(err, server) { should.not.exist(err); server.walletId.should.equal(wallet.id); server.copayerId.should.equal(wallet.copayers[0].id); server.clientVersion.should.equal('bwc-2.0.0'); done(); }); }); }); it('should fail when requesting for non-existent copayer', function(done) { var message = 'hello world'; var opts = { copayerId: 'dummy', message: message, signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0), }; WalletService.getInstanceWithAuth(opts, function(err, server) { err.code.should.equal('NOT_AUTHORIZED'); err.message.should.contain('Copayer not found'); done(); }); }); it('should fail when message signature cannot be verified', function(done) { helpers.createAndJoinWallet(1, 2, function(s, wallet) { WalletService.getInstanceWithAuth({ copayerId: wallet.copayers[0].id, message: 'dummy', signature: 'dummy', }, function(err, server) { err.code.should.equal('NOT_AUTHORIZED'); err.message.should.contain('Invalid signature'); done(); }); }); }); it('should get server instance for support staff', function(done) { helpers.createAndJoinWallet(1, 1, function(s, wallet) { var collections = Storage.collections; s.storage.db.collection(collections.COPAYERS_LOOKUP).update({ copayerId: wallet.copayers[0].id }, { $set: { isSupportStaff: true } }, () => { var xpriv = TestData.copayers[0].xPrivKey; var priv = TestData.copayers[0].privKey_1H_0; var sig = helpers.signMessage('hello world', priv); WalletService.getInstanceWithAuth({ copayerId: wallet.copayers[0].id, message: 'hello world', signature: sig, walletId: '123', }, function(err, server) { should.not.exist(err); // AQUI server.walletId.should.equal('123'); server.copayerId.should.equal(wallet.copayers[0].id); done(); }); }); }); }); it('should get server instance for marketing staff', function(done) { helpers.createAndJoinWallet(1, 1, function(s, wallet) { var collections = Storage.collections; s.storage.db.collection(collections.COPAYERS_LOOKUP).updateOne({ copayerId: wallet.copayers[0].id }, { $set: { isMarketingStaff: true } }, () => { var xpriv = TestData.copayers[0].xPrivKey; var priv = TestData.copayers[0].privKey_1H_0; var sig = helpers.signMessage('hello world', priv); WalletService.getInstanceWithAuth({ copayerId: wallet.copayers[0].id, message: 'hello world', signature: sig, walletId: '123', }, function(err, server) { should.not.exist(err); server.walletId.should.not.equal('123'); server.copayerIsMarketingStaff.should.equal(true); server.copayerId.should.equal(wallet.copayers[0].id); done(); }); }); }); }); }); // tests for adding and retrieving adds from db describe('Creating ads, retrieve ads, active/inactive', function(done) { var server, wallet, adOpts; adOpts = { advertisementId: '123', name: 'name', title:'title', body: 'body', country: 'US', type: 'standard', linkText: 'linkText', linkUrl: 'linkUrl', dismissible: true, isAdActive: false, isTesting: true, signature: '304050302480413401348a3b34902403434512535e435463', app: 'bitpay' }; beforeEach(function(done) { helpers.createAndJoinWallet(1, 2, function(s, w) { server = s; wallet = w; done(); }); }); it('should create/get ad', function(done) { async.series([function(next) { server.createAdvert(adOpts, function (err, ad) { should.not.exist(err); next(); }); }, function(next) { server.getAdvert({adId: '123'}, function (err, ad) { should.not.exist(err); should.exist(ad); ad.advertisementId.should.equal('123'); ad.name.should.equal('name'); ad.title.should.equal('title'); ad.body.should.equal('body'); ad.country.should.equal('US'); ad.type.should.equal('standard'); ad.linkText.should.equal('linkText'); ad.linkUrl.should.equal('linkUrl'); ad.dismissible.should.equal(true); ad.isAdActive.should.equal(false); ad.isTesting.should.equal(true); ad.signature.should.equal('304050302480413401348a3b34902403434512535e435463'), ad.app.should.equal('bitpay'); next(); }); }], function(err) { should.not.exist(err); done(); }) }); it('should create/get/delete an ad', function(done) { async.series([function(next) { server.createAdvert(adOpts, function (err, ad) { next(); }); }, function(next) { server.getAdvert({adId: '123'}, function (err, ad) { should.not.exist(err); should.exist(ad); next(); }); }, server.removeAdvert({adId: '123'}, function(err, nextArg) { should.not.exist(err); }) ], function(err) { should.not.exist(err); }) done(); }); it('should create ad initially inactive, retrieve, make active, retrieve again', function(done) { async.series([function(next) { server.createAdvert(adOpts, function (err, ad) { next(); }); }, function(next) { server.getAdvert({adId: '123'}, function(err, ad) { should.not.exist(err); should.exist(ad); ad.advertisementId.should.equal('123'); ad.isAdActive.should.equal(false); ad.isTesting.should.equal(true); }); next(); }, function(next) { server.activateAdvert({adId: '123'}, function (err, ad) { should.not.exist(err); next(); }); }, function(next) { server.getAdvert({adId: '123'}, function (err, ad) { should.not.exist(err); should.exist(ad); ad.advertisementId.should.equal('123'); ad.isAdActive.should.equal(true); ad.isTesting.should.equal(false); }); next(); }], function(err) { should.not.exist(err); }); done(); }); }); describe('Session management (#login, #logout, #authenticate)', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(1, 2, function(s, w) { server = s; wallet = w; done(); }); }); it('should get a new session & authenticate', function(done) { WalletService.getInstanceWithAuth({ copayerId: server.copayerId, session: 'dummy', }, function(err, server2) { should.exist(err); err.code.should.equal('NOT_AUTHORIZED'); err.message.toLowerCase().should.contain('session'); should.not.exist(server2); server.login({}, function(err, token) { should.not.exist(err); should.exist(token); WalletService.getInstanceWithAuth({ copayerId: server.copayerId, session: token, }, function(err, server2) { should.not.exist(err); should.exist(server2); server2.copayerId.should.equal(server.copayerId); server2.walletId.should.equal(server.walletId); done(); }); }); }); }); it('should get the same session token for two requests in a row', function(done) { server.login({}, function(err, token) { should.not.exist(err); should.exist(token); server.login({}, function(err, token2) { should.not.exist(err); token2.should.equal(token); done(); }); }); }); it('should create a new session if the previous one has expired', function(done) { var timer = sinon.useFakeTimers({ toFake: ['Date'] }); var token; async.series([ function(next) { server.login({}, function(err, t) { should.not.exist(err); should.exist(t); token = t; next(); }); }, function(next) { WalletService.getInstanceWithAuth({ copayerId: server.copayerId, session: token, }, function(err, server2) { should.not.exist(err); should.exist(server2); next(); }); }, function(next) { timer.tick((Defaults.SESSION_EXPIRATION + 1) * 1000); next(); }, function(next) { server.login({}, function(err, t) { should.not.exist(err); t.should.not.equal(token); next(); }); }, function(next) { WalletService.getInstanceWithAuth({ copayerId: server.copayerId, session: token, }, function(err, server2) { should.exist(err); err.code.should.equal('NOT_AUTHORIZED'); err.message.should.contain('expired'); next(); }); }, ], function(err) { should.not.exist(err); timer.restore(); done(); }); }); }); describe('#createWallet', function() { var server; beforeEach(function() { server = new WalletService(); }); it('should create and store wallet', function(done) { var opts = { name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, }; server.createWallet(opts, function(err, walletId) { should.not.exist(err); server.storage.fetchWallet(walletId, function(err, wallet) { should.not.exist(err); wallet.id.should.equal(walletId); wallet.name.should.equal('my wallet'); done(); }); }); }); it('should create wallet with given id', function(done) { var opts = { name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, id: '1234', }; server.createWallet(opts, function(err, walletId) { should.not.exist(err); server.storage.fetchWallet('1234', function(err, wallet) { should.not.exist(err); wallet.id.should.equal(walletId); wallet.name.should.equal('my wallet'); done(); }); }); }); it('should fail to create wallets with same id', function(done) { var opts = { name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, id: '1234', }; server.createWallet(opts, function(err, walletId) { server.createWallet(opts, function(err, walletId) { err.message.should.contain('Wallet already exists'); done(); }); }); }); it('should create wallet BCH if n > 1 and BWC version is 8.3.0 or higher', function(done) { var opts = { coin: 'bch', name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub }; server.clientVersion = 'bwc-8.3.0'; server.createWallet(opts, function(err, walletId) { should.not.exist(err); should.exist(walletId); done(); }); }); it('should create wallet BTC if n > 1 and BWC version is lower than 8.3.0', function(done) { var opts = { coin: 'btc', name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub }; server.clientVersion = 'bwc-8.3.0'; server.createWallet(opts, function(err, walletId) { should.not.exist(err); should.exist(walletId); done(); }); }); it('should fail to create wallets BCH if n > 1 and BWC version is lower than 8.3.0', function(done) { var opts = { coin: 'bch', name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub }; server.clientVersion = 'bwc-8.2.0'; server.createWallet(opts, function(err, walletId) { should.not.exist(walletId); should.exist(err); err.message.should.contain('BWC clients < 8.3 are no longer supported for multisig BCH wallets.'); done(); }); }); it('should create wallet BCH if n == 1 and BWC version is lower than 8.3.0', function(done) { var opts = { coin: 'bch', name: 'my wallet', m: 1, n: 1, pubKey: TestData.keyPair.pub }; server.clientVersion = 'bwc-8.2.0'; server.createWallet(opts, function(err, walletId) { should.not.exist(err); should.exist(walletId); done(); }); }); it('should fail to create wallet with no name', function(done) { var opts = { name: '', m: 2, n: 3, pubKey: TestData.keyPair.pub, }; server.createWallet(opts, function(err, walletId) { should.not.exist(walletId); should.exist(err); err.message.should.contain('name'); done(); }); }); it('should check m-n combination', function(done) { var pairs = [{ m: 0, n: 0, valid: false, }, { m: 1, n: 1, valid: true, }, { m: 2, n: 3, valid: true, }, { m: 0, n: 2, valid: false, }, { m: 2, n: 1, valid: false, }, { m: 0, n: 10, valid: false, }, { m: 1, n: 20, valid: false, }, { m: 10, n: 10, valid: true, }, { m: 15, n: 15, valid: true, }, { m: 16, n: 16, valid: false, }, { m: 1, n: 15, valid: true, }, { m: -2, n: -2, valid: false, },]; async.eachSeries(pairs, function(pair, cb) { let opts = { name: 'my wallet', pubKey: TestData.keyPair.pub, }; var pub = (new Bitcore.PrivateKey()).toPublicKey(); opts.m = pair.m; opts.n = pair.n; opts.pubKey = pub.toString(); server.createWallet(opts, function(err) { if(!pair.valid) { should.exist(err); err.message.should.equal('Invalid combination of required copayers / total copayers'); } else { if(err) console.log("ERROR", opts, err); should.not.exist(err); } return cb(); }); }, function(err) { done(); }); }); it('should fail to create wallet with invalid pubKey argument', function(done) { var opts = { name: 'my wallet', m: 2, n: 3, pubKey: 'dummy', }; server.createWallet(opts, function(err, walletId) { should.not.exist(walletId); should.exist(err); err.message.should.contain('Invalid public key'); done(); }); }); it('should create wallet for another coin', function(done) { var opts = { coin: 'bch', name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, }; server.createWallet(opts, function(err, walletId) { should.not.exist(err); server.storage.fetchWallet(walletId, function(err, wallet) { should.not.exist(err); wallet.coin.should.equal('bch'); done(); }); }); }); it('should create a P2WPKH Segwit wallet', function(done) { var opts = { coin: 'btc', name: 'my segwit wallet', m: 1, n: 1, pubKey: TestData.keyPair.pub, useNativeSegwit: true }; server.createWallet(opts, function(err, walletId) { should.not.exist(err); server.storage.fetchWallet(walletId, function(err, wallet) { should.not.exist(err); wallet.addressType.should.equal('P2WPKH'); wallet.coin.should.equal('btc'); done(); }); }); }); it('should create a P2WSH Segwit wallet', function(done) { var opts = { coin: 'btc', name: 'my multisig segwit wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, useNativeSegwit: true }; server.createWallet(opts, function(err, walletId) { should.not.exist(err); server.storage.fetchWallet(walletId, function(err, wallet) { should.not.exist(err); wallet.addressType.should.equal('P2WSH'); wallet.coin.should.equal('btc'); done(); }); }); }); ['eth','xrp'].forEach(c => { it(`should fail to create a multisig ${c} wallet`, function(done) { var opts = { coin: c, name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, }; server.createWallet(opts, function(err, walletId) { should.exist(err); err.message.should.contain('not supported'); done(); }); }); it(`should create ${c} wallet with singleAddress flag`, function(done) { helpers.createAndJoinWallet(1, 1, { coin: c }, function(s, wallet) { wallet.singleAddress.should.equal(true); done(); }); }); }); describe('Address derivation strategy', function() { var server; beforeEach(function() { server = WalletService.getInstance(); }); it('should use BIP44 & P2PKH for 1-of-1 wallet if supported', function(done) { var walletOpts = { name: 'my wallet', m: 1, n: 1, pubKey: TestData.keyPair.pub, }; server.createWallet(walletOpts, function(err, wid) { should.not.exist(err); server.storage.fetchWallet(wid, function(err, wallet) { should.not.exist(err); wallet.derivationStrategy.should.equal('BIP44'); wallet.addressType.should.equal('P2PKH'); done(); }); }); }); it('should use BIP44 & P2SH for shared wallet if supported', function(done) { var walletOpts = { name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, }; server.createWallet(walletOpts, function(err, wid) { should.not.exist(err); server.storage.fetchWallet(wid, function(err, wallet) { should.not.exist(err); wallet.derivationStrategy.should.equal('BIP44'); wallet.addressType.should.equal('P2SH'); done(); }); }); }); }); }); describe('#joinWallet', function() { describe('New clients', function() { var server, serverForBch, walletId, walletIdForBch; beforeEach(function(done) { server = new WalletService(); var walletOpts = { name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, clientVersion: 'bwc-8.3.0' }; server.createWallet(walletOpts, function(err, wId) { should.not.exist(err); walletId = wId; should.exist(walletId); done(); }); }); it('should join existing wallet', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data', }); server.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); var copayerId = result.copayerId; helpers.getAuthServer(copayerId, function(server) { server.getWallet({}, function(err, wallet) { wallet.id.should.equal(walletId); wallet.copayers.length.should.equal(1); var copayer = wallet.copayers[0]; copayer.name.should.equal('me'); copayer.id.should.equal(copayerId); copayer.customData.should.equal('dummy custom data'); server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = _.find(notifications, { type: 'NewCopayer' }); should.exist(notif); notif.data.walletId.should.equal(walletId); notif.data.copayerId.should.equal(copayerId); notif.data.copayerName.should.equal('me'); notif = _.find(notifications, { type: 'WalletComplete' }); should.not.exist(notif); done(); }); }); }); }); }); it('should join existing wallet, getStatus + v8', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data', }); server.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); var copayerId = result.copayerId; helpers.getAuthServer(copayerId, function(server) { server.getStatus({ includeExtendedInfo: true }, function(err, status) { should.not.exist(err); status.wallet.m.should.equal(1); status.wallet.beRegistered.should.equal(false); status.balance.totalAmount.should.equal(0); status.balance.availableAmount.should.equal(0); done(); }); }); }); }); it('should join wallet BTC if BWC version is lower than 8.3.0', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, coin: 'btc', name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, walletId: walletId, xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data', }); server.clientVersion = 'bwc-8.2.0'; server.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); should.exist(result); should.exist(result.copayerId); done(); }); }); it('should fail join existing wallet with bad xpub', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: 'Ttub4pHUfyVU2mpjaM6YDGDJXWP6j5SL5AJzbViBuTaJEsybcrWZZoGkW7RSUSH9VRQKJtjqY2LfC2bF3FM4UqC1Ba9EP5M64SdTsv9575VAUwh', requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data', }); server.joinWallet(copayerOpts, function(err, result) { err.message.should.match(/Invalid extended public key/); done(); }); }); it('should fail join existing wallet with wrong network xpub', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: 'tpubD6NzVbkrYhZ4Wbwwqah5kj1RGPK9BYeGbowB1jegxMoAkKbNhYUAcRTZ5fyxDcpjNXxziiy2ZkUQ3kR1ycPNycTD7Q2Dr6UfLcNTYHrzS3U', requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data', }); server.joinWallet(copayerOpts, function(err, result) { err.message.should.match(/different network/); done(); }); }); it('should fail to join with no name', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: '', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, }); server.joinWallet(copayerOpts, function(err, result) { should.not.exist(result); should.exist(err); err.message.should.contain('name'); done(); }); }); it('should fail to join non-existent wallet', function(done) { var copayerOpts = { walletId: '123', name: 'me', xPubKey: 'dummy', requestPubKey: 'dummy', copayerSignature: 'dummy', }; server.joinWallet(copayerOpts, function(err) { should.exist(err); done(); }); }); it('should fail to join full wallet', function(done) { helpers.createAndJoinWallet(1, 1, function(s, wallet) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: wallet.id, name: 'me', xPubKey: TestData.copayers[1].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[1].pubKey_1H_0, }); server.joinWallet(copayerOpts, function(err) { should.exist(err); err.code.should.equal('WALLET_FULL'); err.message.should.equal('Wallet full'); done(); }); }); }); it('should fail to join wallet for different coin', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, coin: 'bch', }); server.joinWallet(copayerOpts, function(err) { should.exist(err); err.message.should.contain('different coin'); done(); }); }); it('should return copayer in wallet error before full wallet', function(done) { helpers.createAndJoinWallet(1, 1, function(s, wallet) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: wallet.id, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, }); server.joinWallet(copayerOpts, function(err) { should.exist(err); err.code.should.equal('COPAYER_IN_WALLET'); done(); }); }); }); it('should fail to re-join wallet', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, }); server.joinWallet(copayerOpts, function(err) { should.not.exist(err); server.joinWallet(copayerOpts, function(err) { should.exist(err); err.code.should.equal('COPAYER_IN_WALLET'); err.message.should.equal('Copayer already in wallet'); done(); }); }); }); it('should be able to get wallet info without actually joining', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data', dryRun: true, }); server.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); should.exist(result); should.not.exist(result.copayerId); result.wallet.id.should.equal(walletId); result.wallet.m.should.equal(1); result.wallet.n.should.equal(2); result.wallet.copayers.should.be.empty; server.storage.fetchWallet(walletId, function(err, wallet) { should.not.exist(err); wallet.id.should.equal(walletId); wallet.copayers.should.be.empty; done(); }); }); }); it('should fail to join two wallets with same xPubKey', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, }); server.joinWallet(copayerOpts, function(err) { should.not.exist(err); var walletOpts = { name: 'my other wallet', m: 1, n: 1, pubKey: TestData.keyPair.pub, }; server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, }); server.joinWallet(copayerOpts, function(err) { should.exist(err); err.code.should.equal('COPAYER_REGISTERED'); err.message.should.equal('Copayer ID already registered on server'); done(); }); }); }); }); it('should fail to join with bad formated signature', function(done) { var copayerOpts = { walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, copayerSignature: 'bad sign', }; server.joinWallet(copayerOpts, function(err) { err.message.should.equal('Bad request'); done(); }); }); it('should fail to join with invalid xPubKey', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'copayer 1', xPubKey: 'invalid', requestPubKey: TestData.copayers[0].pubKey_1H_0, }); server.joinWallet(copayerOpts, function(err, result) { should.not.exist(result); should.exist(err); err.message.should.contain('extended public key'); done(); }); }); it('should fail to join with null signature', function(done) { var copayerOpts = { walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, }; server.joinWallet(copayerOpts, function(err) { should.exist(err); err.message.should.contain('argument: copayerSignature missing'); done(); }); }); it('should fail to join with wrong signature', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, }); copayerOpts.name = 'me2'; server.joinWallet(copayerOpts, function(err) { err.message.should.equal('Bad request'); done(); }); }); it('should set pkr and status = complete on last copayer joining (2-3)', function(done) { helpers.createAndJoinWallet(2, 3, function(server) { server.getWallet({}, function(err, wallet) { should.not.exist(err); wallet.status.should.equal('complete'); wallet.publicKeyRing.length.should.equal(3); server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = _.find(notifications, { type: 'WalletComplete' }); should.exist(notif); notif.data.walletId.should.equal(wallet.id); done(); }); }); }); }); it('should not notify WalletComplete if 1-of-1', function(done) { helpers.createAndJoinWallet(1, 1, function(server) { server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = _.find(notifications, { type: 'WalletComplete' }); should.not.exist(notif); done(); }); }); }); }); describe('New clients 2', function() { var server, serverForBch, walletId, walletIdForBch; it('should join wallet BCH if BWC version is 8.3.0 or higher', function(done) { serverForBch = new WalletService(); var walletOpts = { coin: 'bch', name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub }; serverForBch.clientVersion = 'bwc-8.3.4'; serverForBch.createWallet(walletOpts, function(err, wId) { should.not.exist(err); walletIdForBch = wId; should.exist(walletIdForBch); var copayerOpts = helpers.getSignedCopayerOpts({ coin: 'bch', walletId: walletIdForBch, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data' }); serverForBch.clientVersion = 'bwc-8.3.0'; serverForBch.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); should.exist(result); should.exist(result.copayerId); done(); }); }); }); it('should fail to join BIP48 wallets from old clients ', function(done) { serverForBch = new WalletService(); var walletOpts = { coin: 'bch', name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, walletId: walletId, usePurpose48: true, }; serverForBch.createWallet(walletOpts, function(err, wId) { should.not.exist(err); walletIdForBch = wId; should.exist(walletIdForBch); var copayerOpts = helpers.getSignedCopayerOpts({ coin: 'bch', walletId: walletIdForBch, name: 'me', m: 2, n: 3, xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0 }); serverForBch.clientVersion = 'bwc-8.3.0'; serverForBch.joinWallet(copayerOpts, function(err, result) { should.not.exist(result); should.exist(err); err.message.should.contain('upgrade'); done(); }); }); }); it('should join BIP48 wallets from new clients ', function(done) { serverForBch = new WalletService(); var walletOpts = { coin: 'bch', name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, walletId: walletId, usePurpose48: true, }; serverForBch.createWallet(walletOpts, function(err, wId) { should.not.exist(err); walletIdForBch = wId; should.exist(walletIdForBch); var copayerOpts = helpers.getSignedCopayerOpts({ coin: 'bch', walletId: walletIdForBch, name: 'me', m: 2, n: 3, xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0 }); serverForBch.clientVersion = 'bwc-8.7.0'; serverForBch.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); should.exist(result); done(); }); }); }); }); describe('New clients 3', function() { var server, walletId, walletIdForSegwit; it('should join wallet segwit if BWC version is 8.17.0 or higher', function(done) { server = new WalletService(); var walletOpts = { coin: 'btc', name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, useNativeSegwit: true }; server.clientVersion = 'bwc-8.17.0'; server.createWallet(walletOpts, function(err, wId) { should.not.exist(err); walletIdForSegwit = wId; should.exist(walletIdForSegwit); var copayerOpts = helpers.getSignedCopayerOpts({ coin: 'btc', walletId: walletIdForSegwit, name: 'me', xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0, customData: 'dummy custom data' }); server.clientVersion = 'bwc-8.17.0'; server.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); should.exist(result); should.exist(result.copayerId); result.wallet.addressType.should.equal('P2WSH'); done(); }); }); }); it('should fail to join segwit wallets from old clients ', function(done) { server = new WalletService(); var walletOpts = { coin: 'btc', name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, walletId: walletId, useNativeSegwit: true }; server.createWallet(walletOpts, function(err, wId) { should.not.exist(err); walletIdForSegwit = wId; should.exist(walletIdForSegwit); var copayerOpts = helpers.getSignedCopayerOpts({ coin: 'btc', walletId: walletIdForSegwit, name: 'me', m: 2, n: 3, xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0 }); server.clientVersion = 'bwc-8.4.0'; server.joinWallet(copayerOpts, function(err, result) { should.not.exist(result); should.exist(err); err.message.should.contain('upgrade'); done(); }); }); }); it('should join segwit wallets from new clients', function(done) { server = new WalletService(); var walletOpts = { coin: 'btc', name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, walletId: walletId, useNativeSegwit: true, }; server.createWallet(walletOpts, function(err, wId) { should.not.exist(err); walletIdForSegwit = wId; should.exist(walletIdForSegwit); var copayerOpts = helpers.getSignedCopayerOpts({ coin: 'btc', walletId: walletIdForSegwit, name: 'me', m: 2, n: 3, xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[0].pubKey_1H_0 }); server.clientVersion = 'bwc-9.0.0'; server.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); should.exist(result); done(); }); }); }); }); }); describe('#removeWallet', function() { var server, wallet, clock; beforeEach(function(done) { helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { outputs: [{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 0.1e8, }], feePerKb: 100e2, }; async.eachSeries(_.range(2), function(i, next) { helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function() { next(); }); }, done); }); }); }); it('should delete a wallet', function(done) { server.removeWallet({}, function(err) { should.not.exist(err); server.getWallet({}, function(err, w) { should.exist(err); err.code.should.equal('WALLET_NOT_FOUND'); should.not.exist(w); async.parallel([ function(next) { server.storage.fetchAddresses(wallet.id, function(err, items) { items.length.should.equal(0); next(); }); }, function(next) { server.storage.fetchTxs(wallet.id, {}, function(err, items) { items.length.should.equal(0); next(); }); }, function(next) { server.storage.fetchNotifications(wallet.id, null, 0, function(err, items) { items.length.should.equal(0); next(); }); }, ], function(err) { should.not.exist(err); done(); }); }); }); }); // creates 2 wallet, and deletes only 1. it('should delete a wallet, and only that wallet', function(done) { var server2, wallet2; async.series([ function(next) { helpers.createAndJoinWallet(1, 1, { offset: 1 }, function(s, w) { server2 = s; wallet2 = w; helpers.stubUtxos(server2, wallet2, [1, 2, 3], function() { var txOpts = { outputs: [{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 0.1e8, }], feePerKb: 100e2, }; async.eachSeries(_.range(2), function(i, next) { helpers.createAndPublishTx(server2, txOpts, TestData.copayers[1].privKey_1H_0, function() { next(); }); }, next); }); }); }, function(next) { server.removeWallet({}, next); }, function(next) { server.getWallet({}, function(err, wallet) { should.exist(err); err.code.should.equal('WALLET_NOT_FOUND'); next(); }); }, function(next) { server2.getWallet({}, function(err, wallet) { should.not.exist(err); should.exist(wallet); wallet.id.should.equal(wallet2.id); next(); }); }, function(next) { server2.getMainAddresses({}, function(err, addresses) { should.not.exist(err); should.exist(addresses); addresses.length.should.above(0); next(); }); }, function(next) { server2.getTxs({}, function(err, txs) { should.not.exist(err); should.exist(txs); txs.length.should.equal(2); next(); }); }, function(next) { server2.getNotifications({}, function(err, notifications) { should.not.exist(err); should.exist(notifications); notifications.length.should.above(0); next(); }); }, ], function(err) { should.not.exist(err); done(); }); }); }); describe('#getStatus', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(1, 2, function(s, w) { server = s; wallet = w; done(); }); }); it('should get status', function(done) { server.getStatus({}, function(err, status) { should.not.exist(err); should.exist(status); should.exist(status.wallet); status.wallet.name.should.equal(wallet.name); should.exist(status.wallet.copayers); status.wallet.copayers.length.should.equal(2); should.exist(status.balance); status.balance.totalAmount.should.equal(0); should.exist(status.preferences); should.exist(status.pendingTxps); status.pendingTxps.should.be.empty; should.not.exist(status.wallet.publicKeyRing); should.not.exist(status.wallet.pubKey); should.not.exist(status.wallet.addressManager); _.each(status.wallet.copayers, function(copayer) { should.not.exist(copayer.xPubKey); should.not.exist(copayer.requestPubK