UNPKG

@owstack/wallet-service

Version:

A service for multisignature HD wallets

1,536 lines (1,430 loc) 261 kB
'use strict'; var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); var Service = require('../../'); var serviceName = 'BTC'; var WalletService = Service[serviceName].WalletService; var btcLib = require('@owstack/btc-lib'); var Networks = btcLib.Networks; var Unit = btcLib.Unit; var LIVENET = Networks.livenet; var TESTNET = Networks.testnet; var owsCommon = require('@owstack/ows-common'); var keyLib = require('@owstack/key-lib'); var async = require('async'); var Constants = WalletService.Constants; var Copayer = WalletService.Model.Copayer; var Defaults = WalletService.Common.Defaults; var helpers = require('./helpers'); var log = require('npmlog'); var PrivateKey = keyLib.PrivateKey; var Storage = WalletService.Storage; var testConfig = require('../testconfig'); var TestData = require('../testdata'); var TxProposal = WalletService.Model.TxProposal; var Utils = WalletService.Utils; var Server = WalletService.Server; var storage, blockchainExplorer, request; var atomicsName = Unit().atomicsName(); var lodash = owsCommon.deps.lodash; log.debug = log.verbose; log.level = 'info'; describe('Wallet service', function() { before(function(done) { helpers.before(serviceName, done); }); beforeEach(function(done) { helpers.beforeEach(serviceName, function(err, res) { storage = res.storage; blockchainExplorer = res.blockchainExplorer; request = res.request; done(); }); }); describe('#getServiceVersion', function() { it('should get version from package', function() { Server.getServiceVersion().should.equal('ws-' + require('../../package').version); }); }); describe('#getInstance', function() { it('should get server instance', function() { Server.getInstance({ clientVersion: 'wc-0.0.1', blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, function(server) { server.getClientVersion().should.equal('wc-0.0.1'); }); }); it('should get server instance for non-wc clients', function() { Server.getInstance({ clientVersion: 'dummy-1.0.0', blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, function(server) { server.clientVersion.should.equal('dummy-1.0.0'); Server.getInstance({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, function(server) { (server.clientVersion == null).should.be.true; }); }); }); }); describe('#getInstanceWithAuth', function() { it('should get server instance for existing copayer', function(done) { helpers.createAndJoinWallet(serviceName, 1, 2, function(s, wallet) { var xpriv = TestData.copayers[0].xPrivKey; var priv = TestData.copayers[0].privKey_1H_0; var sig = helpers.signMessage(serviceName, 'hello world', priv); Server.getInstanceWithAuth({ clientVersion: 'wc-2.0.0', blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { copayerId: wallet.copayers[0].id, message: 'hello world', signature: sig, 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('wc-2.0.0'); done(); }); }); }); it('should fail when requesting for non-existent copayer', function(done) { var message = 'hello world'; Server.getInstanceWithAuth({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { copayerId: 'dummy', message: message, signature: helpers.signMessage(serviceName, message, TestData.copayers[0].privKey_1H_0) }, 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(serviceName, 1, 2, function(s, wallet) { Server.getInstanceWithAuth({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { 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(serviceName, 1, 1, function(s, wallet) { var collections = require('../../base-service/lib/storage').collections; s.getStorage().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(serviceName, 'hello world', priv); Server.getInstanceWithAuth({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { 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(serviceName, 1, 2, function(s, w) { server = s; wallet = w; done(); }); }); it('should get a new session & authenticate', function(done) { Server.getInstanceWithAuth({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { 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); Server.getInstanceWithAuth({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { 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) { Server.getInstanceWithAuth({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { 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) { Server.getInstanceWithAuth({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, { 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(done) { new Server({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName) }, testConfig, function(s) { server = s; done(); }); }); 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.getStorage().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.getStorage().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(); }); }); describe('Address derivation strategy', function() { var server; beforeEach(function(done) { Server.getInstance({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, function(s) { server = s; done(); }); }); 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.getStorage().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.getStorage().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.getStorage().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.getStorage().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.getInstance({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, function(s) { server = s; 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(serviceName, { 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(serviceName, 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 = lodash.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 = lodash.find(notifications, { type: 'WalletComplete' }); should.not.exist(notif); done(); }); }); }); }); }); it('should fail to join with no name', function(done) { var copayerOpts = helpers.getSignedCopayerOpts(serviceName, { 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(serviceName, 1, 1, function(s, wallet) { var copayerOpts = helpers.getSignedCopayerOpts(serviceName, { 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 return copayer in wallet error before full wallet', function(done) { helpers.createAndJoinWallet(serviceName, 1, 1, function(s, wallet) { var copayerOpts = helpers.getSignedCopayerOpts(serviceName, { 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(serviceName, { 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(serviceName, { 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.getStorage().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(serviceName, { 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(serviceName, { 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(serviceName, { 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(serviceName, { 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(serviceName, 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 = lodash.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(serviceName, 1, 1, function(server) { server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = lodash.find(notifications, { type: 'WalletComplete' }); should.not.exist(notif); done(); }); }); }); }); describe('Interaction new/legacy clients', function() { var server; beforeEach(function(done) { Server.getInstance({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, function(s) { server = s; done(); }); }); 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(serviceName, { 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(serviceName, { 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(serviceName, 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(lodash.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.getStorage().fetchAddresses(wallet.id, function(err, items) { items.length.should.equal(0); next(); }); }, function(next) { server.getStorage().fetchTxs(wallet.id, {}, function(err, items) { items.length.should.equal(0); next(); }); }, function(next) { server.getStorage().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(serviceName, 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(lodash.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(serviceName, 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); lodash.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].requestPubKeys); should.exist(status.wallet.copayers[0].customData); // Do not return other copayer's custom data lodash.each(lodash.tail(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: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 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][atomicsName]); balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); done(); }); }); }); }); }); describe('#verifyMessageSignature', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(serviceName, 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(serviceName, 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(serviceName, message, TestData.copayers[0].privKey_1H_0), }; helpers.getAuthServer(serviceName, 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(serviceName, 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.networkName.should.equal(LIVENET.name); address.address.should.equal('3BVJZ4CYzeTtawDtgwHvWV5jbvnXtYe97i'); 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 = lodash.find(notifications, { type: 'NewAddress' }); should.exist(notif); notif.data.address.should.equal(address.address); done(); }); }); }); it('should protect against storing same address multiple times', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); should.exist(address); delete address._id; server.getStorage().storeAddressAndWallet(wallet, address, function(err) { should.not.exist(err); server.getMainAddresses({}, function(err, addresses) { should.not.exist(err); addresses.length.should.equal(1); done(); }); }); }); }); it('should create many addresses on simultaneous requests', function(done) { var N = 5; async.mapSeries(lodash.range(N), function(i, cb) { server.createAddress({}, cb); }, function(err, addresses) { var x = lodash.map(addresses, 'path'); addresses.length.should.equal(N); lodash.each(lodash.range(N), function(i) { addresses[i].path.should.equal('m/2147483647/0/' + i); }); // No two identical addresses lodash.uniq(lodash.map(addresses, 'address')).length.should.equal(N); done(); }); }); }); describe('shared wallets (BIP44)', function() { beforeEach(function(done) { helpers.createAndJoinWallet(serviceName, 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.networkName.should.equal(LIVENET.name); address.address.should.equal('36q2G5FMGvJbPgAVEaiyAsFGmpkhPKwk2r'); address.isChange.should.be.false; address.path.should.equal('m/0/0'); address.type.should.equal('P2SH'); server.getNotifications({}, function(err, notifications) { should.not.exist(err); var notif = lodash.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(lodash.range(N), function(i, cb) { server.createAddress({}, cb); }, function(err, addresses) { addresses.length.should.equal(N); lodash.each(lodash.range(N), function(i) { addresses[i].path.should.equal('m/0/' + i); }); // No two identical addresses lodash.uniq(lodash.map(addresses, 'address')).length.should.equal(N); done(); }); }); it('should not create address if unable to store it', function(done) { sinon.stub(server.getStorage(), '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.getStorage().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(serviceName, 1, 1, function(s, w) { server = s; wallet = w; w.copayers[0].id.should.equal(TestData.copayers[0].id44); 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.networkName.should.equal(LIVENET.name); address.address.should.equal('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG'); 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 = lodash.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(lodash.range(N), function(i, cb) { server.createAddress({}, cb); }, function(err, addresses) { addresses = lodash.sortBy(addresses, 'path'); addresses.length.should.equal(N); lodash.each(lodash.range(N), function(i) { addresses[i].path.should.equal('m/0/' + i); }); // No two identical addresses lodash.uniq(lodash.map(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(serviceName, []); async.map(lodash.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(serviceName, [ '1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // 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(serviceName, []); async.mapSeries(lodash.range(2), function(i, next) { server.createAddress({}, next); }, function(err, addresses) { addresses.length.should.equal(2); helpers.stubAddressActivity(serviceName, [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(serviceName, 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(serviceName, 2, 2, function(s, w) { server = s; wallet = w; done(); }); }); it('should save & retrieve preferences', function(done) { server.savePreferences({ email: 'dummy@dummy.com', language: 'es', unit: 'bit', dummy: 'ignored', }, function(err) { should.not.exist(err); server.getPreferences({}, function(err, preferences) { should.not.exist(err); should.exist(preferences); preferences.email.should.equal('dummy@dummy.com'); preferences.language.should.equal('es'); preferences.unit.should.equal('bit'); should.not.exist(preferences.dummy); done(); }); }); }); it('should save preferences only for requesting copayer', function(done) { server.savePreferences({ email: 'dummy@dummy.com' }, function(err) { should.not.exist(err); helpers.getAuthServer(serviceName, wallet.copayers[1].id, function(server2) { server2.getPreferences({}, function(err, preferences) { should.not.exist(err); should.not.exist(preferences.email); done(); }); });