UNPKG

@owstack/wallet-service

Version:

A service for multisignature HD wallets

1,238 lines (1,156 loc) 335 kB
const chai = require('chai'); const sinon = require('sinon'); const should = chai.should(); const Service = require('../../'); const serviceName = 'BTC'; const WalletService = Service[serviceName].WalletService; const btcLib = require('@owstack/btc-lib'); const Networks = btcLib.Networks; const Unit = btcLib.Unit; const LIVENET = Networks.livenet; const TESTNET = Networks.testnet; const owsCommon = require('@owstack/ows-common'); const keyLib = require('@owstack/key-lib'); const async = require('async'); const Copayer = WalletService.Model.Copayer; const Defaults = WalletService.Common.Defaults; const helpers = require('./helpers'); const log = require('npmlog'); const PrivateKey = keyLib.PrivateKey; const Storage = WalletService.Storage; const testConfig = require('config'); const TestData = require('../testdata'); const TxProposal = WalletService.Model.TxProposal; const Server = WalletService.Server; let storage; let blockchainExplorer; let request; const atomicsAccessor = Unit().atomicsAccessor(); const 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) { const priv = TestData.copayers[0].privKey_1H_0; const 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) { const 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) { 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) { 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) { const collections = require('../../base-service/lib/storage').collections; s.getStorage().getDB().collection(collections.COPAYERS_LOOKUP).update({ copayerId: wallet.copayers[0].id }, { $set: { isSupportStaff: true } }); const priv = TestData.copayers[0].privKey_1H_0; const 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 () { let server; beforeEach(function (done) { helpers.createAndJoinWallet(serviceName, 1, 2, function (s) { server = s; 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) { const timer = sinon.useFakeTimers('Date'); let 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) { 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 () { let 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) { const 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) { const 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) { const opts = { name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, id: '1234', }; server.createWallet(opts, function () { server.createWallet(opts, function (err) { err.message.should.contain('Wallet already exists'); done(); }); }); }); it('should fail to create wallet with no name', function (done) { const 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) { const 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, }]; const 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 () { done(); }); }); it('should fail to create wallet with invalid pubKey argument', function (done) { const 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 () { let 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) { const 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) { const 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) { const 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) { const 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 () { let server; let walletId; beforeEach(function (done) { Server.getInstance({ blockchainExplorer: blockchainExplorer, request: request, storage: helpers.getStorage(serviceName), force: true }, testConfig, function (s) { server = s; const 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) { const 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); const 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); const 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); let 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) { const 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) { const 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) { const 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) { const 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) { const 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) { const 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) { let 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); const 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) { const 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) { const 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) { const 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) { const 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); const 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); const notif = lodash.find(notifications, { type: 'WalletComplete' }); should.not.exist(notif); done(); }); }); }); }); describe('Interaction new/legacy clients', function () { let 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) { const 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); const 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) { const 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); const 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 () { let server; let wallet; let clock; beforeEach(function (done) { helpers.createAndJoinWallet(serviceName, 1, 1, function (s, w) { server = s; wallet = w; helpers.stubUtxos(server, wallet, [1, 2], function () { const 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) { let server2; let 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 () { const 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 () { let server; let 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 () { const 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); const balance = status.balance; balance.totalAmount.should.equal(3e8); balance.lockedAmount.should.equal(tx.inputs[0][atomicsAccessor]); balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); done(); }); }); }); }); }); describe('#verifyMessageSignature', function () { let server; let 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) { const message = 'hello world'; const 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) { const message = 'hello world'; const 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 () { let server; let 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); const 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(er