bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
11,559 lines • 394 kB
JavaScript
'use strict';
const _ = require('lodash');
const async = require('async');
const chai = require('chai');
const sinon = require('sinon');
const CWC = require('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').default;
config.moralis = config.moralis ?? {
apiKey: 'apiKey',
whitelist: []
};
const Bitcore = require('bitcore-lib');
const Bitcore_ = {
btc: Bitcore,
bch: require('bitcore-lib-cash'),
eth: Bitcore,
xrp: Bitcore,
doge: require('bitcore-lib-doge'),
ltc: require('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,
'sol': 1e9
};
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
for (const x of Object.keys(VanillaDefaults)) {
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();
});
});
});
it('should create a P2TR Taproot wallet', function(done) {
var opts = {
coin: 'btc',
name: 'my multisig segwit wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub,
useNativeSegwit: true,
segwitVersion: 1 // taproot
};
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('P2TR');
wallet.coin.should.equal('btc');
done();
});
});
});
for (const c of ['eth','xrp','matic','arb','base','op','sol']) {
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();
});
});
it(`should create, store, and fetch ${c} wallet`, function(done) {
var opts = {
coin: c,
name: 'my wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
should.exist(walletId);
server.storage.fetchWallet(walletId, function(err, wallet) {
should.not.exist(err);
wallet.id.should.equal(walletId);
wallet.name.should.equal('my wallet');
wallet.chain.should.equal(c);
wallet.coin.should.equal(c);
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 = notifications.find(n => n.type === 'NewCopayer');
should.exist(notif);
notif.data.walletId.should.equal(walletId);
notif.data.copayerId.should.equal(copayerId);
notif.data.copayerName.should.equal('me');
notif = notifications.find(n => n.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 chain');
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 = notifications.find(n => n.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 = notifications.find(n => n.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(new Array(2).fill(0), 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(new Array(2).fill(0), 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);
for (const copayer of status.wallet.copayers) {
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
for (const copayer of status.wallet.copayers.slice(1)) {
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);
Array.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);
(typeof status.serverMessage === 'object').should.be.true;
(Array.isArray(status.serverMessage)).should.be.false;
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 = notifications.find(n => n.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(new Array(N).fill(0), function(i, cb) {
server.createAddress({}, cb);
}, function(err, addresses) {
addresses.length.should.equal(N);
for (var i = 0; i < N; i++) {
addresses[i].path.should.equal('m/0/' + i);
}
// No two identical addresses
_.uniq(addresses.map(m => m.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) {
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('pqu9c0xe7g0ngz9hzpky64nva9790m64esxxjmcv2k');
address.isChange.should.be.false;
address.path.should.equal('m/0/0');
address.type.should.equal('P2SH');
address.coin.should.equal('bch');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
it('should create many addresses on simultaneous requests', function(done) {
var N = 5;
async.mapSeries(new Array(N).fill(0), function(i, cb) {
server.createAddress({}, cb);
}, function(err, addresses) {
addresses.length.should.equal(N);
for (let i = 0; i < N; i++) {
addresses[i].path.should.equal('m/0/' + i);
}
// No two identical addresses
_.uniq(addresses.map(a => a.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) {
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('pqu9c0xe7g0ngz9hzpky64nva9790m64esxxjmcv2k');
address.isChange.should.be.false;
address.path.should.equal('m/0/0');
address.type.should.equal('P2SH');
address.coin.should.equal('bch');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
it('should create many addresses on simultaneous requests', function(done) {
var N = 5;
async.mapSeries(new Array(N).fill(0), function(i, cb) {
server.createAddress({}, cb);
}, function(err, addresses) {
addresses.length.should.equal(N);
for (let i = 0; i < N; i++) {
addresses[i].path.should.equal('m/0/' + i);
}
// No two identical addresses
_.uniq(addresses.map(a => a.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('1-1 wallet (BIP44/BCH)', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, 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('qrg04mz8h67j9dck3f3f3sa560taep87yqnwra9ak6');
address.isChange.should.be.false;
address.path.should.equal('m/0/0');
address.type.should.equal('P2PKH');
address.coin.should.equal('bch');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
it('should create address (no cash addr)', function(done) {
server.createAddress({ noCashAddr: true }, 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('CbWsiNjh18ynQYc5jfYhhespEGrAaW8YUq');
address.isChange.should.be.false;
address.path.should.equal('m/0/0');
address.type.should.equal('P2PKH');
address.coin.should.equal('bch');
// notified address is Copay format
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
// stored address should be new format
server.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
addresses[0].address.should.equal('qrg04mz8h67j9dck3f3f3sa560taep87yqnwra9ak6');
done();
});
});
});
});
});
describe('1-1 wallet (BIP44/BCH/Testnet)', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
network: 'testnet4',
}, 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('testnet');
address.address.should.equal('qpmvku3x8j9pz7mee89c590xsl3k5l02mqeyfhf3ce');
address.isChange.should.be.false;
address.path.should.equal('m/0/0');
address.type.should.equal('P2PKH');
address.coin.should.equal('bch');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
});
describe('1-of-1 (BIP44 & P2PKH)', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44btc);
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('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG');
address.isChange.should.be.false;
address.path.should.equal('m/0/0');
address.type.should.equal('P2PKH');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
it('should create many addresses on simultaneous requests', function(done) {
var N = 5;
async.mapSeries(new Array(N).fill(0), function(i, cb) {
server.createAddress({}, cb);
}, function(err, addresses) {
addresses = addresses.sort((a, b) => a.path - b.path);
addresses.length.should.equal(N);
for (let i = 0; i < N; i++) {
addresses[i].path.should.equal('m/0/' + i);
}
// No two identical addresses
_.uniq(addresses.map(a => a.address)).length.should.equal(N);
done();
});
});
it('should fail to create more consecutive addresses with no activity than allowed', function(done) {
var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP;
Defaults.MAX_MAIN_ADDRESS_GAP = 2;
helpers.stubAddressActivity([]);
async.map(new Array(2).fill(0), function(i, next) {
server.createAddress({}, next);
}, function(err, addresses) {
addresses.length.should.equal(2);
server.createAddress({}, function(err, address) {
should.exist(err);
should.not.exist(address);
err.code.should.equal('MAIN_ADDRESS_GAP_REACHED');
server.createAddress({
ignoreMaxGap: true
}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.path.should.equal('m/0/2');
helpers.stubAddressActivity([
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
]);
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.path.should.equal('m/0/3');
Defaults.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old;
done();
});
});
});
});
});
it('should cache address activity', function(done) {
var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP;
Defaults.MAX_MAIN_ADDRESS_GAP = 2;
helpers.stubAddressActivity([]);
async.mapSeries(new Array(2).fill(0), function(i, next) {
server.createAddress({}, next);
}, function(err, addresses) {
addresses.length.should.equal(2);
helpers.stubAddressActivity([addresses[1].address]);
var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity');
server.createAddress({}, function(err, address) {
should.not.exist(err);
server.createAddress({}, function(err, address) {
should.not.exist(err);
getAddressActivitySpy.callCount.should.equal(1);
Defaults.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old;
done();
});
});
});
});
});
describe('ETH', function() {
var server, wallet;
describe('BIP44 livenet', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'eth' }, 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('0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622');
address.isChange.should.be.false;
address.coin.should.equal('eth');
address.path.should.equal('m/0/0');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
it('should not create new addresses ', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.walletId.should.equal(wallet.id);
address.network.should.equal('livenet');
address.address.should.equal('0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622');
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.id);
address.network.should.equal('livenet');
address.path.should.equal('m/0/0');
address.address.should.equal('0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622');
address.isChange.should.be.false;
address.coin.should.equal('eth');
done();
});
});
});
});
describe('BIP44 testnet (with storage transformation)', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'eth', network: 'sepolia' }, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should create addresses', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.walletId.should.equal(wallet.id);
address.path.should.equal('m/0/0');
address.network.should.equal('sepolia');
address.address.should.equal('0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622');
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.id);
address.network.should.equal('sepolia');
address.path.should.equal('m/0/0');
address.address.should.equal('0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622');
address.isChange.should.be.false;
address.coin.should.equal('eth');
// main addresses should transfrom addresses
server.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
addresses[0].address.should.equal('0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622');
done();
});
});
});
});
it('should sync addresses with transformed strings', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.walletId.should.equal(wallet.id);
address.path.should.equal('m/0/0');
address.network.should.equal('sepolia');
address.address.should.equal('0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622');
server.syncWallet(wallet, function(err) {
should.not.exist(err);
var calls = blockchainExplorer.addAddresses.getCalls();
calls[0].args[1].should.deep.equal(['0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622']);
done();
});
});
});
});
});
describe('XRP', function() {
var server, wallet;
describe('BIP44 livenet', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'xrp' }, 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('rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG');
address.isChange.should.be.false;
address.coin.should.equal('xrp');
address.path.should.equal('m/0/0');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var notif = notifications.find(n => n.type === 'NewAddress');
should.exist(notif);
notif.data.address.should.equal(address.address);
done();
});
});
});
it('should not create new addresses ', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.walletId.should.equal(wallet.id);
address.network.should.equal('livenet');
address.address.should.equal('rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG');
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.id);
address.network.should.equal('livenet');
address.path.should.equal('m/0/0');
address.address.should.equal('rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG');
address.isChange.should.be.false;
address.coin.should.equal('xrp');
done();
});
});
});
});
describe('BIP44 testnet (with storage transformation)', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'xrp', network: 'testnet' }, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should create addresses', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.walletId.should.equal(wallet.id);
address.path.should.equal('m/0/0');
address.network.should.equal('testnet');
address.address.should.equal('rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG');
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.id);
address.network.should.equal('testnet');
address.path.should.equal('m/0/0');
address.address.should.equal('rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG');
address.isChange.should.be.false;
address.coin.should.equal('xrp');
// main addresses should transfrom addresses
server.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
addresses[0].address.should.equal('rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG');
done();
});
});
});
});
it('should sync addresses with transformed strings', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.walletId.should.equal(wallet.id);
address.path.should.equal('m/0/0');
address.network.should.equal('testnet');
address.address.should.equal('rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG');
server.syncWallet(wallet, function(err) {
should.not.exist(err);
var calls = blockchainExplorer.addAddresses.getCalls();
calls[0].args[1].should.deep.equal(['rLsz9LPd3arEWQ6CsvD839E8c9dkdBopUG']);
done();
});
});
});
});
});
});
describe('#getMainAddresses', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 2, {}, function(s, w) {
server = s;
wallet = w;
helpers.createAddresses(server, wallet, 5, 0, function() {
done();
});
});
});
it('should get all addresses', function(done) {
server.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(5);
addresses[0].path.should.equal('m/0/0');
addresses[4].path.should.equal('m/0/4');
done();
});
});
it('should get first N addresses', function(done) {
server.getMainAddresses({
limit: 3
}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(3);
addresses[0].path.should.equal('m/0/0');
addresses[2].path.should.equal('m/0/2');
done();
});
});
it('should get last N addresses in reverse order', function(done) {
server.getMainAddresses({
limit: 3,
reverse: true,
}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(3);
addresses[0].path.should.equal('m/0/4');
addresses[2].path.should.equal('m/0/2');
done();
});
});
});
describe('Preferences tokens', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'eth' }, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should save & retrieve preferences', function(done) {
server.savePreferences({
email: 'dummy@dummy.com',
language: 'es',
unit: 'bit',
dummy: 'ignored',
tokenAddresses: TOKENS,
}, function(err) {
should.not.exist(err);
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.exist(preferences);
preferences.email.should.equal('dummy@dummy.com');
preferences.language.should.equal('es');
preferences.unit.should.equal('bit');
preferences.tokenAddresses.should.deep.equal(TOKENS);
should.not.exist(preferences.dummy);
done();
});
});
});
it('should concatenate token preferences', function(done) {
server.savePreferences({
email: 'dummy@dummy.com',
language: 'es',
unit: 'bit',
dummy: 'ignored',
tokenAddresses: [TOKENS[0]],
}, function(err) {
should.not.exist(err);
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
preferences.tokenAddresses.should.deep.equal([TOKENS[0]]);
server.savePreferences({
email: 'dummy@dummy.com',
language: 'es',
unit: 'bit',
dummy: 'ignored',
tokenAddresses: [TOKENS[1]],
}, function(err) {
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.exist(preferences);
preferences.email.should.equal('dummy@dummy.com');
preferences.language.should.equal('es');
preferences.unit.should.equal('bit');
preferences.tokenAddresses.should.deep.equal(TOKENS);
should.not.exist(preferences.dummy);
done();
});
});
});
});
});
it('should concatenate token preferences (case2)', function(done) {
server.savePreferences({
email: 'dummy@dummy.com',
language: 'es',
unit: 'bit',
dummy: 'ignored',
tokenAddresses: [TOKENS[0]],
}, function(err) {
should.not.exist(err);
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
preferences.tokenAddresses.should.deep.equal([TOKENS[0]]);
server.savePreferences({
email: 'dummy@dummy.com',
language: 'es',
unit: 'bit',
dummy: 'ignored',
tokenAddresses: TOKENS,
}, function(err) {
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.exist(preferences);
preferences.email.should.equal('dummy@dummy.com');
preferences.language.should.equal('es');
preferences.unit.should.equal('bit');
preferences.tokenAddresses.should.deep.equal(TOKENS);
should.not.exist(preferences.dummy);
done();
});
});
});
});
});
});
describe('Preferences', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should save & retrieve preferences', function(done) {
server.savePreferences({
email: 'dummy@dummy.com',
language: 'es',
unit: 'bit',
dummy: 'ignored',
}, function(err) {
should.not.exist(err);
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.exist(preferences);
preferences.email.should.equal('dummy@dummy.com');
preferences.language.should.equal('es');
preferences.unit.should.equal('bit');
should.not.exist(preferences.dummy);
done();
});
});
});
it('should fail to save wrong preferences', function(done) {
server.savePreferences({
email: 'dummy@dummy.com',
tokenAddresses: ['hola'],
}, function(err) {
err.message.toString().should.contain('tokenAddresses');
done();
});
});
it('should save preferences only for requesting copayer', function(done) {
server.savePreferences({
email: 'dummy@dummy.com'
}, function(err) {
should.not.exist(err);
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
server2.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.not.exist(preferences.email);
done();
});
});
});
});
it('should save preferences incrementally', function(done) {
async.series([
function(next) {
server.savePreferences({
email: 'dummy@dummy.com',
}, next);
},
function(next) {
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.exist(preferences);
preferences.email.should.equal('dummy@dummy.com');
should.not.exist(preferences.language);
next();
});
},
function(next) {
server.savePreferences({
language: 'es',
}, next);
},
function(next) {
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.exist(preferences);
preferences.language.should.equal('es');
preferences.email.should.equal('dummy@dummy.com');
next();
});
},
function(next) {
server.savePreferences({
language: null,
unit: 'bit',
}, next);
},
function(next) {
server.getPreferences({}, function(err, preferences) {
should.not.exist(err);
should.exist(preferences);
preferences.unit.should.equal('bit');
should.not.exist(preferences.language);
preferences.email.should.equal('dummy@dummy.com');
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
it.skip('should save preferences only for requesting wallet', function(done) { });
it('should validate entries', function(done) {
var invalid = [{
preferences: {
email: ' ',
},
expected: 'email'
}, {
preferences: {
email: 'dummy@' + new Array(50).fill('domain').join(''),
},
expected: 'email'
}, {
preferences: {
language: 'xxxxx',
},
expected: 'language'
}, {
preferences: {
language: 123,
},
expected: 'language'
}, {
preferences: {
unit: 'xxxxx',
},
expected: 'unit'
},];
async.each(invalid, function(item, next) {
server.savePreferences(item.preferences, function(err) {
should.exist(err);
var regex = new RegExp(item.expected, "gm");
err.message.should.match(regex);
next();
});
}, done);
});
});
describe('#getUtxos', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should get UTXOs for wallet addresses', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
server.getUtxos({}, function(err, utxos) {
should.not.exist(err);
should.exist(utxos);
utxos.length.should.equal(2);
utxos.reduce((sum, u) => sum += u.satoshis, 0).should.equal(3 * 1e8);
server.getMainAddresses({}, function(err, addresses) {
var utxo = utxos[0];
var address = addresses.find(a => a.address === utxo.address);
should.exist(address);
utxo.path.should.equal(address.path);
utxo.publicKeys.should.deep.equal(address.publicKeys);
done();
});
});
});
});
it('should get UTXOs for wallet addresses', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
server.getUtxos({}, function(err, utxos) {
should.not.exist(err);
should.exist(utxos);
utxos.length.should.equal(2);
utxos.reduce((sum, u) => sum += u.satoshis, 0).should.equal(3 * 1e8);
server.getMainAddresses({}, function(err, addresses) {
var utxo = utxos[0];
var address = addresses.find(a => a.address === utxo.address);
should.exist(address);
utxo.path.should.equal(address.path);
utxo.publicKeys.should.deep.equal(address.publicKeys);
done();
});
});
});
});
it('should return empty UTXOs for specific addresses if network mismatch', function(done) {
helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) {
_.uniqBy(utxos, 'address').length.should.be.above(1);
server.getUtxos({
addresses: ['mrM5kMkqZccK5MxZYSsM3SjqdMaNKLJgrJ']
}, function(err, utxos) {
should.not.exist(err);
utxos.should.be.empty;
done();
});
});
});
it('should return empty UTXOs for specific addresses if coin mismatch', function(done) {
helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) {
_.uniqBy(utxos, 'address').length.should.be.above(1);
server.getUtxos({
addresses: ['CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X']
}, function(err, utxos) {
should.not.exist(err);
utxos.should.be.empty;
done();
});
});
});
it('should get UTXOs for specific addresses', function(done) {
server.createAddress({}, function(err, address) {
helpers.stubUtxos(server, wallet, [1, 2, 3], { addresses: [address] }, function(utxos) {
server.getUtxos({
addresses: [address.address]
}, function(err, utxos) {
utxos.length.should.equal(3);
done();
});
});
});
});
it('should not fail when getting UTXOs for wallet with 0 UTXOs and pending txps', function(done) {
helpers.stubUtxos(server, wallet, [1, 1], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
blockchainExplorer.getUtxos = function(addresses, height, cb) {
return cb(null, []);
};
server.getUtxos({}, function(err, utxos) {
should.not.exist(err);
utxos.should.be.empty;
done();
});
});
});
});
it('should skip dust UTXOs ', function(done) {
helpers.stubUtxos(server, wallet, ['1 sat', 2, '10 sat', '100 sat', '1000 sat'], function() {
server.getUtxos({}, function(err, utxos) {
should.not.exist(err);
should.exist(utxos);
utxos.length.should.equal(2);
utxos.reduce((sum, u) => sum += u.satoshis, 0).should.equal(2 * 1e8 + 1000);
server.getMainAddresses({}, function(err, addresses) {
var utxo = utxos[0];
var address = addresses.find(a => a.address === utxo.address);
should.exist(address);
utxo.path.should.equal(address.path);
utxo.publicKeys.should.deep.equal(address.publicKeys);
done();
});
});
});
});
it('should report no UTXOs if only dust', function(done) {
helpers.stubUtxos(server, wallet, ['1 sat', '10 sat', '100 sat', '500 sat'], function() {
server.getUtxos({}, function(err, utxos) {
should.not.exist(err);
should.exist(utxos);
utxos.length.should.equal(0);
utxos.reduce((sum, u) => sum += u.satoshis, 0).should.equal(0);
done();
});
});
});
});
describe('Multiple request Pub Keys', function() {
var server, wallet;
var opts, reqPrivKey, ws;
var getAuthServer = function(copayerId, privKey, cb) {
var msg = 'dummy';
var sig = helpers.signMessage(msg, privKey);
WalletService.getInstanceWithAuth({
copayerId: copayerId,
message: msg,
signature: sig,
clientVersion: helpers.CLIENT_VERSION,
}, function(err, server) {
return cb(err, server);
});
};
beforeEach(function() {
reqPrivKey = new Bitcore.PrivateKey();
var requestPubKey = reqPrivKey.toPublicKey();
var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H;
var requestPubKeyStr = requestPubKey.toString();
var sig = helpers.signRequestPubKey(requestPubKeyStr, xPrivKey);
var copayerId = Model.Copayer._xPubToCopayerId('btc', TestData.copayers[0].xPubKey_44H_0H_0H);
opts = {
copayerId: copayerId,
requestPubKey: requestPubKeyStr,
signature: sig,
};
ws = new WalletService();
});
describe('#addAccess 1-1', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, 1, function() {
done();
});
});
});
it('should be able to re-gain access from xPrivKey', function(done) {
ws.addAccess(opts, function(err, res) {
should.not.exist(err);
res.wallet.copayers[0].requestPubKeys.length.should.equal(2);
res.wallet.copayers[0].requestPubKeys[0].selfSigned.should.equal(true);
server.getBalance(res.wallet.walletId, function(err, bal) {
should.not.exist(err);
bal.totalAmount.should.equal(1e8);
getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) {
server2.getBalance(res.wallet.walletId, function(err, bal2) {
should.not.exist(err);
bal2.totalAmount.should.equal(1e8);
done();
});
});
});
});
});
it('should fail to gain access with wrong xPrivKey', function(done) {
opts.signature = 'xx';
ws.addAccess(opts, function(err, res) {
err.code.should.equal('NOT_AUTHORIZED');
done();
});
});
it('should fail to access with wrong privkey after gaining access', function(done) {
ws.addAccess(opts, function(err, res) {
should.not.exist(err);
server.getBalance(res.wallet.walletId, function(err, bal) {
should.not.exist(err);
var privKey = new Bitcore.PrivateKey();
(getAuthServer(opts.copayerId, privKey, function(err, server2) {
err.code.should.equal('NOT_AUTHORIZED');
done();
}));
});
});
});
it('should be able to create TXs after regaining access', function(done) {
ws.addAccess(opts, function(err, res) {
should.not.exist(err);
getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8
}],
feePerKb: 100e2
};
server2.createTx(txOpts, function(err, tx) {
should.not.exist(err);
done();
});
});
});
});
});
describe('#addAccess 2-2', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 2, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, 1, function() {
done();
});
});
});
it('should be able to re-gain access from xPrivKey', function(done) {
ws.addAccess(opts, function(err, res) {
should.not.exist(err);
server.getBalance(res.wallet.walletId, function(err, bal) {
should.not.exist(err);
bal.totalAmount.should.equal(1e8);
getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) {
server2.getBalance(res.wallet.walletId, function(err, bal2) {
should.not.exist(err);
bal2.totalAmount.should.equal(1e8);
done();
});
});
});
});
});
it('TX proposals should include info to be verified', function(done) {
ws.addAccess(opts, function(err, res) {
should.not.exist(err);
getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8
}],
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, reqPrivKey, function() {
server2.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
should.exist(txs[0].proposalSignaturePubKey);
should.exist(txs[0].proposalSignaturePubKeySig);
done();
});
});
});
});
});
});
});
describe('#getBalance', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should get balance', function(done) {
helpers.stubUtxos(server, wallet, [1, 'u2', 3], function() {
server.getBalance({}, function(err, balance) {
should.not.exist(err);
should.exist(balance);
balance.totalAmount.should.equal(helpers.toSatoshi(6));
balance.lockedAmount.should.equal(0);
balance.availableAmount.should.equal(helpers.toSatoshi(6));
balance.totalConfirmedAmount.should.equal(helpers.toSatoshi(4));
balance.lockedConfirmedAmount.should.equal(0);
balance.availableConfirmedAmount.should.equal(helpers.toSatoshi(4));
should.exist(balance.byAddress);
balance.byAddress.length.should.equal(2);
balance.byAddress[0].amount.should.equal(helpers.toSatoshi(4));
balance.byAddress[1].amount.should.equal(helpers.toSatoshi(2));
server.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
var addresses = _.uniq(addresses.map(a => a.address));
_.intersection(addresses, balance.byAddress.map(a => a.address)).length.should.equal(2);
done();
});
});
});
});
it('should get balance when there are no addresses', function(done) {
server.getBalance({}, function(err, balance) {
should.not.exist(err);
should.exist(balance);
balance.totalAmount.should.equal(0);
balance.lockedAmount.should.equal(0);
balance.availableAmount.should.equal(0);
should.exist(balance.byAddress);
balance.byAddress.length.should.equal(0);
done();
});
});
it('should get balance when there are no funds', function(done) {
blockchainExplorer.getUtxos = sinon.stub().callsArgWith(2, null, []);
server.createAddress({}, function(err, address) {
should.not.exist(err);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
should.exist(balance);
balance.totalAmount.should.equal(0);
balance.lockedAmount.should.equal(0);
balance.availableAmount.should.equal(0);
should.exist(balance.byAddress);
balance.byAddress.length.should.equal(0);
done();
});
});
});
it('should only include addresses with balance', function(done) {
helpers.stubUtxos(server, wallet, 1, function(utxos) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.byAddress.length.should.equal(1);
balance.byAddress[0].amount.should.equal(helpers.toSatoshi(1));
balance.byAddress[0].address.should.equal(utxos[0].address);
done();
});
});
});
});
it('should fail gracefully when blockchain is unreachable', function(done) {
blockchainExplorer.getUtxos = sinon.stub().callsArgWith(2, 'dummy error');
server.createAddress({}, function(err, address) {
should.not.exist(err);
server.getBalance({}, function(err, balance) {
should.exist(err);
err.toString().should.equal('dummy error');
done();
});
});
});
it('should get balance for a different coin', function(done) {
helpers.stubUtxos(server, wallet, 1, function() {
var spy = sinon.spy(server, '_getBlockchainExplorer');
server.getBalance({
coin: 'bch'
}, function(err, balance) {
err.message.should.contain('not longer supported');
done();
});
});
});
});
describe('#getFeeLevels', function() {
var server, wallet, levels;
beforeEach(function() {
levels = Defaults.FEE_LEVELS;
Defaults.FEE_LEVELS = {
btc: [{
name: 'urgent',
nbBlocks: 1,
multiplier: 1.5,
defaultValue: 50000,
}, {
name: 'priority',
nbBlocks: 1,
defaultValue: 50000
}, {
name: 'normal',
nbBlocks: 2,
defaultValue: 40000
}, {
name: 'economy',
nbBlocks: 6,
defaultValue: 25000
}, {
name: 'superEconomy',
nbBlocks: 24,
defaultValue: 10000
}]
};
});
after(function() {
Defaults.FEE_LEVELS = levels;
});
var clock;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
done();
});
clock = sinon.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
});
afterEach(function() {
clock.restore();
});
it('should get current fee levels', function(done) {
helpers.stubFeeLevels({
1: 40000,
2: 20000,
6: 18000,
24: 9000,
});
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60000);
fees.urgent.nbBlocks.should.equal(1);
fees.priority.feePerKb.should.equal(40000);
fees.priority.nbBlocks.should.equal(1);
fees.normal.feePerKb.should.equal(20000);
fees.normal.nbBlocks.should.equal(2);
fees.economy.feePerKb.should.equal(18000);
fees.economy.nbBlocks.should.equal(6);
fees.superEconomy.feePerKb.should.equal(9000);
fees.superEconomy.nbBlocks.should.equal(24);
done();
});
});
it('should get default fees if network cannot be accessed', function(done) {
blockchainExplorer.estimateFee = sinon.stub().yields('dummy error');
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item.feePerKb]));
const defaults = Object.fromEntries(Defaults.FEE_LEVELS['btc'].map(item => [item.name, item.defaultValue]));
fees.priority.should.equal(defaults.priority);
fees.normal.should.equal(defaults.normal);
fees.economy.should.equal(defaults.economy);
done();
});
});
it('should get old fee levels after fail', function(done) {
let x = Defaults.FEE_LEVEL_CACHE_DURATION;
Defaults.FEE_LEVEL_CACHE_DURATION = 0;
helpers.stubFeeLevels({
1: 40002,
2: 20000,
6: 18000,
24: 9001,
}, true);
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60003);
blockchainExplorer.estimateFee = sinon.stub().yields('dummy error');
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60003);
fees.superEconomy.feePerKb.should.equal(9001);
Defaults.FEE_LEVEL_CACHE_DURATION = x;
done();
});
});
});
it('should get not cache old fee levels after fail', function(done) {
let x = Defaults.FEE_LEVEL_CACHE_DURATION;
Defaults.FEE_LEVEL_CACHE_DURATION = 0;
helpers.stubFeeLevels({
1: 40002,
2: 20000,
6: 18000,
24: 9001,
}, true);
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60003);
blockchainExplorer.estimateFee = sinon.stub().yields('dummy error');
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = _.fromPairs(_.map(fees, function(item) {
return [item.level, item];
}));
fees.urgent.feePerKb.should.equal(60003);
fees.superEconomy.feePerKb.should.equal(9001);
helpers.stubFeeLevels({
1: 400,
2: 200,
6: 180,
24: 90,
}, true);
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(600);
fees.superEconomy.feePerKb.should.equal(90);
Defaults.FEE_LEVEL_CACHE_DURATION = x;
done();
});
});
});
});
it('should not store cache old fee levels after 1+ fail', function(done) {
let x = Defaults.FEE_LEVEL_CACHE_DURATION;
Defaults.FEE_LEVEL_CACHE_DURATION = 100;
helpers.stubFeeLevels({
1: -1,
2: -1,
6: 200,
24: 101,
}, true);
server.getFeeLevels({}, function(err, fees, cached) {
should.not.exist(err);
should.not.exist(cached);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
//should use default value
//because value and fallback = -1
fees.priority.feePerKb.should.equal(
Defaults.FEE_LEVELS.btc[1].defaultValue
);
//using the given value
fees.superEconomy.feePerKb.should.equal(101);
helpers.stubFeeLevels({
1: 400,
2: 200,
6: 180,
24: 90,
}, true);
// should query again, NOT using cache.
server.getFeeLevels({}, function(err, fees, cached) {
should.not.exist(cached);
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(600);
fees.superEconomy.feePerKb.should.equal(90);
// now, it should be cached.
server.getFeeLevels({}, function(err, fees, cached) {
should.exist(cached);
Defaults.FEE_LEVEL_CACHE_DURATION = x;
done();
});
});
});
});
it('should not store cache old fee levels after 1+ fail (Case 2)', function(done) {
let x = Defaults.FEE_LEVEL_CACHE_DURATION;
Defaults.FEE_LEVEL_CACHE_DURATION = 100;
// not given values will fail
helpers.stubFeeLevels({
6: 200,
24: 101,
});
server.getFeeLevels({}, function(err, fees, cached) {
should.not.exist(err);
should.not.exist(cached);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
//should use default value
//because value and fallback = -1
fees.priority.feePerKb.should.equal(
Defaults.FEE_LEVELS.btc[1].defaultValue
);
//using the given value
fees.superEconomy.feePerKb.should.equal(101);
helpers.stubFeeLevels({
1: 400,
2: 200,
6: 180,
24: 90,
}, true);
// should query again, NOT using cache.
server.getFeeLevels({}, function(err, fees, cached) {
should.not.exist(cached);
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(600);
fees.superEconomy.feePerKb.should.equal(90);
// now, it should be cached.
server.getFeeLevels({}, function(err, fees, cached) {
should.exist(cached);
Defaults.FEE_LEVEL_CACHE_DURATION = x;
done();
});
});
});
});
it('should STORE cache old fee levels if NO fail', function(done) {
let x = Defaults.FEE_LEVEL_CACHE_DURATION;
Defaults.FEE_LEVEL_CACHE_DURATION = 100;
helpers.stubFeeLevels({
1: 500,
2: 400,
6: 200,
24: 101,
}, true);
server.getFeeLevels({}, function(err, fees, cached) {
should.not.exist(err);
should.not.exist(cached);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
//using the given value
fees.priority.feePerKb.should.equal(500);
fees.superEconomy.feePerKb.should.equal(101);
helpers.stubFeeLevels({
1: 400,
2: 200,
6: 180,
24: 90,
}, true);
// should USE cache.
server.getFeeLevels({}, function(err, fees, cached) {
should.not.exist(err);
should.exist(cached);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
//old cached values
fees.urgent.feePerKb.should.equal(750);
fees.superEconomy.feePerKb.should.equal(101);
Defaults.FEE_LEVEL_CACHE_DURATION = x;
done();
});
});
});
it('should fallback to slower confirmation times if network cannot estimate (returns -1)', function(done) {
helpers.stubFeeLevels({
1: -1,
2: 18000,
6: -1,
7: 11000,
24: 9000,
});
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.priority.feePerKb.should.equal(18000);
fees.priority.nbBlocks.should.equal(2);
fees.normal.feePerKb.should.equal(18000);
fees.normal.nbBlocks.should.equal(2);
fees.economy.feePerKb.should.equal(11000);
fees.economy.nbBlocks.should.equal(7);
fees.superEconomy.feePerKb.should.equal(9000);
fees.superEconomy.nbBlocks.should.equal(24);
done();
});
});
it('should get default fees if network cannot estimate (returns -1 including fallback)', function(done) {
helpers.stubFeeLevels({
1: 45000,
2: 36000,
6: -1,
7: -1,
8: -1,
24: 9000,
});
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.priority.feePerKb.should.equal(45000);
fees.priority.nbBlocks.should.equal(1);
fees.normal.feePerKb.should.equal(36000);
fees.normal.nbBlocks.should.equal(2);
fees.economy.feePerKb.should.equal(25000);
should.not.exist(fees.economy.nbBlocks);
done();
});
});
it('should get monotonically decreasing fee values', function(done) {
Defaults.FEE_LEVELS['btc'].find(lvl => lvl.nbBlocks === 6).defaultValue.should.equal(25000);
helpers.stubFeeLevels({
1: 45000,
2: 18000,
6: -1,
7: -1,
8: -1,
24: 9000,
});
server.getFeeLevels({}, function(err, fees) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.priority.feePerKb.should.equal(45000);
fees.priority.nbBlocks.should.equal(1);
fees.normal.feePerKb.should.equal(18000);
fees.normal.nbBlocks.should.equal(2);
fees.economy.feePerKb.should.equal(18000);
should.not.exist(fees.economy.nbBlocks);
fees.superEconomy.feePerKb.should.equal(9000);
fees.superEconomy.nbBlocks.should.equal(24);
done();
});
});
it('should get current fee levels FROM CACHE', function(done) {
helpers.stubFeeLevels({
1: 40000,
2: 20000,
}, true);
server.getFeeLevels({}, function(err, fees, fromCache) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60000);
fees.priority.feePerKb.should.equal(40000);
should.not.exist(fromCache);
server.getFeeLevels({}, function(err, fees, fromCache) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60000);
fees.priority.feePerKb.should.equal(40000);
fromCache.should.equal(true);
done();
});
});
});
it('should expire CACHE', function(done) {
helpers.stubFeeLevels({
1: 40000,
2: 20000,
});
server.getFeeLevels({}, function(err, fees, fromCache) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60000);
fees.priority.feePerKb.should.equal(40000);
should.not.exist(fromCache);
clock.tick(31 * 60 * 1000);
server.getFeeLevels({}, function(err, fees, fromCache) {
should.not.exist(err);
fees = Object.fromEntries(fees.map(item => [item.level, item]));
fees.urgent.feePerKb.should.equal(60000);
fees.priority.feePerKb.should.equal(40000);
should.not.exist(fromCache);
done();
});
});
});
it('should not use cache on different opts', function(done) {
helpers.stubFeeLevels({
1: 40000,
2: 20000,
});
server.getFeeLevels({}, function(err, fees, fromCache) {
should.not.exist(err);
should.not.exist(fromCache);
server.getFeeLevels({ coin: 'bch' }, function(err, fees, fromCache) {
should.not.exist(err);
should.not.exist(fromCache);
server.getFeeLevels({ coin: 'bch', network: 'testnet3' }, function(err, fees, fromCache) {
should.not.exist(err);
should.not.exist(fromCache);
done();
});
});
});
});
});
describe('Wallet not complete tests', function() {
it('should fail to create address when wallet is not complete', function(done) {
var server = new WalletService();
var walletOpts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
};
server.createWallet(walletOpts, function(err, walletId) {
should.not.exist(err);
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_45H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
helpers.getAuthServer(result.copayerId, function(server) {
server.createAddress({}, function(err, address) {
should.not.exist(address);
should.exist(err);
err.code.should.equal('WALLET_NOT_COMPLETE');
err.message.should.equal('Wallet is not complete');
done();
});
});
});
});
});
it('should fail to create tx when wallet is not complete', function(done) {
var server = new WalletService();
var walletOpts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
};
server.createWallet(walletOpts, function(err, walletId) {
should.not.exist(err);
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_45H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
helpers.getAuthServer(result.copayerId, function(server, wallet) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8
}],
feePerKb: 100e2
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.code.should.equal('WALLET_NOT_COMPLETE');
done();
});
});
});
});
});
});
describe('Network suspended tests', function() {
it('should fail to create tx when wallet is BCH and suspendedChains includes bch', function(done) {
config.suspendedChains = ['bch', 'xrp'];
var server = new WalletService();
var walletOpts = {
coin: 'bch',
name: 'my wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub,
};
server.createWallet(walletOpts, function(err, walletId) {
should.not.exist(err);
var copayerOpts = helpers.getSignedCopayerOpts({
coin: 'bch',
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.not.exist(err);
helpers.getAuthServer(result.copayerId, function(server, wallet) {
var txOpts = {
outputs: [{
toAddress: 'qz0d6gueltx0feta7z9777yk97sz9p6peu98mg5vac',
amount: 0.8e8
}],
feePerKb: 100e2
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.code.should.equal('NETWORK_SUSPENDED');
err.message.should.include('BCH');
done();
});
});
});
});
});
});
let testSet = [
{
coin: 'btc',
key: 'id44btc',
addr: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
lockedFunds: 0,
flags: {},
},
{
coin: 'bch',
key: 'id44bch',
addr: 'qpgjyj728rhu4gca2dqfzlpl8acnhzequshhgvev53',
lockedFunds: 0,
flags: {},
},
{
coin: 'bch',
key: 'id44bch',
addr: 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X',
lockedFunds: 0,
flags: { noCashAddr: true },
},
{
coin: 'eth',
key: 'id44btc',
addr: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
lockedFunds: 0,
flags: { noChange: true, noUtxoTests: true, },
},
{
coin: 'xrp',
key: 'id44btc',
addr: 'rDzTZxa7NwD9vmNf5dvTbW4FQDNSRsfPv6',
lockedFunds: Defaults.MIN_XRP_BALANCE,
flags: { noChange: true, noUtxoTests: true },
},
{
coin: 'doge',
key: 'id44btc',
addr: 'DTZ1W1qmXM9w4xJa9MMp2cAjdQv4PWSs9V',
lockedFunds: 0,
flags: {},
},
{
coin: 'ltc',
key: 'id44btc',
addr: 'LUDZDsJHVwgZBc5HdfbbBgqU6hJZwWNseV',
lockedFunds: 0,
flags: {},
}
];
for (const x of testSet) {
const coin = x.coin;
const ts = TO_SAT[coin];
const idKey = x.key;
const addressStr = x.addr;
const flags = x.flags;
const lockedFunds = x.lockedFunds;
let fromAddr;
describe('#createTx ' + coin + ' flags' + JSON.stringify(flags), function() {
describe('Tx proposal creation & publishing ' + coin, function() {
var server, wallet;
let sandbox;
beforeEach(function(done) {
sandbox = sinon.createSandbox();
helpers.createAndJoinWallet(1, 1, {
coin: coin,
}, function(s, w) {
server = s;
wallet = w;
fromAddr = null;
server.createAddress({}, function(err, address) {
should.not.exist(err);
fromAddr = address.address;
done();
});
});
});
afterEach(() => {
sandbox.restore();
})
it('should create a tx', function(done) {
let old = blockchainExplorer.getTransactionCount;
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '5');
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
const coinAmount = {
btc: 8000,
bch: 8000,
eth: 8000,
xrp: 8000,
doge: 1e8,
ltc: 8000
}
let amount = coinAmount[coin];
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.walletM.should.equal(1);
tx.walletN.should.equal(1);
tx.requiredRejections.should.equal(1);
tx.requiredSignatures.should.equal(1);
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
tx.isPending().should.equal.true;
tx.isTemporary().should.equal.true;
tx.amount.should.equal(coinAmount[coin]);
tx.feePerKb.should.equal(123e2);
tx.outputs[0].toAddress.should.equal(addressStr);
tx.outputs[0].amount.should.equal(amount);
if (coin == 'eth') {
tx.gasPrice.should.equal(12300);
tx.nonce.should.equal('5');
tx.outputs.should.deep.equal([{
toAddress: addressStr,
gasLimit: 21000,
amount: amount,
}]);
}
should.not.exist(tx.feeLevel);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
blockchainExplorer.getTransactionCount = old;
done();
});
});
});
});
describe('Validations', function() {
it('should fail to create a tx without outputs', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [],
feePerKb: 123e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
should.not.exist(tx);
err.message.should.equal('No outputs were specified');
done();
});
});
});
it('should fail to create tx for invalid address', function(done) {
helpers.stubUtxos(server, wallet, 1, function() {
var txOpts = {
outputs: [{
toAddress: 'invalid address',
amount: 1e8
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
should.not.exist(tx);
// may fail due to Non-base58 character, or Checksum mismatch, or other
done();
});
});
});
if (['btc', 'bch', 'doge', 'ltc'].includes(coin)) {
const coinAmount = {
btc: 8000,
bch: 8000,
doge: 1e8,
ltc: 8000
}
it(`should not fail to create ${coin} tx with valid OP_RETURN script and amount`, function(done) {
let old = blockchainExplorer.getTransactionCount;
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '5');
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
let amount = coinAmount[coin];
const testScript = '6a423d3a4554482e555344433a3078466231343633393239633337414531334533624232343337413330633039423239373031323732313a36303335373834393136373a743a3330';
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: amount,
},
{
script: testScript,
amount: 0,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.walletM.should.equal(1);
tx.walletN.should.equal(1);
tx.requiredRejections.should.equal(1);
tx.requiredSignatures.should.equal(1);
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
tx.isPending().should.equal.true;
tx.isTemporary().should.equal.true;
tx.amount.should.equal(coinAmount[coin]);
tx.feePerKb.should.equal(123e2);
tx.outputs[0].toAddress.should.equal(addressStr);
tx.outputs[0].amount.should.equal(amount);
tx.outputs[1].script.should.equal(testScript);
tx.outputs[1].amount.should.equal(0);
should.not.exist(tx.feeLevel);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
blockchainExplorer.getTransactionCount = old;
done();
});
});
});
});
it(`should fail to create ${coin} tx for invalid amount`, function(done) {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 0,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.message.should.equal('Invalid amount');
done();
});
});
it(`should fail to create ${coin} tx with a script other than OP_RETURN`, function(done) {
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var txOpts = {
outputs: [
{
toAddress: addressStr,
amount: coinAmount[coin],
},
{
script: '76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac',
amount: 5000000000,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.message.should.equal('The only supported script is OP_RETURN');
done();
});
});
});
it(`should fail to create ${coin} tx with OP_RETURN script and invalid amount`, function(done) {
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var txOpts = {
outputs: [
{
toAddress: addressStr,
amount: coinAmount[coin],
},
{
script: '6a423d3a4554482e555344433a3078466231343633393239633337414531334533624232343337413330633039423239373031323732313a36303335373834393136373a743a3330',
amount: 10,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.message.should.equal('The amount of an output with OP_RETURN script must be 0');
done();
});
});
});
it(`should fail to create ${coin} tx with wrong data type of OP_RETURN script`, function(done) {
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var txOpts = {
outputs: [
{
toAddress: addressStr,
amount: coinAmount[coin],
},
{
script: 123,
amount: 0,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.message.should.equal('Script must be a valid data type');
done();
});
});
});
it(`should fail to create ${coin} tx with an output with invalid script and valid data type`, function(done) {
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var txOpts = {
outputs: [
{
toAddress: addressStr,
amount: coinAmount[coin],
},
{
script: 'wrong script',
amount: 0,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.message.should.equal('The only supported script is OP_RETURN');
done();
});
});
});
} else if (coin === 'eth') {
it('should not fail to create ETH chain based tx for 0 amount', function(done) {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 0,
}],
feePerKb: 2000000000,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
done();
});
});
}
it('should fail to specify both feeLevel & feePerKb', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
feeLevel: 'normal',
feePerKb: 123e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.toString().should.contain('Only one of feeLevel/feePerKb');
done();
});
});
});
if (!flags.noUtxoTests) {
it('should fail to create tx for address of different network', function(done) {
helpers.stubUtxos(server, wallet, 1, function() {
var txOpts = {
outputs: [{
toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD',
amount: 1e8
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.code.should.equal('INCORRECT_ADDRESS_NETWORK');
err.message.should.equal('Incorrect address network');
done();
});
});
});
it('should be able to create tx with inputs argument', function(done) {
helpers.stubUtxos(server, wallet, [1, 3, 2], function(utxos) {
server.getUtxos({}, function(err, utxos) {
should.not.exist(err);
var inputs = [utxos[0], utxos[2]];
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
feePerKb: 100e2,
inputs: inputs,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.inputs.length.should.equal(2);
var txids = tx.inputs.map(i => i.txid);
txids.should.contain(utxos[0].txid);
txids.should.contain(utxos[2].txid);
done();
});
});
});
});
if (coin !== 'doge') { // TODO
it('should be able to specify change address', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 0.8e8,
}],
feePerKb: 100e2,
changeAddress: utxos[0].address,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
var t = ChainService.getBitcoreTx(tx);
t.getChangeOutput().script.toAddress().toString(true).should.equal(txOpts.changeAddress);
done();
});
});
});
}
it('should be fail if specified change address is not from the wallet', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
var addr = (new Bitcore_[coin].PrivateKey()).toAddress();
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
feePerKb: 100e2,
changeAddress: addr.toString(true),
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INVALID_CHANGE_ADDRESS');
done();
});
});
});
it('should be able to specify inputs & absolute fee', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
inputs: utxos,
fee: 1000e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.amount.should.equal(helpers.toSatoshi(1));
should.not.exist(tx.feePerKb);
tx.fee.should.equal(1000e2);
var t = ChainService.getBitcoreTx(tx);
t.getFee().should.equal(1000e2);
t.getChangeOutput().satoshis.should.equal(3e8 - 1e8 - 1000e2);
done();
});
});
});
}
});
describe('Foreign ID', function() {
it('should create a tx with foreign ID', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var txOpts = {
txProposalId: '123',
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.id.should.equal('123');
done();
});
});
});
it('should return already created tx if same foreign ID is specified and tx still unpublished', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var txOpts = {
txProposalId: '123',
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.id.should.equal('123');
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.id.should.equal('123');
server.storage.fetchTxs(wallet.id, {}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(1);
done();
});
});
});
});
});
it('should return already published tx if same foreign ID is specified and tx already published', function(done) {
helpers.stubUtxos(server, wallet, [2, 2, 2], function() {
var txOpts = {
txProposalId: '123',
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.id.should.equal('123');
var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.id.should.equal('123');
tx.status.should.equal('pending');
server.storage.fetchTxs(wallet.id, {}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
done();
});
});
});
});
});
});
});
describe('Publishing', function() {
it('should be able to publish a temporary tx proposal', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
message: 'some message',
customData: 'some custom data',
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
should.exist(txs[0].proposalSignature);
done();
});
});
});
});
});
it('should not be able to publish a temporary tx proposal created in a dry run', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
dryRun: true,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err) {
should.exist(err);
err.code.should.equal('TX_NOT_FOUND');
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(0);
done();
});
});
});
});
});
it('should delay NewTxProposal notification until published', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
message: 'some message',
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
notifications.map(n => n.type).should.not.contain('NewTxProposal');
var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err) {
should.not.exist(err);
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var n = notifications.find(n => n.type === 'NewTxProposal');
should.exist(n);
should.exist(n.data.txProposalId);
should.exist(n.data.message);
should.exist(n.data.creatorId);
n.data.creatorId.should.equal(server.copayerId);
done();
});
});
});
});
});
});
it('should fail to publish non-existent tx proposal', function(done) {
server.publishTx({
txProposalId: 'wrong-id',
proposalSignature: 'dummy',
}, function(err) {
should.exist(err);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
done();
});
});
});
it('should fail to publish tx proposal with wrong signature', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
message: 'some message',
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
server.publishTx({
txProposalId: txp.id,
proposalSignature: 'dummy'
}, function(err) {
should.exist(err);
err.message.should.contain('Invalid proposal signature');
done();
});
});
});
});
it('should fail to publish tx proposal not signed by the creator', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
message: 'some message',
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var publishOpts = {
txProposalId: txp.id,
proposalSignature: helpers.signMessage(txp.getRawTx(), TestData.copayers[1].privKey_1H_0),
}
server.publishTx(publishOpts, function(err) {
should.exist(err);
err.message.should.contain('Invalid proposal signature');
done();
});
});
});
});
if (!flags.noUtxoTests) {
it('should fail to publish a temporary tx proposal if utxos are locked by other pending proposals', function(done) {
const coinAmount = {
btc: 0.8,
bch: 0.8,
eth: 0.8,
xrp: 0.8,
doge: 1,
ltc: 0.8
}
var txp1, txp2;
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: coinAmount[coin] * ts,
}],
message: 'some message',
feePerKb: 100e2,
from: fromAddr,
};
async.waterfall([
function(next) {
helpers.stubUtxos(server, wallet, [2, 2], function() {
next();
});
},
function(next) {
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, next);
},
function(txp, next) {
txp1 = txp;
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, next);
},
function(txp, next) {
txp2 = txp;
should.exist(txp1);
should.exist(txp2);
var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next);
},
function(txp, next) {
var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err) {
should.exist(err);
err.code.should.equal('UNAVAILABLE_UTXOS');
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
next();
});
},
function(next) {
// A new tx proposal should use the next available UTXO
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, next);
},
function(txp3, next) {
should.exist(txp3);
var publishOpts = helpers.getProposalSignatureOpts(txp3, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next);
},
function(txp, next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(2);
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
it('should fail to publish a temporary tx proposal if utxos are already spent', function(done) {
var txp1, txp2;
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
message: 'some message',
feePerKb: 100e2,
};
async.waterfall([
function(next) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
next();
});
},
function(next) {
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, next);
},
function(txp, next) {
txp1 = txp;
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, next);
},
function(txp, next) {
txp2 = txp;
should.exist(txp1);
should.exist(txp2);
var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next);
},
function(txp, next) {
// Sign & Broadcast txp1
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures
}, function(err, txp) {
should.not.exist(err);
helpers.stubBroadcast(txp.txid);
server.broadcastTx({
txProposalId: txp.id
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.status.should.equal('broadcasted');
next();
});
});
},
function(next) {
var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err) {
should.exist(err);
err.code.should.equal('UNAVAILABLE_UTXOS');
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
if (coin === 'btc') {
it('should fail to publish ( replaceTxByFee -> undefined ) a temporary tx proposal if utxos are already spent in a RBF tx', function(done) {
var txp1, txp2;
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
message: 'some message',
feePerKb: 100e2,
enableRBF: true
};
async.waterfall([
function(next) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
next();
});
},
function(next) {
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, next);
},
function(txp, next) {
txp1 = txp;
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, next);
},
function(txp, next) {
txp2 = txp;
should.exist(txp1);
should.exist(txp2);
var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next);
},
function(txp, next) {
// Sign & Broadcast txp1
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures
}, function(err, txp) {
should.not.exist(err);
helpers.stubBroadcast(txp.txid);
server.broadcastTx({
txProposalId: txp.id
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.status.should.equal('broadcasted');
next();
});
});
},
function(next) {
var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err, txp) {
err.code.should.equal('UNAVAILABLE_UTXOS');
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
it('should not fail to publish, sign and broadcast ( replaceTxByFee -> true ) a tx proposal if utxos are already spent in a RBF tx', function(done) {
var txp1, txp2;
var txOpts1 = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
message: 'some message',
feePerKb: 100e2,
enableRBF: true
};
var txOpts2 = {
outputs: [{
toAddress: addressStr,
amount: 1e8,
}],
message: 'some message',
feePerKb: 120e2
};
async.waterfall([
function(next) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
next();
});
},
function(next) {
txOpts1 = Object.assign(txOpts1, flags);
server.createTx(txOpts1, next);
},
function(txp, next) {
txp1 = txp;
txOpts2 = Object.assign(txOpts2, flags);
server.createTx({ ...txOpts2, ...{ replaceTxByFee: true, inputs: txp1.inputs } }, next);
},
function(txp, next) {
txp2 = txp;
should.exist(txp1);
should.exist(txp2);
var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next);
},
function(txp, next) {
// Sign & Broadcast txp1
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures
}, function(err, txp) {
should.not.exist(err);
helpers.stubBroadcast(txp.txid);
server.broadcastTx({
txProposalId: txp.id
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.status.should.equal('broadcasted');
next();
});
});
},
function(next) {
var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next);
},
function(txp, next) {
// Sign & Broadcast txp2
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures
}, function(err, txp) {
should.not.exist(err);
helpers.stubBroadcast(txp.txid);
server.broadcastTx({
txProposalId: txp.id
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.status.should.equal('broadcasted');
next();
});
});
},
], function(err) {
should.not.exist(err);
done();
});
});
}
}
});
describe('Fee levels', function() {
var level, expected, expectedNormal;
before(() => {
if (Constants.UTXO_CHAINS[coin.toUpperCase()]) {
const normal = coin == 'doge' ? 1e8 : 200e2; // normal BCH, DOGE
helpers.stubFeeLevels({
1: 400e2,
2: normal,
6: 180e2, // economy BTC
10: 180e2,
15: 100e2,
24: 90e2,
25: 90e2
}, null, coin);
} else if (coin === 'eth') {
helpers.stubFeeLevels({
1: 10e9, // urgent ETH
2: 5e9, // priority ETH
3: 1e9, // normal ETH
4: 1e9 // economy/superEconomy ETH
}, null, coin);
} else if (coin === 'xrp') {
helpers.stubFeeLevels({
1: 12
}, null, coin);
}
switch (coin) {
case 'bch':
level = 'normal';
expected = 210e2;
expectedNormal = 210e2;
break;
case 'eth':
level = 'normal';
expected = 1e9;
expectedNormal = 1e9;
break;
case 'xrp':
level = 'normal';
expected = 12;
expectedNormal = 12;
break;
case 'doge':
level = 'normal';
expected = 1e8;
expectedNormal = 1e8;
break;
case 'ltc':
level = 'normal';
expected = 200e2;
expectedNormal = 200e2;
break;
default:
level = 'economy';
expected = 180e2;
expectedNormal = 200e2;
};
});
it('should create a tx specifying feeLevel', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: ts,
}],
gasPrice: 1,
feeLevel: level,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.feeLevel.should.equal(level);
txp.feePerKb.should.equal(expected);
done();
});
});
});
it('should fail if the specified fee level does not exist', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: ts,
}],
feeLevel: 'madeUpLevel',
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.toString().should.contain('Invalid fee level');
done();
});
});
});
it('should assume "normal" fee level if no feeLevel and no feePerKb/fee is specified', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: ts,
}],
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.feePerKb.should.equal(expectedNormal);
txp.feeLevel.should.equal('normal');
done();
});
});
});
});
it('should generate new change address for each created tx', function(done) {
const coinAmount = {
btc: 8000,
bch: 8000,
eth: 8000,
xrp: 8000,
doge: 1e8,
ltc: 8000
}
let amount = coinAmount[coin];
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount
}],
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(_.clone(txOpts), function(err, tx1) {
should.not.exist(err);
should.exist(tx1);
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx2) {
should.not.exist(err);
should.exist(tx2);
if (flags.noChange) {
should.not.exist(tx1.changeAddress);
should.not.exist(tx2.changeAddress);
} else {
tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address);
}
done();
});
});
});
});
it('should support creating a tx with no change address', function(done) {
const coinFee = {
btc: 3800,
bch: 3800,
xrp: 3800,
eth: 210000000,
doge: 1e6,
ltc: 3800
}
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var max = 3 * ts - coinFee[coin]; // Fees for this tx at 100bits/kB = 3740 sat
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: max,
gasLimit: (coin == 'eth') ? 21000 : null
}],
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var t = ChainService.getBitcoreTx(txp).toObject();
t.outputs.length.should.equal(1);
t.outputs[0].satoshis.should.equal(max);
done();
});
});
});
it('should support creating a txp with multiple transactions', function(done) {
const coinFee = {
btc: 3800,
bch: 3800,
xrp: 3800,
eth: 210000000,
doge: 1e6,
ltc: 3800
}
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var amount = ts - coinFee[coin];
var txOpts = {
outputs: [{
toAddress: addressStr,
amount,
}, {
toAddress: addressStr,
amount: 2 * amount,
}],
multiTx: true,
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
if (coin != 'xrp') {
should.exist(err);
return done();
}
should.not.exist(err);
should.exist(txp);
txp.outputOrder.length.should.equal(2);
var t = ChainService.getBitcoreTx(txp).toObject();
t.outputs.length.should.equal(2);
t.outputs[txp.outputOrder[0]].amount.should.equal(txOpts.outputs[txp.outputOrder[0]].amount);
t.outputs[txp.outputOrder[1]].amount.should.equal(txOpts.outputs[txp.outputOrder[1]].amount);
done();
});
});
});
it('should fail gracefully if unable to reach the blockchain', function(done) {
blockchainExplorer.getUtxos = sinon.stub().callsArgWith(2, 'dummy error');
blockchainExplorer.getBalance = sinon.stub().callsArgWith(1, 'dummy error');
server.createAddress({}, function(err, address) {
should.not.exist(err);
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1 * ts,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.toString().should.equal('dummy error');
done();
});
});
});
it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) {
const coinAmount = {
btc: 0.5,
bch: 0.5,
eth: 0.5,
xrp: 0.5,
doge: 1,
ltc: 0.5
}
helpers.stubUtxos(server, wallet, 2, { coin }, function() {
sandbox.stub(CWC.Transactions, 'create').throws(new Error('dummy exception'));
sandbox.stub(Bitcore_.btc, 'Transaction').throws(new Error('dummy exception'));
sandbox.stub(Bitcore_.bch, 'Transaction').throws(new Error('dummy exception'));
sandbox.stub(Bitcore_.doge, 'Transaction').throws(new Error('dummy exception'));
sandbox.stub(Bitcore_.ltc, 'Transaction').throws(new Error('dummy exception'));
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: coinAmount[coin] * ts,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.message.should.equal('dummy exception');
sandbox.restore();
done();
});
});
});
if (coin !== 'doge') { // TODO
it('should fail with different error for insufficient funds and locked funds', function(done) {
const ts = TO_SAT[coin];
helpers.stubUtxos(server, wallet, [1, 1], { coin }, function() {
let txAmount = +((1.1 * ts).toFixed(0));
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: txAmount,
}],
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(2 * ts + lockedFunds);
if(flags.noChange) {
balance.lockedAmount.should.equal(txAmount + tx.fee + lockedFunds);
txOpts.outputs[0].amount = 2 * ts;
} else {
balance.lockedAmount.should.equal(2 * ts);
txOpts.outputs[0].amount = 1 * ts;
}
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('LOCKED_FUNDS');
err.message.should.equal('Funds are locked by pending transaction proposals');
done();
});
});
});
});
});
}
if (!flags.noUtxoTests) {
it('should fail to create a tx exceeding max size in kb', function(done) {
Defaults.MAX_TX_SIZE_IN_KB_BTC = 1;
helpers.stubUtxos(server, wallet, new Array(9).fill(1), { coin }, function() {
let x = [];
x.push({
toAddress: addressStr,
amount: 8 * ts,
});
var txOpts = {
outputs: x,
feePerKb: 100e2,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('TX_MAX_SIZE_EXCEEDED');
done();
});
});
});
it('should fail to create tx for dust amount in outputs', function(done) {
helpers.stubUtxos(server, wallet, 1, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: Defaults.MIN_OUTPUT_AMOUNT - 1,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('DUST_AMOUNT');
err.message.should.equal('Amount below dust threshold');
done();
});
});
});
if (coin !== 'doge') { // TODO
it('should create tx with 0 change output', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var fee = 2260; // The exact fee of the resulting tx
var amount = 2e8 - fee;
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: amount,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
var bitcoreTx = ChainService.getBitcoreTx(tx);
bitcoreTx.outputs.length.should.equal(1);
bitcoreTx.outputs[0].satoshis.should.equal(tx.amount);
done();
});
});
});
it('should create tx when there is a pending tx and enough UTXOs', function(done) {
helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], { coin }, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1.5 * TO_SAT[coin],
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
txOpts.outputs[0].amount = 1 * TO_SAT[coin];
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(2);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(3.6 * TO_SAT[coin]);
if (coin == 'eth') {
balance.lockedAmount.should.equal(2300000000000000000);
} else {
balance.lockedAmount.should.equal(3.6 * TO_SAT[coin]);
}
done();
});
});
});
});
});
});
}
it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) {
helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], { coin }, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 1.5 * TO_SAT[coin],
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
txOpts.outputs[0].amount = TO_SAT[coin] * ((coin == 'eth') ? 2.2 : 1.8);
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
err.code.should.equal('LOCKED_FUNDS');
should.not.exist(tx);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(3.6 * TO_SAT[coin]);
if (coin != 'eth') {
var amountInputs = txs[0].inputs.reduce((sum, input) => sum += input.satoshis, 0);
balance.lockedAmount.should.equal(amountInputs);
balance.lockedAmount.should.be.below(balance.totalAmount);
balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount);
} else {
balance.lockedAmount.should.equal(1.5 * TO_SAT[coin]);
}
done();
});
});
});
});
});
});
}
it('should be able to send max funds', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: null,
gasLimit: 21000
}],
feePerKb: (coin == 'eth') ? 1e8 : 10000,
sendMax: true,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
should.not.exist(tx.changeAddress);
tx.amount.should.equal(3 * TO_SAT[coin] - tx.fee);
var t = ChainService.getBitcoreTx(tx);
t.getFee().should.equal(tx.fee);
should.not.exist(t.getChangeOutput());
t.toObject().inputs.length.should.equal(tx.inputs.length);
t.toObject().outputs[0].satoshis.should.equal(tx.amount);
done();
});
});
});
if (!flags.noUtxoTests) {
it('should send max with appropiate fee', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: null,
gasLimit: 21000
}],
feePerKb: coin == 'bch' ? 1000 : 10000,
sendMax: true,
from: fromAddr,
};
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
should.not.exist(txp.changeAddress);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
helpers.stubBroadcast(txp.txid);
server.broadcastTx({
txProposalId: txp.id
}, function(err, txp) {
txp.amount.should.equal(3 * TO_SAT[coin] - txp.fee);
var t = ChainService.getBitcoreTx(txp);
t.getFee().should.equal(txp.fee);
const actualFeeRate = t.getFee() / (txp.raw.length / 2);
done();
});
});
});
});
});
if (coin !== 'doge' && coin !== 'ltc') { // TODO
it('should accept a tx proposal signed with a custom key', function(done) {
var reqPrivKey = new Bitcore.PrivateKey();
var reqPubKey = reqPrivKey.toPublicKey().toString();
var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H;
var accessOpts = {
copayerId: TestData.copayers[0][idKey],
requestPubKey: reqPubKey,
signature: helpers.signRequestPubKey(reqPubKey, xPrivKey),
};
server.addAccess(accessOpts, function(err) {
should.not.exist(err);
helpers.stubUtxos(server, wallet, [1, 2], { coin }, function() {
var txOpts = {
outputs: [{
toAddress: addressStr,
amount: 0.8 * 1e8,
}],
message: 'some message',
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var publishOpts = {
txProposalId: txp.id,
proposalSignature: helpers.signMessage(txp.getRawTx(), reqPrivKey),
}
server.publishTx(publishOpts, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txp.id
}, function(err, x) {
should.not.exist(err);
x.proposalSignature.should.equal(publishOpts.proposalSignature);
x.proposalSignaturePubKey.should.equal(accessOpts.requestPubKey);
x.proposalSignaturePubKeySig.should.equal(accessOpts.signature);
done();
});
});
});
});
});
});
}
it('should shuffle outputs unless specified', function(done) {
let amount, outputAmount;
if (coin === 'doge') {
amount = 1000;
outputAmount = 1e8;
} else {
amount = 1;
outputAmount = 100e2;
}
helpers.stubUtxos(server, wallet, amount, function() {
var txOpts = {
outputs: new Array(30).fill(0).map((_, i) => ({
toAddress: addressStr,
amount: (i + 1) * outputAmount,
})),
feePerKb: 123e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var t = ChainService.getBitcoreTx(txp);
var changeOutput = t.getChangeOutput().satoshis;
var outputs = t.outputs.map(o => o.satoshis).filter(o => o !== changeOutput);
outputs.should.not.deep.equal(txOpts.outputs.map(o => o.amount));
txOpts.noShuffleOutputs = true;
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
t = ChainService.getBitcoreTx(txp);
changeOutput = t.getChangeOutput().satoshis;
outputs = t.outputs.map(o => o.satoshis).filter(o => o !== changeOutput);
outputs.should.deep.equal(txOpts.outputs.map(o => o.amount));
done();
});
});
});
});
}
});
});
describe('Backoff time ' + coin, function(done) {
var server, wallet, txid, clock;
var _oldBackoffOffset = Defaults.BACKOFF_OFFSET;
beforeEach(function(done) {
Defaults.BACKOFF_OFFSET = 3;
helpers.createAndJoinWallet(2, 2, function(s, w) {
server = s;
wallet = w;
const range2to5 = new Array(4).fill(0).map((_, i) => i + 2);
helpers.stubUtxos(server, wallet, range2to5, function() {
done();
});
});
});
afterEach(function(done) {
Defaults.BACKOFF_OFFSET = _oldBackoffOffset;
clock.restore();
done();
});
it('should follow backoff time after consecutive rejections', function(done) {
clock = sinon.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
feePerKb: 100e2,
};
async.series([
function(next) {
async.each(new Array(3).fill(0), function(i, next) {
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
server.rejectTx({
txProposalId: tx.id,
reason: 'some reason',
}, next);
});
},
next);
},
function(next) {
// Allow a 4th tx
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
server.rejectTx({
txProposalId: tx.id,
reason: 'some reason',
}, next);
});
},
function(next) {
// Do not allow before backoff time
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('TX_CANNOT_CREATE');
next();
});
},
function(next) {
clock.tick((Defaults.BACKOFF_TIME + 1) * 1000);
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
server.rejectTx({
txProposalId: tx.id,
reason: 'some reason',
}, next);
});
},
function(next) {
// Do not allow a 5th tx before backoff time
clock.tick((Defaults.BACKOFF_TIME - 1) * 1000);
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('TX_CANNOT_CREATE');
next();
});
},
function(next) {
clock.tick(2000);
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
server.rejectTx({
txProposalId: tx.id,
reason: 'some reason',
}, next);
});
},
], function(err) {
should.not.exist(err);
done();
});
});
});
if (Constants.UTXO_CHAINS[coin.toUpperCase()]) {
describe('UTXO Selection ' + coin, function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
afterEach(function() {
transport.level = LOG_LEVEL;
});
it('should exclude unconfirmed utxos if specified', function(done) {
helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 3e8
}],
feePerKb: 100e2,
excludeUnconfirmedUtxos: true,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS');
err.message.should.equal('Insufficient funds');
txOpts.outputs[0].amount = 2.5e8;
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
err.message.should.include('Insufficient funds for fee. RequiredFee: 4500 Coin: btc feePerKb: 10000');
err.messageData.should.deep.equal({
requiredFee: 4500,
coin: 'btc',
feePerKb: 10000
});
done();
});
});
});
});
it('should use non-locked confirmed utxos when specified', function(done) {
helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1.4e8
}],
feePerKb: 100e2,
excludeUnconfirmedUtxos: true,
};
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
tx.inputs.length.should.equal(2);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
balance.lockedConfirmedAmount.should.equal(helpers.toSatoshi(2.5));
balance.availableConfirmedAmount.should.equal(0);
txOpts.outputs[0].amount = 0.01e8;
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('LOCKED_FUNDS');
done();
});
});
});
});
});
it('should not use UTXO provided in utxosToExclude option', function(done) {
helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 3.5e8,
}],
feePerKb: 100e2,
utxosToExclude: [utxos[2].txid + ':' + utxos[2].vout],
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS');
err.message.should.equal('Insufficient funds');
txOpts.utxosToExclude = [utxos[0].txid + ':' + utxos[0].vout];
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
done();
});
});
});
});
it('should select a single utxo if within thresholds relative to tx amount', function(done) {
helpers.stubUtxos(server, wallet, [1, '350bit', '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(35000);
done();
});
});
});
it('should return inputs in random order', function(done) {
// NOTE: this test has a chance of failing of 1 in 1'073'741'824 :P
const range1to30 = new Array(30).fill(0).map((_, i) => i + 1);
helpers.stubUtxos(server, wallet, range1to30, function(utxos) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: utxos.reduce((sum, u) => sum += u.satoshis, 0) - 0.5e8,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var amounts = txp.inputs.map(i => i.satoshis);
amounts.length.should.equal(30);
amounts.every(function(amount, i) {
if (i == 0) return true;
return amount < amounts[i - 1];
}).should.be.false;
done();
});
});
});
it('should select a confirmed utxos if within thresholds relative to tx amount', function(done) {
helpers.stubUtxos(server, wallet, [1, 'u 350bit', '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
txp.inputs[0].satoshis.should.equal(10000);
done();
});
});
});
it('should select smaller utxos if within fee constraints', function(done) {
helpers.stubUtxos(server, wallet, [1, '800bit', '800bit', '800bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 2000e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
txp.inputs.every(function(input) {
return input == 100e2;
});
done();
});
});
});
it('should select smallest big utxo if small utxos are insufficient', function(done) {
helpers.stubUtxos(server, wallet, [3, 1, 2, '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 300e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
it('should account for fee when selecting smallest big utxo', function(done) {
var _old = Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR;
Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR = 2;
// The 605 bits input cannot be selected even if it is > 2 * tx amount
// because it cannot cover for fee on its own.
helpers.stubUtxos(server, wallet, [1, '605bit', '100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 300e2,
}],
feePerKb: 1200e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR = _old;
done();
});
});
});
it('should select smallest big utxo if small utxos exceed maximum fee', function(done) {
helpers.stubUtxos(server, wallet, [3, 1, 2].concat(new Array(20).fill('1000bit')), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 12000e2,
}],
feePerKb: 20e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
it('should select smallest big utxo if small utxos are below accepted ratio of txp amount', function(done) {
helpers.stubUtxos(server, wallet, [9, 1, 1, 0.5, 0.2, 0.2, 0.2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 3e8,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(9e8);
done();
});
});
});
it('should not fail with tx exceeded max size if there is at least 1 big input', function(done) {
Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = 0.0001;
Defaults.MAX_TX_SIZE_IN_KB_BTC = 2;
Defaults.MAX_TX_SIZE_IN_KB_ETH = 2;
Defaults.MAX_TX_SIZE_IN_KB_XRP = 2;
helpers.stubUtxos(server, wallet, [100].concat(new Array(19).fill(1)), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 15e8,
}],
feePerKb: 120e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(100e8);
done();
});
});
});
it('should ignore utxos not contributing enough to cover increase in fee', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 80e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
txOpts.feePerKb = 160e2;
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
done();
});
});
});
});
it('should fail to select utxos if not enough to cover tx amount', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 400e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('INSUFFICIENT_FUNDS');
done();
});
});
});
it('should fail to select utxos if not enough to cover fees', function(done) {
helpers.stubUtxos(server, wallet, ['100bit', '100bit', '100bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 299e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
done();
});
});
});
it('should prefer a higher fee (breaking all limits) if inputs have 6+ confirmations', function(done) {
helpers.stubUtxos(server, wallet, ['2c 2000bit'].concat(new Array(20).fill('100bit')), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1500e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.every(function(input) {
return input == 100e2;
});
done();
});
});
});
it('should select unconfirmed utxos if not enough confirmed utxos', function(done) {
helpers.stubUtxos(server, wallet, ['u 1btc', '0.5btc'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(1);
txp.inputs[0].satoshis.should.equal(1e8);
done();
});
});
});
it('should ignore utxos too small to pay for fee', function(done) {
helpers.stubUtxos(server, wallet, ['1c200bit', '200bit'].concat(new Array(20).fill('1bit')), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 90e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(2);
done();
});
});
});
it('should ignore utxos not economically worth to send and fail if not enough utxos to cover fees', function(done) {
helpers.stubUtxos(server, wallet, [].concat(new Array(20).fill('10bit')), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 90e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
done();
});
});
});
it('should use small utxos if fee is low', function(done) {
helpers.stubUtxos(server, wallet, [].concat(new Array(10).fill('30bit')), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 10e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(8);
done();
});
});
});
it('should correct fee if resulting change would be below threshold', function(done) {
helpers.stubUtxos(server, wallet, ['180bit', '500sat'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 150e2,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
txp.inputs.length.should.equal(1);
(txp.inputs.reduce((sum, i) => sum += i.satoshis, 0) - txp.outputs[0].amount - txp.fee).should.equal(0);
var changeOutput = ChainService.getBitcoreTx(txp).getChangeOutput();
should.not.exist(changeOutput);
done();
});
});
});
it('should ignore small utxos if fee is higher', function(done) {
helpers.stubUtxos(server, wallet, [].concat(new Array(10).fill('30bit')), function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 200e2,
}],
feePerKb: 80e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
done();
});
});
});
it('should always select inputs as long as there are sufficient funds', function(done) {
helpers.stubUtxos(server, wallet, [80, '50bit', '50bit', '50bit', '50bit', '50bit'], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 101e2,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
done();
});
});
});
it('should not use UTXOs of recently broadcasted txs', function(done) {
helpers.stubUtxos(server, wallet, [1, 1], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1.5e8,
}],
feePerKb: 100e2,
};
txOpts = Object.assign(txOpts, flags);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
helpers.stubBroadcast(txp.txid);
server.broadcastTx({
txProposalId: txp.id
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.status.should.equal('broadcasted');
txOpts = Object.assign(txOpts, flags);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS');
should.not.exist(txp);
done();
});
});
});
});
});
});
});
};
}
describe('#createTX Segwit tests', () => {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, {
coin: 'btc',
useNativeSegwit: true,
}, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should set the desired Fee rate on segwit TXs', function(done) {
helpers.stubFeeLevels({
});
let addr = '134kthjj3BaGTRMPiB1moohBdtKfyCrt9c';
let amount = 75909000;
helpers.stubUtxos(server, wallet, [0.36023362, 0.39923362], function() {
var txOpts = {
outputs: [{
toAddress: addr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 129 * 1000,
};
txOpts = Object.assign(txOpts);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err, err);
should.exist(txp);
helpers.stubBroadcast(txp.txid);
server.broadcastTx({
txProposalId: txp.id
}, function(err, txp) {
should.not.exist(err, err);
txp.outputs.should.deep.equal([{
toAddress: addr,
amount: amount,
}]);
done();
});
});
});
});
});
});
describe('#createTX ETH Only tests', () => {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, {
coin: 'eth',
}, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should allow to create a TX with fee and no inputs', function(done) {
helpers.stubFeeLevels({
});
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [1, 2], function() {
let amount = 0.8 * 1e8;
var txOpts = {
outputs: [{
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
amount: amount,
}],
from,
message: 'some message',
customData: 'some custom data',
fee: 252000000000000,
};
txOpts = Object.assign(txOpts);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.outputs.should.deep.equal([{
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
gasLimit: 21000,
amount: amount,
}]);
tx.gasPrice.should.equal(12000000000);
tx.outputs[0].gasLimit.should.equal(21000);
(tx.gasPrice * tx.outputs[0].gasLimit).should.equal(txOpts.fee);
done();
});
});
});
});
it('should allow to create a TX with multiple outputs and set the correct fee', function(done) {
helpers.stubFeeLevels({
});
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [1, 2], function() {
let amount = 0;
var txOpts = {
outputs: [{
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
amount: amount,
}, {
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
amount: amount,
}],
from,
message: 'some message',
customData: 'some custom data'
};
txOpts = Object.assign(txOpts);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.outputs.should.deep.equal([{
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
gasLimit: 21000,
amount: amount
}, {
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
gasLimit: 21000,
amount: amount
}]);
tx.gasPrice.should.equal(1000000000);
tx.outputs[0].gasLimit.should.equal(21000);
(tx.gasPrice * tx.outputs[0].gasLimit).should.equal(21000000000000);
done();
});
});
});
});
it('should fail if not enough available amount to cover fees', function(done) {
helpers.stubFeeLevels({
});
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [1, 2], function() {
let amount = 2.1 * 1e18;
var txOpts = {
outputs: [{
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
amount: amount,
}],
from,
message: 'some message',
customData: 'some custom data',
fee: 1 * 1e18
};
txOpts = Object.assign(txOpts);
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
done();
});
});
});
});
});
describe("cashAddr backwards compat", (x) => {
/// LEGACY MODE
it('should create a BCH tx proposal with cashaddr outputs (w/o prefix) and return Copay addr', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: cashAddr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
noCashAddr: true,
};
s.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.walletM.should.equal(1);
tx.walletN.should.equal(1);
tx.requiredRejections.should.equal(1);
tx.requiredSignatures.should.equal(1);
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
tx.isPending().should.equal.true;
tx.isTemporary().should.equal.true;
tx.outputs.should.deep.equal([{
toAddress: cashAddr,
amount: amount,
}]);
tx.amount.should.equal(helpers.toSatoshi(0.8));
tx.feePerKb.should.equal(123e2);
should.not.exist(tx.feeLevel);
var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0);
publishOpts.noCashAddr = true;
s.publishTx(publishOpts, function(err, txp) {
txp.changeAddress.address.should.equal('CWwtFMy3GMr5qMEtvEdUDjePfShzkJXCnh');
s.getPendingTxs({ noCashAddr: true }, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
txs[0].outputs.should.deep.equal([{
toAddress: copayAddr,
amount: amount,
}]);
done();
});
});
});
});
});
});
/// CASHADDR MODE
it('should create a BCH tx proposal with cashaddr outputs (w/o prefix) and return CASH addr', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: copayAddr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
noCashAddr: true,
};
s.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.walletM.should.equal(1);
tx.walletN.should.equal(1);
tx.requiredRejections.should.equal(1);
tx.requiredSignatures.should.equal(1);
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
tx.isPending().should.equal.true;
tx.isTemporary().should.equal.true;
tx.outputs.should.deep.equal([{
toAddress: copayAddr,
amount: amount,
}]);
tx.amount.should.equal(helpers.toSatoshi(0.8));
tx.feePerKb.should.equal(123e2);
should.not.exist(tx.feeLevel);
var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0);
publishOpts.noCashAddr = false;
s.publishTx(publishOpts, function(err, txp) {
txp.changeAddress.address.should.equal('qz0d6gueltx0feta7z9777yk97sz9p6peu98mg5vac');
s.getPendingTxs({ noCashAddr: false }, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
txs[0].changeAddress.address.should.equal('qz0d6gueltx0feta7z9777yk97sz9p6peu98mg5vac');
txs[0].outputs.should.deep.equal([{
toAddress: cashAddr,
amount: amount,
}]);
done();
});
});
});
});
});
});
it('should create a BCH tx proposal with cashaddr outputs (w/ prefix) and return Copay addr', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: 'bitcoincash:' + cashAddr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
noCashAddr: true,
};
s.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.walletM.should.equal(1);
tx.walletN.should.equal(1);
tx.requiredRejections.should.equal(1);
tx.requiredSignatures.should.equal(1);
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
tx.isPending().should.equal.true;
tx.isTemporary().should.equal.true;
tx.outputs.should.deep.equal([{
toAddress: 'bitcoincash:' + cashAddr,
amount: amount,
}]);
tx.amount.should.equal(helpers.toSatoshi(0.8));
tx.feePerKb.should.equal(123e2);
should.not.exist(tx.feeLevel);
var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0);
publishOpts.noCashAddr = true;
s.publishTx(publishOpts, function(err, txp) {
txp.changeAddress.address.should.equal('CWwtFMy3GMr5qMEtvEdUDjePfShzkJXCnh');
s.getPendingTxs({ noCashAddr: true }, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
txs[0].outputs.should.deep.equal([{
toAddress: copayAddr,
amount: amount,
}]);
done();
});
});
});
});
});
});
it('should create a BCH tx proposal with cashaddr and keep message', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: cashAddr,
amount: amount,
message: 'xxx',
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
noCashAddr: true,
};
s.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.walletM.should.equal(1);
tx.walletN.should.equal(1);
tx.requiredRejections.should.equal(1);
tx.requiredSignatures.should.equal(1);
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
tx.isPending().should.equal.true;
tx.isTemporary().should.equal.true;
tx.outputs.should.deep.equal([{
toAddress: cashAddr,
amount: amount,
message: 'xxx',
}]);
tx.amount.should.equal(helpers.toSatoshi(0.8));
tx.feePerKb.should.equal(123e2);
should.not.exist(tx.feeLevel);
var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0);
s.publishTx(publishOpts, function(err) {
s.getPendingTxs({ noCashAddr: true }, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
txs[0].outputs.should.deep.equal([{
toAddress: copayAddr,
message: 'xxx',
amount: amount,
}]);
done();
});
});
});
});
});
});
});
describe("cashAddr edge cases (v3 api)", (x) => {
it('should fail to create BCH tx proposal with cashaddr w/prefix', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: 'bitcoincash:' + cashAddr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
};
s.createTx(txOpts, function(err, tx) {
err.message.should.contain('cashaddr wo prefix');
done();
});
});
});
});
it('should fail to create BCH tx proposal with legacy addr ', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: copayAddr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
};
s.createTx(txOpts, function(err, tx) {
err.message.should.contain('cashaddr wo prefix');
done();
});
});
});
});
it('should allow cashaddr on change address', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.createAddresses(s, w, 1, 1, function(mainAddresses, changeAddress) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: cashAddr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
changeAddress: changeAddress[0].address,
};
s.createTx(txOpts, function(err, tx) {
should.not.exist(err);
tx.changeAddress.address.should.equal(changeAddress[0].address);
tx.changeAddress.address.should.equal('qz0d6gueltx0feta7z9777yk97sz9p6peu98mg5vac');
done();
});
});
});
});
});
it('should not allow cashaddr w prefix on change address', function(done) {
let copayAddr = 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X';
let cashAddr = BCHAddressTranslator.translate(copayAddr, 'cashaddr');
let amount = 0.8 * 1e8;
helpers.createAndJoinWallet(1, 1, {
coin: 'bch',
}, function(s, w) {
helpers.createAddresses(s, w, 1, 1, function(mainAddresses, changeAddress) {
helpers.stubUtxos(s, w, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: cashAddr,
amount: amount,
}],
message: 'some message',
customData: 'some custom data',
feePerKb: 123e2,
changeAddress: 'bitcoincash:' + changeAddress[0].address,
};
s.createTx(txOpts, function(err, tx) {
err.message.should.contain('wo prefix');
done();
});
});
});
});
});
});
describe('Transaction notes', function(done) {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should edit a note for an arbitrary txid', function(done) {
server.editTxNote({
txid: '123',
body: 'note body'
}, function(err, note) {
should.not.exist(err);
note.txid.should.equal('123');
note.walletId.should.equal(wallet.id);
note.body.should.equal('note body');
note.editedBy.should.equal(server.copayerId);
note.editedByName.should.equal('copayer 1');
note.createdOn.should.equal(note.editedOn);
server.getTxNote({
txid: '123',
}, function(err, note) {
should.not.exist(err);
should.exist(note);
note.body.should.equal('note body');
note.editedBy.should.equal(server.copayerId);
done();
});
});
});
it('should preserve last edit', function(done) {
var clock = sinon.useFakeTimers({ toFake: ['Date'] });
server.editTxNote({
txid: '123',
body: 'note body'
}, function(err) {
should.not.exist(err);
server.getTxNote({
txid: '123',
}, function(err, note) {
should.not.exist(err);
should.exist(note);
note.editedBy.should.equal(server.copayerId);
note.createdOn.should.equal(note.editedOn);
var creator = note.editedBy;
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
clock.tick(60 * 1000);
server.editTxNote({
txid: '123',
body: 'edited text'
}, function(err) {
should.not.exist(err);
server.getTxNote({
txid: '123',
}, function(err, note) {
should.not.exist(err);
should.exist(note);
note.editedBy.should.equal(server.copayerId);
note.createdOn.should.be.below(note.editedOn);
creator.should.not.equal(note.editedBy);
clock.restore();
done();
});
});
});
});
});
});
it('should edit a note for an outgoing tx and retrieve it', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
message: 'some message',
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
should.exist(txp.txid);
server.editTxNote({
txid: txp.txid,
body: 'note body'
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txp.id,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.note);
txp.note.txid.should.equal(txp.txid);
txp.note.walletId.should.equal(wallet.id);
txp.note.body.should.equal('note body');
txp.note.editedBy.should.equal(server.copayerId);
done();
});
});
});
});
});
});
it('should share notes between copayers', function(done) {
server.editTxNote({
txid: '123',
body: 'note body'
}, function(err) {
should.not.exist(err);
server.getTxNote({
txid: '123',
}, function(err, note) {
should.not.exist(err);
should.exist(note);
note.editedBy.should.equal(server.copayerId);
var creator = note.editedBy;
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.getTxNote({
txid: '123',
}, function(err, note) {
should.not.exist(err);
should.exist(note);
note.body.should.equal('note body');
note.editedBy.should.equal(creator);
done();
});
});
});
});
});
it('should be possible to set an empty note', function(done) {
server.editTxNote({
txid: '123',
body: 'note body'
}, function(err) {
should.not.exist(err);
server.getTxNote({
txid: '123',
}, function(err, note) {
should.not.exist(err);
should.exist(note);
server.editTxNote({
txid: '123',
body: null,
}, function(err) {
should.not.exist(err);
server.getTxNote({
txid: '123',
}, function(err, note) {
should.not.exist(err);
should.exist(note);
note.should.have.property('body');
should.equal(note.body, null);
server.getTxNotes({
minTs: 0
}, function(err, notes) {
should.not.exist(err);
should.exist(notes);
notes.length.should.equal(1);
should.equal(notes[0].body, null);
done();
});
});
});
});
});
});
it('should include the note in tx history listing', function(done) {
helpers.createAddresses(server, wallet, 1, 1, function(mainAddresses, changeAddress) {
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 1000);
server._normalizeTxHistory = function(a, b, c, e, d) { return d(null, b); }
var txs = [{
txid: '123',
blockheight: 100,
height: 100,
fees: 100,
time: 20,
inputs: [{
address: 'external',
amount: 500,
}],
outputs: [{
address: mainAddresses[0].address,
amount: 200,
}],
}];
helpers.stubHistory(null, null, txs);
helpers.stubFeeLevels({
24: 10000,
});
server.editTxNote({
txid: '123',
body: 'just some note'
}, function(err) {
should.not.exist(err);
server.getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(1);
var tx = txs[0];
should.exist(tx.note);
tx.note.body.should.equal('just some note');
tx.note.editedBy.should.equal(server.copayerId);
should.exist(tx.note.editedOn);
done();
});
});
});
});
it('should get all notes edited past a given date', function(done) {
var clock = sinon.useFakeTimers({ toFake: ['Date'] });
async.series([
function(next) {
server.getTxNotes({}, function(err, notes) {
should.not.exist(err);
notes.should.be.empty;
next();
});
},
function(next) {
server.editTxNote({
txid: '123',
body: 'note body'
}, next);
},
function(next) {
server.getTxNotes({
minTs: 0,
}, function(err, notes) {
should.not.exist(err);
notes.length.should.equal(1);
notes[0].txid.should.equal('123');
next();
});
},
function(next) {
clock.tick(60 * 1000);
server.editTxNote({
txid: '456',
body: 'another note'
}, next);
},
function(next) {
server.getTxNotes({
minTs: 0,
}, function(err, notes) {
should.not.exist(err);
notes.length.should.equal(2);
_.difference(notes.map(n => n.txid), ['123', '456']).should.be.empty;
next();
});
},
function(next) {
server.getTxNotes({
minTs: 50,
}, function(err, notes) {
should.not.exist(err);
notes.length.should.equal(1);
notes[0].txid.should.equal('456');
next();
});
},
function(next) {
clock.tick(60 * 1000);
server.editTxNote({
txid: '123',
body: 'an edit'
}, next);
},
function(next) {
server.getTxNotes({
minTs: 100,
}, function(err, notes) {
should.not.exist(err);
notes.length.should.equal(1);
notes[0].txid.should.equal('123');
notes[0].body.should.equal('an edit');
next();
});
},
function(next) {
server.getTxNotes({}, function(err, notes) {
should.not.exist(err);
notes.length.should.equal(2);
next();
});
},
], function(err) {
should.not.exist(err);
clock.restore();
done();
});
});
});
describe('Single-address wallet', function() {
var server, wallet, firstAddress;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, {
singleAddress: true,
}, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, a) {
should.not.exist(err);
should.exist(a.address);
firstAddress = a;
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 1000);
done();
});
});
});
it('should include singleAddress property', function(done) {
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.singleAddress.should.be.true;
done();
});
});
it('should always return same address', function(done) {
firstAddress.path.should.equal('m/0/0');
server.createAddress({}, function(err, x) {
should.not.exist(err);
should.exist(x);
x.path.should.equal('m/0/0');
x.address.should.equal(firstAddress.address);
server.getMainAddresses({}, function(err, addr) {
should.not.exist(err);
addr.length.should.equal(1);
done();
});
});
});
it('should reuse address as change address on tx proposal creation', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
var opts = {
outputs: [{
amount: 1e8,
toAddress: toAddress,
}],
feePerKb: 100e2,
};
server.createTx(opts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
should.exist(txp.changeAddress);
txp.changeAddress.address.should.equal(firstAddress.address);
txp.changeAddress.path.should.equal(firstAddress.path);
done();
});
});
});
it('should not duplicate address on storage after TX creation', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
var opts = {
outputs: [{
amount: 1e8,
toAddress: toAddress,
}],
feePerKb: 100e2,
};
server.createTx(opts, function(err, txp) {
should.not.exist(err);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
done();
});
});
});
});
it('should not be able to specify custom changeAddress', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
var opts = {
outputs: [{
amount: 1e8,
toAddress: toAddress,
}],
feePerKb: 100e2,
changeAddress: firstAddress.address,
};
server.createTx(opts, function(err, txp) {
should.exist(err);
err.message.should.contain('single-address');
done();
});
});
});
// this tests seems irrelevant for v8
it.skip('should correctly handle change in tx history', function(done) {
});
});
describe('#getSendMaxInfo', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
done();
});
});
function sendTx(info, cb) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: info.amount,
}],
inputs: info.inputs,
fee: info.fee,
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
var t = ChainService.getBitcoreTx(tx);
t.toObject().inputs.length.should.equal(info.inputs.length);
t.getFee().should.equal(info.fee);
should.not.exist(t.getChangeOutput());
return cb();
});
};
it('should be able to get send max info on 0 utxo wallet', function(done) {
server.getSendMaxInfo({
feePerKb: 10000,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.size.should.equal(0);
info.amount.should.equal(0);
info.fee.should.equal(0);
info.inputs.should.be.empty;
info.utxosBelowFee.should.equal(0);
info.amountBelowFee.should.equal(0);
info.utxosAboveMaxSize.should.equal(0);
info.amountAboveMaxSize.should.equal(0);
done();
});
});
it('should correctly get send max info', function(done) {
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({
feePerKb: 10000,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.inputs.length.should.equal(4);
info.size.should.equal(1238);
info.fee.should.equal(info.size * 10000 / 1000.);
info.amount.should.equal(1e8 - info.fee);
info.utxosBelowFee.should.equal(0);
info.amountBelowFee.should.equal(0);
info.utxosAboveMaxSize.should.equal(0);
info.amountAboveMaxSize.should.equal(0);
sendTx(info, done);
});
});
});
it('should correctly get send max info when resulting amount is below dust', function(done) {
helpers.stubUtxos(server, wallet, [300e-6, 300e-6], function() {
server.getSendMaxInfo({
feePerKb: 500e2,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.size.should.equal(646);
info.fee.should.equal(32300);
info.amount.should.equal(27700);
var _min_output_amount = Defaults.MIN_OUTPUT_AMOUNT;
Defaults.MIN_OUTPUT_AMOUNT = 300e2;
server.getSendMaxInfo({
feePerKb: 500e2,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.size.should.equal(0);
info.amount.should.equal(0);
Defaults.MIN_OUTPUT_AMOUNT = _min_output_amount;
done();
});
});
});
});
describe('Fee level', function() {
it('should correctly get send max info using feeLevel', function(done) {
helpers.stubFeeLevels({
1: 400e2,
2: 200e2,
6: 180e2,
24: 90e2,
});
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({
feeLevel: 'economy',
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.feePerKb.should.equal(180e2);
info.fee.should.equal(info.size * 180e2 / 1000.);
sendTx(info, done);
});
});
});
it('should assume "normal" fee level if not specified', function(done) {
helpers.stubFeeLevels({
1: 400e2,
2: 200e2,
6: 180e2,
24: 90e2,
});
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.feePerKb.should.equal(200e2);
info.fee.should.equal(info.size * 200e2 / 1000.);
done();
});
});
});
it('should fail on invalid fee level', function(done) {
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({
feeLevel: 'madeUpLevel',
}, function(err, info) {
should.exist(err);
should.not.exist(info);
err.toString().should.contain('Invalid fee level');
done();
});
});
});
});
it('should return inputs in random order', function(done) {
// NOTE: this test has a chance of failing of 1 in 1'073'741'824 :P
const range1to30 = new Array(30).fill(0).map((_, i) => i + 1);
helpers.stubUtxos(server, wallet, range1to30, function(utxos) {
server.getSendMaxInfo({
feePerKb: 100e2,
returnInputs: true
}, function(err, info) {
should.not.exist(err);
should.exist(info);
var amounts = info.inputs.map(i => i.satoshis);
amounts.length.should.equal(30);
amounts.every(function(amount, i) {
if (i == 0) return true;
return amount < amounts[i - 1];
}).should.be.false;
done();
});
});
});
it('should exclude unconfirmed inputs', function(done) {
helpers.stubUtxos(server, wallet, ['u0.1', 0.2, 0.3, 0.4], function() {
server.getSendMaxInfo({
feePerKb: 10000,
excludeUnconfirmedUtxos: true,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.inputs.length.should.equal(3);
info.size.should.equal(942);
info.fee.should.equal(info.size * 10000 / 1000.);
info.amount.should.equal(0.9e8 - info.fee);
sendTx(info, done);
});
});
});
it('should exclude locked inputs', function(done) {
helpers.stubUtxos(server, wallet, ['u0.1', 0.1, 0.1, 0.1], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.09e8,
}],
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
server.getSendMaxInfo({
feePerKb: 10000,
excludeUnconfirmedUtxos: true,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.inputs.length.should.equal(2);
info.size.should.equal(646);
info.fee.should.equal(info.size * 10000 / 1000.);
info.amount.should.equal(0.2e8 - info.fee);
sendTx(info, done);
});
});
});
});
it('should ignore utxos not contributing to total amount (below their cost in fee)', function(done) {
// 10 sat and 100 sat should be completely ignored. (under dust)
helpers.stubUtxos(server, wallet, ['u0.1', '100 sat', 0.2, 0.3, 0.4, '10bit', '100bit', '200bit', '10 sat'], function() {
server.getSendMaxInfo({
feePerKb: 0.001e8,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.inputs.length.should.equal(4);
info.size.should.equal(1238);
info.fee.should.equal(info.size * 0.001e8 / 1000.);
info.amount.should.equal(1e8 - info.fee);
info.utxosBelowFee.should.equal(3);
info.amountBelowFee.should.equal(310e2);
server.getSendMaxInfo({
feePerKb: 0.0001e8,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.inputs.length.should.equal(6);
info.size.should.equal(1830);
info.fee.should.equal(info.size * 0.0001e8 / 1000.);
info.amount.should.equal(1.0003e8 - info.fee);
info.utxosBelowFee.should.equal(1);
info.amountBelowFee.should.equal(1e3);
sendTx(info, done);
});
});
});
});
it('should work when all inputs are below their cost in fee', function(done) {
helpers.stubUtxos(server, wallet, ['u 10bit', '10bit', '20bit'], function() {
server.getSendMaxInfo({
feePerKb: 500e2,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.inputs.should.be.empty;
info.size.should.equal(0);
info.fee.should.equal(0);
info.amount.should.equal(0);
info.utxosBelowFee.should.equal(3);
info.amountBelowFee.should.equal(40e2);
done();
});
});
});
it('should not go beyond max tx size', function(done) {
Defaults.MAX_TX_SIZE_IN_KB_BTC = 2;
Defaults.MAX_TX_SIZE_IN_KB_ETH = 2;
Defaults.MAX_TX_SIZE_IN_KB_XRP = 2;
helpers.stubUtxos(server, wallet, new Array(9).fill(1), function() {
server.getSendMaxInfo({
feePerKb: 10000,
returnInputs: true,
}, function(err, info) {
should.not.exist(err);
should.exist(info);
info.size.should.be.below(2000);
info.inputs.length.should.be.below(9);
info.utxosAboveMaxSize.should.equal(3);
info.amountAboveMaxSize.should.equal(3e8);
sendTx(info, done);
});
});
});
})
describe('Check requiredFeeRate BTC', function() {
var server, wallet;
// some of these tests, particularly case 26, can run a bit long
// and cause the ci pipeline to fail
this.timeout(4000);
beforeEach(function(done) {
helpers.stubFeeLevels({
1: 40002,
2: 20000,
6: 18000,
24: 9001,
}, true);
done();
});
const cases = [
{
name: 'Legacy, sendmax',
requiredFeeRate: 10000,
sendMax: true,
fromSegwit: false,
utxos: [1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
name: 'Segwit, sendmax',
requiredFeeRate: 10000,
sendMax: true,
fromSegwit: true,
utxos: [1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
name: 'Segwit, sendmax, 4 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
utxos: [0.1, 0.2, 0.3, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
name: 'Segwit, sendmax, 3 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
utxos: [0.1, 0.3, 0.4],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10, // overwritten in sendMax
}],
},
{
name: 'Segwit, non-sendmax, 2 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [1, 2],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
name: 'Segwit, non-sendmax, 3 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: ['100000 sat', '20000 sat', 1],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
name: 'Segwit, non-sendmax, 1 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [1.2],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
vSize: 144,
},
{
name: 'Segwit, non-sendmax, 3 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [0.4, 0.4, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
},
// CASE 8
{
name: 'Segwit, non-sendmax, 6 inputs',
requiredFeeRate: 30000,
fromSegwit: true,
utxos: [0.2, 0.2, 0.1, 0.1, 0.3, 0.15],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 1e8,
}],
},
{
n: 2,
name: 'Legacy, sendmax',
requiredFeeRate: 10000,
sendMax: true,
fromSegwit: false,
utxos: [1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
n: 2,
vSize: 123, // from https://btc.com/tools/tx/decode
name: 'Segwit, sendmax',
requiredFeeRate: 10000,
sendMax: true,
fromSegwit: true,
utxos: [1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
n: 2,
name: 'Segwit, sendmax, 4 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
utxos: [0.1, 0.2, 0.3, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
n: 2,
name: 'Segwit, sendmax, 3 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
utxos: [0.1, 0.3, 0.4],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10, // overwritten in sendMax
}],
},
{
n: 2,
name: 'Segwit, non-sendmax, 2 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [1, 2],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
n: 2,
name: 'Segwit, non-sendmax, 3 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: ['100000 sat', '20000 sat', 1],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
n: 2,
name: 'Segwit, non-sendmax, 1 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [1.1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
vSize: 144,
},
{
n: 2,
vSize: 321,
name: 'Segwit, non-sendmax, 3 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [0.4, 0.4, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
},
{
n: 2,
name: 'Segwit, non-sendmax, 6 inputs',
requiredFeeRate: 30000,
fromSegwit: true,
utxos: [0.2, 0.2, 0.1, 0.1, 0.3, 0.15],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 1e8,
}],
},
{
m: 2,
n: 3,
name: 'Legacy, sendmax',
requiredFeeRate: 10000,
sendMax: true,
fromSegwit: false,
utxos: [1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
m: 2,
n: 3,
vSize: 123, // from https://btc.com/tools/tx/decode
name: 'Segwit, sendmax',
requiredFeeRate: 10000,
sendMax: true,
fromSegwit: true,
utxos: [1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
m: 2,
n: 3,
name: 'Segwit, sendmax, 4 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
utxos: [0.1, 0.2, 0.3, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
m: 2,
n: 3,
name: 'Segwit, sendmax, 3 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
utxos: [0.1, 0.3, 0.4],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10, // overwritten in sendMax
}],
},
{
m: 2,
n: 3,
name: 'Segwit, non-sendmax, 2 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [1, 2],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
m: 2,
n: 3,
name: 'Segwit, non-sendmax, 3 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: ['100000 sat', '20000 sat', 1],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
m: 2,
n: 3,
name: 'Segwit, non-sendmax, 1 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [1.1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
vSize: 144,
},
{
m: 2,
n: 3,
vSize: 321,
name: 'Segwit, non-sendmax, 3 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
utxos: [0.4, 0.4, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
},
{
m: 2,
n: 3,
name: 'Segwit, non-sendmax, 6 inputs',
requiredFeeRate: 30000,
fromSegwit: true,
utxos: [0.2, 0.2, 0.1, 0.1, 0.3, 0.15],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 1e8,
}],
},
// Taproot
{
name: 'Taproot, sendmax',
requiredFeeRate: 10000,
sendMax: true,
fromSegwit: true,
segwitVersion: 1,
utxos: [1],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
name: 'Taproot, sendmax, 4 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
segwitVersion: 1,
utxos: [0.1, 0.2, 0.3, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10, // overwritten in sendMax
}],
},
{
name: 'Taproot, sendmax, 3 inputs',
requiredFeeRate: 25000,
sendMax: true,
fromSegwit: true,
segwitVersion: 1,
utxos: [0.1, 0.3, 0.4],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10, // overwritten in sendMax
}],
},
{
name: 'Taproot, non-sendmax, 2 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
segwitVersion: 1,
utxos: [1, 2],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
name: 'Taproot, non-sendmax, 3 inputs',
requiredFeeRate: 25000,
fromSegwit: true,
segwitVersion: 1,
utxos: ['100000 sat', '20000 sat', 1],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 10000,
}],
vSize: 141, // from https://btc.com/tools/tx/decode
},
{
name: 'Taproot, non-sendmax, 1 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
segwitVersion: 1,
utxos: [1.2],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
vSize: 144,
},
{
name: 'Taproot, non-sendmax, 3 inputs, 1 legacy output',
requiredFeeRate: 25000,
fromSegwit: true,
segwitVersion: 1,
utxos: [0.4, 0.4, 0.4],
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1e8,
}],
},
// CASE 34
{
name: 'Taproot, non-sendmax, 6 inputs',
requiredFeeRate: 30000,
fromSegwit: true,
segwitVersion: 1,
utxos: [0.2, 0.2, 0.1, 0.1, 0.3, 0.15],
outputs: [{
toAddress: 'bc1q9ytgh0jywlxv0zr8w3ytd6z5rpgct6tuvmh4pl',
amount: 1e8,
}],
},
];
function checkTx(txOpts, x, cb) {
function sign(copayerM, tx, cb) {
helpers.getAuthServer(wallet.copayers[copayerM].id, function(server) {
var signatures = helpers.clientSign(tx, TestData.copayers[copayerM].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: tx.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err, err);
if (++copayerM == x.m) {
return cb(txp);
} else {
return sign(copayerM, tx, cb);
}
});
});
}
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
sign(0, tx, (txp) => {
should.exist(txp.raw);
txp.status.should.equal('accepted');
var t = ChainService.getBitcoreTx(txp);
// Check size and fee rate
(txp.raw.length / 2).should.equal(t.size);
const feeRate = t.getFee() / (x.fromSegwit ? t.vsize : t.size) * 1000;
console.log(`Wire Size:${t.size} vSize: ${t.vsize} (Segwit: ${x.fromSegwit}, SegwitVersion: ${x.segwitVersion || 0}) Fee: ${t.getFee()} ActualRate:${Math.round(feeRate)} RequiredRate:${x.requiredFeeRate}`);
// size should be above (or equal) the required FeeRate
feeRate.should.not.be.below(x.requiredFeeRate);
feeRate.should.be.below(x.requiredFeeRate * 1.5); // no more that 50% extra
return cb(feeRate);
});
});
};
for (let i = 0; i < cases.length; i++) {
const x = cases[i];
x.i = i;
x.m = x.m || 1;
x.n = x.n || 1;
it(`case ${i++} : ${x.name} (${x.m}-of-${x.n})`, function(done) {
helpers.createAndJoinWallet(x.m, x.n, {useNativeSegwit: x.fromSegwit, segwitVersion: x.segwitVersion}, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, x.utxos, function() {
server.getSendMaxInfo({
feePerKb: x.requiredFeeRate,
returnInputs: true,
}, function(err, info) {
should.not.exist(err, err);
should.exist(info);
var txOpts = {
outputs: x.outputs,
};
if (x.sendMax) {
txOpts.fee = info.fee;
txOpts.inputs = info.inputs;
txOpts.outputs[0].amount = info.amount;
} else {
txOpts.feePerKb = x.requiredFeeRate;
}
txOpts.payProUrl = 'aaa.com';
// CASE 8
checkTx(txOpts, x, (fee1) => {
if (x.i == 8) {
helpers.beforeEach(() => {
// check with paypro fee is bigger.
console.log(`## case ${x.i} : Again with no paypro`);
helpers.createAndJoinWallet(x.m, x.n, { useNativeSegwit: x.fromSegwit }, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, x.utxos, function() {
txOpts.payProUrl = null;
checkTx(txOpts, x, (fee2) => {
console.log(`## Fee PayPro: ${fee1} vs ${fee2}`);
fee1.should.be.above(fee2);
done();
});
});
});
});
} else {
done();
}
});
});
});
});
});
}
});
describe('Check requiredFeeRate DOGE', function() {
var server, wallet;
beforeEach(function(done) {
helpers.stubFeeLevels({
1: 40002,
2: 1e8,
24: 0.5e8,
}, true);
done();
});
const cases = [
{
name: 'Legacy',
requiredFeeRate: 755000,
utxos: [100],
outputs: [{
toAddress: 'DMHR9z3hVfEMkfsxfP7CbVtYdPh2f5ESqo',
amount: 2048378600,
}],
},
{
n: 2,
name: 'Legacy, sendmax',
requiredFeeRate: 755000,
sendMax: true,
fromSegwit: false,
utxos: [100],
outputs: [{
toAddress: 'DMHR9z3hVfEMkfsxfP7CbVtYdPh2f5ESqo',
}],
},
{
n: 2,
name: 'Legacy, above min relay fee',
requiredFeeRate: 1e8,
fromSegwit: false,
utxos: Array(10).fill(1), // 10 utxo's of 1 DOGE each
outputs: [{
toAddress: 'DMHR9z3hVfEMkfsxfP7CbVtYdPh2f5ESqo',
amount: 8e8
}]
},
];
function checkTx(txOpts, x, cb) {
function sign(copayerM, tx, cb) {
helpers.getAuthServer(wallet.copayers[copayerM].id, function(server) {
var signatures = helpers.clientSign(tx, TestData.copayers[copayerM].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: tx.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err, err);
if (++copayerM == x.m) {
return cb(txp);
} else {
return sign(copayerM, tx, cb);
}
});
});
}
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
sign(0, tx, (txp) => {
should.exist(txp.raw);
// console.log('[server.js.7038]', txp.raw); // TODO
txp.status.should.equal('accepted');
//console.log('[server.js.6981:txp:]',txp); // TODO
var t = ChainService.getBitcoreTx(txp);
const vSize = x.vSize || t._estimateSize(); // use given vSize if available
// Check size and fee rate
const actualSize = txp.raw.length / 2;
const actualFeeRate = t.getFee() / (x.fromSegwit ? vSize : actualSize) * 1000;
//console.log('[server.js.7001:log:]',txp.raw); // TODO
console.log(`Wire Size:${actualSize} vSize: ${vSize} (Segwit: ${x.fromSegwit}) Fee: ${t.getFee()} ActualRate:${Math.round(actualFeeRate)} RequiredRate:${x.requiredFeeRate}`);
// Fee should be more than min relay fee
t.getFee().should.be.gte(CWC.BitcoreLibDoge.Transaction.DUST_AMOUNT);
if (t.getFee() > CWC.BitcoreLibDoge.Transaction.DUST_AMOUNT) {
// size should be above (or equal) the required FeeRate
actualFeeRate.should.not.be.below(x.requiredFeeRate);
actualFeeRate.should.be.below(x.requiredFeeRate * 1.5); // no more that 50% extra
}
return cb(actualFeeRate);
});
});
};
for (let i = 0; i < cases.length; i++) {
const x = cases[i];
x.i = i;
x.m = x.m || 1;
x.n = x.n || 1;
it(`case ${i++} : ${x.name} (${x.m}-of-${x.n})`, function(done) {
helpers.createAndJoinWallet(x.m, x.n, { useNativeSegwit: x.fromSegwit, coin: 'doge' }, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, x.utxos, function() {
server.getSendMaxInfo({
feePerKb: x.requiredFeeRate,
returnInputs: true,
}, function(err, info) {
should.not.exist(err, err);
should.exist(info);
var txOpts = {
outputs: x.outputs,
};
if (x.sendMax) {
txOpts.fee = info.fee;
txOpts.inputs = info.inputs;
txOpts.outputs[0].amount = info.amount;
} else {
txOpts.feePerKb = x.requiredFeeRate;
}
txOpts.payProUrl = 'aaa.com';
// CASE 8
checkTx(txOpts, x, (fee1) => {
if (x.i == 8) {
helpers.beforeEach(() => {
// check with paypro fee is bigger.
console.log(`## case ${x.i} : Again with no paypro`);
helpers.createAndJoinWallet(x.m, x.n, { useNativeSegwit: x.fromSegwit }, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, x.utxos, function() {
txOpts.payProUrl = null;
checkTx(txOpts, x, (fee2) => {
console.log(`## Fee PayPro: ${fee1} vs ${fee2}`);
fee1.should.be.above(fee2);
done();
});
});
});
});
} else {
done();
}
});
});
});
});
});
}
});
describe('#rejectTx', function() {
var server, wallet, txid;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 2, function(s, w) {
server = s;
wallet = w;
const range1to8 = new Array(8).fill(0).map((_, i) => i + 1);
helpers.stubUtxos(server, wallet, range1to8, function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10e8,
}],
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
txid = tx.id;
done();
});
});
});
});
it('should reject a TX', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
server.rejectTx({
txProposalId: txid,
reason: 'some reason',
}, function(err) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
server.getTx({
txProposalId: txid
}, function(err, tx) {
var actors = tx.getActors();
actors.length.should.equal(1);
actors[0].should.equal(wallet.copayers[0].id);
var action = tx.getActionBy(wallet.copayers[0].id);
action.type.should.equal('reject');
action.comment.should.equal('some reason');
done();
});
});
});
});
});
it('should fail to reject non-pending TX', function(done) {
async.waterfall([
function(next) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
next();
});
},
function(next) {
server.rejectTx({
txProposalId: txid,
reason: 'some reason',
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.rejectTx({
txProposalId: txid,
reason: 'some other reason',
}, function(err) {
should.exist(err);
err.code.should.equal('TX_NOT_PENDING');
done();
});
});
},
]);
});
});
describe('#signTx', function() {
describe('1-of-1 (BIP44 & P2PKH)', function() {
var server, wallet, txid;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, from) {
should.not.exist(err);
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 2.5e8,
}],
from,
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
tx.addressType.should.equal('P2PKH');
txid = tx.id;
done();
});
});
});
});
});
it('should sign a TX with multiple inputs, different paths, and return raw', function(done) {
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
should.not.exist(tx.raw);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
txp.status.should.equal('accepted');
// The raw Tx should contain the Signatures.
txp.raw.should.contain(signatures[0]);
// Get pending should also contains the raw TX
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
should.not.exist(err);
tx.status.should.equal('accepted');
tx.raw.should.contain(signatures[0]);
done();
});
});
});
});
});
describe('1-of-1 (BIP44 ETH)', function() {
var server, wallet, txpid;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'eth' }, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, from) {
should.not.exist(err);
helpers.stubUtxos(server, wallet, [3], function() {
var txOpts = {
outputs: [{
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
amount: 2.5e8,
}],
from,
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
tx.addressType.should.equal('P2PKH');
txpid = tx.id;
done();
});
});
});
});
});
it('should sign a TX and return raw', function(done) {
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txpid);
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
should.not.exist(tx.raw);
server.signTx({
txProposalId: txpid,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
txp.status.should.equal('accepted');
// The raw Tx should contain the Signatures.
// Raw hash String
txp.raw[0].length.should.equal(208);
// Array length of 1
txp.raw.length.should.equal(1);
// this depends on transaction count stub
txp.txid.should.equal('0x7805fbd1b393552dc3be013fcfdc00f5ba30a6c7931ca7c3b9832d9f69fbf7bc');
// Get pending should also contains the raw TX
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
should.not.exist(err);
tx.status.should.equal('accepted');
// Raw hash String
txp.raw[0].length.should.equal(208);
// Array length of 1
txp.raw.length.should.equal(1);
done();
});
});
});
});
it('should fail sign a TX with empty signature', function(done) {
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txpid);
should.not.exist(tx.raw);
server.signTx({
txProposalId: txpid,
signatures: '',
}, function(err, txp) {
err.code.should.contain('BAD_SIG');
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
should.not.exist(err);
tx.status.should.equal('pending');
done();
});
});
});
});
it('should fail sign a TX with wrong signature', function(done) {
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txpid);
should.not.exist(tx.raw);
server.signTx({
txProposalId: txpid,
signatures: 'a bad signature',
}, function(err, txp) {
err.code.should.contain('BAD_SIG');
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
should.not.exist(err);
tx.status.should.equal('pending');
done();
});
});
});
});
});
describe('Multisig', function() {
var server, wallet, txid;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
const range1to8 = new Array(8).fill(0).map((_, i) => i + 1);
helpers.stubUtxos(server, wallet, range1to8, function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 20e8,
}],
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
should.exist(tx);
txid = tx.id;
done();
});
});
});
});
it('should sign a TX with multiple inputs, different paths', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.not.exist(tx.raw);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
var tx = txs[0];
tx.id.should.equal(txid);
var actors = tx.getActors();
actors.length.should.equal(1);
actors[0].should.equal(wallet.copayers[0].id);
tx.getActionBy(wallet.copayers[0].id).type.should.equal('accept');
done();
});
});
});
});
it('should fail to sign with a xpriv from other copayer', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, TestData.copayers[1].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
err.code.should.equal('BAD_SIGNATURES');
done();
});
});
});
it('should fail if one signature is broken', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
signatures[0] = 1;
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
err.message.should.contain('signatures');
done();
});
});
});
it('should fail on invalid signature', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = ['11', '22', '33', '44', '55'];
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
should.exist(err);
err.message.should.contain('Bad signatures');
done();
});
});
});
it('should fail on wrong number of invalid signatures', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H).slice(0, tx.inputs.length - 1);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
should.exist(err);
err.message.should.contain('Bad signatures');
done();
});
});
});
it('should fail when signing a TX previously rejected', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
server.rejectTx({
txProposalId: txid,
}, function(err) {
err.code.should.contain('COPAYER_VOTED');
done();
});
});
});
});
it('should fail when rejected a previously signed TX', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
server.rejectTx({
txProposalId: txid,
}, function(err) {
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
err.code.should.contain('COPAYER_VOTED');
done();
});
});
});
});
it('should fail to sign a non-pending TX', function(done) {
async.waterfall([
function(next) {
server.rejectTx({
txProposalId: txid,
reason: 'some reason',
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.rejectTx({
txProposalId: txid,
reason: 'some reason',
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[2].id, function(server) {
server.getTx({
txProposalId: txid
}, function(err, tx) {
should.not.exist(err);
var signatures = helpers.clientSign(tx, TestData.copayers[2].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
should.exist(err);
err.code.should.equal('TX_NOT_PENDING');
done();
});
});
});
},
]);
});
});
});
describe('#broadcastTx & #broadcastRawTx', function() {
var server, wallet, txpid, txid;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, [10, 10], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 9e8,
}],
message: 'some message',
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.isAccepted().should.be.true;
txp.isBroadcasted().should.be.false;
txid = txp.txid;
txpid = txp.id;
done();
});
});
});
});
});
it('should broadcast a tx', function(done) {
var clock = sinon.useFakeTimers({ now: 1234000, toFake: ['Date'] });
helpers.stubBroadcast(txid);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.raw);
// used to be like this. No sure why we won't like raw to be shown.
//should.not.exist(txp.raw);
txp.txid.should.equal(txid);
txp.isBroadcasted().should.be.true;
txp.broadcastedOn.should.equal(1234);
clock.restore();
done();
});
});
});
it('should broadcast a tx and set locktime & version', function(done) {
var clock = sinon.useFakeTimers({ now: 1234000, toFake: ['Date'] });
helpers.stubBroadcast(txid);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.raw);
const tx = new Bitcore.Transaction(txp.raw);
clock.restore();
done();
});
});
});
it('should broadcast a raw tx', function(done) {
helpers.stubBroadcast(txid);
server.broadcastRawTx({
network: 'testnet',
rawTx: 'raw tx',
}, function(err, txid) {
should.not.exist(err);
should.exist(txid);
done();
});
});
it('should fail to brodcast a tx already marked as broadcasted', function(done) {
helpers.stubBroadcast(txid);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.code.should.equal('TX_ALREADY_BROADCASTED');
done();
});
});
});
it('should fail to brodcast a tx is txid is wrong', function(done) {
helpers.stubBroadcast('xx');
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.should.contain('broadcast error');
done();
});
});
it('should auto process already broadcasted txs', function(done) {
helpers.stubBroadcast(txid);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: 999
});
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(0);
done();
});
});
});
it('should process only broadcasted txs', function(done) {
helpers.stubBroadcast(txid);
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 9e8,
}],
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(2);
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: 999
});
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
txs[0].status.should.equal('pending');
should.not.exist(txs[0].txid);
done();
});
});
});
});
it('should fail to brodcast a not yet accepted tx', function(done) {
helpers.stubBroadcast(txid);
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 9e8,
}],
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
server.broadcastTx({
txProposalId: txp.id
}, function(err) {
should.exist(err);
err.code.should.equal('TX_NOT_ACCEPTED');
done();
});
});
});
it('should keep tx as accepted if unable to broadcast it', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.toString().should.equal('broadcast error');
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.false;
should.not.exist(txp.broadcastedOn);
txp.isAccepted().should.be.true;
done();
});
});
});
it('should mark tx as broadcasted if accepted but already in blockchain', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: '999'
});
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.true;
should.exist(txp.broadcastedOn);
done();
});
});
});
it('should keep tx as accepted if broadcast fails and cannot check tx in blockchain', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, 'bc check error');
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.toString().should.equal('bc check error');
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.false;
should.not.exist(txp.broadcastedOn);
txp.isAccepted().should.be.true;
done();
});
});
});
});
describe('#broadcastTx ETH', function() {
var server, wallet, txpid, txid;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'eth' }, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [10, 10], function() {
var txOpts = {
outputs: [{
toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A',
amount: 9e8,
}],
from,
message: 'some message',
feePerKb: 100e2,
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.isAccepted().should.be.true;
txp.isBroadcasted().should.be.false;
txid = txp.txid;
txpid = txp.id;
done();
});
});
});
});
});
});
it('should broadcast a tx', function(done) {
var clock = sinon.useFakeTimers({ now: 1234000, toFake: ['Date'] });
helpers.stubBroadcast(txid);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.raw);
// used to be like this. No sure why we won't like raw to be shown.
//should.not.exist(txp.raw);
txp.txid.should.equal(txid);
txp.isBroadcasted().should.be.true;
txp.broadcastedOn.should.equal(1234);
clock.restore();
done();
});
});
});
});
describe('Tx proposal workflow', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
const range1to8 = new Array(8).fill(0).map((_, i) => i + 1);
helpers.stubUtxos(server, wallet, range1to8, function() {
done();
});
});
});
it('other copayers should see pending proposal created by one copayer', function(done) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10e8
}],
feePerKb: 100e2,
message: 'some message',
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
helpers.getAuthServer(wallet.copayers[1].id, function(server2, wallet) {
server2.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txps.length.should.equal(1);
txps[0].id.should.equal(txp.id);
txps[0].message.should.equal('some message');
done();
});
});
});
});
it('tx proposals should not be finally accepted until quorum is reached', function(done) {
var txpId;
async.waterfall([
function(next) {
server.createAddress({}, from => {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10e8
}],
from,
feePerKb: 100e2,
message: 'some message',
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
txpId = txp.id;
should.exist(txp);
next();
});
});
},
function(next) {
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txps.length.should.equal(1);
var txp = txps[0];
txp.actions.should.be.empty;
next(null, txp);
});
},
function(txp, next) {
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txpId,
signatures: signatures,
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txps.length.should.equal(1);
var txp = txps[0];
txp.isPending().should.be.true;
txp.isAccepted().should.be.false;
txp.isRejected().should.be.false;
txp.isBroadcasted().should.be.false;
txp.actions.length.should.equal(1);
var action = txp.getActionBy(wallet.copayers[0].id);
action.type.should.equal('accept');
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var last = notifications[notifications.length - 1];
last.type.should.not.equal('TxProposalFinallyAccepted');
next(null, txp);
});
});
},
function(txp, next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) {
var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txpId,
signatures: signatures,
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txps.length.should.equal(1);
var txp = txps[0];
txp.isPending().should.be.true;
txp.isAccepted().should.be.true;
txp.isBroadcasted().should.be.false;
should.exist(txp.txid);
txp.actions.length.should.equal(2);
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var last = notifications[notifications.length - 1];
last.type.should.equal('TxProposalFinallyAccepted');
last.walletId.should.equal(wallet.id);
last.creatorId.should.equal(wallet.copayers[1].id);
last.data.txProposalId.should.equal(txp.id);
done();
});
});
},
]);
});
it('tx proposals should accept as many rejections as possible without finally rejecting', function(done) {
var txpId;
async.waterfall([
function(next) {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 10e8
}],
feePerKb: 100e2,
message: 'some message',
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
txpId = txp.id;
should.exist(txp);
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txps.length.should.equal(1);
var txp = txps[0];
txp.actions.should.be.empty;
next();
});
},
function(next) {
server.rejectTx({
txProposalId: txpId,
reason: 'just because'
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txps.length.should.equal(1);
var txp = txps[0];
txp.isPending().should.be.true;
txp.isRejected().should.be.false;
txp.isAccepted().should.be.false;
txp.actions.length.should.equal(1);
var action = txp.getActionBy(wallet.copayers[0].id);
action.type.should.equal('reject');
action.comment.should.equal('just because');
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) {
server.rejectTx({
txProposalId: txpId,
reason: 'some other reason'
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txps.length.should.equal(0);
next();
});
},
function(next) {
server.getTx({
txProposalId: txpId
}, function(err, txp) {
should.not.exist(err);
txp.isPending().should.be.false;
txp.isRejected().should.be.true;
txp.isAccepted().should.be.false;
txp.actions.length.should.equal(2);
done();
});
},
]);
});
});
describe('#getTx', function() {
var server, wallet, txpid;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, 1, function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.5e8
}],
feePerKb: 100e2,
message: 'some message',
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
txpid = txp.id;
done();
});
});
});
});
it('should get own transaction proposal', function(done) {
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.id.should.equal(txpid);
done();
});
});
it('should get someone elses transaction proposal', function(done) {
helpers.getAuthServer(wallet.copayers[1].id, function(server2, wallet) {
server2.getTx({
txProposalId: txpid
}, function(err, res) {
should.not.exist(err);
res.id.should.equal(txpid);
done();
});
});
});
it('should fail to get non-existent transaction proposal', function(done) {
server.getTx({
txProposalId: 'dummy'
}, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.code.should.equal('TX_NOT_FOUND')
err.message.should.equal('Transaction proposal not found');
done();
});
});
it.skip('should get accepted/rejected transaction proposal', function(done) { });
it.skip('should get broadcasted transaction proposal', function(done) { });
});
describe('#getTxs', function() {
var server, wallet, clock;
beforeEach(function(done) {
this.timeout(5000);
clock = sinon.useFakeTimers({ toFake: ['Date'] });
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
const range1to10 = new Array(10).fill(0).map((_, i) => i + 1);
helpers.stubUtxos(server, wallet, range1to10, function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.1e8
}],
feePerKb: 100e2,
message: 'some message',
};
async.eachSeries(new Array(10).fill(0), function(i, next) {
clock.tick(10 * 1000);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
next();
});
}, function(err) {
clock.restore();
return done(err);
});
});
});
});
afterEach(function() {
// clock.restore();
});
it('should pull 4 txs, down to to time 60', function(done) {
server.getTxs({
minTs: 60,
limit: 8
}, function(err, txps) {
should.not.exist(err);
var times = txps.map(txp => txp.createdOn);
times.should.deep.equal([100, 90, 80, 70, 60]);
done();
});
});
it('should pull the first 5 txs', function(done) {
server.getTxs({
maxTs: 50,
limit: 5
}, function(err, txps) {
should.not.exist(err);
var times = txps.map(txp => txp.createdOn);
times.should.deep.equal([50, 40, 30, 20, 10]);
done();
});
});
it('should pull the last 4 txs', function(done) {
server.getTxs({
limit: 4
}, function(err, txps) {
should.not.exist(err);
var times = txps.map(txp => txp.createdOn);
times.should.deep.equal([100, 90, 80, 70]);
done();
});
});
it('should pull all txs', function(done) {
server.getTxs({}, function(err, txps) {
should.not.exist(err);
var times = txps.map(txp => txp.createdOn);
times.should.deep.equal([100, 90, 80, 70, 60, 50, 40, 30, 20, 10]);
done();
});
});
it('should txs from times 50 to 70',
function(done) {
server.getTxs({
minTs: 50,
maxTs: 70,
}, function(err, txps) {
should.not.exist(err);
var times = txps.map(txp => txp.createdOn);
times.should.deep.equal([70, 60, 50]);
done();
});
});
});
describe('#getNotifications', function() {
var clock;
var server, wallet;
beforeEach(function(done) {
clock = sinon.useFakeTimers({ now: 10 * 1000, toFake: ['Date'] });
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
clock.tick(25 * 1000);
const range0to4 = new Array(4).fill(0).map((_, i) => i);
helpers.stubUtxos(server, wallet, range0to4, function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.1e8
}],
feePerKb: 100e2,
message: 'some message',
};
async.eachSeries(new Array(3).fill(0), function(i, next) {
clock.tick(25 * 1000);
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
next();
});
}, function(err) {
clock.tick(20 * 1000);
return done(err);
});
});
});
});
afterEach(function() {
clock.restore();
});
it('should pull all notifications', function(done) {
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var types = notifications.map(n => n.type);
types.should.deep.equal(['NewCopayer', 'NewAddress', 'NewAddress', 'NewTxProposal', 'NewTxProposal', 'NewTxProposal']);
var walletIds = _.uniq(notifications.map(n => n.walletId));
walletIds.length.should.equal(1);
walletIds[0].should.equal(wallet.id);
var creators = _.uniq(notifications.map(n => n.creatorId).filter(c => !!c));
creators.length.should.equal(1);
creators[0].should.equal(wallet.copayers[0].id);
done();
});
});
it('should pull new payment notifications with correct format', function(done) {
var s2, w2, addr;
helpers.createAndJoinWallet(1, 1, { coin: 'bch' }, function(s, w) {
s2 = s;
w2 = w;
clock.tick(25 * 1000);
helpers.createAddresses(s2, w2, 1, 1, function(main, change) {
addr = main[0].address;
// Simulate new block notification
s2.walletId = w2.id;
s2._notify('NewIncomingTx', {
txid: 'txid',
address: addr,
amount: 5435, // 5434 sats
hash: 'dummy hash',
}, {
isGlobal: true
}, function(err) {
should.not.exist(err);
s2.getNotifications({
minTs: +Date.now() - (60 * 1000),
}, function(err, notifications) {
should.not.exist(err);
var types = notifications.map(n => n.type);
types.should.deep.equal(['NewCopayer', 'NewIncomingTx']);
var walletIds = _.uniq(notifications.map(n => n.walletId));
walletIds.length.should.equal(1);
walletIds[0].should.equal(w2.id);
done();
});
});
});
});
});
it('should pull new block notifications along with wallet notifications in the last 60 seconds', function(done) {
// Simulate new block notification
server.walletId = 'btc:livenet';
server._notify('NewBlock', {
hash: 'dummy hash',
}, {
isGlobal: true
}, function(err) {
should.not.exist(err);
server.walletId = 'btc:testnet';
server._notify('NewBlock', {
hash: 'dummy hash',
}, {
isGlobal: true
}, function(err) {
should.not.exist(err);
server.walletId = wallet.id;
server.getNotifications({
minTs: +Date.now() - (60 * 1000),
}, function(err, notifications) {
should.not.exist(err);
var types = notifications.map(n => n.type);
types.should.deep.equal(['NewTxProposal', 'NewTxProposal', 'NewBlock']);
var walletIds = _.uniq(notifications.map(n => n.walletId));
walletIds.length.should.equal(1);
walletIds[0].should.equal(wallet.id);
done();
});
});
});
});
it('should pull notifications in the last 60 seconds', function(done) {
server.getNotifications({
minTs: +Date.now() - (60 * 1000),
}, function(err, notifications) {
should.not.exist(err);
var types = notifications.map(n => n.type);
types.should.deep.equal(['NewTxProposal', 'NewTxProposal']);
done();
});
});
it('should pull notifications after a given notification id', function(done) {
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var from = notifications.slice(-2)[0].id; // second to last
server.getNotifications({
notificationId: from,
minTs: +Date.now() - (60 * 1000),
}, function(err, res) {
should.not.exist(err);
res.length.should.equal(1);
res[0].id.should.equal(notifications[notifications.length -1].id);
done();
});
});
});
it('should return empty if no notifications found after a given id', function(done) {
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var from = notifications[notifications.length - 1].id; // last one
server.getNotifications({
notificationId: from,
}, function(err, res) {
should.not.exist(err);
res.length.should.equal(0);
done();
});
});
});
it('should return empty if no notifications exist in the given timespan', function(done) {
clock.tick(100 * 1000);
server.getNotifications({
minTs: +Date.now() - (60 * 1000),
}, function(err, res) {
should.not.exist(err);
res.length.should.equal(0);
done();
});
});
it('should contain walletId & creatorId on NewCopayer', function(done) {
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
var newCopayer = notifications[0];
newCopayer.type.should.equal('NewCopayer');
newCopayer.walletId.should.equal(wallet.id);
newCopayer.creatorId.should.equal(wallet.copayers[0].id);
done();
});
});
it('should notify sign and acceptance', function(done) {
server.getPendingTxs({}, function(err, txs) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
var tx = txs[0];
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: tx.id,
signatures: signatures,
}, function(err) {
server.getNotifications({
minTs: Date.now(),
}, function(err, notifications) {
should.not.exist(err);
notifications.length.should.equal(2);
var types = notifications.map(n => n.type);
types.should.deep.equal(['TxProposalAcceptedBy', 'TxProposalFinallyAccepted']);
done();
});
});
});
});
it('should notify rejection', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[1];
server.rejectTx({
txProposalId: tx.id,
}, function(err) {
should.not.exist(err);
server.getNotifications({
minTs: Date.now(),
}, function(err, notifications) {
should.not.exist(err);
notifications.length.should.equal(2);
var types = notifications.map(n => n.type);
types.should.deep.equal(['TxProposalRejectedBy', 'TxProposalFinallyRejected']);
done();
});
});
});
});
it('should notify sign, acceptance, and broadcast, and emit', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[2];
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: tx.id,
signatures: signatures,
}, function(err, tx) {
should.not.exist(err);
helpers.stubBroadcast(tx.txid);
server.broadcastTx({
txProposalId: tx.id
}, function(err, txp) {
should.not.exist(err);
server.getNotifications({
minTs: Date.now(),
}, function(err, notifications) {
should.not.exist(err);
notifications.length.should.equal(3);
var types = notifications.map(n => n.type);
types.should.deep.equal(['TxProposalAcceptedBy', 'TxProposalFinallyAccepted', 'NewOutgoingTx']);
done();
});
});
});
});
});
it('should notify sign, acceptance, and broadcast, and emit (with 3rd party broadcast', function(done) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[2];
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: tx.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'err');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: 11
});
server.broadcastTx({
txProposalId: tx.id
}, function(err, txp) {
should.not.exist(err);
server.getNotifications({
minTs: Date.now(),
}, function(err, notifications) {
should.not.exist(err);
notifications.length.should.equal(3);
var types = notifications.map(n => n.type);
types.should.deep.equal(['TxProposalAcceptedBy', 'TxProposalFinallyAccepted', 'NewOutgoingTxByThirdParty']);
done();
});
});
});
});
});
});
describe('#removePendingTx', function() {
var server, wallet, txp;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8
}],
feePerKb: 100e2,
message: 'some message',
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function() {
server.getPendingTxs({}, function(err, txs) {
txp = txs[0];
done();
});
});
});
});
});
it('should allow creator to remove an unsigned TX', function(done) {
server.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txs) {
txs.length.should.equal(0);
done();
});
});
});
it('should allow creator to remove a signed TX by himself', function(done) {
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
server.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txs) {
txs.length.should.equal(0);
done();
});
});
});
});
it('should fail to remove non-pending TX', function(done) {
async.waterfall([
function(next) {
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.rejectTx({
txProposalId: txp.id,
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[2].id, function(server) {
server.rejectTx({
txProposalId: txp.id,
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
next();
});
},
function(next) {
server.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.exist(err);
err.code.should.equal('TX_NOT_PENDING');
done();
});
},
]);
});
it('should not allow non-creator copayer to remove an unsigned TX ', function(done) {
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
server2.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.exist(err);
err.code.should.contain('TX_CANNOT_REMOVE');
server2.getPendingTxs({}, function(err, txs) {
txs.length.should.equal(1);
done();
});
});
});
});
it('should not allow creator copayer to remove a TX signed by other copayer, in less than 24hrs', function(done) {
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H);
server2.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
server.removePendingTx({
txProposalId: txp.id
}, function(err) {
err.code.should.equal('TX_CANNOT_REMOVE');
err.message.should.contain('Cannot remove');
done();
});
});
});
});
it('should allow creator copayer to remove a TX rejected by other copayer, in less than 24hrs', function(done) {
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H);
server2.rejectTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
server.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.not.exist(err);
done();
});
});
});
});
it('should allow creator copayer to remove a TX signed by other copayer, after 24hrs', function(done) {
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H);
server2.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs[0].deleteLockTime.should.be.above(Defaults.DELETE_LOCKTIME - 10);
var clock = sinon.useFakeTimers({ now: Date.now() + 1 + 24 * 3600 * 1000, toFake: ['Date'] });
server.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.not.exist(err);
clock.restore();
done();
});
});
});
});
});
it('should allow other copayer to remove a TX signed, after 24hrs', function(done) {
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H);
server2.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
var clock = sinon.useFakeTimers({ now: Date.now() + 2000 + Defaults.DELETE_LOCKTIME * 1000, toFake: ['Date'] });
server2.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.not.exist(err);
clock.restore();
done();
});
});
});
});
});
describe('#scan', function() {
var server, wallet;
describe('1-of-1 wallet (BIP44 & P2PKH)', function() {
beforeEach(function(done) {
this.timeout(5000);
Defaults.SCAN_ADDRESS_GAP = 2;
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
done();
});
});
afterEach(function() { });
it('should scan main addresses', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
]);
var expectedPaths = [
'm/0/0',
'm/0/1',
'm/0/2',
'm/1/0',
];
server.scan({}, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.scanStatus.should.equal('success');
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
addresses.length.should.equal(expectedPaths.length);
var paths = addresses.map(n => n.path);
_.difference(paths, expectedPaths).length.should.equal(0);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/3');
done();
});
});
});
});
});
it('should not go beyond max gap', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1DY9exavapgnCUWDnSTJe1BPzXcpgwAQC4', // m/0/5
'1LD7Cr68LvBPTUeXrr6YXfGrogR7TVj3WQ', // m/1/3
]);
var expectedPaths = [
'm/0/0',
'm/0/1',
'm/0/2',
];
server.scan({}, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.scanStatus.should.equal('success');
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
addresses.length.should.equal(expectedPaths.length);
var paths = addresses.map(n => n.path);
_.difference(paths, expectedPaths).length.should.equal(0);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/3');
// A rescan should see the m/0/5 address initially beyond the gap
server.scan({}, function(err) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/6');
done();
});
});
});
});
});
});
});
it('should not affect indexes on new wallet', function(done) {
helpers.stubAddressActivity([]);
server.scan({}, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.scanStatus.should.equal('success');
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(0);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/0');
done();
});
});
});
});
});
it('should not rewind already generated addresses on error', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/0');
blockchainExplorer.getAddressActivity = sinon.stub().callsArgWith(1, 'dummy error');
server.scan({}, function(err) {
should.exist(err);
err.toString().should.equal('dummy error');
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.scanStatus.should.equal('error');
wallet.addressManager.receiveAddressIndex.should.equal(1);
wallet.addressManager.changeAddressIndex.should.equal(0);
server.createAddress({}, function(err, address) {
should.exist(err);
err.code.should.equal('WALLET_NEED_SCAN');
done();
});
});
});
});
});
it('should abort scan if there is an error checking address activity', function(done) {
blockchainExplorer.getAddressActivity = sinon.stub().callsArgWith(1, 'dummy error');
server.scan({}, function(err) {
should.exist(err);
err.toString().should.equal('dummy error');
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.scanStatus.should.equal('error');
wallet.addressManager.receiveAddressIndex.should.equal(0);
wallet.addressManager.changeAddressIndex.should.equal(0);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.not.exist(err);
addresses.should.be.empty;
server.getStatus({}, function(err, status) {
should.exist(err);
err.code.should.equal('WALLET_NEED_SCAN');
done();
});
});
});
});
});
it.skip('index cache: should use cache, if previous scan failed', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
], 4);
// First without activity
var addr = '1KbTiFvjbN6B5reCVS4tTT49vPQkvsqnE2'; // m/0/3
server.scan({ startingStep: 1 }, function(err) {
should.exist('failed on request');
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
// Because it failed
wallet.addressManager.receiveAddressIndex.should.equal(0);
wallet.addressManager.changeAddressIndex.should.equal(0);
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
], -1);
var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity');
server.scan({ startingStep: 1 }, function(err) {
should.not.exist(err);
// should prederive 3 address, so
// First call should be m/0/3
var calls = getAddressActivitySpy.getCalls();
calls[0].args[0].should.equal(addr);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/3');
done();
});
});
});
});
});
});
it.skip('index cache: should not use cache, if scan worked ok', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
]);
// First without activity
var addr = '1KbTiFvjbN6B5reCVS4tTT49vPQkvsqnE2'; // m/0/3
server.scan({ start }, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.addressManager.receiveAddressIndex.should.equal(3);
wallet.addressManager.changeAddressIndex.should.equal(1);
var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity');
server.scan({}, function(err) {
should.not.exist(err);
var calls = getAddressActivitySpy.getCalls();
calls[0].args[0].should.equal(addr);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/3');
done();
});
});
});
});
});
});
it('powerScan: should add not add skipped addresses if there is no activity', function(done) {
Defaults.SCAN_ADDRESS_GAP = 5;
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
]);
// First without activity
var addr = '1KbTiFvjbN6B5reCVS4tTT49vPQkvsqnE2'; // m/0/3
server.scan({ startingStep: 1000 }, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.addressManager.receiveAddressIndex.should.equal(3);
wallet.addressManager.changeAddressIndex.should.equal(1);
server.getMainAddresses({}, function(err, addr) {
should.not.exist(err);
addr.length.should.equal(3);
done();
});
});
});
});
it('powerScan: should add skipped addresses', function(done) {
Defaults.SCAN_ADDRESS_GAP = 5;
this.timeout(10000);
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1Lz4eBV8xVkSGkjhHSRkgQvi79ieYgWJWc', //m/0/99
'1HhAmuUfUszfAdK1jyumvBQoSj9tLB3PE', //m/0/199
'1PTrZzp5Kk78uVxnPUHYEHBktADgv3RhrC', //m/0/200
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
'12vSXvVzY1KjAVRz18KrsKgMoy89fQ7Xo4', //m/1/9
]);
// First without activity
var addr = '1KbTiFvjbN6B5reCVS4tTT49vPQkvsqnE2'; // m/0/3
server.scan({ startingStep: 1000 }, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.addressManager.receiveAddressIndex.should.equal(201);
wallet.addressManager.changeAddressIndex.should.equal(10);
server.getMainAddresses({}, function(err, addr) {
should.not.exist(err);
//201 MAIN addresses (0 to 200)
addr.length.should.equal(201);
done();
});
});
});
});
});
});
describe('#startScan', function() {
var server, wallet;
beforeEach(function(done) {
this.timeout(5000);
Defaults.SCAN_ADDRESS_GAP = 2;
helpers.createAndJoinWallet(1, 1, {
}, function(s, w) {
server = s;
wallet = w;
done();
});
});
afterEach(function() {
server.messageBroker.removeAllListeners();
});
it('should start an asynchronous scan', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/0/1/0
]);
var expectedPaths = [
'm/0/0',
'm/0/1',
'm/0/2',
'm/1/0',
];
server.messageBroker.onMessage(function(n) {
if (n.type == 'ScanFinished') {
server.getWallet({}, function(err, wallet) {
should.exist(wallet.scanStatus);
wallet.scanStatus.should.equal('success');
should.not.exist(n.creatorId);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
addresses.length.should.equal(expectedPaths.length);
var paths = addresses.map(n => n.path);
_.difference(paths, expectedPaths).length.should.equal(0);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/3');
done();
});
})
});
}
});
server.startScan({}, function(err, ret) {
should.not.exist(err);
ret.started.should.equal(true);
});
});
it('should set scan status error when unable to reach blockchain', function(done) {
blockchainExplorer.getAddressActivity = sinon.stub().yields('dummy error');
server.messageBroker.onMessage(function(n) {
if (n.type == 'ScanFinished') {
should.exist(n.data.error);
server.getWallet({}, function(err, wallet) {
should.exist(wallet.scanStatus);
wallet.scanStatus.should.equal('error');
done();
});
}
});
server.startScan({}, function(err) {
should.not.exist(err);
});
});
it('should start multiple asynchronous scans for different wallets', function(done) {
helpers.stubAddressActivity(['3K2VWMXheGZ4qG35DyGjA2dLeKfaSr534A']);
Defaults.SCAN_ADDRESS_GAP = 1;
var scans = 0;
server.messageBroker.onMessage(function(n) {
if (n.type == 'ScanFinished') {
scans++;
if (scans == 2) done();
}
});
// Create a second wallet
var server2 = new WalletService();
var opts = {
name: 'second wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub,
};
server2.createWallet(opts, function(err, walletId) {
should.not.exist(err);
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'copayer 1',
xPubKey: TestData.copayers[3].xPubKey_45H,
requestPubKey: TestData.copayers[3].pubKey_1H_0,
});
server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(err);
helpers.getAuthServer(result.copayerId, function(server2) {
server.startScan({}, function(err) {
should.not.exist(err);
scans.should.equal(0);
});
server2.startScan({}, function(err) {
should.not.exist(err);
scans.should.equal(0);
});
scans.should.equal(0);
});
});
});
});
});
describe('#startScan ETH', function() {
var server, wallet;
beforeEach(function(done) {
this.timeout(5000);
Defaults.SCAN_ADDRESS_GAP = 2;
helpers.createAndJoinWallet(1, 1, {
coin: 'eth',
}, function(s, w) {
server = s;
wallet = w;
done();
});
});
afterEach(function() {
server.messageBroker.removeAllListeners();
});
it('should start an asynchronous scan', function(done) {
server.startScan({}, function(err, ret) {
should.not.exist(err);
should.exist(ret);
ret.should.deep.equal({ started: true });
return done();
});
});
});
describe('PayPro', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should create a paypro tx', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8e8
}],
feePerKb: 100e2,
message: 'some message',
customData: 'some custom data',
payProUrl: 'http:/fakeurl.com',
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.payProUrl.should.equal('http:/fakeurl.com');
done();
});
});
});
});
describe('Push notifications', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should subscribe copayer to push notifications service (backward compatible)', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN',
packageName: 'com.wallet',
platform: 'Android',
}, function(err) {
should.not.exist(err);
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(1);
var s = subs[0];
s.token.should.equal('DEVICE_TOKEN');
s.packageName.should.equal('com.wallet');
s.platform.should.equal('Android')
done();
});
});
});
});
it('should subscribe copayer to push notifications service', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN',
packageName: 'com.wallet',
platform: 'Android',
walletId: '123'
}, function(err) {
should.not.exist(err);
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(1);
var s = subs[0];
s.token.should.equal('DEVICE_TOKEN');
s.packageName.should.equal('com.wallet');
s.platform.should.equal('Android')
done();
});
});
});
});
it('should allow multiple subscriptions for the same copayer (backward compatible)', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN',
packageName: 'com.wallet',
platform: 'Android',
}, function(err) {
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN2',
packageName: 'com.my-other-wallet',
platform: 'iOS',
}, function(err) {
should.not.exist(err);
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(2);
done();
});
});
});
});
});
it('should allow multiple subscriptions for the same copayer', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN',
packageName: 'com.wallet',
platform: 'Android',
walletId: '123'
}, function(err) {
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN2',
packageName: 'com.my-other-wallet',
platform: 'iOS',
walletId: '123'
}, function(err) {
should.not.exist(err);
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(2);
done();
});
});
});
});
});
it('should unsubscribe copayer to push notifications service (backward compatible)', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
async.series([
function(next) {
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN',
packageName: 'com.wallet',
platform: 'Android',
}, next);
},
function(next) {
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN2',
packageName: 'com.my-other-wallet',
platform: 'iOS'
}, next);
},
function(next) {
server.pushNotificationsUnsubscribe({
token: 'DEVICE_TOKEN2'
}, next);
},
function(next) {
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(1);
var s = subs[0];
s.token.should.equal('DEVICE_TOKEN');
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.pushNotificationsUnsubscribe({
token: 'DEVICE_TOKEN'
}, next);
});
},
function(next) {
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(1);
var s = subs[0];
s.token.should.equal('DEVICE_TOKEN');
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
});
it('should unsubscribe copayer to push notifications service', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
async.series([
function(next) {
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN',
packageName: 'com.wallet',
platform: 'Android',
walletId: '123'
}, next);
},
function(next) {
server.pushNotificationsSubscribe({
token: 'DEVICE_TOKEN2',
packageName: 'com.my-other-wallet',
platform: 'iOS',
walletId: '123'
}, next);
},
function(next) {
server.pushNotificationsUnsubscribe({
token: 'DEVICE_TOKEN2'
}, next);
},
function(next) {
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(1);
var s = subs[0];
s.token.should.equal('DEVICE_TOKEN');
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.pushNotificationsUnsubscribe({
token: 'DEVICE_TOKEN'
}, next);
});
},
function(next) {
server.storage.fetchPushNotificationSubs(wallet.copayers[0].id, function(err, subs) {
should.not.exist(err);
should.exist(subs);
subs.length.should.equal(1);
var s = subs[0];
s.token.should.equal('DEVICE_TOKEN');
next();
});
},
], function(err) {
should.not.exist(err);
done();
});
});
});
});
describe('Tx confirmation notifications', function() {
this.timeout(5000);
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should subscribe copayer to a tx confirmation', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
server.txConfirmationSubscribe({
txid: '123',
}, async function(err) {
should.not.exist(err);
const stream = server.storage.streamActiveTxConfirmationSubs(wallet.copayers[0].id, ['123']);
let txSub = (await stream.next());
should.exist(txSub);
txSub.txid.should.equal('123');
txSub.isActive.should.be.true;
done();
});
});
});
it('should overwrite last subscription', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
server.txConfirmationSubscribe({
txid: '123',
}, function(err) {
server.txConfirmationSubscribe({
txid: '123',
}, async function(err) {
should.not.exist(err);
const stream = server.storage.streamActiveTxConfirmationSubs(wallet.copayers[0].id, ['123']);
let txSub = (await stream.next());
should.exist(txSub);
txSub.txid.should.equal('123');
txSub = (await stream.next());
should.not.exist(txSub);
done();
});
});
});
});
it('should unsubscribe copayer to the specified tx', function(done) {
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
should.exist(server);
async.series([
function(next) {
server.txConfirmationSubscribe({
txid: '123',
}, next);
},
function(next) {
server.txConfirmationSubscribe({
txid: '456',
}, next);
},
function(next) {
server.txConfirmationUnsubscribe({
txid: '123',
}, next);
},
async function(next) {
const stream = server.storage.streamActiveTxConfirmationSubs(wallet.copayers[0].id, ['456']);
let txSub = (await stream.next());
should.exist(txSub);
txSub.txid.should.equal('456');
next();
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.txConfirmationUnsubscribe({
txid: '456'
}, next);
});
},
async function(next) {
const stream = server.storage.streamActiveTxConfirmationSubs(wallet.copayers[0].id, ['456']);
let txSub = (await stream.next());
should.exist(txSub);
txSub.txid.should.equal('456');
next();
},
], function(err) {
should.not.exist(err);
done();
});
});
});
});
describe('#getWalletFromIdentifier', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, {}, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('should get wallet from id', function(done) {
server.getWalletFromIdentifier({
identifier: wallet.id
}, function(err, w) {
should.not.exist(err);
should.exist(w);
w.id.should.equal(wallet.id);
done();
});
});
it('should get wallet from address', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
server.getWalletFromIdentifier({
identifier: address.address
}, function(err, w) {
should.not.exist(err);
should.exist(w);
w.id.should.equal(wallet.id);
done();
});
});
});
it('should get wallet from tx proposal', function(done) {
helpers.stubBroadcast('111');
helpers.stubUtxos(server, wallet, '1 btc', function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 1000e2
}],
feePerKb: 100e2,
message: 'some message',
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
txp = txps[0];
server.getWalletFromIdentifier({
identifier: txp.txid
}, function(err, w) {
should.not.exist(err);
should.exist(w);
w.id.should.equal(wallet.id);
done();
});
});
});
});
});
});
it('should return nothing if identifier not associated with a wallet', function(done) {
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.getWalletFromIdentifier({
identifier: 'dummy'
}, function(err, w) {
should.not.exist(err);
should.not.exist(w);
done();
});
});
});
describe('Sync wallet', function() {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44btc);
done();
});
});
it('should create and register and address', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
blockchainExplorer.register.calledOnce.should.equal(true);
blockchainExplorer.addAddresses.calledOnce.should.equal(true);
var calls = blockchainExplorer.addAddresses.getCalls();
calls[0].args[1].should.deep.equal([address.address]);
done();
});
});
it('should create and register a wallet with 2 addresses', function(done) {
server.createAddress({}, function(err, address1) {
server.createAddress({}, function(err, address2) {
should.not.exist(err);
blockchainExplorer.register.calledOnce.should.equal(true);
blockchainExplorer.addAddresses.calledTwice.should.equal(true);
var calls = blockchainExplorer.addAddresses.getCalls();
// should only sync address 2
calls[1].args[1].should.deep.equal([address2.address]);
done();
});
});
});
it('should reregisted address is wallet is deregistered', function(done) {
helpers.stubFeeLevels({
1: 40002,
2: 20000,
6: 18000,
24: 9001,
}, true);
server.createAddress({}, function(err, address1) {
helpers.stubHistory(2, 1000);
// deregisted the wallet
server.storage.deregisterWallet(wallet.id, () => {
wallet.beRegistered = false;
server.getTxHistory({}, function(err) {
should.not.exist(err);
blockchainExplorer.register.calledTwice.should.equal(true);
blockchainExplorer.addAddresses.calledTwice.should.equal(true);
var calls = blockchainExplorer.addAddresses.getCalls();
// both calls should registed the same addr
calls[0].args[1].should.deep.equal([address1.address]);
calls[1].args[1].should.deep.equal([address1.address]);
done();
});
});
});
});
it('should sync all wallet address if a first sync failed', function(done) {
blockchainExplorer.addAddresses = sinon.stub().callsArgWith(2, 'error');
server.createAddress({}, function(err, address1) {
blockchainExplorer.addAddresses = sinon.stub().callsArgWith(2, null, null);
server.createAddress({}, function(err, address2) {
should.not.exist(err);
var calls = blockchainExplorer.addAddresses.getCalls();
// should sync both addresses, since it failed the first time
// (call is 0 becuase the stub was rewritten)
calls[0].args[1].should.deep.equal([address1.address, address2.address]);
done();
});
});
});
it.skip('TODO: should sync address in batch', function(done) { });
});
describe('BTC & BCH wallets with same seed', function() {
var server = {},
wallet = {};
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server.btc = s;
wallet.btc = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44btc);
helpers.createAndJoinWallet(1, 1, {
coin: 'bch'
}, function(s, w) {
server.bch = s;
wallet.bch = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44bch);
done();
});
});
});
it('should create address', function(done) {
server.btc.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.btc.id);
address.coin.should.equal('btc');
address.network.should.equal('livenet');
address.address.should.equal('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG');
server.bch.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.bch.id);
address.coin.should.equal('bch');
address.network.should.equal('livenet');
address.address.should.equal('qrg04mz8h67j9dck3f3f3sa560taep87yqnwra9ak6');
server.btc.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
addresses[0].coin.should.equal('btc');
addresses[0].walletId.should.equal(wallet.btc.id);
addresses[0].address.should.equal('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG');
server.bch.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
addresses[0].coin.should.equal('bch');
addresses[0].walletId.should.equal(wallet.bch.id);
addresses[0].address.should.equal('qrg04mz8h67j9dck3f3f3sa560taep87yqnwra9ak6');
done();
});
});
});
});
});
});
describe('ERC20 createTx (ETH)', function() {
var server, wallet;
let sandbox;
let addressStr = '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A';
beforeEach(function(done) {
sandbox = sinon.createSandbox();
helpers.createAndJoinWallet(1, 1, {
coin: 'eth',
}, function(s, w) {
server = s;
wallet = w;
done();
});
});
afterEach(() => {
sandbox.restore();
})
it('should fail with different error for ERC20 txs with insufficient ETH to cover miner fee', function(done) {
const ts = TO_SAT['usdc'];
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [1, 1], { tokenAddress: TOKENS[0] }, function() {
let txAmount = 1e6;
var txOpts = {
coin: 'usdc_e',
outputs: [{
toAddress: addressStr,
amount: txAmount
}],
from,
fee: 4e18,
tokenAddress: TOKENS[0]
};
txOpts = Object.assign(txOpts);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_ETH_FEE');
err.message.should.equal('Your linked ETH wallet does not have enough ETH for fee. RequiredFee: 3999999999999990000');
err.messageData.should.deep.equal({ requiredFee: 3999999999999990000 });
server.getBalance({ tokenAddress: txOpts.tokenAddress }, function(err, tokenBalance) {
should.not.exist(err);
tokenBalance.totalAmount.should.equal(2 * ts);
tokenBalance.lockedAmount.should.equal(0);
txOpts.outputs[0].amount = 1 * ts;
server.getBalance({}, function(err, ethBalance) {
should.not.exist(err);
ethBalance.should.not.equal(tokenBalance);
ethBalance.totalAmount.should.equal(2000000000000000000);
ethBalance.lockedAmount.should.equal(0);
done();
});
});
});
});
});
});
it('should decode ouput data correctly to get invoice value when paypro', function(done) {
const ts = TO_SAT['usdc'];
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [1, 1], { tokenAddress: TOKENS[0] }, function() {
var txOpts = {
coin: 'usdc_e',
payProUrl: 'payProUrl',
outputs: [{
toAddress: addressStr,
amount: 0,
data: '0xb6b4af05000000000000000000000000000000000000000000000000000939f52e7b500000000000000000000000000000000000000000000000000000000006a5b66d80000000000000000000000000000000000000000000000000000001758d7da01d546ec66322bb962a8ba8c9c7c1b2ea37f0e4d5e92dfcd938796eeb41fb4aaa6efe746af63df9f38740a10c477b055f4f96fb26962d8d4050dac6d68280c28b60000000000000000000000000000000000000000000000000000000000000001cd7f7eb38ca6bd66b9006c66e42c1400f1921e5134adf77fcf577c267c9210a1d3230a734142b8810a7a7244f14da12fc052904fd68e885ce955f74ed57250bd50000000000000000000000000000000000000000000000000000000000000000'
}],
from,
tokenAddress: TOKENS[0]
};
txOpts = Object.assign(txOpts);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS');
err.message.should.equal('Insufficient funds');
done();
});
});
});
});
});
describe('ERC20 createTx (MATIC)', function() {
var server, wallet;
let sandbox;
let addressStr = '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A';
beforeEach(function(done) {
sandbox = sinon.createSandbox();
helpers.createAndJoinWallet(1, 1, {
coin: 'matic',
}, function(s, w) {
server = s;
wallet = w;
done();
});
});
afterEach(() => {
sandbox.restore();
})
it('should fail with different error for ERC20 txs with insufficient MATIC to cover miner fee', function(done) {
const ts = TO_SAT['usdc'];
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [1, 1], { tokenAddress: TOKENS[0] }, function() {
let txAmount = 1e6;
var txOpts = {
chain: 'matic',
coin: 'usdc_m',
outputs: [{
toAddress: addressStr,
amount: txAmount
}],
from,
fee: 4e18,
tokenAddress: TOKENS[0]
};
txOpts = Object.assign(txOpts);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_MATIC_FEE');
err.message.should.equal('Your linked POLYGON wallet does not have enough MATIC for fee. RequiredFee: 3999999999999990000');
err.messageData.should.deep.equal({ requiredFee: 3999999999999990000 });
server.getBalance({ tokenAddress: txOpts.tokenAddress }, function(err, tokenBalance) {
should.not.exist(err);
tokenBalance.totalAmount.should.equal(2 * ts);
tokenBalance.lockedAmount.should.equal(0);
txOpts.outputs[0].amount = 1 * ts;
server.getBalance({}, function(err, ethBalance) {
should.not.exist(err);
ethBalance.should.not.equal(tokenBalance);
ethBalance.totalAmount.should.equal(2000000000000000000);
ethBalance.lockedAmount.should.equal(0);
done();
});
});
});
});
});
});
it('should decode ouput data correctly to get invoice value when paypro', function(done) {
const ts = TO_SAT['usdc'];
server.createAddress({}, from => {
helpers.stubUtxos(server, wallet, [1, 1], { tokenAddress: TOKENS[0] }, function() {
var txOpts = {
chain: 'matic',
coin: 'usdc_m',
payProUrl: 'payProUrl',
outputs: [{
toAddress: addressStr,
amount: 0,
data: '0xb6b4af05000000000000000000000000000000000000000000000000000939f52e7b500000000000000000000000000000000000000000000000000000000006a5b66d80000000000000000000000000000000000000000000000000000001758d7da01d546ec66322bb962a8ba8c9c7c1b2ea37f0e4d5e92dfcd938796eeb41fb4aaa6efe746af63df9f38740a10c477b055f4f96fb26962d8d4050dac6d68280c28b60000000000000000000000000000000000000000000000000000000000000001cd7f7eb38ca6bd66b9006c66e42c1400f1921e5134adf77fcf577c267c9210a1d3230a734142b8810a7a7244f14da12fc052904fd68e885ce955f74ed57250bd50000000000000000000000000000000000000000000000000000000000000000'
}],
from,
tokenAddress: TOKENS[0]
};
txOpts = Object.assign(txOpts);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.code.should.equal('INSUFFICIENT_FUNDS');
err.message.should.equal('Insufficient funds');
done();
});
});
});
});
});
describe('getServicesData', () => {
let server;
beforeEach(() => {
server = new WalletService();
});
describe('Case with config.services defined in an unusual manner', () => {
it('should return config.services if it is included in the config file and no opts provided', () => {
const configServices = {
buyCrypto: {
disabled: false,
moonpay: {
disabled: true,
disabledMessage: 'Moonpay is out of service',
removed: false
},
},
sellCrypto: {
disabled: false,
moonpay: {
disabled: false,
removed: false
}
},
swapCrypto: {
disabled: false,
changelly: {
disabled: false,
removed: false
}
},
}
config.services = configServices;
const opts = undefined;
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config);
config.should.deep.equal(configServices);
});
});
it('should return config.services with swap crypto disabled if it is included in the config file, the user is logged out and located in NY', () => {
const configServices = {
buyCrypto: {
disabled: false,
moonpay: {
disabled: true,
disabledMessage: 'Moonpay is out of service',
removed: false
},
ramp: {
disabled: false,
removed: false
},
simplex: {
disabled: false,
removed: false
},
wyre: {
disabled: false,
removed: false
}
},
sellCrypto: {
disabled: false,
moonpay: {
disabled: false,
removed: false
}
},
swapCrypto: {
disabled: false,
changelly: {
disabled: false,
removed: false
},
thorswap: {
disabled: false,
removed: false
}
},
}
config.services = configServices;
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'NY',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(true);
config.swapCrypto.disabledMessage.should.equal('Swaps are currently unavailable in your area.');
});
});
it('should return config.services with swap crypto disabled if it is included in the config file and incomplete, the user is logged out and located in NY', () => {
const configServices = {
buyCrypto: {
disabled: false,
moonpay: {
disabled: true,
disabledMessage: 'Moonpay is out of service',
removed: false
}
},
}
config.services = configServices;
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'NY',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(true);
config.swapCrypto.disabledMessage.should.equal('Swaps are currently unavailable in your area.');
});
});
});
describe('Case with config.services defined as expected', () => {
beforeEach(() => {
config.services = {
buyCrypto: {
disabled: false,
banxa: {
disabled: false,
removed: false
},
moonpay: {
disabled: false,
removed: false
},
ramp: {
disabled: false,
removed: false
},
sardine: {
disabled: false,
removed: false
},
simplex: {
disabled: false,
removed: false
},
transak: {
disabled: false,
removed: false
},
wyre: {
disabled: false,
removed: false
}
},
sellCrypto: {
disabled: false,
moonpay: {
disabled: false,
removed: false
},
simplex: {
disabled: false,
removed: false
}
},
swapCrypto: {
disabled: false,
changelly: {
disabled: false,
removed: false
},
thorswap: {
disabled: false,
removed: false
}
}
}
});
describe('User logged out', () => {
const swapUsaBannedStates = ['HI', 'LA', 'NY'];
for (const bannedState of swapUsaBannedStates) {
it(`should return swap crypto disabled if the user is located in ${bannedState}`, () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: bannedState,
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(true);
config.swapCrypto.disabledMessage.should.equal('Swaps are currently unavailable in your area.');
});
});
};
it('should return swap crypto enabled if the user is in USA located outside NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'FL',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(false);
});
});
it('should return swap crypto enabled if the user is in other country than USA', () => {
const opts = {
currentLocationCountry: 'AR',
currentLocationState: 'T',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(false);
});
});
const buyCryptoUsaBannedStates = ['NY'];
for (const bannedState of buyCryptoUsaBannedStates) {
it(`should return buy crypto disabled if the user is located in ${bannedState}`, () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: bannedState,
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.buyCrypto);
config.buyCrypto.disabled.should.equal(true);
config.buyCrypto.disabledMessage.should.equal('This service is currently unavailable in your area.');
});
});
};
it('should return buy crypto enabled if the user is in USA located outside NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'FL',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.buyCrypto);
config.buyCrypto.disabled.should.equal(false);
});
});
const sellCryptoUsaBannedStates = ['NY'];
for (const bannedState of sellCryptoUsaBannedStates) {
it(`should return sell crypto disabled if the user is located in ${bannedState}`, () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: bannedState,
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.sellCrypto);
config.sellCrypto.disabled.should.equal(true);
config.sellCrypto.disabledMessage.should.equal('This service is currently unavailable in your area.');
});
});
};
it('should return sell crypto enabled if the user is in USA located outside NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'FL',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.sellCrypto);
config.sellCrypto.disabled.should.equal(false);
});
});
});
describe('User logged in', () => {
it('should return swap crypto disabled if the user is registred in NY and located in NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'NY',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'NY',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(true);
config.swapCrypto.disabledMessage.should.equal('Swaps are currently unavailable in your area.');
});
});
it('should return swap crypto disabled if the user is registred in NY and located outside NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'FL',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'NY',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(true);
config.swapCrypto.disabledMessage.should.equal('Swaps are currently unavailable in your area.');
});
});
it('should return swap crypto disabled if the user is registred in NY and located in other country than USA', () => {
const opts = {
currentLocationCountry: 'AR',
currentLocationState: 'T',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'NY',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(true);
config.swapCrypto.disabledMessage.should.equal('Swaps are currently unavailable in your area.');
});
});
it('should return swap crypto enabled if the user is registred outside NY and located in NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'NY',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'FL',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(false);
});
});
it('should return swap crypto enabled if the user is registred in other country than USA and located in NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'NY',
bitpayIdLocationCountry: 'AR',
bitpayIdLocationState: 'T',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(false);
});
});
it('should return swap crypto disabled if platform is ios and version of the app is 14.11.5', () => {
const opts = {
currentAppVersion: '14.11.5',
currentLocationCountry: 'US',
currentLocationState: 'GA',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'GA',
platform: {
os: 'ios',
version: '1.1.1'
},
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(true);
config.swapCrypto.disabledTitle.should.equal('Unavailable');
config.swapCrypto.disabledMessage.should.equal('Swaps are currently unavailable in your area.');
});
});
it('should return swap crypto enabled if platform is ios and version of the app is other than 14.11.5', () => {
const opts = {
currentAppVersion: '14.11.4',
currentLocationCountry: 'US',
currentLocationState: 'GA',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'GA',
platform: {
os: 'ios',
version: '1.1.1'
},
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(false);
});
});
it('should return swap crypto enabled if platform is other than ios', () => {
const opts = {
currentAppVersion: '14.11.5',
currentLocationCountry: 'US',
currentLocationState: 'GA',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'GA',
platform: {
os: 'android',
version: '1.1.2'
},
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.swapCrypto);
config.swapCrypto.disabled.should.equal(false);
});
});
it('should return buy crypto disabled if the user is registred in NY and located outside NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'FL',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'NY',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.buyCrypto);
config.buyCrypto.disabled.should.equal(true);
config.buyCrypto.disabledMessage.should.equal('This service is currently unavailable in your area.');
});
});
it('should return buy crypto enabled if the user is registred outside NY and located in NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'NY',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'FL',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.buyCrypto);
config.buyCrypto.disabled.should.equal(false);
});
});
it('should return sell crypto disabled if the user is registred in NY and located outside NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'FL',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'NY',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.sellCrypto);
config.sellCrypto.disabled.should.equal(true);
config.sellCrypto.disabledMessage.should.equal('This service is currently unavailable in your area.');
});
});
it('should return sell crypto enabled if the user is registred outside NY and located in NY', () => {
const opts = {
currentLocationCountry: 'US',
currentLocationState: 'NY',
bitpayIdLocationCountry: 'US',
bitpayIdLocationState: 'FL',
};
server.getServicesData(opts, (err, config) => {
should.not.exist(err);
should.exist(config.sellCrypto);
config.sellCrypto.disabled.should.equal(false);
});
});
});
});
});
describe('#getCoinsForTx', function() {
let server, wallet;
beforeEach(function() {
blockchainExplorer.getCoinsForTx = sinon.stub().callsArgWith(1, null, [{ txid: '11' }]);
});
it('should get Coins', function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
server.getCoinsForTx('abab', function(err, coins) {
should.not.exist(err);
coins[0].txid.should.equal('11');
done();
});
});
});
it('should get not get Coins for not utxo chain', function(done) {
helpers.createAndJoinWallet(1, 1, { coin: 'eth' }, function(s, w) {
server = s;
wallet = w;
server.getCoinsForTx('abab', function(err, coins) {
should.not.exist(err);
coins.should.deep.equal({
inputs: [],
outputs: [],
});
done();
});
});
});
});
describe('#getPayId', () => {
const url = 'https://ematiu.sandbox.payid.org/matias';
let server, fakeRequest, req;
beforeEach(() => {
server = new WalletService();
req = {
headers: {},
body: {}
}
fakeRequest = {
get: (_url, _opts, _cb) => { return _cb(null, { body: {} }) },
};
});
it('should work properly if url is OK', () => {
server.request = fakeRequest;
server.getPayId(url).then(data => {
should.exist(data);
}).catch(err => {
should.not.exist(err);
});
});
it('should return error if get returns error', () => {
const fakeRequest2 = {
get: (_url, _opts, _cb) => { return _cb(new Error('Error')) },
};
server.request = fakeRequest2;
server.getPayId(url).then(data => {
should.not.exist(data);
}).catch(err => {
should.exist(err);
err.message.should.equal('Error');
});
});
});
describe('#discoverPayId', () => {
// payId: matias$ematiu.sandbox.payid.org
let server, fakeRequest, req;
beforeEach(() => {
server = new WalletService();
req = {
headers: {},
body: {
domain: 'ematiu.sandbox.payid.org',
handle: 'matias',
}
}
fakeRequest = {
get: (_url, _opts, _cb) => { return _cb(null, { body: {} }) },
};
});
it('should work properly if req is OK', () => {
server.request = fakeRequest;
server.discoverPayId(req).then(data => {
should.exist(data);
}).catch(err => {
should.not.exist(err);
});
});
it('should return error if there is some missing arguments', () => {
delete req.body.domain;
server.request = fakeRequest;
server.discoverPayId(req).then(data => {
should.not.exist(data);
}).catch(err => {
should.exist(err);
});
});
it('should return error if get returns error', () => {
const fakeRequest2 = {
get: (_url, _opts, _cb) => { return _cb(new Error('Error')) },
};
server.request = fakeRequest2;
server.discoverPayId(req).then(data => {
should.not.exist(data);
}).catch(err => {
should.exist(err);
err.message.should.equal('Error');
});
});
it('should call getPayId with a url obtained from the template field if it exists', () => {
const fakeRequest2 = {
get: (_url, _opts, _cb) => {
return _cb(null, {
body: {
subject: "payid:matias$ematiu.sandbox.payid.org",
links: [{
rel: "https://payid.org/ns/payid-easy-checkout-uri/1.0",
href: "https://xpring.io/portal/wallet/xrp/testnet/payto",
template: "https://ematiu.sandbox.payid.org/payid/{acctpart}"
}]
}
})
}
};
server.request = fakeRequest2;
var spy = sinon.spy(server, 'getPayId');
const url = 'https://ematiu.sandbox.payid.org/payid/matias';
server.discoverPayId(req.body).then(data => {
var calls = spy.getCalls();
calls[0].args[0].should.equal(url);
should.exist(data);
}).catch(err => {
should.not.exist(err);
});
});
it('should call getPayId with a default url if the template field does not exist', () => {
const fakeRequest2 = {
get: (_url, _opts, _cb) => {
return _cb(null, {
body: {
subject: "payid:matias$ematiu.sandbox.payid.org",
links: [{
rel: "https://payid.org/ns/payid-easy-checkout-uri/1.0",
href: "https://xpring.io/portal/wallet/xrp/testnet/payto",
}]
}
})
}
};
const url = 'https://ematiu.sandbox.payid.org/matias';
server.request = fakeRequest2;
var spy = sinon.spy(server, 'getPayId');
server.discoverPayId(req.body).then(data => {
var calls = spy.getCalls();
calls[0].args[0].should.equal(url);
should.exist(data);
}).catch(err => {
should.not.exist(err);
});
});
});
describe('#clearCache', () => {
var server, wallet;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 2, function(s, w) {
server = s;
wallet = w;
done();
});
});
it('clearWalletCache', () => {
server.clearWalletCache({}).then((val) => {
should.exist(val);
val.should.equal(true);
});
});
});
})