@abcpros/bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
1,621 lines (1,478 loc) • 369 kB
JavaScript
'use strict';
const _ = require('lodash');
const async = require('async');
const chai = require('chai');
const sinon = require('sinon');
const CWC = require('@abcpros/crypto-wallet-core');
const LOG_LEVEL = 'info';
//const LOG_LEVEL = 'debug';
const should = chai.should();
const { logger, transport } = require('../../ts_build/lib/logger.js');
const { ChainService } = require('../../ts_build/lib/chain/index');
var config = require('../../ts_build/config.js');
const Bitcore = require('@abcpros/bitcore-lib');
const Bitcore_ = {
btc: Bitcore,
bch: require('@abcpros/bitcore-lib-cash'),
xec: require('@abcpros/bitcore-lib-xec'),
eth: Bitcore,
xrp: Bitcore,
doge: require('@abcpros/bitcore-lib-doge'),
xpi: require('@abcpros/bitcore-lib-xpi'),
ltc: require('@abcpros/bitcore-lib-ltc')
};
const { WalletService } = require('../../ts_build/lib/server');
const { Storage } = require('../../ts_build/lib/storage')
const Common = require('../../ts_build/lib/common');
const Utils = Common.Utils;
const Constants = Common.Constants;
const Defaults = Common.Defaults;
const VanillaDefaults = _.cloneDeep(Defaults);
const Model = require('../../ts_build/lib/model');
const 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,
'doge': 1e8,
'ltc': 1e8
};
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) {
transport.level= LOG_LEVEL;
config.suspendedChains = [];
// restore defaults, cp values
_.each(_.keys(VanillaDefaults), (x) => {
Defaults[x] = VanillaDefaults[x];
});
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();
});
});
});
});
it('should get server instance for marketing staff', function(done) {
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
var collections = Storage.collections;
s.storage.db.collection(collections.COPAYERS_LOOKUP).updateOne({
copayerId: wallet.copayers[0].id
}, {
$set: {
isMarketingStaff: 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);
server.walletId.should.not.equal('123');
server.copayerIsMarketingStaff.should.equal(true);
server.copayerId.should.equal(wallet.copayers[0].id);
done();
});
});
});
});
});
// tests for adding and retrieving adds from db
describe('Creating ads, retrieve ads, active/inactive', function(done) {
var server, wallet, adOpts;
adOpts = {
advertisementId: '123',
name: 'name',
title:'title',
body: 'body',
country: 'US',
type: 'standard',
linkText: 'linkText',
linkUrl: 'linkUrl',
dismissible: true,
isAdActive: false,
isTesting: true,
signature: '304050302480413401348a3b34902403434512535e435463',
app: 'bitpay'
};
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should create/get ad', function(done) {
async.series([function(next) {
server.createAdvert(adOpts, function (err, ad) {
should.not.exist(err);
next();
});
}, function(next) {
server.getAdvert({adId: '123'}, function (err, ad) {
should.not.exist(err);
should.exist(ad);
ad.advertisementId.should.equal('123');
ad.name.should.equal('name');
ad.title.should.equal('title');
ad.body.should.equal('body');
ad.country.should.equal('US');
ad.type.should.equal('standard');
ad.linkText.should.equal('linkText');
ad.linkUrl.should.equal('linkUrl');
ad.dismissible.should.equal(true);
ad.isAdActive.should.equal(false);
ad.isTesting.should.equal(true);
ad.signature.should.equal('304050302480413401348a3b34902403434512535e435463'),
ad.app.should.equal('bitpay');
next();
});
}], function(err) {
should.not.exist(err);
done();
})
});
it('should create/get/delete an ad', function(done) {
async.series([function(next) {
server.createAdvert(adOpts, function (err, ad) {
next();
});
}, function(next) {
server.getAdvert({adId: '123'}, function (err, ad) {
should.not.exist(err);
should.exist(ad);
next();
});
},
server.removeAdvert({adId: '123'}, function(err, nextArg) {
should.not.exist(err);
})
], function(err) {
should.not.exist(err);
})
done();
});
it('should create ad initially inactive, retrieve, make active, retrieve again', function(done) {
async.series([function(next) {
server.createAdvert(adOpts, function (err, ad) {
next();
});
}, function(next) {
server.getAdvert({adId: '123'}, function(err, ad) {
should.not.exist(err);
should.exist(ad);
ad.advertisementId.should.equal('123');
ad.isAdActive.should.equal(false);
ad.isTesting.should.equal(true);
});
next();
}, function(next) {
server.activateAdvert({adId: '123'}, function (err, ad) {
should.not.exist(err);
next();
});
}, function(next) {
server.getAdvert({adId: '123'}, function (err, ad) {
should.not.exist(err);
should.exist(ad);
ad.advertisementId.should.equal('123');
ad.isAdActive.should.equal(true);
ad.isTesting.should.equal(false);
});
next();
}], function(err) {
should.not.exist(err);
});
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('New clients 3', function() {
var server, walletId, walletIdForSegwit;
it('should join wallet segwit if BWC version is 8.17.0 or higher', function(done) {
server = new WalletService();
var walletOpts = {
coin: 'btc',
name: 'my wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub,
useNativeSegwit: true
};
server.clientVersion = 'bwc-8.17.0';
server.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
walletIdForSegwit = wId;
should.exist(walletIdForSegwit);
var copayerOpts = helpers.getSignedCopayerOpts({
coin: 'btc',
walletId: walletIdForSegwit,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
customData: 'dummy custom data'
});
server.clientVersion = 'bwc-8.17.0';
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
should.exist(result);
should.exist(result.copayerId);
result.wallet.addressType.should.equal('P2WSH');
done();
});
});
});
it('should fail to join segwit wallets from old clients ', function(done) {
server = new WalletService();
var walletOpts = {
coin: 'btc',
name: 'my wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub,
walletId: walletId,
useNativeSegwit: true
};
server.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
walletIdForSegwit = wId;
should.exist(walletIdForSegwit);
var copayerOpts = helpers.getSignedCopayerOpts({
coin: 'btc',
walletId: walletIdForSegwit,
name: 'me',
m: 2,
n: 3,
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0
});
server.clientVersion = 'bwc-8.4.0';
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(result);
should.exist(err);
err.message.should.contain('upgrade');
done();
});
});
});
it('should join segwit wallets from new clients', function(done) {
server = new WalletService();
var walletOpts = {
coin: 'btc',
name: 'my wallet',
m: 1,
n: 2,
pubKey: TestData.keyPair.pub,
walletId: walletId,
useNativeSegwit: true,
};
server.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
walletIdForSegwit = wId;
should.exist(walletIdForSegwit);
var copayerOpts = helpers.getSignedCopayerOpts({
coin: 'btc',
walletId: walletIdForSegwit,
name: 'me',
m: 2,
n: 3,
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0
});
server.clientVersion = 'bwc-9.0.0';
server.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.requestPubK