7801 lines
255 KiB
JavaScript
7801 lines
255 KiB
JavaScript
'use strict';
|
|
|
|
var _ = require('lodash');
|
|
var async = require('async');
|
|
|
|
var chai = require('chai');
|
|
var sinon = require('sinon');
|
|
var should = chai.should();
|
|
var log = require('npmlog');
|
|
log.debug = log.verbose;
|
|
log.level = 'info';
|
|
|
|
var Bitcore = require('bitcore-lib');
|
|
var Bitcore_ = {
|
|
btc: Bitcore,
|
|
bch: require('bitcore-lib-cash')
|
|
};
|
|
|
|
|
|
|
|
var Common = require('../../lib/common');
|
|
var Utils = Common.Utils;
|
|
var Constants = Common.Constants;
|
|
var Defaults = Common.Defaults;
|
|
|
|
var Model = require('../../lib/model');
|
|
|
|
var WalletService = require('../../lib/server');
|
|
|
|
var TestData = require('../testdata');
|
|
var helpers = require('./helpers');
|
|
var storage, blockchainExplorer, request;
|
|
|
|
|
|
describe('Wallet service', function() {
|
|
before(function(done) {
|
|
helpers.before(done);
|
|
});
|
|
beforeEach(function(done) {
|
|
helpers.beforeEach(function(res) {
|
|
storage = res.storage;
|
|
blockchainExplorer = res.blockchainExplorer;
|
|
request = res.request;
|
|
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) {
|
|
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,
|
|
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 = require('../../lib/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);
|
|
server.walletId.should.equal('123');
|
|
server.copayerId.should.equal(wallet.copayers[0].id);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Session management (#login, #logout, #authenticate)', function() {
|
|
var server, wallet;
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(1, 2, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should get a new session & authenticate', function(done) {
|
|
WalletService.getInstanceWithAuth({
|
|
copayerId: server.copayerId,
|
|
session: 'dummy',
|
|
}, function(err, server2) {
|
|
should.exist(err);
|
|
err.code.should.equal('NOT_AUTHORIZED');
|
|
err.message.toLowerCase().should.contain('session');
|
|
should.not.exist(server2);
|
|
server.login({}, function(err, token) {
|
|
should.not.exist(err);
|
|
should.exist(token);
|
|
WalletService.getInstanceWithAuth({
|
|
copayerId: server.copayerId,
|
|
session: token,
|
|
}, function(err, server2) {
|
|
should.not.exist(err);
|
|
should.exist(server2);
|
|
server2.copayerId.should.equal(server.copayerId);
|
|
server2.walletId.should.equal(server.walletId);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
it('should get the same session token for two requests in a row', function(done) {
|
|
server.login({}, function(err, token) {
|
|
should.not.exist(err);
|
|
should.exist(token);
|
|
server.login({}, function(err, token2) {
|
|
should.not.exist(err);
|
|
token2.should.equal(token);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should create a new session if the previous one has expired', function(done) {
|
|
var timer = sinon.useFakeTimers('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 fail to create wallet with no name', function(done) {
|
|
var opts = {
|
|
name: '',
|
|
m: 2,
|
|
n: 3,
|
|
pubKey: TestData.keyPair.pub,
|
|
};
|
|
server.createWallet(opts, function(err, walletId) {
|
|
should.not.exist(walletId);
|
|
should.exist(err);
|
|
err.message.should.contain('name');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should check m-n combination', function(done) {
|
|
var pairs = [{
|
|
m: 0,
|
|
n: 0,
|
|
valid: false,
|
|
}, {
|
|
m: 1,
|
|
n: 1,
|
|
valid: true,
|
|
}, {
|
|
m: 2,
|
|
n: 3,
|
|
valid: true,
|
|
}, {
|
|
m: 0,
|
|
n: 2,
|
|
valid: false,
|
|
}, {
|
|
m: 2,
|
|
n: 1,
|
|
valid: false,
|
|
}, {
|
|
m: 0,
|
|
n: 10,
|
|
valid: false,
|
|
}, {
|
|
m: 1,
|
|
n: 20,
|
|
valid: false,
|
|
}, {
|
|
m: 10,
|
|
n: 10,
|
|
valid: true,
|
|
}, {
|
|
m: 15,
|
|
n: 15,
|
|
valid: true,
|
|
}, {
|
|
m: 16,
|
|
n: 16,
|
|
valid: false,
|
|
}, {
|
|
m: 1,
|
|
n: 15,
|
|
valid: true,
|
|
}, {
|
|
m: -2,
|
|
n: -2,
|
|
valid: false,
|
|
}, ];
|
|
var opts = {
|
|
id: '123',
|
|
name: 'my wallet',
|
|
pubKey: TestData.keyPair.pub,
|
|
};
|
|
async.each(pairs, function(pair, cb) {
|
|
opts.m = pair.m;
|
|
opts.n = pair.n;
|
|
server.createWallet(opts, function(err) {
|
|
if (!pair.valid) {
|
|
should.exist(err);
|
|
err.message.should.equal('Invalid combination of required copayers / total copayers');
|
|
} else {
|
|
should.not.exist(err);
|
|
}
|
|
return cb();
|
|
});
|
|
}, function(err) {
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail to create wallet with invalid pubKey argument', function(done) {
|
|
var opts = {
|
|
name: 'my wallet',
|
|
m: 2,
|
|
n: 3,
|
|
pubKey: 'dummy',
|
|
};
|
|
server.createWallet(opts, function(err, walletId) {
|
|
should.not.exist(walletId);
|
|
should.exist(err);
|
|
err.message.should.contain('Invalid public key');
|
|
done();
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
|
|
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 BIP45 & P2SH for 1-of-1 wallet if not supported', function(done) {
|
|
var walletOpts = {
|
|
name: 'my wallet',
|
|
m: 1,
|
|
n: 1,
|
|
pubKey: TestData.keyPair.pub,
|
|
supportBIP44AndP2PKH: false,
|
|
};
|
|
server.createWallet(walletOpts, function(err, wid) {
|
|
should.not.exist(err);
|
|
server.storage.fetchWallet(wid, function(err, wallet) {
|
|
should.not.exist(err);
|
|
wallet.derivationStrategy.should.equal('BIP45');
|
|
wallet.addressType.should.equal('P2SH');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should use BIP44 & P2SH for shared wallet if supported', function(done) {
|
|
var walletOpts = {
|
|
name: 'my wallet',
|
|
m: 2,
|
|
n: 3,
|
|
pubKey: TestData.keyPair.pub,
|
|
};
|
|
server.createWallet(walletOpts, function(err, wid) {
|
|
should.not.exist(err);
|
|
server.storage.fetchWallet(wid, function(err, wallet) {
|
|
should.not.exist(err);
|
|
wallet.derivationStrategy.should.equal('BIP44');
|
|
wallet.addressType.should.equal('P2SH');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should use BIP45 & P2SH for shared wallet if supported', function(done) {
|
|
var walletOpts = {
|
|
name: 'my wallet',
|
|
m: 2,
|
|
n: 3,
|
|
pubKey: TestData.keyPair.pub,
|
|
supportBIP44AndP2PKH: false,
|
|
};
|
|
server.createWallet(walletOpts, function(err, wid) {
|
|
should.not.exist(err);
|
|
server.storage.fetchWallet(wid, function(err, wallet) {
|
|
should.not.exist(err);
|
|
wallet.derivationStrategy.should.equal('BIP45');
|
|
wallet.addressType.should.equal('P2SH');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#joinWallet', function() {
|
|
describe('New clients', function() {
|
|
|
|
var server, walletId;
|
|
beforeEach(function(done) {
|
|
server = new WalletService();
|
|
var walletOpts = {
|
|
name: 'my wallet',
|
|
m: 1,
|
|
n: 2,
|
|
pubKey: TestData.keyPair.pub,
|
|
};
|
|
server.createWallet(walletOpts, function(err, wId) {
|
|
should.not.exist(err);
|
|
walletId = wId;
|
|
should.exist(walletId);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should join existing wallet', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
customData: 'dummy custom data',
|
|
});
|
|
server.joinWallet(copayerOpts, function(err, result) {
|
|
should.not.exist(err);
|
|
var copayerId = result.copayerId;
|
|
helpers.getAuthServer(copayerId, function(server) {
|
|
server.getWallet({}, function(err, wallet) {
|
|
wallet.id.should.equal(walletId);
|
|
wallet.copayers.length.should.equal(1);
|
|
var copayer = wallet.copayers[0];
|
|
copayer.name.should.equal('me');
|
|
copayer.id.should.equal(copayerId);
|
|
copayer.customData.should.equal('dummy custom data');
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var notif = _.find(notifications, {
|
|
type: 'NewCopayer'
|
|
});
|
|
should.exist(notif);
|
|
notif.data.walletId.should.equal(walletId);
|
|
notif.data.copayerId.should.equal(copayerId);
|
|
notif.data.copayerName.should.equal('me');
|
|
|
|
notif = _.find(notifications, {
|
|
type: 'WalletComplete'
|
|
});
|
|
should.not.exist(notif);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail to join with no name', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: '',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err, result) {
|
|
should.not.exist(result);
|
|
should.exist(err);
|
|
err.message.should.contain('name');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail to join non-existent wallet', function(done) {
|
|
var copayerOpts = {
|
|
walletId: '123',
|
|
name: 'me',
|
|
xPubKey: 'dummy',
|
|
requestPubKey: 'dummy',
|
|
copayerSignature: 'dummy',
|
|
};
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail to join full wallet', function(done) {
|
|
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: wallet.id,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[1].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[1].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.exist(err);
|
|
err.code.should.equal('WALLET_FULL');
|
|
err.message.should.equal('Wallet full');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail to join wallet for different coin', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
coin: 'bch',
|
|
});
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.exist(err);
|
|
err.message.should.contain('different coin');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should return copayer in wallet error before full wallet', function(done) {
|
|
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: wallet.id,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.exist(err);
|
|
err.code.should.equal('COPAYER_IN_WALLET');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail to re-join wallet', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.not.exist(err);
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.exist(err);
|
|
err.code.should.equal('COPAYER_IN_WALLET');
|
|
err.message.should.equal('Copayer already in wallet');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should be able to get wallet info without actually joining', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
customData: 'dummy custom data',
|
|
dryRun: true,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err, result) {
|
|
should.not.exist(err);
|
|
should.exist(result);
|
|
should.not.exist(result.copayerId);
|
|
result.wallet.id.should.equal(walletId);
|
|
result.wallet.m.should.equal(1);
|
|
result.wallet.n.should.equal(2);
|
|
result.wallet.copayers.should.be.empty;
|
|
server.storage.fetchWallet(walletId, function(err, wallet) {
|
|
should.not.exist(err);
|
|
wallet.id.should.equal(walletId);
|
|
wallet.copayers.should.be.empty;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail to join two wallets with same xPubKey', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.not.exist(err);
|
|
|
|
var walletOpts = {
|
|
name: 'my other wallet',
|
|
m: 1,
|
|
n: 1,
|
|
pubKey: TestData.keyPair.pub,
|
|
};
|
|
server.createWallet(walletOpts, function(err, walletId) {
|
|
should.not.exist(err);
|
|
copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.exist(err);
|
|
err.code.should.equal('COPAYER_REGISTERED');
|
|
err.message.should.equal('Copayer ID already registered on server');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail to join with bad formated signature', function(done) {
|
|
var copayerOpts = {
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
copayerSignature: 'bad sign',
|
|
};
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
err.message.should.equal('Bad request');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail to join with invalid xPubKey', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'copayer 1',
|
|
xPubKey: 'invalid',
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err, result) {
|
|
should.not.exist(result);
|
|
should.exist(err);
|
|
err.message.should.contain('extended public key');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail to join with null signature', function(done) {
|
|
var copayerOpts = {
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
};
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
should.exist(err);
|
|
err.message.should.contain('argument copayerSignature missing');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail to join with wrong signature', function(done) {
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
copayerOpts.name = 'me2';
|
|
server.joinWallet(copayerOpts, function(err) {
|
|
err.message.should.equal('Bad request');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should set pkr and status = complete on last copayer joining (2-3)', function(done) {
|
|
helpers.createAndJoinWallet(2, 3, function(server) {
|
|
server.getWallet({}, function(err, wallet) {
|
|
should.not.exist(err);
|
|
wallet.status.should.equal('complete');
|
|
wallet.publicKeyRing.length.should.equal(3);
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var notif = _.find(notifications, {
|
|
type: 'WalletComplete'
|
|
});
|
|
should.exist(notif);
|
|
notif.data.walletId.should.equal(wallet.id);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not notify WalletComplete if 1-of-1', function(done) {
|
|
helpers.createAndJoinWallet(1, 1, function(server) {
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var notif = _.find(notifications, {
|
|
type: 'WalletComplete'
|
|
});
|
|
should.not.exist(notif);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Interaction new/legacy clients', function() {
|
|
var server;
|
|
beforeEach(function() {
|
|
server = new WalletService();
|
|
});
|
|
|
|
it('should fail to join legacy wallet from new client', function(done) {
|
|
var walletOpts = {
|
|
name: 'my wallet',
|
|
m: 1,
|
|
n: 2,
|
|
pubKey: TestData.keyPair.pub,
|
|
supportBIP44AndP2PKH: false,
|
|
};
|
|
server.createWallet(walletOpts, function(err, walletId) {
|
|
should.not.exist(err);
|
|
should.exist(walletId);
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err, result) {
|
|
should.exist(err);
|
|
err.message.should.contain('The wallet you are trying to join was created with an older version of the client app');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail to join new wallet from legacy client', function(done) {
|
|
var walletOpts = {
|
|
name: 'my wallet',
|
|
m: 1,
|
|
n: 2,
|
|
pubKey: TestData.keyPair.pub,
|
|
};
|
|
server.createWallet(walletOpts, function(err, walletId) {
|
|
should.not.exist(err);
|
|
should.exist(walletId);
|
|
var copayerOpts = helpers.getSignedCopayerOpts({
|
|
walletId: walletId,
|
|
name: 'me',
|
|
xPubKey: TestData.copayers[0].xPubKey_45H,
|
|
requestPubKey: TestData.copayers[0].pubKey_1H_0,
|
|
supportBIP44AndP2PKH: false,
|
|
});
|
|
server.joinWallet(copayerOpts, function(err, result) {
|
|
should.exist(err);
|
|
err.code.should.equal('UPGRADE_NEEDED');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#removeWallet', function() {
|
|
var server, wallet, clock;
|
|
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 0.1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
async.eachSeries(_.range(2), function(i, next) {
|
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function() {
|
|
next();
|
|
});
|
|
}, done);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should delete a wallet', function(done) {
|
|
server.removeWallet({}, function(err) {
|
|
should.not.exist(err);
|
|
server.getWallet({}, function(err, w) {
|
|
should.exist(err);
|
|
err.code.should.equal('WALLET_NOT_FOUND');
|
|
should.not.exist(w);
|
|
async.parallel([
|
|
|
|
function(next) {
|
|
server.storage.fetchAddresses(wallet.id, function(err, items) {
|
|
items.length.should.equal(0);
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server.storage.fetchTxs(wallet.id, {}, function(err, items) {
|
|
items.length.should.equal(0);
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server.storage.fetchNotifications(wallet.id, null, 0, function(err, items) {
|
|
items.length.should.equal(0);
|
|
next();
|
|
});
|
|
},
|
|
], function(err) {
|
|
should.not.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// creates 2 wallet, and deletes only 1.
|
|
it('should delete a wallet, and only that wallet', function(done) {
|
|
var server2, wallet2;
|
|
async.series([
|
|
|
|
function(next) {
|
|
helpers.createAndJoinWallet(1, 1, {
|
|
offset: 1
|
|
}, function(s, w) {
|
|
server2 = s;
|
|
wallet2 = w;
|
|
|
|
helpers.stubUtxos(server2, wallet2, [1, 2, 3], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 0.1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
async.eachSeries(_.range(2), function(i, next) {
|
|
helpers.createAndPublishTx(server2, txOpts, TestData.copayers[1].privKey_1H_0, function() {
|
|
next();
|
|
});
|
|
}, next);
|
|
});
|
|
});
|
|
},
|
|
function(next) {
|
|
server.removeWallet({}, next);
|
|
},
|
|
function(next) {
|
|
server.getWallet({}, function(err, wallet) {
|
|
should.exist(err);
|
|
err.code.should.equal('WALLET_NOT_FOUND');
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server2.getWallet({}, function(err, wallet) {
|
|
should.not.exist(err);
|
|
should.exist(wallet);
|
|
wallet.id.should.equal(wallet2.id);
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server2.getMainAddresses({}, function(err, addresses) {
|
|
should.not.exist(err);
|
|
should.exist(addresses);
|
|
addresses.length.should.above(0);
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server2.getTxs({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(2);
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server2.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
should.exist(notifications);
|
|
notifications.length.should.above(0);
|
|
next();
|
|
});
|
|
},
|
|
], function(err) {
|
|
should.not.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getStatus', function() {
|
|
var server, wallet;
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(1, 2, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should get status', function(done) {
|
|
server.getStatus({}, function(err, status) {
|
|
should.not.exist(err);
|
|
should.exist(status);
|
|
should.exist(status.wallet);
|
|
status.wallet.name.should.equal(wallet.name);
|
|
should.exist(status.wallet.copayers);
|
|
status.wallet.copayers.length.should.equal(2);
|
|
should.exist(status.balance);
|
|
status.balance.totalAmount.should.equal(0);
|
|
should.exist(status.preferences);
|
|
should.exist(status.pendingTxps);
|
|
status.pendingTxps.should.be.empty;
|
|
|
|
should.not.exist(status.wallet.publicKeyRing);
|
|
should.not.exist(status.wallet.pubKey);
|
|
should.not.exist(status.wallet.addressManager);
|
|
_.each(status.wallet.copayers, function(copayer) {
|
|
should.not.exist(copayer.xPubKey);
|
|
should.not.exist(copayer.requestPubKey);
|
|
should.not.exist(copayer.signature);
|
|
should.not.exist(copayer.requestPubKey);
|
|
should.not.exist(copayer.addressManager);
|
|
should.not.exist(copayer.customData);
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
it('should get status including extended info', function(done) {
|
|
server.getStatus({
|
|
includeExtendedInfo: true
|
|
}, function(err, status) {
|
|
should.not.exist(err);
|
|
should.exist(status);
|
|
should.exist(status.wallet.publicKeyRing);
|
|
should.exist(status.wallet.pubKey);
|
|
should.exist(status.wallet.addressManager);
|
|
should.exist(status.wallet.copayers[0].xPubKey);
|
|
should.exist(status.wallet.copayers[0].requestPubKey);
|
|
should.exist(status.wallet.copayers[0].signature);
|
|
should.exist(status.wallet.copayers[0].requestPubKey);
|
|
should.exist(status.wallet.copayers[0].customData);
|
|
// Do not return other copayer's custom data
|
|
_.each(_.rest(status.wallet.copayers), function(copayer) {
|
|
should.not.exist(copayer.customData);
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
it('should get status after tx creation', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 0.8e8
|
|
}],
|
|
feePerKb: 100e2
|
|
};
|
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
should.exist(tx);
|
|
server.getStatus({}, function(err, status) {
|
|
should.not.exist(err);
|
|
status.pendingTxps.length.should.equal(1);
|
|
var balance = status.balance;
|
|
balance.totalAmount.should.equal(3e8);
|
|
balance.lockedAmount.should.equal(tx.inputs[0].satoshis);
|
|
balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount);
|
|
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 (BIP45)', function() {
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(2, 2, {
|
|
supportBIP44AndP2PKH: false
|
|
}, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should create address', function(done) {
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
should.exist(address);
|
|
address.walletId.should.equal(wallet.id);
|
|
address.network.should.equal('livenet');
|
|
address.address.should.equal('3BVJZ4CYzeTtawDtgwHvWV5jbvnXtYe97i');
|
|
address.isChange.should.be.false;
|
|
address.path.should.equal('m/2147483647/0/0');
|
|
address.type.should.equal('P2SH');
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var notif = _.find(notifications, {
|
|
type: 'NewAddress'
|
|
});
|
|
should.exist(notif);
|
|
notif.data.address.should.equal(address.address);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should create many addresses on simultaneous requests', function(done) {
|
|
var N = 5;
|
|
async.mapSeries(_.range(N), function(i, cb) {
|
|
server.createAddress({}, cb);
|
|
}, function(err, addresses) {
|
|
var x = _.pluck(addresses, 'path');
|
|
addresses.length.should.equal(N);
|
|
_.each(_.range(N), function(i) {
|
|
addresses[i].path.should.equal('m/2147483647/0/' + i);
|
|
});
|
|
// No two identical addresses
|
|
_.uniq(_.pluck(addresses, 'address')).length.should.equal(N);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('shared wallets (BIP44)', function() {
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(2, 2, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should create address', function(done) {
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
should.exist(address);
|
|
address.walletId.should.equal(wallet.id);
|
|
address.network.should.equal('livenet');
|
|
address.address.should.equal('36q2G5FMGvJbPgAVEaiyAsFGmpkhPKwk2r');
|
|
address.isChange.should.be.false;
|
|
address.coin.should.equal('btc');
|
|
address.path.should.equal('m/0/0');
|
|
address.type.should.equal('P2SH');
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var notif = _.find(notifications, {
|
|
type: 'NewAddress'
|
|
});
|
|
should.exist(notif);
|
|
notif.data.address.should.equal(address.address);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should create many addresses on simultaneous requests', function(done) {
|
|
var N = 5;
|
|
async.mapSeries(_.range(N), function(i, cb) {
|
|
server.createAddress({}, cb);
|
|
}, function(err, addresses) {
|
|
addresses.length.should.equal(N);
|
|
_.each(_.range(N), function(i) {
|
|
addresses[i].path.should.equal('m/0/' + i);
|
|
});
|
|
// No two identical addresses
|
|
_.uniq(_.pluck(addresses, 'address')).length.should.equal(N);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not create address if unable to store it', function(done) {
|
|
sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error');
|
|
server.createAddress({}, function(err, address) {
|
|
should.exist(err);
|
|
should.not.exist(address);
|
|
|
|
server.getMainAddresses({}, function(err, addresses) {
|
|
addresses.length.should.equal(0);
|
|
|
|
server.storage.storeAddressAndWallet.restore();
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
should.exist(address);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe('shared wallets (BIP44/BCH)', function() {
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(2, 2, {
|
|
coin: 'bch'
|
|
}, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should create address', function(done) {
|
|
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('HBf8isgS8EXG1r3X6GP89FmooUmiJ42wHS');
|
|
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 = _.find(notifications, {
|
|
type: 'NewAddress'
|
|
});
|
|
should.exist(notif);
|
|
notif.data.address.should.equal(address.address);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should create many addresses on simultaneous requests', function(done) {
|
|
var N = 5;
|
|
async.mapSeries(_.range(N), function(i, cb) {
|
|
server.createAddress({}, cb);
|
|
}, function(err, addresses) {
|
|
addresses.length.should.equal(N);
|
|
_.each(_.range(N), function(i) {
|
|
addresses[i].path.should.equal('m/0/' + i);
|
|
});
|
|
// No two identical addresses
|
|
_.uniq(_.pluck(addresses, 'address')).length.should.equal(N);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not create address if unable to store it', function(done) {
|
|
sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error');
|
|
server.createAddress({}, function(err, address) {
|
|
should.exist(err);
|
|
should.not.exist(address);
|
|
|
|
server.getMainAddresses({}, function(err, addresses) {
|
|
addresses.length.should.equal(0);
|
|
|
|
server.storage.storeAddressAndWallet.restore();
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
should.exist(address);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('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 = _.find(notifications, {
|
|
type: 'NewAddress'
|
|
});
|
|
should.exist(notif);
|
|
notif.data.address.should.equal(address.address);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should create many addresses on simultaneous requests', function(done) {
|
|
var N = 5;
|
|
async.mapSeries(_.range(N), function(i, cb) {
|
|
server.createAddress({}, cb);
|
|
}, function(err, addresses) {
|
|
addresses = _.sortBy(addresses, 'path');
|
|
addresses.length.should.equal(N);
|
|
_.each(_.range(N), function(i) {
|
|
addresses[i].path.should.equal('m/0/' + i);
|
|
});
|
|
// No two identical addresses
|
|
_.uniq(_.pluck(addresses, 'address')).length.should.equal(N);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail to create more consecutive addresses with no activity than allowed', function(done) {
|
|
var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP;
|
|
Defaults.MAX_MAIN_ADDRESS_GAP = 2;
|
|
helpers.stubAddressActivity([]);
|
|
async.map(_.range(2), function(i, next) {
|
|
server.createAddress({}, next);
|
|
}, function(err, addresses) {
|
|
addresses.length.should.equal(2);
|
|
|
|
server.createAddress({}, function(err, address) {
|
|
should.exist(err);
|
|
should.not.exist(address);
|
|
err.code.should.equal('MAIN_ADDRESS_GAP_REACHED');
|
|
server.createAddress({
|
|
ignoreMaxGap: true
|
|
}, function(err, address) {
|
|
should.not.exist(err);
|
|
should.exist(address);
|
|
address.path.should.equal('m/0/2');
|
|
|
|
helpers.stubAddressActivity([
|
|
'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(_.range(2), 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('#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', 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: '[email protected]',
|
|
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('[email protected]');
|
|
preferences.language.should.equal('es');
|
|
preferences.unit.should.equal('bit');
|
|
should.not.exist(preferences.dummy);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should save preferences only for requesting copayer', function(done) {
|
|
server.savePreferences({
|
|
email: '[email protected]'
|
|
}, 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: '[email protected]',
|
|
}, next);
|
|
},
|
|
function(next) {
|
|
server.getPreferences({}, function(err, preferences) {
|
|
should.not.exist(err);
|
|
should.exist(preferences);
|
|
preferences.email.should.equal('[email protected]');
|
|
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('[email protected]');
|
|
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('[email protected]');
|
|
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@' + _.repeat('domain', 50),
|
|
},
|
|
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);
|
|
err.message.should.contain(item.expected);
|
|
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);
|
|
_.sum(utxos, 'satoshis').should.equal(3 * 1e8);
|
|
server.getMainAddresses({}, function(err, addresses) {
|
|
var utxo = utxos[0];
|
|
var address = _.find(addresses, {
|
|
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 specific addresses', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) {
|
|
_.uniq(utxos, 'address').length.should.be.above(1);
|
|
var address = utxos[0].address;
|
|
var amount = _.sum(_.filter(utxos, {
|
|
address: address
|
|
}), 'satoshis');
|
|
server.getUtxos({
|
|
addresses: [address]
|
|
}, function(err, utxos) {
|
|
should.not.exist(err);
|
|
should.exist(utxos);
|
|
_.sum(utxos, 'satoshis').should.equal(amount);
|
|
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, cb) {
|
|
return cb(null, []);
|
|
};
|
|
|
|
server.getUtxos({}, function(err, utxos) {
|
|
should.not.exist(err);
|
|
utxos.should.be.empty;
|
|
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(_.pluck(addresses, 'address'));
|
|
_.intersection(addresses, _.pluck(balance.byAddress, '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(1, 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(1, '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) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
var args = spy.getCalls()[0].args;
|
|
args[0].should.equal('bch');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getBalance 2 steps', function() {
|
|
var server, wallet, clock;
|
|
var _threshold = Defaults.TWO_STEP_BALANCE_THRESHOLD;
|
|
beforeEach(function(done) {
|
|
clock = sinon.useFakeTimers(Date.now(), 'Date');
|
|
Defaults.TWO_STEP_BALANCE_THRESHOLD = 0;
|
|
|
|
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
afterEach(function() {
|
|
clock.restore();
|
|
Defaults.TWO_STEP_BALANCE_THRESHOLD = _threshold;
|
|
});
|
|
|
|
it('should get balance', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 'u2', 3], function() {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, 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));
|
|
setTimeout(done, 100);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should trigger notification when balance of non-prioritary addresses is updated', function(done) {
|
|
var oldAddrs, newAddrs;
|
|
|
|
async.series([
|
|
|
|
function(next) {
|
|
helpers.createAddresses(server, wallet, 2, 0, function(addrs) {
|
|
oldAddrs = addrs;
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
clock.tick(7 * 24 * 3600 * 1000);
|
|
helpers.createAddresses(server, wallet, 2, 0, function(addrs) {
|
|
newAddrs = addrs;
|
|
server._getActiveAddresses(function(err, active) {
|
|
should.not.exist(err);
|
|
should.not.exist(active);
|
|
helpers.stubUtxos(server, wallet, [1, 2], {
|
|
addresses: [oldAddrs[0], newAddrs[0]],
|
|
}, function() {
|
|
next();
|
|
});
|
|
});
|
|
});
|
|
},
|
|
function(next) {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, function(err, balance) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3));
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
setTimeout(next, 100);
|
|
},
|
|
function(next) {
|
|
server._getActiveAddresses(function(err, active) {
|
|
should.not.exist(err);
|
|
should.exist(active);
|
|
active.length.should.equal(3);
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
helpers.stubUtxos(server, wallet, 0.5, {
|
|
addresses: oldAddrs[1],
|
|
keepUtxos: true,
|
|
}, function() {
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, function(err, balance) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3));
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
setTimeout(next, 100);
|
|
},
|
|
function(next) {
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var last = _.last(notifications);
|
|
last.type.should.equal('BalanceUpdated');
|
|
var balance = last.data;
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3.5));
|
|
next();
|
|
});
|
|
},
|
|
], function(err) {
|
|
should.not.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not trigger notification when only balance of prioritary addresses is updated', function(done) {
|
|
var oldAddrs, newAddrs;
|
|
|
|
async.series([
|
|
|
|
function(next) {
|
|
helpers.createAddresses(server, wallet, 2, 0, function(addrs) {
|
|
oldAddrs = addrs;
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
clock.tick(7 * 24 * 3600 * 1000);
|
|
helpers.createAddresses(server, wallet, 2, 0, function(addrs) {
|
|
newAddrs = addrs;
|
|
helpers.stubUtxos(server, wallet, [1, 2], {
|
|
addresses: newAddrs,
|
|
}, function() {
|
|
next();
|
|
});
|
|
});
|
|
},
|
|
function(next) {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, function(err, balance) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3));
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
setTimeout(next, 100);
|
|
},
|
|
function(next) {
|
|
helpers.stubUtxos(server, wallet, 0.5, {
|
|
addresses: newAddrs[0],
|
|
keepUtxos: true,
|
|
}, function() {
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, function(err, balance) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3.5));
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
setTimeout(next, 100);
|
|
},
|
|
function(next) {
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var last = _.last(notifications);
|
|
last.type.should.not.equal('BalanceUpdated');
|
|
next();
|
|
});
|
|
},
|
|
], function(err) {
|
|
should.not.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should resolve balance of new addresses immediately', function(done) {
|
|
var addresses;
|
|
|
|
async.series([
|
|
|
|
function(next) {
|
|
helpers.createAddresses(server, wallet, 4, 0, function(addrs) {
|
|
addresses = addrs;
|
|
helpers.stubUtxos(server, wallet, [1, 2], {
|
|
addresses: _.take(addresses, 2),
|
|
}, function() {
|
|
next();
|
|
});
|
|
});
|
|
},
|
|
function(next) {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, function(err, balance) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3));
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server.createAddress({}, function(err, addr) {
|
|
helpers.stubUtxos(server, wallet, 0.5, {
|
|
addresses: addr,
|
|
keepUtxos: true,
|
|
}, function() {
|
|
next();
|
|
});
|
|
});
|
|
},
|
|
function(next) {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, function(err, balance) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3.5));
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
setTimeout(next, 100);
|
|
},
|
|
function(next) {
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var last = _.last(notifications);
|
|
last.type.should.not.equal('BalanceUpdated');
|
|
next();
|
|
});
|
|
},
|
|
], function(err) {
|
|
should.not.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not perform 2 steps when nb of addresses below threshold', function(done) {
|
|
var oldAddrs, newAddrs;
|
|
Defaults.TWO_STEP_BALANCE_THRESHOLD = 5;
|
|
|
|
async.series([
|
|
|
|
function(next) {
|
|
helpers.createAddresses(server, wallet, 2, 0, function(addrs) {
|
|
oldAddrs = addrs;
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
clock.tick(7 * 24 * 3600 * 1000);
|
|
helpers.createAddresses(server, wallet, 2, 0, function(addrs) {
|
|
newAddrs = addrs;
|
|
helpers.stubUtxos(server, wallet, [1, 2], {
|
|
addresses: [oldAddrs[0], newAddrs[0]],
|
|
}, function() {
|
|
next();
|
|
});
|
|
});
|
|
},
|
|
function(next) {
|
|
server.getBalance({
|
|
twoStep: true
|
|
}, function(err, balance) {
|
|
should.not.exist(err);
|
|
should.exist(balance);
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(3));
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
setTimeout(next, 100);
|
|
},
|
|
function(next) {
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
var last = _.last(notifications);
|
|
last.type.should.not.equal('BalanceUpdated');
|
|
next();
|
|
});
|
|
},
|
|
], function(err) {
|
|
should.not.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getFeeLevels', function() {
|
|
var server, wallet, levels;
|
|
before(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;
|
|
});
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
|
|
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 = _.zipObject(_.map(fees, function(item) {
|
|
return [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 = _.zipObject(_.map(fees, function(item) {
|
|
return [item.level, item.feePerKb];
|
|
}));
|
|
var defaults = _.zipObject(_.map(Defaults.FEE_LEVELS['btc'], function(item) {
|
|
return [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 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 = _.zipObject(_.map(fees, function(item) {
|
|
return [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 = _.zipObject(_.map(fees, function(item) {
|
|
return [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) {
|
|
_.find(Defaults.FEE_LEVELS['btc'], {
|
|
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 = _.zipObject(_.map(fees, function(item) {
|
|
return [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();
|
|
});
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
var addrMap = {
|
|
btc: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
bch: 'CPrtPWbp8cCftTQu5fzuLG5zPJNDHMMf8X',
|
|
}
|
|
|
|
var idKeyMap = {
|
|
btc: 'id44btc',
|
|
bch: 'id44bch',
|
|
};
|
|
|
|
_.each(['bch', 'btc'], function(coin) {
|
|
|
|
describe('#createTx ' + coin, function() {
|
|
var addressStr, idKey;
|
|
before(function() {
|
|
addressStr = addrMap[coin];
|
|
idKey = idKeyMap[coin];
|
|
});
|
|
|
|
|
|
|
|
describe('Tx proposal creation & publishing ' + coin, function() {
|
|
var server, wallet;
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(1, 1, {
|
|
coin: coin,
|
|
}, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
|
|
|
|
it('should create a tx', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 0.8 * 1e8,
|
|
}],
|
|
message: 'some message',
|
|
customData: 'some custom data',
|
|
feePerKb: 123e2,
|
|
};
|
|
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(helpers.toSatoshi(0.8));
|
|
tx.feePerKb.should.equal(123e2);
|
|
should.not.exist(tx.feeLevel);
|
|
server.getPendingTxs({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
txs.should.be.empty;
|
|
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,
|
|
};
|
|
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: 0.5e8
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
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: 0.5e8
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
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 fail to create tx for invalid amount', function(done) {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 0,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.not.exist(tx);
|
|
should.exist(err);
|
|
err.message.should.equal('Invalid amount');
|
|
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,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.exist(err);
|
|
should.not.exist(txp);
|
|
err.toString().should.contain('Only one of feeLevel/feePerKb');
|
|
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: 2.5e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
inputs: inputs,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.not.exist(err);
|
|
should.exist(tx);
|
|
tx.inputs.length.should.equal(2);
|
|
var txids = _.pluck(tx.inputs, 'txid');
|
|
txids.should.contain(utxos[0].txid);
|
|
txids.should.contain(utxos[2].txid);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
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,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.not.exist(err);
|
|
should.exist(tx);
|
|
var t = tx.getBitcoreTx();
|
|
t.getChangeOutput().script.toAddress().toString().should.equal(txOpts.changeAddress);
|
|
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: 0.8e8,
|
|
}],
|
|
inputs: utxos,
|
|
fee: 1000e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.not.exist(err);
|
|
should.exist(tx);
|
|
tx.amount.should.equal(helpers.toSatoshi(0.8));
|
|
should.not.exist(tx.feePerKb);
|
|
tx.fee.should.equal(1000e2);
|
|
var t = tx.getBitcoreTx();
|
|
t.getFee().should.equal(1000e2);
|
|
t.getChangeOutput().satoshis.should.equal(3e8 - 0.8e8 - 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: 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
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: 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.not.exist(err);
|
|
should.exist(tx);
|
|
tx.id.should.equal('123');
|
|
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: 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
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);
|
|
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: 0.8 * 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
message: 'some message',
|
|
customData: 'some custom data',
|
|
};
|
|
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: 0.8 * 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
dryRun: true,
|
|
};
|
|
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: 0.8 * 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
message: 'some message',
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
server.getNotifications({}, function(err, notifications) {
|
|
should.not.exist(err);
|
|
_.pluck(notifications, '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 = _.find(notifications, {
|
|
'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: 0.8 * 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
message: 'some message',
|
|
};
|
|
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: 0.8 * 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
message: 'some message',
|
|
};
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
it('should fail to publish a temporary tx proposal if utxos are locked by other pending proposals', function(done) {
|
|
var txp1, txp2;
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 0.8 * 1e8,
|
|
}],
|
|
message: 'some message',
|
|
feePerKb: 100e2,
|
|
};
|
|
|
|
async.waterfall([
|
|
|
|
function(next) {
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server.createTx(txOpts, next);
|
|
},
|
|
function(txp, next) {
|
|
txp1 = txp;
|
|
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
|
|
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: 0.8 * 1e8,
|
|
}],
|
|
message: 'some message',
|
|
feePerKb: 100e2,
|
|
};
|
|
|
|
async.waterfall([
|
|
|
|
function(next) {
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
server.createTx(txOpts, next);
|
|
},
|
|
function(txp, next) {
|
|
txp1 = txp;
|
|
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();
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Fee levels', function() {
|
|
it('should create a tx specifying feeLevel', function(done) {
|
|
//ToDo
|
|
var level = wallet.coin == 'btc' ? 'economy' : 'normal';
|
|
var expected = wallet.coin == 'btc' ? 180e2 : 200e2;
|
|
helpers.stubFeeLevels({
|
|
1: 400e2,
|
|
2: 200e2,
|
|
6: 180e2,
|
|
24: 90e2,
|
|
});
|
|
helpers.stubUtxos(server, wallet, 2, function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 1e8,
|
|
}],
|
|
// ToDo
|
|
feeLevel: level,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
txp.feePerKb.should.equal(expected);
|
|
txp.feeLevel.should.equal(level);
|
|
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: 1e8,
|
|
}],
|
|
feeLevel: 'madeUpLevel',
|
|
};
|
|
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.stubFeeLevels({
|
|
1: 400e2,
|
|
2: 200e2,
|
|
6: 180e2,
|
|
24: 90e2,
|
|
});
|
|
helpers.stubUtxos(server, wallet, 2, function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 1e8,
|
|
}],
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
txp.feePerKb.should.equal(200e2);
|
|
txp.feeLevel.should.equal('normal');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
it('should generate new change address for each created tx', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 0.8e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx1) {
|
|
should.not.exist(err);
|
|
should.exist(tx1);
|
|
server.createTx(txOpts, function(err, tx2) {
|
|
should.not.exist(err);
|
|
should.exist(tx2);
|
|
tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
it('should support creating a tx with no change address', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
var max = 3e8 - 7000; // Fees for this tx at 100bits/kB = 7000 sat
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: max,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
var t = txp.getBitcoreTx().toObject();
|
|
t.outputs.length.should.equal(1);
|
|
t.outputs[0].satoshis.should.equal(max);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should fail gracefully if unable to reach the blockchain', function(done) {
|
|
blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error');
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 1e8
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
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) {
|
|
helpers.stubUtxos(server, wallet, 1, function() {
|
|
var bitcoreStub = sinon.stub(Bitcore_[coin], 'Transaction');
|
|
bitcoreStub.throws({
|
|
name: 'dummy',
|
|
message: 'dummy exception'
|
|
});
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 0.5e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.exist(err);
|
|
err.message.should.equal('dummy exception');
|
|
bitcoreStub.restore();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should fail to create a tx exceeding max size in kb', function(done) {
|
|
var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB;
|
|
Defaults.MAX_TX_SIZE_IN_KB = 1;
|
|
helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 8e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.exist(err);
|
|
err.code.should.equal('TX_MAX_SIZE_EXCEEDED');
|
|
Defaults.MAX_TX_SIZE_IN_KB = _oldDefault;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should fail with different error for insufficient funds and locked funds', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 1.1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
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(2e8);
|
|
balance.lockedAmount.should.equal(2e8);
|
|
txOpts.outputs[0].amount = 0.8e8;
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
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: 20e2,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.exist(err);
|
|
err.code.should.equal('DUST_AMOUNT');
|
|
err.message.should.equal('Amount below dust threshold');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should create tx with 0 change output', function(done) {
|
|
helpers.stubUtxos(server, wallet, 1, function() {
|
|
var fee = 4100; // The exact fee of the resulting tx
|
|
var amount = 1e8 - fee;
|
|
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: amount,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.not.exist(err);
|
|
should.exist(tx);
|
|
var bitcoreTx = tx.getBitcoreTx();
|
|
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], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 1.5e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
should.exist(tx);
|
|
txOpts.outputs[0].amount = 0.8e8;
|
|
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.6e8);
|
|
balance.lockedAmount.should.equal(3.6e8);
|
|
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], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 1.5e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
should.exist(tx);
|
|
txOpts.outputs[0].amount = 1.8e8;
|
|
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.6e8);
|
|
var amountInputs = _.sum(txs[0].inputs, 'satoshis');
|
|
balance.lockedAmount.should.equal(amountInputs);
|
|
balance.lockedAmount.should.be.below(balance.totalAmount);
|
|
balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
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], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: 0.8 * 1e8,
|
|
}],
|
|
message: 'some message',
|
|
feePerKb: 100e2,
|
|
};
|
|
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 be able to send max funds', function(done) {
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: addressStr,
|
|
amount: null,
|
|
}],
|
|
feePerKb: 10000,
|
|
sendMax: true,
|
|
};
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.not.exist(err);
|
|
should.exist(tx);
|
|
should.not.exist(tx.changeAddress);
|
|
tx.amount.should.equal(3e8 - tx.fee);
|
|
|
|
var t = tx.getBitcoreTx();
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
it('should shuffle outputs unless specified', function(done) {
|
|
helpers.stubUtxos(server, wallet, 1, function() {
|
|
var txOpts = {
|
|
outputs: _.times(30, function(i) {
|
|
return {
|
|
toAddress: addressStr,
|
|
amount: (i + 1) * 100e2,
|
|
};
|
|
}),
|
|
feePerKb: 123e2,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
var t = txp.getBitcoreTx();
|
|
var changeOutput = t.getChangeOutput().satoshis;
|
|
var outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput);
|
|
|
|
outputs.should.not.deep.equal(_.pluck(txOpts.outputs, 'amount'));
|
|
txOpts.noShuffleOutputs = true;
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
|
|
t = txp.getBitcoreTx();
|
|
changeOutput = t.getChangeOutput().satoshis;
|
|
outputs = _.without(_.pluck(t.outputs, 'satoshis'), changeOutput);
|
|
|
|
outputs.should.deep.equal(_.pluck(txOpts.outputs, 'amount'));
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Backoff time', 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;
|
|
helpers.stubUtxos(server, wallet, _.range(2, 6), 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(Date.now(), 'Date');
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 1e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
async.series([
|
|
|
|
function(next) {
|
|
async.each(_.range(3), function(i, next) {
|
|
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
|
|
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
|
|
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);
|
|
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);
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.exist(err);
|
|
err.code.should.equal('TX_CANNOT_CREATE');
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
clock.tick(2000);
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('UTXO Selection', function() {
|
|
var server, wallet;
|
|
beforeEach(function(done) {
|
|
// log.level = 'debug';
|
|
helpers.createAndJoinWallet(1, 2, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
afterEach(function() {
|
|
log.level = 'info';
|
|
});
|
|
|
|
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,
|
|
};
|
|
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;
|
|
server.createTx(txOpts, function(err, tx) {
|
|
should.exist(err);
|
|
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
|
|
err.message.should.equal('Insufficient funds for fee');
|
|
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,
|
|
};
|
|
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;
|
|
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],
|
|
};
|
|
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];
|
|
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,
|
|
};
|
|
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
|
|
helpers.stubUtxos(server, wallet, _.range(1, 31), function(utxos) {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: _.sum(utxos, 'satoshis') - 0.5e8,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
var amounts = _.pluck(txp.inputs, 'satoshis');
|
|
amounts.length.should.equal(30);
|
|
_.all(amounts, 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,
|
|
};
|
|
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,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
txp.inputs.length.should.equal(3);
|
|
_.all(txp.inputs, 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,
|
|
};
|
|
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,
|
|
};
|
|
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(_.times(20, function() {
|
|
return '1000bit';
|
|
})), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 12000e2,
|
|
}],
|
|
feePerKb: 20e2,
|
|
};
|
|
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,
|
|
};
|
|
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) {
|
|
var _old1 = Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR;
|
|
var _old2 = Defaults.MAX_TX_SIZE_IN_KB;
|
|
Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = 0.0001;
|
|
Defaults.MAX_TX_SIZE_IN_KB = 2;
|
|
|
|
helpers.stubUtxos(server, wallet, [100].concat(_.range(1, 20, 0)), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 15e8,
|
|
}],
|
|
feePerKb: 120e2,
|
|
};
|
|
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);
|
|
Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = _old1;
|
|
Defaults.MAX_TX_SIZE_IN_KB = _old2;
|
|
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,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
txp.inputs.length.should.equal(3);
|
|
txOpts.feePerKb = 160e2;
|
|
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,
|
|
};
|
|
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,
|
|
};
|
|
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(_.times(20, function() {
|
|
return '100bit';
|
|
})), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 1500e2,
|
|
}],
|
|
feePerKb: 10e2,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
_.all(txp.inputs, 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,
|
|
};
|
|
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(_.times(20, function() {
|
|
return '1bit';
|
|
})), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 200e2,
|
|
}],
|
|
feePerKb: 90e2,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp);
|
|
txp.inputs.length.should.equal(2);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should use small utxos if fee is low', function(done) {
|
|
helpers.stubUtxos(server, wallet, [].concat(_.times(10, function() {
|
|
return '30bit';
|
|
})), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 200e2,
|
|
}],
|
|
feePerKb: 10e2,
|
|
};
|
|
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, ['200bit', '500sat'], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 150e2,
|
|
}],
|
|
feePerKb: 100e2,
|
|
};
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.not.exist(err);
|
|
txp.inputs.length.should.equal(1);
|
|
(_.sum(txp.inputs, 'satoshis') - txp.outputs[0].amount - txp.fee).should.equal(0);
|
|
var changeOutput = txp.getBitcoreTx().getChangeOutput();
|
|
should.not.exist(changeOutput);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('should ignore small utxos if fee is higher', function(done) {
|
|
helpers.stubUtxos(server, wallet, [].concat(_.times(10, function() {
|
|
return '30bit';
|
|
})), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 200e2,
|
|
}],
|
|
feePerKb: 80e2,
|
|
};
|
|
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,
|
|
};
|
|
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,
|
|
};
|
|
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();
|
|
server.broadcastTx({
|
|
txProposalId: txp.id
|
|
}, function(err, txp) {
|
|
should.not.exist(err);
|
|
should.exist(txp.txid);
|
|
txp.status.should.equal('broadcasted');
|
|
server.createTx(txOpts, function(err, txp) {
|
|
should.exist(err);
|
|
err.code.should.equal('INSUFFICIENT_FUNDS');
|
|
should.not.exist(txp);
|
|
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('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 = sinon.stub().returnsArg(0);
|
|
var txs = [{
|
|
txid: '123',
|
|
confirmations: 1,
|
|
fees: 100,
|
|
time: 20,
|
|
inputs: [{
|
|
address: 'external',
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 200,
|
|
}],
|
|
}];
|
|
helpers.stubHistory(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('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(_.pluck(notes, '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;
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
it('should correctly handle change in tx history', function(done) {
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
helpers.stubUtxos(server, wallet, 2, function() {
|
|
var txs = [{
|
|
txid: '1',
|
|
confirmations: 1,
|
|
fees: 150,
|
|
time: Date.now() / 1000,
|
|
inputs: [{
|
|
address: firstAddress.address,
|
|
amount: 550,
|
|
}],
|
|
outputs: [{
|
|
address: firstAddress.address,
|
|
amount: 100,
|
|
}, {
|
|
address: 'external',
|
|
amount: 300,
|
|
}],
|
|
}];
|
|
helpers.stubHistory(txs);
|
|
helpers.stubFeeLevels({
|
|
24: 10000,
|
|
});
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(1);
|
|
var tx = txs[0];
|
|
tx.action.should.equal('sent');
|
|
tx.amount.should.equal(300);
|
|
tx.fees.should.equal(150);
|
|
tx.outputs.length.should.equal(1);
|
|
tx.outputs[0].address.should.equal('external');
|
|
tx.outputs[0].amount.should.equal(300);
|
|
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 = tx.getBitcoreTx();
|
|
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(1304);
|
|
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(700);
|
|
info.fee.should.equal(350e2);
|
|
info.amount.should.equal(250e2);
|
|
|
|
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
|
|
helpers.stubUtxos(server, wallet, _.range(1, 31), function(utxos) {
|
|
server.getSendMaxInfo({
|
|
feePerKb: 100e2,
|
|
returnInputs: true
|
|
}, function(err, info) {
|
|
should.not.exist(err);
|
|
should.exist(info);
|
|
var amounts = _.pluck(info.inputs, 'satoshis');
|
|
amounts.length.should.equal(30);
|
|
_.all(amounts, 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(1002);
|
|
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(700);
|
|
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) {
|
|
helpers.stubUtxos(server, wallet, ['u0.1', 0.2, 0.3, 0.4, '1bit', '100bit', '200bit'], 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(1304);
|
|
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(301e2);
|
|
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(1907);
|
|
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(1e2);
|
|
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) {
|
|
var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB;
|
|
Defaults.MAX_TX_SIZE_IN_KB = 2;
|
|
helpers.stubUtxos(server, wallet, _.range(1, 10, 0), 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);
|
|
Defaults.MAX_TX_SIZE_IN_KB = _oldDefault;
|
|
sendTx(info, done);
|
|
});
|
|
});
|
|
});
|
|
})
|
|
|
|
describe('#rejectTx', function() {
|
|
var server, wallet, txid;
|
|
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(2, 2, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
helpers.stubUtxos(server, wallet, _.range(1, 9), 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;
|
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 2.5e8,
|
|
}],
|
|
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('Multisig', function() {
|
|
var server, wallet, txid;
|
|
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
helpers.stubUtxos(server, wallet, _.range(1, 9), 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 = _.take(helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H), 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(1234000, 'Date');
|
|
helpers.stubBroadcast();
|
|
server.broadcastTx({
|
|
txProposalId: txpid
|
|
}, function(err) {
|
|
should.not.exist(err);
|
|
server.getTx({
|
|
txProposalId: txpid
|
|
}, function(err, txp) {
|
|
should.not.exist(err);
|
|
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 raw tx', function(done) {
|
|
helpers.stubBroadcast();
|
|
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();
|
|
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 auto process already broadcasted txs', function(done) {
|
|
helpers.stubBroadcast();
|
|
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();
|
|
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();
|
|
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('Tx proposal workflow', function() {
|
|
var server, wallet;
|
|
beforeEach(function(done) {
|
|
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
helpers.stubUtxos(server, wallet, _.range(1, 9), function() {
|
|
helpers.stubBroadcast();
|
|
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) {
|
|
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(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 = _.last(notifications);
|
|
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 = _.last(notifications);
|
|
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('Date');
|
|
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
helpers.stubUtxos(server, wallet, _.range(1, 11), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 0.1e8
|
|
}],
|
|
feePerKb: 100e2,
|
|
message: 'some message',
|
|
};
|
|
async.eachSeries(_.range(10), 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 = _.pluck(txps, '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 = _.pluck(txps, '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 = _.pluck(txps, '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 = _.pluck(txps, '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 = _.pluck(txps, 'createdOn');
|
|
times.should.deep.equal([70, 60, 50]);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getNotifications', function() {
|
|
var clock;
|
|
var server, wallet;
|
|
|
|
beforeEach(function(done) {
|
|
clock = sinon.useFakeTimers(10 * 1000, 'Date');
|
|
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
helpers.stubUtxos(server, wallet, _.range(4), function() {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
amount: 0.1e8
|
|
}],
|
|
feePerKb: 100e2,
|
|
message: 'some message',
|
|
};
|
|
async.eachSeries(_.range(3), 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 = _.pluck(notifications, 'type');
|
|
types.should.deep.equal(['NewCopayer', 'NewAddress', 'NewAddress', 'NewTxProposal', 'NewTxProposal', 'NewTxProposal']);
|
|
var walletIds = _.uniq(_.pluck(notifications, 'walletId'));
|
|
walletIds.length.should.equal(1);
|
|
walletIds[0].should.equal(wallet.id);
|
|
var creators = _.uniq(_.compact(_.pluck(notifications, 'creatorId')));
|
|
creators.length.should.equal(1);
|
|
creators[0].should.equal(wallet.copayers[0].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 = 'livenet';
|
|
server._notify('NewBlock', {
|
|
hash: 'dummy hash',
|
|
}, {
|
|
isGlobal: true
|
|
}, function(err) {
|
|
should.not.exist(err);
|
|
server.walletId = '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 = _.pluck(notifications, 'type');
|
|
types.should.deep.equal(['NewTxProposal', 'NewTxProposal', 'NewBlock']);
|
|
var walletIds = _.uniq(_.pluck(notifications, '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 = _.pluck(notifications, '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 = _.first(_.takeRight(notifications, 2)).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(_.first(_.takeRight(notifications)).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 = _.first(_.takeRight(notifications)).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 = _.pluck(notifications, '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 = _.pluck(notifications, '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) {
|
|
should.not.exist(err);
|
|
helpers.stubBroadcast();
|
|
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 = _.pluck(notifications, '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 = _.pluck(notifications, '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(Date.now() + 1 + 24 * 3600 * 1000, '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(Date.now() + 2000 + Defaults.DELETE_LOCKTIME * 1000, 'Date');
|
|
server2.removePendingTx({
|
|
txProposalId: txp.id
|
|
}, function(err) {
|
|
should.not.exist(err);
|
|
clock.restore();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getTxHistory', function() {
|
|
var server, wallet, mainAddresses, changeAddresses;
|
|
beforeEach(function(done) {
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 1000);
|
|
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
helpers.createAddresses(server, wallet, 1, 1, function(main, change) {
|
|
mainAddresses = main;
|
|
changeAddresses = change;
|
|
helpers.stubFeeLevels({
|
|
24: 10000,
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should get tx history from insight', function(done) {
|
|
helpers.stubHistory(TestData.history);
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(TestData.history.length);
|
|
var i = 0;
|
|
_.each(txs, function(tx) {
|
|
var h = TestData.history[i++];
|
|
tx.time.should.equal(h.confirmations ? h.blocktime : h.firstSeenTs);
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
it('should get tx history for incoming txs', function(done) {
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
var txs = [{
|
|
txid: '1',
|
|
confirmations: 1,
|
|
fees: 100,
|
|
time: 20,
|
|
inputs: [{
|
|
address: 'external',
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 200,
|
|
}],
|
|
}];
|
|
helpers.stubHistory(txs);
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(1);
|
|
var tx = txs[0];
|
|
tx.action.should.equal('received');
|
|
tx.amount.should.equal(200);
|
|
tx.fees.should.equal(100);
|
|
tx.time.should.equal(20);
|
|
done();
|
|
});
|
|
});
|
|
it('should get tx history for outgoing txs', function(done) {
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
var txs = [{
|
|
txid: '1',
|
|
confirmations: 1,
|
|
fees: 100,
|
|
time: 12345,
|
|
inputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: 'external',
|
|
amount: 400,
|
|
}],
|
|
}];
|
|
helpers.stubHistory(txs);
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(1);
|
|
var tx = txs[0];
|
|
tx.action.should.equal('sent');
|
|
tx.amount.should.equal(400);
|
|
tx.fees.should.equal(100);
|
|
tx.time.should.equal(12345);
|
|
done();
|
|
});
|
|
});
|
|
it('should get tx history for outgoing txs + change', function(done) {
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
var txs = [{
|
|
txid: '1',
|
|
confirmations: 1,
|
|
fees: 100,
|
|
time: Date.now() / 1000,
|
|
inputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: 'external',
|
|
amount: 300,
|
|
}, {
|
|
address: changeAddresses[0].address,
|
|
amount: 100,
|
|
}],
|
|
}];
|
|
helpers.stubHistory(txs);
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(1);
|
|
var tx = txs[0];
|
|
tx.action.should.equal('sent');
|
|
tx.amount.should.equal(300);
|
|
tx.fees.should.equal(100);
|
|
tx.outputs[0].address.should.equal('external');
|
|
tx.outputs[0].amount.should.equal(300);
|
|
done();
|
|
});
|
|
});
|
|
it('should get tx history with accepted proposal', function(done) {
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
var external = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
|
|
|
|
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
|
|
var txOpts = {
|
|
outputs: [{
|
|
toAddress: external,
|
|
amount: 0.5e8,
|
|
message: undefined // no message
|
|
}, {
|
|
toAddress: external,
|
|
amount: 0.3e8,
|
|
message: 'message #2'
|
|
}],
|
|
feePerKb: 100e2,
|
|
message: 'some message',
|
|
customData: {
|
|
"test": true
|
|
},
|
|
};
|
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
should.exist(tx);
|
|
|
|
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();
|
|
server.broadcastTx({
|
|
txProposalId: tx.id
|
|
}, function(err, txp) {
|
|
should.not.exist(err);
|
|
var txs = [{
|
|
txid: txp.txid,
|
|
confirmations: 1,
|
|
fees: 5460,
|
|
time: Date.now() / 1000,
|
|
inputs: [{
|
|
address: tx.inputs[0].address,
|
|
amount: utxos[0].satoshis,
|
|
}],
|
|
outputs: [{
|
|
address: changeAddresses[0].address,
|
|
amount: 0.2e8 - 5460,
|
|
}, {
|
|
address: external,
|
|
amount: 0.5e8,
|
|
}, {
|
|
address: external,
|
|
amount: 0.3e8,
|
|
}]
|
|
}];
|
|
helpers.stubHistory(txs);
|
|
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(1);
|
|
var tx = txs[0];
|
|
tx.createdOn.should.equal(txp.createdOn);
|
|
tx.action.should.equal('sent');
|
|
tx.amount.should.equal(0.8e8);
|
|
tx.message.should.equal('some message');
|
|
tx.addressTo.should.equal(external);
|
|
tx.actions.length.should.equal(1);
|
|
tx.actions[0].type.should.equal('accept');
|
|
tx.actions[0].copayerName.should.equal('copayer 1');
|
|
tx.outputs[0].address.should.equal(external);
|
|
tx.outputs[0].amount.should.equal(0.5e8);
|
|
should.not.exist(tx.outputs[0].message);
|
|
should.not.exist(tx.outputs[0]['isMine']);
|
|
should.not.exist(tx.outputs[0]['isChange']);
|
|
tx.outputs[1].address.should.equal(external);
|
|
tx.outputs[1].amount.should.equal(0.3e8);
|
|
should.exist(tx.outputs[1].message);
|
|
tx.outputs[1].message.should.equal('message #2');
|
|
should.exist(tx.customData);
|
|
should.exist(tx.customData["test"]);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
it('should get various paginated tx history', function(done) {
|
|
var testCases = [{
|
|
opts: {},
|
|
expected: [50, 40, 30, 20, 10],
|
|
}, {
|
|
opts: {
|
|
skip: 1,
|
|
limit: 3,
|
|
},
|
|
expected: [40, 30, 20],
|
|
}, {
|
|
opts: {
|
|
skip: 1,
|
|
limit: 2,
|
|
},
|
|
expected: [40, 30],
|
|
}, {
|
|
opts: {
|
|
skip: 2,
|
|
},
|
|
expected: [30, 20, 10],
|
|
}, {
|
|
opts: {
|
|
limit: 4,
|
|
},
|
|
expected: [50, 40, 30, 20],
|
|
}, {
|
|
opts: {
|
|
skip: 0,
|
|
limit: 3,
|
|
},
|
|
expected: [50, 40, 30],
|
|
}, {
|
|
opts: {
|
|
skip: 0,
|
|
limit: 0,
|
|
},
|
|
expected: [],
|
|
}, {
|
|
opts: {
|
|
skip: 4,
|
|
limit: 10,
|
|
},
|
|
expected: [10],
|
|
}, {
|
|
opts: {
|
|
skip: 20,
|
|
limit: 1,
|
|
},
|
|
expected: [],
|
|
}];
|
|
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
var timestamps = [50, 40, 30, 20, 10];
|
|
var txs = _.map(timestamps, function(ts, idx) {
|
|
return {
|
|
txid: (idx + 1).toString(),
|
|
confirmations: ts / 10,
|
|
fees: 100,
|
|
time: ts,
|
|
inputs: [{
|
|
address: 'external',
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 200,
|
|
}],
|
|
};
|
|
});
|
|
helpers.stubHistory(txs);
|
|
|
|
async.each(testCases, function(testCase, next) {
|
|
server.getTxHistory(testCase.opts, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
_.pluck(txs, 'time').should.deep.equal(testCase.expected);
|
|
next();
|
|
});
|
|
}, done);
|
|
});
|
|
it('should fail gracefully if unable to reach the blockchain', function(done) {
|
|
blockchainExplorer.getTransactions = sinon.stub().callsArgWith(3, 'dummy error');
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.exist(err);
|
|
err.toString().should.equal('dummy error');
|
|
done();
|
|
});
|
|
});
|
|
it('should handle invalid tx in history ', function(done) {
|
|
var h = _.clone(TestData.history);
|
|
h.push({
|
|
txid: 'xx'
|
|
})
|
|
helpers.stubHistory(h);
|
|
var l = TestData.history.length;
|
|
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(l + 1);
|
|
txs[l].action.should.equal('invalid');
|
|
done();
|
|
});
|
|
});
|
|
it('should handle exceeded limit', function(done) {
|
|
server.getTxHistory({
|
|
limit: 1000
|
|
}, function(err, txs) {
|
|
err.code.should.equal('HISTORY_LIMIT_EXCEEDED');
|
|
done();
|
|
});
|
|
});
|
|
it('should set lowFees atribute for sub-superEconomy level fees on unconfirmed txs', function(done) {
|
|
helpers.stubFeeLevels({
|
|
24: 10000,
|
|
});
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
var txs = [{
|
|
txid: '1',
|
|
confirmations: 0,
|
|
fees: 100,
|
|
time: 20,
|
|
inputs: [{
|
|
address: 'external',
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 200,
|
|
}],
|
|
size: 500,
|
|
}, {
|
|
txid: '2',
|
|
confirmations: 0,
|
|
fees: 6000,
|
|
time: 20,
|
|
inputs: [{
|
|
address: 'external',
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 200,
|
|
}],
|
|
size: 500,
|
|
}, {
|
|
txid: '3',
|
|
confirmations: 6,
|
|
fees: 100,
|
|
time: 20,
|
|
inputs: [{
|
|
address: 'external',
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 200,
|
|
}],
|
|
size: 500,
|
|
}];
|
|
helpers.stubHistory(txs);
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
var tx = txs[0];
|
|
tx.feePerKb.should.equal(200);
|
|
tx.lowFees.should.be.true;
|
|
tx = txs[1];
|
|
tx.feePerKb.should.equal(12000);
|
|
tx.lowFees.should.be.false;
|
|
tx = txs[2];
|
|
tx.feePerKb.should.equal(200);
|
|
should.not.exist(tx.lowFees);
|
|
done();
|
|
});
|
|
});
|
|
it('should get tx history even if fee levels are unavailable', function(done) {
|
|
blockchainExplorer.estimateFee = sinon.stub().yields('dummy error');
|
|
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
|
var txs = [{
|
|
txid: '1',
|
|
confirmations: 1,
|
|
fees: 100,
|
|
time: 20,
|
|
inputs: [{
|
|
address: 'external',
|
|
amount: 500,
|
|
}],
|
|
outputs: [{
|
|
address: mainAddresses[0].address,
|
|
amount: 200,
|
|
}],
|
|
size: 500,
|
|
}];
|
|
helpers.stubHistory(txs);
|
|
server.getTxHistory({}, function(err, txs) {
|
|
should.not.exist(err);
|
|
var tx = txs[0];
|
|
tx.feePerKb.should.equal(200);
|
|
should.not.exist(tx.lowFees);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getTxHistory cache', function() {
|
|
var server, wallet, mainAddresses, changeAddresses;
|
|
var _threshold = Defaults.HISTORY_CACHE_ADDRESS_THRESOLD;
|
|
beforeEach(function(done) {
|
|
Defaults.HISTORY_CACHE_ADDRESS_THRESOLD = 1;
|
|
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
helpers.createAddresses(server, wallet, 1, 1, function(main, change) {
|
|
mainAddresses = main;
|
|
changeAddresses = change;
|
|
helpers.stubFeeLevels({
|
|
24: 10000,
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
afterEach(function() {
|
|
Defaults.HISTORY_CACHE_ADDRESS_THRESOLD = _threshold;
|
|
});
|
|
|
|
it('should store partial cache tx history from insight', function(done) {
|
|
var skip = 31;
|
|
var limit = 10;
|
|
var totalItems = 200;
|
|
|
|
var h = helpers.historyCacheTest(totalItems);
|
|
helpers.stubHistory(h);
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 200);
|
|
var storeTxHistoryCacheSpy = sinon.spy(server.storage, 'storeTxHistoryCache');
|
|
|
|
|
|
server.getTxHistory({
|
|
skip: skip,
|
|
limit: limit,
|
|
}, function(err, txs) {
|
|
|
|
// FROM the END, we are getting items
|
|
// End-1, end-2, end-3.
|
|
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(limit);
|
|
var calls = storeTxHistoryCacheSpy.getCalls();
|
|
calls.length.should.equal(1);
|
|
|
|
calls[0].args[1].should.equal(totalItems); // total
|
|
calls[0].args[2].should.equal(totalItems - skip - limit); // position
|
|
calls[0].args[3].length.should.equal(5); // 5 txs have confirmations>= 36
|
|
|
|
// should be reversed!
|
|
calls[0].args[3][0].confirmations.should.equal(skip + limit - 1);
|
|
calls[0].args[3][0].txid.should.equal(h[skip + limit - 1].txid);
|
|
server.storage.storeTxHistoryCache.restore();
|
|
done();
|
|
});
|
|
});
|
|
|
|
|
|
it('should not cache tx history when requesting txs with low # of confirmations', function(done) {
|
|
var h = helpers.historyCacheTest(200);
|
|
helpers.stubHistory(h);
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 1000);
|
|
var storeTxHistoryCacheSpy = sinon.spy(server.storage, 'storeTxHistoryCache');
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 10,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
var calls = storeTxHistoryCacheSpy.getCalls();
|
|
calls.length.should.equal(0);
|
|
server.storage.storeTxHistoryCache.restore();
|
|
done();
|
|
});
|
|
});
|
|
|
|
|
|
it('should store cache all tx history from insight', function(done) {
|
|
var skip = 195;
|
|
var limit = 5;
|
|
var totalItems = 200;
|
|
|
|
var h = helpers.historyCacheTest(totalItems);
|
|
helpers.stubHistory(h);
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 200);
|
|
var storeTxHistoryCacheSpy = sinon.spy(server.storage, 'storeTxHistoryCache');
|
|
|
|
server.getTxHistory({
|
|
skip: skip,
|
|
limit: limit,
|
|
}, function(err, txs) {
|
|
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(limit);
|
|
var calls = storeTxHistoryCacheSpy.getCalls();
|
|
calls.length.should.equal(1);
|
|
|
|
calls[0].args[1].should.equal(totalItems); // total
|
|
calls[0].args[2].should.equal(totalItems - skip - limit); // position
|
|
calls[0].args[3].length.should.equal(5);
|
|
|
|
// should be reversed!
|
|
calls[0].args[3][0].confirmations.should.equal(totalItems - 1);
|
|
calls[0].args[3][0].txid.should.equal(h[totalItems - 1].txid);
|
|
server.storage.storeTxHistoryCache.restore();
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should get real # of confirmations based on current block height', function(done) {
|
|
var _confirmations = Defaults.CONFIRMATIONS_TO_START_CACHING;
|
|
Defaults.CONFIRMATIONS_TO_START_CACHING = 6;
|
|
WalletService._cachedBlockheight = null;
|
|
|
|
var h = helpers.historyCacheTest(20);
|
|
_.each(h, function(x, i) {
|
|
x.confirmations = 500 + i;
|
|
x.blockheight = 1000 - i;
|
|
});
|
|
helpers.stubHistory(h);
|
|
var storeTxHistoryCacheSpy = sinon.spy(server.storage, 'storeTxHistoryCache');
|
|
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 1500);
|
|
|
|
// Cache txs
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 30,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
var calls = storeTxHistoryCacheSpy.getCalls();
|
|
calls.length.should.equal(1);
|
|
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 30,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
txs.length.should.equal(20);
|
|
_.first(txs).confirmations.should.equal(501);
|
|
_.last(txs).confirmations.should.equal(520);
|
|
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 2000);
|
|
server._notify('NewBlock', {
|
|
coin: 'btc',
|
|
network: 'livenet',
|
|
hash: 'dummy hash',
|
|
}, {
|
|
isGlobal: true
|
|
}, function(err) {
|
|
should.not.exist(err);
|
|
setTimeout(function() {
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 30,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
_.first(txs).confirmations.should.equal(1001);
|
|
_.last(txs).confirmations.should.equal(1020);
|
|
|
|
server.storage.storeTxHistoryCache.restore();
|
|
Defaults.CONFIRMATIONS_TO_START_CACHING = _confirmations;
|
|
done();
|
|
});
|
|
}, 100);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should get cached # of confirmations if current height unknown', function(done) {
|
|
var _confirmations = Defaults.CONFIRMATIONS_TO_START_CACHING;
|
|
Defaults.CONFIRMATIONS_TO_START_CACHING = 6;
|
|
WalletService._cachedBlockheight = null;
|
|
|
|
var h = helpers.historyCacheTest(20);
|
|
_.each(h, function(x, i) {
|
|
x.confirmations = 500 + i;
|
|
x.blockheight = 1000 - i;
|
|
});
|
|
helpers.stubHistory(h);
|
|
var storeTxHistoryCacheSpy = sinon.spy(server.storage, 'storeTxHistoryCache');
|
|
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, null);
|
|
|
|
// Cache txs
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 30,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(20);
|
|
var calls = storeTxHistoryCacheSpy.getCalls();
|
|
calls.length.should.equal(1);
|
|
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 30,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
txs.length.should.equal(20);
|
|
_.first(txs).confirmations.should.equal(500);
|
|
_.last(txs).confirmations.should.equal(519);
|
|
|
|
server.storage.storeTxHistoryCache.restore();
|
|
Defaults.CONFIRMATIONS_TO_START_CACHING = _confirmations;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should get returned # of confirmations for non cached txs', function(done) {
|
|
var _confirmations = Defaults.CONFIRMATIONS_TO_START_CACHING;
|
|
Defaults.CONFIRMATIONS_TO_START_CACHING = 6;
|
|
WalletService._cachedBlockheight = null;
|
|
|
|
var h = helpers.historyCacheTest(20);
|
|
helpers.stubHistory(h);
|
|
var storeTxHistoryCacheSpy = sinon.spy(server.storage, 'storeTxHistoryCache');
|
|
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 500);
|
|
|
|
// Cache txs
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 30,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(20);
|
|
var calls = storeTxHistoryCacheSpy.getCalls();
|
|
calls.length.should.equal(1);
|
|
|
|
server.getTxHistory({
|
|
skip: 0,
|
|
limit: 30,
|
|
}, function(err, txs) {
|
|
should.not.exist(err);
|
|
txs.length.should.equal(20);
|
|
_.first(txs).confirmations.should.equal(0);
|
|
_.last(txs).confirmations.should.equal(19);
|
|
|
|
server.storage.storeTxHistoryCache.restore();
|
|
Defaults.CONFIRMATIONS_TO_START_CACHING = _confirmations;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Downloading history', function() {
|
|
var h;
|
|
beforeEach(function(done) {
|
|
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 1000);
|
|
h = helpers.historyCacheTest(200);
|
|
helpers.stubHistory(h);
|
|
server.storage.clearTxHistoryCache(server.walletId, function() {
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('from 0 to 200, two times, in order', function(done) {
|
|
async.eachSeries(_.range(0, 200, 5), function(i, next) {
|
|
server.getTxHistory({
|
|
skip: i,
|
|
limit: 5,
|
|
}, function(err, txs, fromCache) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(5);
|
|
var s = h.slice(i, i + 5);
|
|
_.pluck(txs, 'txid').should.deep.equal(_.pluck(s, 'txid'));
|
|
fromCache.should.equal(false);
|
|
next();
|
|
});
|
|
}, function() {
|
|
async.eachSeries(_.range(0, 200, 5), function(i, next) {
|
|
server.getTxHistory({
|
|
skip: i,
|
|
limit: 5,
|
|
}, function(err, txs, fromCache) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(5);
|
|
var s = h.slice(i, i + 5);
|
|
_.pluck(txs, 'txid').should.deep.equal(_.pluck(s, 'txid'));
|
|
fromCache.should.equal(i >= Defaults.CONFIRMATIONS_TO_START_CACHING && i < 200);
|
|
next();
|
|
});
|
|
}, done);
|
|
});
|
|
});
|
|
|
|
it('from 0 to 200, two times, random', function(done) {
|
|
var indexes = _.range(0, 200, 5);
|
|
async.eachSeries(_.shuffle(indexes), function(i, next) {
|
|
server.getTxHistory({
|
|
skip: i,
|
|
limit: 5,
|
|
}, function(err, txs, fromCache) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(5);
|
|
var s = h.slice(i, i + 5);
|
|
_.pluck(txs, 'txid').should.deep.equal(_.pluck(s, 'txid'));
|
|
fromCache.should.equal(false);
|
|
next();
|
|
});
|
|
}, function() {
|
|
async.eachSeries(_.range(0, 190, 7), function(i, next) {
|
|
server.getTxHistory({
|
|
skip: i,
|
|
limit: 7,
|
|
}, function(err, txs, fromCache) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(7);
|
|
var s = h.slice(i, i + 7);
|
|
_.pluck(txs, 'txid').should.deep.equal(_.pluck(s, 'txid'));
|
|
fromCache.should.equal(i >= Defaults.CONFIRMATIONS_TO_START_CACHING);
|
|
next();
|
|
});
|
|
}, done);
|
|
});
|
|
});
|
|
|
|
|
|
it('from 0 to 200, two times, random, with resets', function(done) {
|
|
var indexes = _.range(0, 200, 5);
|
|
async.eachSeries(_.shuffle(indexes), function(i, next) {
|
|
server.getTxHistory({
|
|
skip: i,
|
|
limit: 5,
|
|
}, function(err, txs, fromCache) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(5);
|
|
var s = h.slice(i, i + 5);
|
|
_.pluck(txs, 'txid').should.deep.equal(_.pluck(s, 'txid'));
|
|
fromCache.should.equal(false);
|
|
next();
|
|
});
|
|
}, function() {
|
|
async.eachSeries(_.range(0, 200, 5), function(i, next) {
|
|
|
|
function resetCache(cb) {
|
|
if (!(i % 25)) {
|
|
storage.softResetTxHistoryCache(server.walletId, function() {
|
|
return cb(true);
|
|
});
|
|
} else {
|
|
return cb(false);
|
|
}
|
|
}
|
|
|
|
resetCache(function(reset) {
|
|
server.getTxHistory({
|
|
skip: i,
|
|
limit: 5,
|
|
}, function(err, txs, fromCache) {
|
|
should.not.exist(err);
|
|
should.exist(txs);
|
|
txs.length.should.equal(5);
|
|
var s = h.slice(i, i + 5);
|
|
_.pluck(txs, 'txid').should.deep.equal(_.pluck(s, 'txid'));
|
|
fromCache.should.equal(i >= Defaults.CONFIRMATIONS_TO_START_CACHING && !reset);
|
|
next();
|
|
});
|
|
});
|
|
}, 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 = _.pluck(addresses, '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 = _.pluck(addresses, '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.not.exist(err);
|
|
address.path.should.equal('m/0/1');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should restore wallet balance', function(done) {
|
|
async.waterfall([
|
|
|
|
function(next) {
|
|
helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) {
|
|
should.exist(utxos);
|
|
helpers.stubAddressActivity(_.pluck(utxos, 'address'));
|
|
server.getBalance({}, function(err, balance) {
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(6));
|
|
next(null, server, wallet);
|
|
});
|
|
});
|
|
},
|
|
function(server, wallet, next) {
|
|
server.removeWallet({}, function(err) {
|
|
next(err);
|
|
});
|
|
},
|
|
function(next) {
|
|
// NOTE: this works because it creates the exact same wallet!
|
|
helpers.createAndJoinWallet(1, 1, function(server, wallet) {
|
|
server.getBalance({}, function(err, balance) {
|
|
balance.totalAmount.should.equal(0);
|
|
next(null, server, wallet);
|
|
});
|
|
});
|
|
},
|
|
function(server, wallet, next) {
|
|
server.scan({}, function(err) {
|
|
should.not.exist(err);
|
|
server.getBalance(wallet.id, function(err, balance) {
|
|
balance.totalAmount.should.equal(helpers.toSatoshi(6));
|
|
next();
|
|
})
|
|
});
|
|
},
|
|
], function(err) {
|
|
should.not.exist(err);
|
|
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;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('shared wallet (BIP45)', function() {
|
|
|
|
beforeEach(function(done) {
|
|
this.timeout(5000);
|
|
Defaults.SCAN_ADDRESS_GAP = 2;
|
|
|
|
helpers.createAndJoinWallet(1, 2, {
|
|
supportBIP44AndP2PKH: false
|
|
}, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
afterEach(function() {});
|
|
|
|
it('should scan main addresses', function(done) {
|
|
helpers.stubAddressActivity(
|
|
['39AA1Y2VvPJhV3RFbc7cKbUax1WgkPwweR', // m/2147483647/0/0
|
|
'3QX2MNSijnhCALBmUVnDo5UGPj3SEGASWx', // m/2147483647/0/2
|
|
'3MzGaz4KKX66w8ShKaR536ZqzVvREBqqYu', // m/2147483647/1/0
|
|
]);
|
|
var expectedPaths = [
|
|
'm/2147483647/0/0',
|
|
'm/2147483647/0/1',
|
|
'm/2147483647/0/2',
|
|
'm/2147483647/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 = _.pluck(addresses, 'path');
|
|
_.difference(paths, expectedPaths).length.should.equal(0);
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
address.path.should.equal('m/2147483647/0/3');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
it('should scan main addresses & copayer addresses', function(done) {
|
|
helpers.stubAddressActivity(
|
|
['39AA1Y2VvPJhV3RFbc7cKbUax1WgkPwweR', // m/2147483647/0/0
|
|
'3MzGaz4KKX66w8ShKaR536ZqzVvREBqqYu', // m/2147483647/1/0
|
|
'3BYoynejwBH9q4Jhr9m9P5YTnLTu57US6g', // m/0/0/1
|
|
'37Pb8c32hzm16tCZaVHj4Dtjva45L2a3A3', // m/1/1/0
|
|
'32TB2n283YsXdseMqUm9zHSRcfS5JxTWxx', // m/1/0/0
|
|
]);
|
|
var expectedPaths = [
|
|
'm/2147483647/0/0',
|
|
'm/2147483647/1/0',
|
|
'm/0/0/0',
|
|
'm/0/0/1',
|
|
'm/1/0/0',
|
|
'm/1/1/0',
|
|
];
|
|
server.scan({
|
|
includeCopayerBranches: true
|
|
}, function(err) {
|
|
should.not.exist(err);
|
|
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
|
|
should.exist(addresses);
|
|
addresses.length.should.equal(expectedPaths.length);
|
|
var paths = _.pluck(addresses, 'path');
|
|
_.difference(paths, expectedPaths).length.should.equal(0);
|
|
done();
|
|
})
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#startScan', function() {
|
|
var server, wallet;
|
|
beforeEach(function(done) {
|
|
this.timeout(5000);
|
|
Defaults.SCAN_ADDRESS_GAP = 2;
|
|
|
|
helpers.createAndJoinWallet(1, 1, {
|
|
supportBIP44AndP2PKH: false
|
|
}, function(s, w) {
|
|
server = s;
|
|
wallet = w;
|
|
done();
|
|
});
|
|
});
|
|
afterEach(function() {
|
|
server.messageBroker.removeAllListeners();
|
|
});
|
|
|
|
it('should start an asynchronous scan', function(done) {
|
|
helpers.stubAddressActivity(
|
|
['3GvvHimEMk2GBZnPxTF89GHZL6QhZjUZVs', // m/2147483647/0/0
|
|
'37pd1jjTUiGBh8JL2hKLDgsyrhBoiz5vsi', // m/2147483647/0/2
|
|
'3C3tBn8Sr1wHTp2brMgYsj9ncB7R7paYuB', // m/2147483647/1/0
|
|
]);
|
|
var expectedPaths = [
|
|
'm/2147483647/0/0',
|
|
'm/2147483647/0/1',
|
|
'm/2147483647/0/2',
|
|
'm/2147483647/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 = _.pluck(addresses, 'path');
|
|
_.difference(paths, expectedPaths).length.should.equal(0);
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
address.path.should.equal('m/2147483647/0/3');
|
|
done();
|
|
});
|
|
})
|
|
});
|
|
}
|
|
});
|
|
server.startScan({}, function(err) {
|
|
should.not.exist(err);
|
|
});
|
|
});
|
|
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('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', 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 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',
|
|
}, 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 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',
|
|
}, 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();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Tx confirmation 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 a tx confirmation', function(done) {
|
|
helpers.getAuthServer(wallet.copayers[0].id, function(server) {
|
|
should.exist(server);
|
|
server.txConfirmationSubscribe({
|
|
txid: '123',
|
|
}, function(err) {
|
|
should.not.exist(err);
|
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
|
should.not.exist(err);
|
|
should.exist(subs);
|
|
subs.length.should.equal(1);
|
|
var s = subs[0];
|
|
s.txid.should.equal('123');
|
|
s.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',
|
|
}, function(err) {
|
|
should.not.exist(err);
|
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
|
should.not.exist(err);
|
|
should.exist(subs);
|
|
subs.length.should.equal(1);
|
|
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);
|
|
},
|
|
function(next) {
|
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
|
should.not.exist(err);
|
|
should.exist(subs);
|
|
subs.length.should.equal(1);
|
|
var s = subs[0];
|
|
s.txid.should.equal('456');
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
|
|
server.txConfirmationUnsubscribe({
|
|
txid: '456'
|
|
}, next);
|
|
});
|
|
},
|
|
function(next) {
|
|
server.storage.fetchActiveTxConfirmationSubs(wallet.copayers[0].id, function(err, subs) {
|
|
should.not.exist(err);
|
|
should.exist(subs);
|
|
subs.length.should.equal(1);
|
|
var s = subs[0];
|
|
s.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.stubUtxos(server, wallet, '1 btc', function() {
|
|
helpers.stubBroadcast();
|
|
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 get wallet from incoming txid', function(done) {
|
|
server.createAddress({}, function(err, address) {
|
|
should.not.exist(err);
|
|
should.exist(address);
|
|
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
|
|
txid: '999',
|
|
vout: [{
|
|
scriptPubKey: {
|
|
addresses: [address.address]
|
|
}
|
|
}],
|
|
});
|
|
server.getWalletFromIdentifier({
|
|
identifier: '999'
|
|
}, 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('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('CbWsiNjh18ynQYc5jfYhhespEGrAaW8YUq');
|
|
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('CbWsiNjh18ynQYc5jfYhhespEGrAaW8YUq');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|