UNPKG

matrixbitcore-wallet-service

Version:
1,546 lines (1,445 loc) 280 kB
'use strict'; var _ = require('lodash'); var async = require('async'); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); var log = require('npmlog'); log.debug = log.verbose; log.level = 'info'; var Bitcore = require('matrixbitcore-lib'); var Bitcore_ = { btc: Bitcore, bch: require('bitcore-lib-cash') }; var Common = require('../../lib/common'); var Utils = Common.Utils; var Constants = Common.Constants; var Defaults = Common.Defaults; var Model = require('../../lib/model'); var WalletService = require('../../lib/server'); var HugeTxs = require('./hugetx'); var TestData = require('../testdata'); var helpers = require('./helpers'); var storage, blockchainExplorer, request; describe('Wallet service', function() { before(function(done) { helpers.before(done); }); beforeEach(function(done) { helpers.beforeEach(function(res) { storage = res.storage; blockchainExplorer = res.blockchainExplorer; request = res.request; 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) { 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, 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 = require('../../lib/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); server.walletId.should.equal('123'); server.copayerId.should.equal(wallet.copayers[0].id); 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('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 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, }, ]; var opts = { id: '123', name: 'my wallet', pubKey: TestData.keyPair.pub, }; async.each(pairs, function(pair, cb) { opts.m = pair.m; opts.n = pair.n; server.createWallet(opts, function(err) { if (!pair.valid) { should.exist(err); err.message.should.equal('Invalid combination of required copayers / total copayers'); } else { 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(); }); }); }); 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 BIP45 & P2SH for 1-of-1 wallet if not supported', function(done) { var walletOpts = { name: 'my wallet', m: 1, n: 1, pubKey: TestData.keyPair.pub, supportBIP44AndP2PKH: false, }; 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('BIP45'); wallet.addressType.should.equal('P2SH'); 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(); }); }); }); it('should use BIP45 & P2SH for shared wallet if supported', function(done) { var walletOpts = { name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, supportBIP44AndP2PKH: false, }; 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('BIP45'); wallet.addressType.should.equal('P2SH'); done(); }); }); }); }); }); describe('#joinWallet', function() { describe('New clients', function() { var server, walletId; beforeEach(function(done) { server = new WalletService(); var walletOpts = { name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, }; 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 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('Interaction new/legacy clients', function() { var server; beforeEach(function() { server = new WalletService(); }); it('should fail to join legacy wallet from new client', function(done) { var walletOpts = { name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, supportBIP44AndP2PKH: false, }; server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); should.exist(walletId); 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, result) { should.exist(err); err.message.should.contain('The wallet you are trying to join was created with an older version of the client app'); done(); }); }); }); it('should fail to join new wallet from legacy client', function(done) { var walletOpts = { name: 'my wallet', m: 1, n: 2, pubKey: TestData.keyPair.pub, }; server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); should.exist(walletId); var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'me', xPubKey: TestData.copayers[0].xPubKey_45H, requestPubKey: TestData.copayers[0].pubKey_1H_0, supportBIP44AndP2PKH: false, }); server.joinWallet(copayerOpts, function(err, result) { should.exist(err); err.code.should.equal('UPGRADE_NEEDED'); 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: 'XqHSiRAXd3EmNUPCAqok6ch5XzVWqKg7VD', 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: 'XqHSiRAXd3EmNUPCAqok6ch5XzVWqKg7VD', 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.requestPubKey); should.not.exist(copayer.signature); should.not.exist(copayer.requestPubKey); should.not.exist(copayer.addressManager); should.not.exist(copayer.customData); }); done(); }); }); it('should get status including extended info', function(done) { server.getStatus({ includeExtendedInfo: true }, function(err, status) { should.not.exist(err); should.exist(status); should.exist(status.wallet.publicKeyRing); should.exist(status.wallet.pubKey); should.exist(status.wallet.addressManager); should.exist(status.wallet.copayers[0].xPubKey); should.exist(status.wallet.copayers[0].requestPubKey); should.exist(status.wallet.copayers[0].signature); should.exist(status.wallet.copayers[0].requestPubKey); should.exist(status.wallet.copayers[0].customData); // Do not return other copayer's custom data _.each(_.rest(status.wallet.copayers), function(copayer) { should.not.exist(copayer.customData); }); done(); }); }); it('should get status after tx creation', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { outputs: [{ toAddress: 'XqHSiRAXd3EmNUPCAqok6ch5XzVWqKg7VD', amount: 0.8e8 }], feePerKb: 100e2 }; helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); server.getStatus({}, function(err, status) { should.not.exist(err); status.pendingTxps.length.should.equal(1); var balance = status.balance; balance.totalAmount.should.equal(3e8); balance.lockedAmount.should.equal(tx.inputs[0].satoshis); balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); done(); }); }); }); }); }); describe('#verifyMessageSignature', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(2, 3, function(s, w) { server = s; wallet = w; done(); }); }); it('should successfully verify message signature', function(done) { var message = 'hello world'; var opts = { message: message, signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0), }; server.verifyMessageSignature(opts, function(err, isValid) { should.not.exist(err); isValid.should.be.true; done(); }); }); it('should fail to verify message signature for different copayer', function(done) { var message = 'hello world'; var opts = { message: message, signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0), }; helpers.getAuthServer(wallet.copayers[1].id, function(server) { server.verifyMessageSignature(opts, function(err, isValid) { should.not.exist(err); isValid.should.be.false; done(); }); }); }); }); describe('#createAddress', function() { var server, wallet; describe('shared wallets (BIP45)', function() { beforeEach(function(done) { helpers.createAndJoinWallet(2, 2, { supportBIP44AndP2PKH: false }, function(s, w) { server = s; wallet = w; done(); }); }); it('should create address', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); address.walletId.should.equal(wallet.id); address.network.should.equal('livenet'); address.address.should.equal('7cCwPFUiodZXahkqxYxRqs5PXUcugBpDZT'); address.isChange.should.be.false; address.path.should.equal('m/2147483647/0/0'); address.type.should.equal('P2SH'); server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = _.find(notifications, { type: 'NewAddress' }); should.exist(notif); notif.data.address.should.equal(address.address); done(); }); }); }); it('should create many addresses on simultaneous requests', function(done) { var N = 5; async.mapSeries(_.range(N), function(i, cb) { server.createAddress({}, cb); }, function(err, addresses) { var x = _.pluck(addresses, 'path'); addresses.length.should.equal(N); _.each(_.range(N), function(i) { addresses[i].path.should.equal('m/2147483647/0/' + i); }); // No two identical addresses _.uniq(_.pluck(addresses, 'address')).length.should.equal(N); done(); }); }); }); describe('shared wallets (BIP44)', function() { beforeEach(function(done) { helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; done(); }); }); it('should create address', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); address.walletId.should.equal(wallet.id); address.network.should.equal('livenet'); address.address.should.equal('7XYf6GXX5uQEPShSWCPUWFEvhNb5Ez2JrE'); address.isChange.should.be.false; address.coin.should.equal('btc'); address.path.should.equal('m/0/0'); address.type.should.equal('P2SH'); server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = _.find(notifications, { type: 'NewAddress' }); should.exist(notif); notif.data.address.should.equal(address.address); done(); }); }); }); it('should create many addresses on simultaneous requests', function(done) { var N = 5; async.mapSeries(_.range(N), function(i, cb) { server.createAddress({}, cb); }, function(err, addresses) { addresses.length.should.equal(N); _.each(_.range(N), function(i) { addresses[i].path.should.equal('m/0/' + i); }); // No two identical addresses _.uniq(_.pluck(addresses, 'address')).length.should.equal(N); done(); }); }); it('should not create address if unable to store it', function(done) { sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error'); server.createAddress({}, function(err, address) { should.exist(err); should.not.exist(address); server.getMainAddresses({}, function(err, addresses) { addresses.length.should.equal(0); server.storage.storeAddressAndWallet.restore(); server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); done(); }); }); }); }); }); describe('shared wallets (BIP44/BCH)', function() { beforeEach(function(done) { helpers.createAndJoinWallet(2, 2, { coin: 'bch' }, function(s, w) { server = s; wallet = w; done(); }); }); it('should create address', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); address.walletId.should.equal(wallet.id); address.network.should.equal('livenet'); address.address.should.equal('HBf8isgS8EXG1r3X6GP89FmooUmiJ42wHS'); address.isChange.should.be.false; address.path.should.equal('m/0/0'); address.type.should.equal('P2SH'); address.coin.should.equal('bch'); server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = _.find(notifications, { type: 'NewAddress' }); should.exist(notif); notif.data.address.should.equal(address.address); done(); }); }); }); it('should create many addresses on simultaneous requests', function(done) { var N = 5; async.mapSeries(_.range(N), function(i, cb) { server.createAddress({}, cb); }, function(err, addresses) { addresses.length.should.equal(N); _.each(_.range(N), function(i) { addresses[i].path.should.equal('m/0/' + i); }); // No two identical addresses _.uniq(_.pluck(addresses, 'address')).length.should.equal(N); done(); }); }); it('should not create address if unable to store it', function(done) { sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error'); server.createAddress({}, function(err, address) { should.exist(err); should.not.exist(address); server.getMainAddresses({}, function(err, addresses) { addresses.length.should.equal(0); server.storage.storeAddressAndWallet.restore(); server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); done(); }); }); }); }); }); describe('1-of-1 (BIP44 & P2PKH)', function() { beforeEach(function(done) { helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; w.copayers[0].id.should.equal(TestData.copayers[0].id44btc); done(); }); }); it('should create address', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); address.walletId.should.equal(wallet.id); address.network.should.equal('livenet'); address.address.should.equal('Xujpyb3X5oDqfMJEuoXzyfwaSVDSg3vFPm'); address.isChange.should.be.false; address.path.should.equal('m/0/0'); address.type.should.equal('P2PKH'); server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = _.find(notifications, { type: 'NewAddress' }); should.exist(notif); notif.data.address.should.equal(address.address); done(); }); }); }); it('should create many addresses on simultaneous requests', function(done) { var N = 5; async.mapSeries(_.range(N), function(i, cb) { server.createAddress({}, cb); }, function(err, addresses) { addresses = _.sortBy(addresses, 'path'); addresses.length.should.equal(N); _.each(_.range(N), function(i) { addresses[i].path.should.equal('m/0/' + i); }); // No two identical addresses _.uniq(_.pluck(addresses, 'address')).length.should.equal(N); done(); }); }); it('should fail to create more consecutive addresses with no activity than allowed', function(done) { var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP; Defaults.MAX_MAIN_ADDRESS_GAP = 2; helpers.stubAddressActivity([]); async.map(_.range(2), function(i, next) { server.createAddress({}, next); }, function(err, addresses) { addresses.length.should.equal(2); server.createAddress({}, function(err, address) { should.exist(err); should.not.exist(address); err.code.should.equal('MAIN_ADDRESS_GAP_REACHED'); server.createAddress({ ignoreMaxGap: true }, function(err, address) { should.not.exist(err); should.exist(address); address.path.should.equal('m/0/2'); helpers.stubAddressActivity([ 'XrKNgqCuec25KrnGv2XHYqqkB6g8KWTEsk', // m/0/2 ]); server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); address.path.should.equal('m/0/3'); Defaults.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old; done(); }); }); }); }); }); it('should cache address activity', function(done) { var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP; Defaults.MAX_MAIN_ADDRESS_GAP = 2; helpers.stubAddressActivity([]); async.mapSeries(_.range(2), function(i, next) { server.createAddress({}, next); }, function(err, addresses) { addresses.length.should.equal(2); helpers.stubAddressActivity([addresses[1].address]); var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity'); server.createAddress({}, function(err, address) { should.not.exist(err); server.createAddress({}, function(err, address) { should.not.exist(err); getAddressActivitySpy.callCount.should.equal(1); Defaults.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old; done(); }); }); }); }); }); }); describe('#getMainAddresses', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(2, 2, {}, function(s, w) { server = s; wallet = w; helpers.createAddresses(server, wallet, 5, 0, function() { done(); }); }); }); it('should get all addresses', function(done) { server.getMainAddresses({}, function(err, addresses) { should.not.exist(err); addresses.length.should.equal(5); addresses[0].path.should.equal('m/0/0'); addresses[4].path.should.equal('m/0/4'); done(); }); }); it('should get first N addresses', function(done) { server.getMainAddresses({ limit: 3 }, function(err, addresses) { should.not.exist(err); addresses.length.should.equal(3); addresses[0].path.should.equal('m/0/0'); addresses[2].path.should.equal('m/0/2'); done(); }); }); it('should get last N addresses in reverse order', function(done) { server.getMainAddresses({ limit: 3, reverse: true, }, function(err, addresses) { should.not.exist(err); addresses.length.should.equal(3); addresses[0].path.should.equal('m/0/4'); addresses[2].path.should.equal('m/0/2'); done(); }); }); }); describe('Preferences', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; done(); }); }); it('should save & retrieve preferences', function(done) { server.savePreferences({ email: 'dum