@owstack/wallet-service
Version:
A service for multisignature HD wallets
1,536 lines (1,430 loc) • 261 kB
JavaScript
'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();
});
});