@owstack/wallet-service
Version:
A service for multisignature HD wallets
1,238 lines (1,156 loc) • 335 kB
JavaScript
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