@ducatus/ducatus-wallet-service-rev
Version:
A service for Mutisig HD Bitcoin Wallets
1,602 lines (1,467 loc) • 312 kB
JavaScript
'use strict';
var _ = require('lodash');
var async = require('async');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var log = require('npmlog');
log.debug = log.verbose;
var config = require('../../ts_build/config.js');
var CWC = require('@ducatus/ducatus-crypto-wallet-core-rev');
var Bitcore = require('bitcore-lib');
var Bitcore_ = {
btc: Bitcore,
bch: require('bitcore-lib-cash')
};
var { WalletService } = require('../../ts_build/lib/server');
const { Storage } = require('../../ts_build/lib/storage')
var Common = require('../../ts_build/lib/common');
var Utils = Common.Utils;
var Constants = Common.Constants;
var Defaults = Common.Defaults;
var Model = require('../../ts_build/lib/model');
var BCHAddressTranslator = require('../../ts_build/lib/bchaddresstranslator');
var HugeTxs = require('./hugetx');
var TestData = require('../testdata');
var helpers = require('./helpers');
var storage, blockchainExplorer, request;
const TO_SAT = {
'bch': 1e8,
'btc': 1e8,
'eth': 1e18,
'usdc': 1e6,
'xrp': 1e6
};
const TOKENS = ['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', '0x056fd409e1d7a124bd7017459dfea2f387b6d5cd'];
describe('Wallet service', function() {
before(function(done) {
helpers.before(function(res) {
storage = res.storage;
blockchainExplorer = res.blockchainExplorer;
request = res.request;
done();
});
});
beforeEach(function(done) {
log.level = 'error';
helpers.beforeEach(function(res) {
done();
});
});
after(function(done) {
helpers.after(done);
});
describe('#getServiceVersion', function() {
it('should get version from package', function() {
WalletService.getServiceVersion().should.equal('bws-' + require('../../package').version);
});
});
describe('#getInstance', function() {
it('should get server instance', function() {
var server = WalletService.getInstance({
clientVersion: 'bwc-2.9.0',
});
server.clientVersion.should.equal('bwc-2.9.0');
});
it('should not get server instance for BWC lower than v1.2', function() {
var err;
try {
var server = WalletService.getInstance({
clientVersion: 'bwc-1.1.99',
});
} catch(ex) {
err = ex;
}
should.exist(err);
err.code.should.equal('UPGRADE_NEEDED');
});
it('should get server instance for non-BWC clients', function() {
var server = WalletService.getInstance({
clientVersion: 'dummy-1.0.0',
});
server.clientVersion.should.equal('dummy-1.0.0');
server = WalletService.getInstance({});
(server.clientVersion == null).should.be.true;
});
});
describe('#getInstanceWithAuth', function() {
it('should not get server instance for BWC lower than v1.2', function(done) {
var server = WalletService.getInstanceWithAuth({
copayerId: '1234',
message: 'hello world',
signature: 'xxx',
clientVersion: 'bwc-1.1.99',
}, function(err, server) {
should.exist(err);
should.not.exist(server);
err.code.should.equal('UPGRADE_NEEDED');
done();
});
});
it('should get server instance for existing copayer', function(done) {
helpers.createAndJoinWallet(1, 2, function(s, wallet) {
// using copayer 0 data.
var xpriv = TestData.copayers[0].xPrivKey;
var priv = TestData.copayers[0].privKey_1H_0;
var sig = helpers.signMessage('hello world', priv);
WalletService.getInstanceWithAuth({
// test assumes wallet's copayer[0] is TestData's copayer[0]
copayerId: wallet.copayers[0].id,
message: 'hello world',
signature: sig,
clientVersion: 'bwc-2.0.0',
walletId: '123',
}, function(err, server) {
should.not.exist(err);
server.walletId.should.equal(wallet.id);
server.copayerId.should.equal(wallet.copayers[0].id);
server.clientVersion.should.equal('bwc-2.0.0');
done();
});
});
});
it('should fail when requesting for non-existent copayer', function(done) {
var message = 'hello world';
var opts = {
copayerId: 'dummy',
message: message,
signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0),
};
WalletService.getInstanceWithAuth(opts, function(err, server) {
err.code.should.equal('NOT_AUTHORIZED');
err.message.should.contain('Copayer not found');
done();
});
});
it('should fail when message signature cannot be verified', function(done) {
helpers.createAndJoinWallet(1, 2, function(s, wallet) {
WalletService.getInstanceWithAuth({
copayerId: wallet.copayers[0].id,
message: 'dummy',
signature: 'dummy',
}, function(err, server) {
err.code.should.equal('NOT_AUTHORIZED');
err.message.should.contain('Invalid signature');
done();
});
});
});
it('should get server instance for support staff', function(done) {
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
var collections = Storage.collections;
s.storage.db.collection(collections.COPAYERS_LOOKUP).update({
copayerId: wallet.copayers[0].id
}, {
$set: {
isSupportStaff: true
}
}, () => {
var xpriv = TestData.copayers[0].xPrivKey;
var priv = TestData.copayers[0].privKey_1H_0;
var sig = helpers.signMessage('hello world', priv);
WalletService.getInstanceWithAuth({
copayerId: wallet.copayers[0].id,
message: 'hello world',
signature: sig,
walletId: '123',
}, function(err, server) {
should.not.exist(err);
// AQUI
server.walletId.should.equal('123');
server.copayerId.should.equal(wallet.copayers[0].id);
done();
});
});
});
});
});
describe('Session management (#login, #logout, #authenticate)', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should get a new session & authenticate', function(done) {
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: 'dummy',
}, function(err, server2) {
should.exist(err);
err.code.should.equal('NOT_AUTHORIZED');
err.message.toLowerCase().should.contain('session');
should.not.exist(server2);
server.login({}, function(err, token) {
should.not.exist(err);
should.exist(token);
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: token,
}, function(err, server2) {
should.not.exist(err);
should.exist(server2);
server2.copayerId.should.equal(server.copayerId);
server2.walletId.should.equal(server.walletId);
done();
});
});
});
});
it('should get the same session token for two requests in a row', function(done) {
server.login({}, function(err, token) {
should.not.exist(err);
should.exist(token);
server.login({}, function(err, token2) {
should.not.exist(err);
token2.should.equal(token);
done();
});
});
});
it('should create a new session if the previous one has expired', function(done) {
var timer = sinon.useFakeTimers({ toFake: ['Date'] });
var token;
async.series([
function(next) {
server.login({}, function(err, t) {
should.not.exist(err);
should.exist(t);
token = t;
next();
});
},
function(next) {
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: token,
}, function(err, server2) {
should.not.exist(err);
should.exist(server2);
next();
});
},
function(next) {
timer.tick((Defaults.SESSION_EXPIRATION + 1) * 1000);
next();
},
function(next) {
server.login({}, function(err, t) {
should.not.exist(err);
t.should.not.equal(token);
next();
});
},
function(next) {
WalletService.getInstanceWithAuth({
copayerId: server.copayerId,
session: token,
}, function(err, server2) {
should.exist(err);
err.code.should.equal('NOT_AUTHORIZED');
err.message.should.contain('expired');
next();
});
},
], function(err) {
should.not.exist(err);
timer.restore();
done();
});
});
});
describe('#createWallet', function() {
var server;
beforeEach(function() {
server = new WalletService();
});
it('should create and store wallet', function(done) {
var opts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
server.storage.fetchWallet(walletId, function(err, wallet) {
should.not.exist(err);
wallet.id.should.equal(walletId);
wallet.name.should.equal('my wallet');
done();
});
});
});
it('should create wallet with given id', function(done) {
var opts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
id: '1234',
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
server.storage.fetchWallet('1234', function(err, wallet) {
should.not.exist(err);
wallet.id.should.equal(walletId);
wallet.name.should.equal('my wallet');
done();
});
});
});
it('should fail to create wallets with same id', function(done) {
var opts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
id: '1234',
};
server.createWallet(opts, function(err, walletId) {
server.createWallet(opts, function(err, walletId) {
err.message.should.contain('Wallet already exists');
done();
});
});
});
it('should create wallet BCH if n > 1 and BWC version is 8.3.0 or higher', function(done) {
var opts = {
coin: 'bch',
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub
};
server.clientVersion = 'bwc-8.3.0';
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
should.exist(walletId);
done();
});
});
it('should create wallet BTC if n > 1 and BWC version is lower than 8.3.0', function(done) {
var opts = {
coin: 'btc',
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub
};
server.clientVersion = 'bwc-8.3.0';
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
should.exist(walletId);
done();
});
});
it('should fail to create wallets BCH if n > 1 and BWC version is lower than 8.3.0', function(done) {
var opts = {
coin: 'bch',
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub
};
server.clientVersion = 'bwc-8.2.0';
server.createWallet(opts, function(err, walletId) {
should.not.exist(walletId);
should.exist(err);
err.message.should.contain('BWC clients < 8.3 are no longer supported for multisig BCH wallets.');
done();
});
});
it('should create wallet BCH if n == 1 and BWC version is lower than 8.3.0', function(done) {
var opts = {
coin: 'bch',
name: 'my wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub
};
server.clientVersion = 'bwc-8.2.0';
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
should.exist(walletId);
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,
},];
async.eachSeries(pairs, function(pair, cb) {
let opts = {
name: 'my wallet',
pubKey: TestData.keyPair.pub,
};
var pub = (new Bitcore.PrivateKey()).toPublicKey();
opts.m = pair.m;
opts.n = pair.n;
opts.pubKey = pub.toString();
server.createWallet(opts, function(err) {
if(!pair.valid) {
should.exist(err);
err.message.should.equal('Invalid combination of required copayers / total copayers');
} else {
if(err) console.log("ERROR", opts, err);
should.not.exist(err);
}
return cb();
});
}, function(err) {
done();
});
});
it('should fail to create wallet with invalid pubKey argument', function(done) {
var opts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: 'dummy',
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(walletId);
should.exist(err);
err.message.should.contain('Invalid public key');
done();
});
});
it('should create wallet for another coin', function(done) {
var opts = {
coin: 'bch',
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
server.storage.fetchWallet(walletId, function(err, wallet) {
should.not.exist(err);
wallet.coin.should.equal('bch');
done();
});
});
});
it('should create a P2WPKH Segwit wallet', function(done) {
var opts = {
coin: 'btc',
name: 'my segwit wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub,
useNativeSegwit: true
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
server.storage.fetchWallet(walletId, function(err, wallet) {
should.not.exist(err);
wallet.addressType.should.equal('P2WPKH');
wallet.coin.should.equal('btc');
done();
});
});
});
it('should create a P2WSH Segwit wallet', function(done) {
var opts = {
coin: 'btc',
name: 'my multisig segwit wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub,
useNativeSegwit: true
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
server.storage.fetchWallet(walletId, function(err, wallet) {
should.not.exist(err);
wallet.addressType.should.equal('P2WSH');
wallet.coin.should.equal('btc');
done();
});
});
});
['eth','xrp'].forEach(c => {
it(`should fail to create a multisig ${c} wallet`, function(done) {
var opts = {
coin: c,
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
};
server.createWallet(opts, function(err, walletId) {
should.exist(err);
err.message.should.contain('not supported');
done();
});
});
it(`should create ${c} wallet with singleAddress flag`, function(done) {
helpers.createAndJoinWallet(1, 1, { coin: c }, function(s, wallet) {
wallet.singleAddress.should.equal(true);
done();
});
});
});
describe('Address derivation strategy', function() {
var server;
beforeEach(function() {
server = WalletService.getInstance();
});
it('should use BIP44 & P2PKH for 1-of-1 wallet if supported', function(done) {
var walletOpts = {
name: 'my wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub,
};
server.createWallet(walletOpts, function(err, wid) {
should.not.exist(err);
server.storage.fetchWallet(wid, function(err, wallet) {
should.not.exist(err);
wallet.derivationStrategy.should.equal('BIP44');
wallet.addressType.should.equal('P2PKH');
done();
});
});
});
it('should use BIP44 & P2SH for shared wallet if supported', function(done) {
var walletOpts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
};
server.createWallet(walletOpts, function(err, wid) {
should.not.exist(err);
server.storage.fetchWallet(wid, function(err, wallet) {
should.not.exist(err);
wallet.derivationStrategy.should.equal('BIP44');
wallet.addressType.should.equal('P2SH');
done();
});
});
});
});
});
describe('#joinWallet', function() {
describe('New clients', function() {
var server, serverForBch, walletId, walletIdForBch;
beforeEach(function(done) {
server = new WalletService();
var walletOpts = {
name: 'my wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub,
clientVersion: 'bwc-8.3.0'
};
server.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
walletId = wId;
should.exist(walletId);
done();
});
});
it('should join existing wallet', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data',
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
var copayerId = result.copayerId;
helpers.getAuthServer(copayerId, function(server) {
server.getWallet({}, function(err, wallet) {
wallet.id.should.equal(walletId);
wallet.copayers.length.should.equal(1);
var copayer = wallet.copayers[0];
copayer.name.should.equal('me');
copayer.id.should.equal(copayerId);
copayer.customData.should.equal('dummy custom data');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = _.find(notifications, {
type: 'NewCopayer'
});
should.exist(notif);
notif.data.walletId.should.equal(walletId);
notif.data.copayerId.should.equal(copayerId);
notif.data.copayerName.should.equal('me');
notif = _.find(notifications, {
type: 'WalletComplete'
});
should.not.exist(notif);
done();
});
});
});
});
});
it('should join existing wallet, getStatus + v8', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data',
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
var copayerId = result.copayerId;
helpers.getAuthServer(copayerId, function(server) {
server.getStatus({
includeExtendedInfo: true
}, function(err, status) {
should.not.exist(err);
status.wallet.m.should.equal(1);
status.wallet.beRegistered.should.equal(false);
status.balance.totalAmount.should.equal(0);
status.balance.availableAmount.should.equal(0);
done();
});
});
});
});
it('should join wallet BTC if BWC version is lower than 8.3.0', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
coin: 'btc',
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
walletId: walletId,
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data',
});
server.clientVersion = 'bwc-8.2.0';
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
should.exist(result);
should.exist(result.copayerId);
done();
});
});
it('should fail join existing wallet with bad xpub', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: 'Ttub4pHUfyVU2mpjaM6YDGDJXWP6j5SL5AJzbViBuTaJEsybcrWZZoGkW7RSUSH9VRQKJtjqY2LfC2bF3FM4UqC1Ba9EP5M64SdTsv9575VAUwh',
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data',
});
server.joinWallet(copayerOpts, function(err, result) {
err.message.should.match(/Invalid extended public key/);
done();
});
});
it('should fail join existing wallet with wrong network xpub', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: 'tpubD6NzVbkrYhZ4Wbwwqah5kj1RGPK9BYeGbowB1jegxMoAkKbNhYUAcRTZ5fyxDcpjNXxziiy2ZkUQ3kR1ycPNycTD7Q2Dr6UfLcNTYHrzS3U',
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data',
});
server.joinWallet(copayerOpts, function(err, result) {
err.message.should.match(/different network/);
done();
});
});
it('should fail to join with no name', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: '',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(result);
should.exist(err);
err.message.should.contain('name');
done();
});
});
it('should fail to join non-existent wallet', function(done) {
var copayerOpts = {
walletId: '123',
name: 'me',
xPubKey: 'dummy',
requestPubKey: 'dummy',
copayerSignature: 'dummy',
};
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
done();
});
});
it('should fail to join full wallet', function(done) {
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: wallet.id,
name: 'me',
xPubKey: TestData.copayers[1].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[1].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.code.should.equal('WALLET_FULL');
err.message.should.equal('Wallet full');
done();
});
});
});
it('should fail to join wallet for different coin', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
coin: 'bch',
});
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.message.should.contain('different coin');
done();
});
});
it('should return copayer in wallet error before full wallet', function(done) {
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: wallet.id,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.code.should.equal('COPAYER_IN_WALLET');
done();
});
});
});
it('should fail to re-join wallet', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err) {
should.not.exist(err);
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.code.should.equal('COPAYER_IN_WALLET');
err.message.should.equal('Copayer already in wallet');
done();
});
});
});
it('should be able to get wallet info without actually joining', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data',
dryRun: true,
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
should.exist(result);
should.not.exist(result.copayerId);
result.wallet.id.should.equal(walletId);
result.wallet.m.should.equal(1);
result.wallet.n.should.equal(2);
result.wallet.copayers.should.be.empty;
server.storage.fetchWallet(walletId, function(err, wallet) {
should.not.exist(err);
wallet.id.should.equal(walletId);
wallet.copayers.should.be.empty;
done();
});
});
});
it('should fail to join two wallets with same xPubKey', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err) {
should.not.exist(err);
var walletOpts = {
name: 'my other wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub,
};
server.createWallet(walletOpts, function(err, walletId) {
should.not.exist(err);
copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.code.should.equal('COPAYER_REGISTERED');
err.message.should.equal('Copayer ID already registered on server');
done();
});
});
});
});
it('should fail to join with bad formated signature', function(done) {
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
copayerSignature: 'bad sign',
};
server.joinWallet(copayerOpts, function(err) {
err.message.should.equal('Bad request');
done();
});
});
it('should fail to join with invalid xPubKey', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'copayer 1',
xPubKey: 'invalid',
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(result);
should.exist(err);
err.message.should.contain('extended public key');
done();
});
});
it('should fail to join with null signature', function(done) {
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
};
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.message.should.contain('argument: copayerSignature missing');
done();
});
});
it('should fail to join with wrong signature', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
copayerOpts.name = 'me2';
server.joinWallet(copayerOpts, function(err) {
err.message.should.equal('Bad request');
done();
});
});
it('should set pkr and status = complete on last copayer joining (2-3)', function(done) {
helpers.createAndJoinWallet(2, 3, function(server) {
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.status.should.equal('complete');
wallet.publicKeyRing.length.should.equal(3);
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = _.find(notifications, {
type: 'WalletComplete'
});
should.exist(notif);
notif.data.walletId.should.equal(wallet.id);
done();
});
});
});
});
it('should not notify WalletComplete if 1-of-1', function(done) {
helpers.createAndJoinWallet(1, 1, function(server) {
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = _.find(notifications, {
type: 'WalletComplete'
});
should.not.exist(notif);
done();
});
});
});
});
describe('New clients 2', function() {
var server, serverForBch, walletId, walletIdForBch;
it('should join wallet BCH if BWC version is 8.3.0 or higher', function(done) {
serverForBch = new WalletService();
var walletOpts = {
coin: 'bch',
name: 'my wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub
};
serverForBch.clientVersion = 'bwc-8.3.4';
serverForBch.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
walletIdForBch = wId;
should.exist(walletIdForBch);
var copayerOpts = helpers.getSignedCopayerOpts({
coin: 'bch',
walletId: walletIdForBch,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data'
});
serverForBch.clientVersion = 'bwc-8.3.0';
serverForBch.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
should.exist(result);
should.exist(result.copayerId);
done();
});
});
});
it('should fail to join BIP48 wallets from old clients ', function(done) {
serverForBch = new WalletService();
var walletOpts = {
coin: 'bch',
name: 'my wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub,
walletId: walletId,
usePurpose48: true,
};
serverForBch.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
walletIdForBch = wId;
should.exist(walletIdForBch);
var copayerOpts = helpers.getSignedCopayerOpts({
coin: 'bch',
walletId: walletIdForBch,
name: 'me',
m: 2,
n: 3,
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0
});
serverForBch.clientVersion = 'bwc-8.3.0';
serverForBch.joinWallet(copayerOpts, function(err, result) {
should.not.exist(result);
should.exist(err);
err.message.should.contain('upgrade');
done();
});
});
});
it('should join BIP48 wallets from new clients ', function(done) {
serverForBch = new WalletService();
var walletOpts = {
coin: 'bch',
name: 'my wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub,
walletId: walletId,
usePurpose48: true,
};
serverForBch.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
walletIdForBch = wId;
should.exist(walletIdForBch);
var copayerOpts = helpers.getSignedCopayerOpts({
coin: 'bch',
walletId: walletIdForBch,
name: 'me',
m: 2,
n: 3,
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0
});
serverForBch.clientVersion = 'bwc-8.7.0';
serverForBch.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
should.exist(result);
done();
});
});
});
});
});
describe('#removeWallet', function() {
var server, wallet, clock;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.1e8,
}],
feePerKb: 100e2,
};
async.eachSeries(_.range(2), function(i, next) {
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function() {
next();
});
}, done);
});
});
});
it('should delete a wallet', function(done) {
server.removeWallet({}, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, w) {
should.exist(err);
err.code.should.equal('WALLET_NOT_FOUND');
should.not.exist(w);
async.parallel([
function(next) {
server.storage.fetchAddresses(wallet.id, function(err, items) {
items.length.should.equal(0);
next();
});
},
function(next) {
server.storage.fetchTxs(wallet.id, {}, function(err, items) {
items.length.should.equal(0);
next();
});
},
function(next) {
server.storage.fetchNotifications(wallet.id, null, 0, function(err, items) {
items.length.should.equal(0);
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
});
});
// creates 2 wallet, and deletes only 1.
it('should delete a wallet, and only that wallet', function(done) {
var server2, wallet2;
async.series([
function(next) {
helpers.createAndJoinWallet(1, 1, {
offset: 1
}, function(s, w) {
server2 = s;
wallet2 = w;
helpers.stubUtxos(server2, wallet2, [1, 2, 3], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.1e8,
}],
feePerKb: 100e2,
};
async.eachSeries(_.range(2), function(i, next) {
helpers.createAndPublishTx(server2, txOpts, TestData.copayers[1].privKey_1H_0, function() {
next();
});
}, next);
});
});
},
function(next) {
server.removeWallet({}, next);
},
function(next) {
server.getWallet({}, function(err, wallet) {
should.exist(err);
err.code.should.equal('WALLET_NOT_FOUND');
next();
});
},
function(next) {
server2.getWallet({}, function(err, wallet) {
should.not.exist(err);
should.exist(wallet);
wallet.id.should.equal(wallet2.id);
next();
});
},
function(next) {
server2.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
should.exist(addresses);
addresses.length.should.above(0);
next();
});
},
function(next) {
server2.getTxs({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(2);
next();
});
},
function(next) {
server2.getNotifications({}, function(err, notifications) {
should.not.exist(err);
should.exist(notifications);
notifications.length.should.above(0);
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
});
describe('#getStatus', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should get status', function(done) {
server.getStatus({}, function(err, status) {
should.not.exist(err);
should.exist(status);
should.exist(status.wallet);
status.wallet.name.should.equal(wallet.name);
should.exist(status.wallet.copayers);
status.wallet.copayers.length.should.equal(2);
should.exist(status.balance);
status.balance.totalAmount.should.equal(0);
should.exist(status.preferences);
should.exist(status.pendingTxps);
status.pendingTxps.should.be.empty;
should.not.exist(status.wallet.publicKeyRing);
should.not.exist(status.wallet.pubKey);
should.not.exist(status.wallet.addressManager);
_.each(status.wallet.copayers, function(copayer) {
should.not.exist(copayer.xPubKey);
should.not.exist(copayer.requestPubKey);
should.not.exist(copayer.signature);
should.not.exist(copayer.requestPubKey);
should.not.exist(copayer.addressManager);
should.not.exist(copayer.customData);
});
done();
});
});
it('should get status including extended info', function(done) {
server.getStatus({
includeExtendedInfo: true
}, function(err, status) {
should.not.exist(err);
should.exist(status);
should.exist(status.wallet.publicKeyRing);
should.exist(status.wallet.pubKey);
should.exist(status.wallet.addressManager);
should.exist(status.wallet.copayers[0].xPubKey);
should.exist(status.wallet.copayers[0].requestPubKey);
should.exist(status.wallet.copayers[0].signature);
should.exist(status.wallet.copayers[0].requestPubKey);
should.exist(status.wallet.copayers[0].customData);
// Do not return other copayer's custom data
_.each(_.tail(status.wallet.copayers), function(copayer) {
should.not.exist(copayer.customData);
});
done();
});
});
it('should get status including extended info with tokens', function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'eth' }, function(s, w) {
s.savePreferences({
email: 'dummy@dummy.com',
tokenAddresses: TOKENS,
}, function(err) {
should.not.exist(err);
s.getStatus({
includeExtendedInfo: true
}, function(err, status) {
should.not.exist(err);
should.exist(status);
status.preferences.tokenAddresses.should.deep.equal(TOKENS);
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].satoshis);
balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount);
done();
});
});
});
});
it('should get status including server messages', function(done) {
server.appName = 'bitpay';
server.appVersion = { major: 5, minor: 0, patch: 0 };
server.getStatus({
includeServerMessages: true
}, function(err, status) {
should.not.exist(err);
should.exist(status);
should.exist(status.serverMessages);
_.isArray(status.serverMessages).should.be.true;
status.serverMessages.should.deep.equal([{
title: 'Test message 2',
body: 'Only for bitpay livenet wallets',
link: 'http://bitpay.com',
id: 'bitpay2',
dismissible: true,
category: 'critical',
app: 'bitpay',
priority: 1
}]);
done();
});
});
it('should get status including deprecated server message', function(done) {
server.appName = 'bitpay';
server.appVersion = { major: 5, minor: 0, patch: 0 };
server.getStatus({}, function(err, status) {
should.not.exist(err);
should.exist(status);
should.exist(status.serverMessage);
_.isObject(status.serverMessage).should.be.true;
status.serverMessage.should.deep.equal({
title: 'Deprecated Test message',
body: 'Only for bitpay, old wallets',
link: 'http://bitpay.com',
id: 'bitpay1',
dismissible: true,
category: 'critical',
app: 'bitpay',
});
done();
});
});
});
describe('#verifyMessageSignature', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should successfully verify message signature', function(done) {
var message = 'hello world';
var opts = {
message: message,
signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0),
};
server.verifyMessageSignature(opts, function(err, isValid) {
should.not.exist(err);
isValid.should.be.true;
done();
});
});
it('should fail to verify message signature for different copayer', function(done) {
var message = 'hello world';
var opts = {
message: message,
signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0),
};
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.verifyMessageSignature(opts, function(err, isValid) {
should.not.exist(err);
isValid.should.be.false;
done();
});
});
});
});
describe('#createAddress', function() {
var server, wallet;
describe('shared wallets (BIP44)', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should create address ', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.id);
address.network.should.equal('livenet');
address.address.should.equal('36q2G5FMGvJbPgAVEaiyAsFGmpkhPKwk2r');
address.isChange.should.be.false;
address.coin.should.equal('btc');
address.path.should.equal('m/0/0');
address.type.should.equal('P2SH');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = _.find(notifications, {
type: 'NewAddress'
});
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
it('should create next address if insertion fail ', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
server.getWallet({}, (err, w) => {
var old = server.getWallet;
server.getWallet = sinon.stub();
// return main address index to 0;
w.addressManager.receiveAddressIndex = 0;
server.getWallet.callsArgWith(1, null, w);
server.createAddress({}, function(err, address) {
server.getWallet = old;
should.not.exist(err);
should.exist(address);
done();
});
});
});
});
it('should create many addresses on simultaneous requests', function(done) {
var N = 5;
async.mapSeries(_.range(N), function(i, cb) {
server.createAddress({}, cb);
}, function(err, addresses) {
addresses.length.should.equal(N);
_.each(_.range(N), function(i) {
addresses[i].path.should.equal('m/0/' + i);
});
// No two identical addresses
_.uniq(_.map(addresses, 'address')).length.should.equal(N);
done();
});
});
it('should not create address if unable to store it', function(done) {
sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error');
server.createAddress({}, function(err, address) {
should.exist(err);
should.not.exist(address);
server.getMainAddresses({}, function(err, addresses) {
addresses.length.should.equal(0);
server.storage.storeAddressAndWallet.restore();
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
done();
});
});
});
});
});
describe('shared wallets (BIP44/BCH)', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 2, {
coin: 'bch'
}, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should create address', function(done) {