bitcore-wallet-service/test/integration.js

1152 lines
35 KiB
JavaScript
Raw Normal View History

2015-01-27 05:18:45 -08:00
'use strict';
var _ = require('lodash');
var async = require('async');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var levelup = require('levelup');
var memdown = require('memdown');
2015-02-03 05:53:59 -08:00
var Bitcore = require('bitcore');
2015-01-27 05:18:45 -08:00
2015-02-04 11:18:36 -08:00
var Utils = require('../lib/utils');
2015-02-03 05:53:59 -08:00
var SignUtils = require('../lib/signutils');
2015-01-28 05:52:45 -08:00
var Storage = require('../lib/storage');
2015-01-27 05:18:45 -08:00
var Wallet = require('../lib/model/wallet');
2015-01-27 11:40:21 -08:00
var Address = require('../lib/model/address');
2015-01-27 05:18:45 -08:00
var Copayer = require('../lib/model/copayer');
var CopayServer = require('../lib/server');
2015-02-01 11:50:58 -08:00
var keyPair = {
priv: '0dea92f1df6675085b5cdd965487bb862f84f2755bcb56fa45dbf5b387a6c4a0',
pub: '026092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80',
};
2015-01-31 14:56:50 -08:00
var aPubKey = '042F65F56A6C06C2B651C473AC221B2460DA57859AFB72564E9781B655EBC0AFAF322B9A732324ECC92A3319DFB1F0D53F0CB7E6620C98BD1EF53106A7CF3F6DB9';
2015-02-01 11:50:58 -08:00
var aXPubKey = 'xpub661MyMwAqRbcFHFFvUP6HaKdd2FYzNcZCGagxMzQEf1J3x2DeASBW2JWox7ToGwPM7V2yRzQAxcD6MdPid9C8kwhKkVWBxQ3dMo8zu3pub7';
var aXPubKeySignature = '3045022100f988737147894bbfdc196c1289e4d970b391c0d8e9d1fcc0397f16e6a31c9df2022014d9af9aceccb540f4a5a2680e2aebb1f3df55bcf3778599b78314a02064c592'; // with keyPair.priv
// Copayers
var someXPrivKey = [
'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e',
];
var someXPubKeys = [
'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9',
'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o',
'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd',
'xpub661MyMwAqRbcGpExxHEzAWxBQX3k76NyerSpjqucSXXfTqH6Wq9sUVRwTjpHZHwapDbG16KEB9w9r3LT2jKYqU9xJf1YBAaZFikbUHiV1tg',
'xpub661MyMwAqRbcEvKQnt9ELHHcangXssm174sWr5gNTSmQYsAtvQJNUpLETDTm1vDxwtABvB4SRjGkNMm37NnMerKg4e3ygqmWEr75Fka4dK7',
'xpub661MyMwAqRbcG67ioS7rz3fFg7EDQNLJ9m1etAPwBecZhL5kKAKe4JU5jCTzRcEWp28XCYA1gKh7jyficSr97gcR2pjDL5jbWua1CwTKWV4',
];
2015-02-04 06:43:12 -08:00
// with keyPair.priv
2015-02-01 11:50:58 -08:00
var someXPubKeysSignatures = [
'30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46',
'30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62',
'304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c',
'304402203ae5bf7fa8935b8ab2ac33724dbb191356cecb47c8371d2c9389e918a3600918022073b48705306730c8fe4ab22d5f6ed3ca3def27eb6e8c5cc8f53e23c11fa5e5ef',
'3045022100eabd2a605403b377a8db9eec57726da0309a7eb385e7e4e5273b9862046f25ef02204d18755a90580a98f45e162ae5d5dc39aa3aa708a0d79433ed259e70a832b49c',
'3045022100c282254773c65025054e18a61ee550cbf78b88fc72ef66770050815b62502d9c02206e0df528203c9201c144f865df71f5d2471668f4ed8387979fcee20f6fa121a9',
2015-02-04 06:43:12 -08:00
];
2015-02-01 11:50:58 -08:00
//Copayer signature
var aText = 'hello world';
2015-02-02 15:13:13 -08:00
var aTextSignature = '3045022100addd20e5413865d65d561ad2979f2289a40d52594b1f804840babd9a63e4ebbf02204b86285e1fcab02df772e7a1325fc4b511ecad79a8f80a2bd1ad8bfa858ac3d4'; // with someXPrivKey[0].derive('m/1/0')=5c0e043a513032907d181325a8e7990b076c0af15ed13dc5e611cda9bb3ae52a;
2015-02-01 11:50:58 -08:00
2015-01-29 10:00:35 -08:00
var helpers = {};
2015-02-06 23:09:45 -08:00
helpers.getAuthServer = function(copayerId, cb) {
2015-02-06 12:56:51 -08:00
var signatureStub = sinon.stub(CopayServer.prototype, '_verifySignature');
signatureStub.returns(true);
CopayServer.getInstanceWithAuth({
copayerId: copayerId,
message: 'dummy',
signature: 'dummy',
2015-02-06 23:09:45 -08:00
}, function(err, server) {
if (err || !server) throw new Error('Could not login as copayerId ' + copayerId);
2015-02-06 12:56:51 -08:00
signatureStub.restore();
return cb(server);
});
};
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet = function(m, n, cb) {
2015-02-06 12:56:51 -08:00
var server = new CopayServer();
2015-02-07 07:48:57 -08:00
var copayerIds = [];
2015-02-06 12:56:51 -08:00
2015-01-29 10:00:35 -08:00
var walletOpts = {
2015-02-07 08:13:29 -08:00
name: 'a wallet',
2015-01-29 10:00:35 -08:00
m: m,
n: n,
2015-02-01 11:50:58 -08:00
pubKey: keyPair.pub,
2015-01-29 10:00:35 -08:00
};
2015-02-07 08:13:29 -08:00
server.createWallet(walletOpts, function(err, walletId) {
2015-01-29 10:00:35 -08:00
if (err) return cb(err);
2015-02-01 11:50:58 -08:00
async.each(_.range(1, n + 1), function(i, cb) {
2015-02-02 06:55:03 -08:00
2015-01-29 10:00:35 -08:00
var copayerOpts = {
2015-02-07 08:13:29 -08:00
walletId: walletId,
2015-01-29 10:00:35 -08:00
name: 'copayer ' + i,
2015-02-01 11:50:58 -08:00
xPubKey: someXPubKeys[i - 1],
xPubKeySignature: someXPubKeysSignatures[i - 1],
2015-01-29 10:00:35 -08:00
};
2015-02-02 06:55:03 -08:00
2015-02-07 07:48:57 -08:00
server.joinWallet(copayerOpts, function(err, copayerId) {
copayerIds.push(copayerId);
2015-01-29 10:00:35 -08:00
return cb(err);
});
2015-02-06 23:09:45 -08:00
}, function(err) {
2015-02-06 12:56:51 -08:00
if (err) return new Error('Could not generate wallet');
2015-02-07 07:48:57 -08:00
helpers.getAuthServer(copayerIds[0], function(s) {
2015-02-06 23:09:45 -08:00
s.getWallet({}, function(err, w) {
2015-02-06 12:56:51 -08:00
cb(s, w);
});
2015-01-29 10:00:35 -08:00
});
});
});
};
2015-02-03 11:46:28 -08:00
helpers.randomTXID = function() {
2015-02-03 18:17:06 -08:00
return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');;
2015-02-03 11:46:28 -08:00
};
2015-02-04 11:18:36 -08:00
helpers.toSatoshi = function(btc) {
if (_.isArray(btc)) {
return _.map(btc, helpers.toSatoshi);
} else {
return Utils.strip(btc * 1e8);
}
};
// Amounts in satoshis
2015-02-03 11:46:28 -08:00
helpers.createUtxos = function(server, wallet, amounts, cb) {
var addresses = [];
async.each(amounts, function(a, next) {
server.createAddress({}, function(err, address) {
2015-02-03 11:46:28 -08:00
addresses.push(address);
next(err);
});
},
function(err) {
amounts = [].concat(amounts);
var i = 0;
2015-02-04 06:43:12 -08:00
var utxos = _.map(amounts, function(amount) {
2015-02-03 11:46:28 -08:00
return {
txid: helpers.randomTXID(),
vout: Math.floor((Math.random() * 10) + 1),
2015-02-04 11:18:36 -08:00
satoshis: amount,
2015-02-03 18:17:06 -08:00
scriptPubKey: addresses[i].getScriptPubKey(wallet.m).toBuffer().toString('hex'),
2015-02-04 16:38:23 -08:00
address: addresses[i++].address,
2015-02-03 11:46:28 -08:00
};
2015-02-04 06:43:12 -08:00
});
2015-02-06 10:15:54 -08:00
return cb(utxos);
});
};
2015-02-04 06:43:12 -08:00
2015-02-06 10:15:54 -08:00
helpers.stubBlockExplorer = function(server, utxos, txid) {
var bc = sinon.stub();
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos);
if (txid) {
bc.broadcast = sinon.stub().callsArgWith(1, null, txid);
} else {
bc.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
}
server._getBlockExplorer = sinon.stub().returns(bc);
2015-01-30 12:37:30 -08:00
};
2015-01-29 10:00:35 -08:00
2015-02-06 10:15:54 -08:00
2015-02-04 06:43:12 -08:00
helpers.clientSign = function(tx, xpriv, n) {
//Derive proper key to sign, for each input
var privs = [],
derived = {};
var xpriv = new Bitcore.HDPrivateKey(someXPrivKey[0]);
_.each(tx.inputs, function(i) {
if (!derived[i.path]) {
2015-02-04 16:38:23 -08:00
derived[i.path] = xpriv.derive(i.path).privateKey;
2015-02-06 10:15:54 -08:00
}
2015-02-04 16:38:23 -08:00
privs.push(derived[i.path]);
2015-02-04 06:43:12 -08:00
});
var t = new Bitcore.Transaction();
_.each(tx.inputs, function(i) {
t.from(i, i.publicKeys, n);
});
t.to(tx.toAddress, tx.amount)
.change(tx.changeAddress)
.sign(privs);
var signatures = [];
_.each(privs, function(p) {
2015-02-06 10:15:54 -08:00
var s = t.getSignatures(p)[0].signature.toDER().toString('hex');
signatures.push(s);
});
2015-02-05 10:50:18 -08:00
//
2015-02-04 06:43:12 -08:00
return signatures;
};
2015-01-28 05:52:45 -08:00
var db, storage;
2015-01-27 05:18:45 -08:00
2015-01-29 10:00:35 -08:00
2015-01-27 05:18:45 -08:00
describe('Copay server', function() {
beforeEach(function() {
2015-02-01 11:50:58 -08:00
db = levelup(memdown, {
valueEncoding: 'json'
});
storage = new Storage({
db: db
});
2015-02-06 23:09:45 -08:00
CopayServer.initialize({
storage: storage
});
2015-01-27 05:18:45 -08:00
});
2015-02-06 12:56:51 -08:00
describe.skip('#getInstanceWithAuth', function() {
2015-02-06 23:09:45 -08:00
beforeEach(function() {});
2015-01-27 05:18:45 -08:00
2015-02-06 23:09:45 -08:00
it('should get server instance for existing copayer', function(done) {});
2015-01-27 05:18:45 -08:00
2015-02-06 23:09:45 -08:00
it('should fail when requesting for non-existent copayer', function(done) {});
2015-01-27 05:18:45 -08:00
2015-02-06 23:09:45 -08:00
it('should fail when message signature cannot be verified', function(done) {});
2015-01-27 05:18:45 -08:00
});
describe('#createWallet', function() {
2015-02-06 12:56:51 -08:00
var server;
2015-01-27 05:18:45 -08:00
beforeEach(function() {
2015-02-06 12:56:51 -08:00
server = new CopayServer();
2015-01-27 05:18:45 -08:00
});
it('should create and store wallet', function(done) {
var opts = {
name: 'my wallet',
m: 2,
n: 3,
2015-01-31 14:56:50 -08:00
pubKey: aPubKey,
2015-01-27 05:18:45 -08:00
};
2015-02-07 08:13:29 -08:00
server.createWallet(opts, function(err, walletId) {
2015-01-27 05:18:45 -08:00
should.not.exist(err);
2015-02-07 08:13:29 -08:00
server.storage.fetchWallet(walletId, function(err, wallet) {
2015-01-27 05:18:45 -08:00
should.not.exist(err);
2015-02-07 08:13:29 -08:00
wallet.id.should.equal(walletId);
2015-01-27 05:18:45 -08:00
wallet.name.should.equal('my wallet');
done();
});
});
});
2015-02-08 08:16:41 -08:00
it('should fail to create wallet with no name', function(done) {
var opts = {
name: '',
m: 2,
n: 3,
pubKey: aPubKey,
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(walletId);
err.should.exist;
err.message.should.contain('name');
done();
});
});
2015-02-03 04:40:39 -08:00
it('should fail to create wallet with invalid copayer pairs', function(done) {
2015-02-03 11:46:28 -08:00
var invalidPairs = [{
m: 0,
n: 0
}, {
m: 0,
n: 2
}, {
m: 2,
n: 1
}, {
m: 0,
n: 10
}, {
m: 1,
n: 20
}, {
m: 10,
n: 10
}, ];
2015-02-03 04:40:39 -08:00
var opts = {
id: '123',
name: 'my wallet',
pubKey: aPubKey,
};
2015-02-03 11:46:28 -08:00
async.each(invalidPairs, function(pair, cb) {
2015-02-03 04:40:39 -08:00
opts.m = pair.m;
opts.n = pair.n;
server.createWallet(opts, function(err) {
should.exist(err);
2015-02-04 07:46:31 -08:00
err.message.should.equal('Invalid combination of required copayers / total copayers');
2015-02-03 04:40:39 -08:00
return cb();
});
2015-02-03 11:46:28 -08:00
}, function(err) {
2015-02-03 04:40:39 -08:00
done();
});
2015-02-03 11:46:28 -08:00
});
2015-01-27 05:18:45 -08:00
});
describe('#joinWallet', function() {
2015-02-08 08:36:19 -08:00
var server, walletId;
beforeEach(function(done) {
2015-02-06 12:56:51 -08:00
server = new CopayServer();
2015-01-27 05:18:45 -08:00
var walletOpts = {
name: 'my wallet',
m: 2,
n: 3,
2015-02-01 11:50:58 -08:00
pubKey: keyPair.pub,
2015-01-27 05:18:45 -08:00
};
2015-02-08 08:36:19 -08:00
server.createWallet(walletOpts, function(err, wId) {
should.not.exist(err);
should.exist.walletId;
walletId = wId;
done();
});
});
2015-02-01 11:50:58 -08:00
2015-02-08 08:36:19 -08:00
it('should join existing wallet', function(done) {
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: aXPubKey,
xPubKeySignature: aXPubKeySignature,
};
server.joinWallet(copayerOpts, function(err, copayerId) {
2015-01-27 05:18:45 -08:00
should.not.exist(err);
2015-02-08 08:36:19 -08:00
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);
done();
2015-01-27 05:18:45 -08:00
});
});
});
});
2015-02-08 08:36:19 -08:00
it('should fail to join with no name', function(done) {
var copayerOpts = {
walletId: walletId,
name: '',
xPubKey: someXPubKeys[0],
xPubKeySignature: someXPubKeysSignatures[0],
};
server.joinWallet(copayerOpts, function(err, copayerId) {
should.not.exist(copayerId);
err.should.exist;
err.message.should.contain('name');
done();
});
});
2015-02-01 11:50:58 -08:00
it('should fail to join non-existent wallet', function(done) {
2015-02-07 08:13:29 -08:00
var copayerOpts = {
2015-02-08 08:36:19 -08:00
walletId: '123',
2015-02-07 08:13:29 -08:00
name: 'me',
xPubKey: 'dummy',
xPubKeySignature: 'dummy',
2015-01-27 05:18:45 -08:00
};
2015-02-07 08:13:29 -08:00
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
done();
2015-01-27 05:18:45 -08:00
});
});
2015-02-01 11:50:58 -08:00
it('should fail to join full wallet', function(done) {
2015-02-08 08:36:19 -08:00
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
var copayerOpts = {
walletId: wallet.id,
2015-01-27 05:18:45 -08:00
name: 'me',
2015-02-01 11:50:58 -08:00
xPubKey: someXPubKeys[1],
xPubKeySignature: someXPubKeysSignatures[1],
2015-01-27 05:18:45 -08:00
};
2015-02-08 08:36:19 -08:00
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.code.should.equal('WFULL');
err.message.should.equal('Wallet full');
done();
2015-01-27 05:18:45 -08:00
});
});
});
2015-02-01 11:50:58 -08:00
it('should fail to re-join wallet', function(done) {
2015-02-08 08:36:19 -08:00
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: someXPubKeys[0],
xPubKeySignature: someXPubKeysSignatures[0],
2015-01-27 05:18:45 -08:00
};
2015-02-08 08:36:19 -08:00
server.joinWallet(copayerOpts, function(err) {
2015-01-27 05:18:45 -08:00
should.not.exist(err);
2015-02-01 11:50:58 -08:00
server.joinWallet(copayerOpts, function(err) {
2015-02-08 08:36:19 -08:00
should.exist(err);
err.code.should.equal('CINWALLET');
err.message.should.equal('Copayer already in wallet');
done();
2015-01-27 05:18:45 -08:00
});
});
});
2015-02-01 11:50:58 -08:00
it('should fail to join with bad formated signature', function(done) {
2015-02-08 08:36:19 -08:00
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: someXPubKeys[0],
xPubKeySignature: 'bad sign',
2015-02-01 11:50:58 -08:00
};
2015-02-08 08:36:19 -08:00
server.joinWallet(copayerOpts, function(err) {
err.message.should.equal('Bad request');
done();
2015-02-01 11:50:58 -08:00
});
});
it('should fail to join with null signature', function(done) {
2015-02-08 08:36:19 -08:00
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: someXPubKeys[0],
2015-02-01 11:50:58 -08:00
};
2015-02-08 08:36:19 -08:00
try {
server.joinWallet(copayerOpts, function(err) {});
} catch (e) {
e.should.contain('xPubKeySignature');
done();
}
2015-02-01 11:50:58 -08:00
});
it('should fail to join with wrong signature', function(done) {
2015-02-08 08:36:19 -08:00
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: someXPubKeys[0],
xPubKeySignature: someXPubKeysSignatures[1],
2015-02-01 11:50:58 -08:00
};
2015-02-08 08:36:19 -08:00
server.joinWallet(copayerOpts, function(err) {
err.message.should.equal('Bad request');
done();
2015-02-01 11:50:58 -08:00
});
});
2015-02-02 11:16:14 -08:00
it('should set pkr and status = complete on last copayer joining (2-3)', function(done) {
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet(2, 3, function(server) {
2015-02-06 12:56:51 -08:00
server.getWallet({}, function(err, wallet) {
2015-01-27 05:18:45 -08:00
should.not.exist(err);
wallet.status.should.equal('complete');
wallet.publicKeyRing.length.should.equal(3);
done();
});
});
2015-02-01 11:50:58 -08:00
});
2015-01-27 05:18:45 -08:00
});
2015-01-28 07:16:19 -08:00
describe('#verifyMessageSignature', function() {
2015-02-06 15:59:59 -08:00
var server, wallet;
beforeEach(function(done) {
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet(2, 2, function(s, w) {
2015-02-06 15:59:59 -08:00
server = s;
wallet = w;
done();
});
2015-01-28 07:06:34 -08:00
});
2015-02-01 11:50:58 -08:00
it('should successfully verify message signature', function(done) {
2015-02-06 15:59:59 -08:00
var opts = {
message: aText,
signature: aTextSignature,
};
server.verifyMessageSignature(opts, function(err, isValid) {
should.not.exist(err);
isValid.should.equal(true);
done();
2015-01-28 07:06:34 -08:00
});
});
2015-02-06 12:56:51 -08:00
it('should fail to verify message signature for different copayer', function(done) {
2015-02-06 15:59:59 -08:00
var opts = {
message: aText,
signature: aTextSignature,
};
2015-02-07 08:13:29 -08:00
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
2015-02-06 15:59:59 -08:00
server.verifyMessageSignature(opts, function(err, isValid) {
should.not.exist(err);
isValid.should.be.false;
done();
2015-01-28 07:06:34 -08:00
});
});
});
});
2015-01-27 11:40:21 -08:00
describe('#createAddress', function() {
2015-02-06 15:59:59 -08:00
var server, wallet;
beforeEach(function(done) {
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet(2, 2, function(s, w) {
2015-02-06 15:59:59 -08:00
server = s;
wallet = w;
done();
});
2015-01-27 05:18:45 -08:00
});
it('should create address', function(done) {
server.createAddress({}, function(err, address) {
2015-02-06 15:59:59 -08:00
should.not.exist(err);
address.should.exist;
address.address.should.equal('36JdLEUDa6UwCfMhhkdZ2VFnDrGUoLedsR');
address.path.should.equal('m/2147483647/0/0');
done();
2015-02-02 11:32:13 -08:00
});
});
it('should fail to create address when wallet is not complete', function(done) {
var server = new CopayServer();
var walletOpts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: keyPair.pub,
};
server.createWallet(walletOpts, function(err, walletId) {
should.not.exist(err);
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: aXPubKey,
xPubKeySignature: aXPubKeySignature,
};
server.joinWallet(copayerOpts, function(err, copayerId) {
should.not.exist(err);
helpers.getAuthServer(copayerId, function(server) {
server.createAddress({}, function(err, address) {
should.not.exist(address);
err.should.exist;
err.message.should.contain('not complete');
done();
});
});
});
});
});
2015-02-07 06:15:54 -08:00
it('should create many addresses on simultaneous requests', function(done) {
2015-02-08 13:35:19 -08:00
var N = 5;
async.map(_.range(N), function(i, cb) {
server.createAddress({}, cb);
2015-02-06 15:59:59 -08:00
}, function(err, addresses) {
2015-02-08 13:35:19 -08:00
addresses.length.should.equal(N);
_.each(_.range(N), function(i) {
addresses[i].path.should.equal('m/2147483647/0/' + i);
});
2015-02-06 15:59:59 -08:00
// No two identical addresses
2015-02-08 13:35:19 -08:00
_.uniq(_.pluck(addresses, 'address')).length.should.equal(N);
2015-02-06 15:59:59 -08:00
done();
});
});
2015-02-03 12:32:40 -08:00
it('should not create address if unable to store wallet', function(done) {
2015-02-06 15:59:59 -08:00
var storeWalletStub = sinon.stub(server.storage, 'storeWallet');
storeWalletStub.yields('dummy error');
2015-02-04 06:43:12 -08:00
server.createAddress({}, function(err, address) {
2015-02-06 15:59:59 -08:00
err.should.exist;
should.not.exist(address);
2015-02-03 12:32:40 -08:00
2015-02-06 15:59:59 -08:00
server.getAddresses({}, function(err, addresses) {
addresses.length.should.equal(0);
server.storage.storeWallet.restore();
server.createAddress({}, function(err, address) {
2015-02-06 15:59:59 -08:00
should.not.exist(err);
address.should.exist;
address.address.should.equal('36JdLEUDa6UwCfMhhkdZ2VFnDrGUoLedsR');
address.path.should.equal('m/2147483647/0/0');
2015-02-06 15:59:59 -08:00
done();
2015-02-03 12:32:40 -08:00
});
});
});
});
it('should not create address if unable to store addresses', function(done) {
2015-02-06 15:59:59 -08:00
var storeAddressStub = sinon.stub(server.storage, 'storeAddress');
storeAddressStub.yields('dummy error');
2015-02-04 06:43:12 -08:00
server.createAddress({}, function(err, address) {
2015-02-06 15:59:59 -08:00
err.should.exist;
should.not.exist(address);
2015-02-03 12:32:40 -08:00
2015-02-06 15:59:59 -08:00
server.getAddresses({}, function(err, addresses) {
addresses.length.should.equal(0);
server.storage.storeAddress.restore();
server.createAddress({}, function(err, address) {
2015-02-06 15:59:59 -08:00
should.not.exist(err);
address.should.exist;
address.address.should.equal('36JdLEUDa6UwCfMhhkdZ2VFnDrGUoLedsR');
address.path.should.equal('m/2147483647/0/0');
2015-02-06 15:59:59 -08:00
done();
2015-02-03 12:32:40 -08:00
});
});
});
});
2015-01-27 05:18:45 -08:00
});
2015-01-27 11:40:21 -08:00
describe('#createTx', function() {
2015-02-06 12:56:51 -08:00
var server, wallet;
2015-01-27 11:40:21 -08:00
beforeEach(function(done) {
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet(2, 2, function(s, w) {
2015-02-06 12:56:51 -08:00
server = s;
2015-02-03 05:53:59 -08:00
wallet = w;
server.createAddress({}, function(err, address) {
2015-01-27 11:40:21 -08:00
done();
});
});
});
2015-02-06 12:51:21 -08:00
it('should create a tx', function(done) {
2015-02-04 11:18:36 -08:00
helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) {
2015-02-06 10:15:54 -08:00
helpers.stubBlockExplorer(server, utxos);
2015-02-03 11:46:28 -08:00
var txOpts = {
2015-02-03 18:17:06 -08:00
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
2015-02-04 11:18:36 -08:00
amount: helpers.toSatoshi(80),
2015-02-03 11:46:28 -08:00
message: 'some message',
};
2015-02-03 18:17:06 -08:00
2015-02-03 11:46:28 -08:00
server.createTx(txOpts, function(err, tx) {
2015-01-28 12:40:37 -08:00
should.not.exist(err);
2015-02-03 11:46:28 -08:00
tx.should.exist;
2015-02-08 14:10:06 -08:00
tx.message.should.equal('some message');
2015-02-03 11:46:28 -08:00
tx.isAccepted().should.equal.false;
tx.isRejected().should.equal.false;
2015-02-06 12:56:51 -08:00
server.getPendingTxs({}, function(err, txs) {
2015-01-30 13:29:46 -08:00
should.not.exist(err);
2015-02-03 11:46:28 -08:00
txs.length.should.equal(1);
2015-02-06 12:56:51 -08:00
server.getBalance({}, function(err, balance) {
2015-02-03 11:46:28 -08:00
should.not.exist(err);
2015-02-04 11:18:36 -08:00
balance.totalAmount.should.equal(helpers.toSatoshi(300));
balance.lockedAmount.should.equal(helpers.toSatoshi(100));
2015-02-03 11:46:28 -08:00
done();
});
2015-01-30 13:29:46 -08:00
});
2015-01-28 12:40:37 -08:00
});
2015-01-27 11:40:21 -08:00
});
});
2015-01-30 12:37:30 -08:00
it('should fail to create tx when wallet is not complete', function(done) {
var server = new CopayServer();
var walletOpts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: keyPair.pub,
};
server.createWallet(walletOpts, function(err, walletId) {
should.not.exist(err);
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: aXPubKey,
xPubKeySignature: aXPubKeySignature,
};
server.joinWallet(copayerOpts, function(err, copayerId) {
should.not.exist(err);
helpers.getAuthServer(copayerId, function(server, wallet) {
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: helpers.toSatoshi(80),
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
err.should.exist;
err.message.should.contain('not complete');
done();
});
});
});
});
});
2015-02-07 06:15:54 -08:00
2015-02-08 14:10:06 -08:00
it('should fail to create tx for address invalid address', function(done) {
helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) {
helpers.stubBlockExplorer(server, utxos);
var txOpts = {
toAddress: 'invalid address',
amount: helpers.toSatoshi(80),
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
err.should.exist;
err.code.should.equal('INVALIDADDRESS');
err.message.should.equal('Invalid address');
done();
});
});
});
it('should fail to create tx for address of different network', function(done) {
helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) {
helpers.stubBlockExplorer(server, utxos);
var txOpts = {
toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD', // testnet
amount: helpers.toSatoshi(80),
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
err.should.exist;
err.code.should.equal('INVALIDADDRESS');
err.message.should.equal('Incorrect address network');
done();
});
});
});
2015-02-08 08:16:41 -08:00
2015-02-03 18:17:06 -08:00
it('should fail to create tx when insufficient funds', function(done) {
2015-02-06 10:15:54 -08:00
helpers.createUtxos(server, wallet, helpers.toSatoshi([100]), function(utxos) {
helpers.stubBlockExplorer(server, utxos);
2015-02-03 18:17:06 -08:00
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
2015-02-04 11:18:36 -08:00
amount: helpers.toSatoshi(120),
2015-02-03 18:17:06 -08:00
};
server.createTx(txOpts, function(err, tx) {
err.code.should.equal('INSUFFICIENTFUNDS');
err.message.should.equal('Insufficient funds');
2015-02-06 12:56:51 -08:00
server.getPendingTxs({}, function(err, txs) {
2015-02-03 18:17:06 -08:00
should.not.exist(err);
txs.length.should.equal(0);
2015-02-06 12:56:51 -08:00
server.getBalance({}, function(err, balance) {
2015-02-03 18:17:06 -08:00
should.not.exist(err);
balance.lockedAmount.should.equal(0);
2015-02-04 11:18:36 -08:00
balance.totalAmount.should.equal(10000000000);
2015-02-03 18:17:06 -08:00
done();
});
});
});
});
});
2015-02-08 08:16:41 -08:00
it.skip('should fail to create tx when insufficient funds for fee', function(done) {});
it.skip('should fail to create tx for dust amount', function(done) {});
2015-02-03 18:17:06 -08:00
it('should create tx when there is a pending tx and enough UTXOs', function(done) {
2015-02-04 11:18:36 -08:00
helpers.createUtxos(server, wallet, helpers.toSatoshi([10.1, 10.2, 10.3]), function(utxos) {
2015-02-06 10:15:54 -08:00
helpers.stubBlockExplorer(server, utxos);
2015-02-03 18:17:06 -08:00
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
2015-02-04 11:18:36 -08:00
amount: helpers.toSatoshi(12),
2015-02-03 18:17:06 -08:00
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
tx.should.exist;
var txOpts2 = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 8,
};
server.createTx(txOpts2, function(err, tx) {
should.not.exist(err);
tx.should.exist;
2015-02-06 12:56:51 -08:00
server.getPendingTxs({}, function(err, txs) {
2015-02-03 18:17:06 -08:00
should.not.exist(err);
txs.length.should.equal(2);
2015-02-06 12:56:51 -08:00
server.getBalance({}, function(err, balance) {
2015-02-03 18:17:06 -08:00
should.not.exist(err);
2015-02-04 11:18:36 -08:00
balance.totalAmount.should.equal(3060000000);
balance.lockedAmount.should.equal(3060000000);
2015-02-03 18:17:06 -08:00
done();
});
});
});
});
});
});
it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) {
2015-02-04 11:18:36 -08:00
helpers.createUtxos(server, wallet, helpers.toSatoshi([10.1, 10.2, 10.3]), function(utxos) {
2015-02-06 10:15:54 -08:00
helpers.stubBlockExplorer(server, utxos);
2015-02-03 18:17:06 -08:00
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
2015-02-04 11:18:36 -08:00
amount: helpers.toSatoshi(12),
2015-02-03 18:17:06 -08:00
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
tx.should.exist;
var txOpts2 = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
2015-02-04 11:18:36 -08:00
amount: helpers.toSatoshi(24),
2015-02-03 18:17:06 -08:00
};
server.createTx(txOpts2, function(err, tx) {
err.code.should.equal('INSUFFICIENTFUNDS');
err.message.should.equal('Insufficient funds');
2015-02-03 18:17:06 -08:00
should.not.exist(tx);
2015-02-06 12:56:51 -08:00
server.getPendingTxs({}, function(err, txs) {
2015-02-03 18:17:06 -08:00
should.not.exist(err);
txs.length.should.equal(1);
2015-02-06 12:56:51 -08:00
server.getBalance({}, function(err, balance) {
2015-02-03 18:17:06 -08:00
should.not.exist(err);
2015-02-04 11:18:36 -08:00
balance.totalAmount.should.equal(helpers.toSatoshi(30.6));
balance.lockedAmount.should.equal(helpers.toSatoshi(20.3));
2015-02-03 18:17:06 -08:00
done();
});
});
});
});
});
});
2015-02-08 13:29:58 -08:00
it('should create tx using different UTXOs for simultaneous requests', function(done) {
2015-02-08 13:35:19 -08:00
var N = 5;
helpers.createUtxos(server, wallet, helpers.toSatoshi(_.times(N, function() {
2015-02-08 13:29:58 -08:00
return 100;
})), function(utxos) {
helpers.stubBlockExplorer(server, utxos);
server.getBalance({}, function(err, balance) {
should.not.exist(err);
2015-02-08 13:35:19 -08:00
balance.totalAmount.should.equal(helpers.toSatoshi(N * 100));
2015-02-08 13:29:58 -08:00
balance.lockedAmount.should.equal(helpers.toSatoshi(0));
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: helpers.toSatoshi(80),
};
2015-02-08 13:35:19 -08:00
async.map(_.range(N), function(i, cb) {
2015-02-08 13:29:58 -08:00
server.createTx(txOpts, function(err, tx) {
cb(err, tx);
});
}, function(err) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
2015-02-08 13:35:19 -08:00
txs.length.should.equal(N);
_.uniq(_.pluck(txs, 'changeAddress')).length.should.equal(N);
2015-02-08 13:29:58 -08:00
server.getBalance({}, function(err, balance) {
should.not.exist(err);
2015-02-08 13:35:19 -08:00
balance.totalAmount.should.equal(helpers.toSatoshi(N * 100));
2015-02-08 13:29:58 -08:00
balance.lockedAmount.should.equal(balance.totalAmount);
done();
});
});
});
});
});
});
2015-01-27 11:40:21 -08:00
});
2015-02-04 06:43:12 -08:00
2015-02-04 11:18:36 -08:00
describe('#signTx', function() {
2015-02-06 12:56:51 -08:00
var server, wallet, txid;
2015-02-04 06:43:12 -08:00
beforeEach(function(done) {
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet(2, 2, function(s, w) {
2015-02-06 12:56:51 -08:00
server = s;
2015-02-04 06:43:12 -08:00
wallet = w;
server.createAddress({}, function(err, address) {
2015-02-04 11:18:36 -08:00
helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(utxos) {
2015-02-06 10:15:54 -08:00
helpers.stubBlockExplorer(server, utxos);
2015-02-04 06:43:12 -08:00
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
2015-02-04 11:18:36 -08:00
amount: helpers.toSatoshi(10),
2015-02-04 06:43:12 -08:00
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
tx.should.exist;
txid = tx.id;
done();
});
});
});
});
});
2015-02-06 23:09:45 -08:00
it('should sign a TX with multiple inputs, different paths', function(done) {
2015-02-06 12:56:51 -08:00
server.getPendingTxs({}, function(err, txs) {
2015-02-04 06:43:12 -08:00
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
2015-02-04 11:18:36 -08:00
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
2015-02-05 10:50:18 -08:00
should.not.exist(err);
2015-02-04 11:18:36 -08:00
done();
});
2015-02-05 10:50:18 -08:00
});
});
2015-02-04 06:43:12 -08:00
2015-02-06 23:09:45 -08:00
it('should fail if one signature is broken', function(done) {
2015-02-06 12:56:51 -08:00
server.getPendingTxs({}, function(err, txs) {
2015-02-05 10:50:18 -08:00
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
2015-02-06 10:15:54 -08:00
signatures[0] = 1;
2015-02-05 10:50:18 -08:00
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
err.message.should.contain('signatures');
done();
});
2015-02-04 06:43:12 -08:00
});
2015-02-05 10:50:18 -08:00
});
2015-02-07 08:13:29 -08:00
it('should fail on invalid signature', function(done) {
2015-02-06 12:56:51 -08:00
server.getPendingTxs({}, function(err, txs) {
2015-02-05 10:50:18 -08:00
var tx = txs[0];
tx.id.should.equal(txid);
2015-02-04 06:43:12 -08:00
2015-02-05 10:50:18 -08:00
var signatures = ['11', '22', '33', '44'];
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
err.message.should.contain('signatures');
done();
});
});
2015-02-04 06:43:12 -08:00
});
});
2015-02-06 10:15:54 -08:00
describe('#signTx and broadcast', function() {
2015-02-06 12:56:51 -08:00
var server, wallet, utxos;
2015-02-06 10:15:54 -08:00
beforeEach(function(done) {
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet(1, 1, function(s, w) {
2015-02-06 12:56:51 -08:00
server = s;
2015-02-06 10:15:54 -08:00
wallet = w;
server.createAddress({}, function(err, address) {
2015-02-06 10:15:54 -08:00
helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(inutxos) {
utxos = inutxos;
done();
});
});
});
});
it('should sign and broadcast a tx', function(done) {
helpers.stubBlockExplorer(server, utxos, '1122334455');
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: helpers.toSatoshi(10),
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
txp.should.exist;
var txpid = txp.id;
2015-02-06 13:22:52 -08:00
server.getPendingTxs({}, function(err, txps) {
2015-02-06 10:15:54 -08:00
var txp = txps[0];
txp.id.should.equal(txpid);
var signatures = helpers.clientSign(txp, someXPrivKey[0], wallet.n);
server.signTx({
txProposalId: txpid,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
txp.status.should.equal('broadcasted');
txp.txid.should.equal('1122334455');
done();
});
});
});
});
it('should keep tx as *accepted* if unable to broadcast it', function(done) {
helpers.stubBlockExplorer(server, utxos);
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: helpers.toSatoshi(10),
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
txp.should.exist;
var txpid = txp.id;
2015-02-06 13:22:52 -08:00
server.getPendingTxs({}, function(err, txps) {
2015-02-06 10:15:54 -08:00
var txp = txps[0];
txp.id.should.equal(txpid);
var signatures = helpers.clientSign(txp, someXPrivKey[0], wallet.n);
server.signTx({
txProposalId: txpid,
signatures: signatures,
}, function(err, txp) {
err.should.contain('broadcast');
2015-02-06 13:22:52 -08:00
server.getPendingTxs({}, function(err, txps) {
should.not.exist(err);
2015-02-08 06:20:22 -08:00
txps.length.should.equal(1);
var txp = txps[0];
txp.status.should.equal('accepted');
should.not.exist(txp.txid);
done();
2015-02-06 13:22:52 -08:00
});
2015-02-06 10:15:54 -08:00
});
});
});
});
});
2015-02-07 06:15:54 -08:00
2015-02-08 08:16:41 -08:00
describe('Tx proposal workflow', function() {
var server, wallet, utxos;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, address) {
helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(inutxos) {
utxos = inutxos;
done();
});
});
});
});
it('other copayers should see pending proposal created by one copayer', function(done) {
helpers.stubBlockExplorer(server, utxos);
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: helpers.toSatoshi(10),
message: 'some message',
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
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);
2015-02-08 13:29:58 -08:00
txps[0].message.should.equal('some message');
done();
});
});
});
});
2015-02-07 06:15:54 -08:00
2015-02-08 08:16:41 -08:00
it.skip('tx proposals should not be broadcast until quorum is reached', function(done) {
});
2015-02-07 06:15:54 -08:00
2015-02-07 08:13:29 -08:00
it.skip('tx proposals should accept as many rejections as possible without finally rejecting', function(done) {});
2015-02-07 06:15:54 -08:00
2015-02-07 08:13:29 -08:00
it.skip('proposal creator should be able to delete proposal if there are no other signatures', function(done) {});
2015-02-07 06:15:54 -08:00
});
2015-02-06 23:09:45 -08:00
describe('#getTxs', function() {
var server, wallet, clock;
beforeEach(function(done) {
if (server)
2015-02-07 09:15:04 -08:00
return done();
this.timeout(5000);
2015-02-06 23:09:45 -08:00
console.log('\tCreating TXS...');
clock = sinon.useFakeTimers();
2015-02-07 08:13:29 -08:00
helpers.createAndJoinWallet(1, 1, function(s, w) {
2015-02-06 23:09:45 -08:00
server = s;
wallet = w;
server.createAddress({}, function(err, address) {
2015-02-06 23:09:45 -08:00
helpers.createUtxos(server, wallet, helpers.toSatoshi(_.range(10)), function(utxos) {
helpers.stubBlockExplorer(server, utxos);
var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: helpers.toSatoshi(0.1),
};
async.eachSeries(_.range(10), function(i, next) {
clock.tick(10000);
server.createTx(txOpts, function(err, tx) {
next();
});
}, function(err) {
return done(err);
});
2015-02-06 23:09:45 -08:00
});
});
});
});
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);
2015-02-07 08:13:29 -08:00
var times = _.pluck(txps, 'createdOn');
times.should.deep.equal([90, 80, 70, 60]);
2015-02-06 23:09:45 -08:00
done();
});
});
it('should pull the first 5 txs', function(done) {
server.getTxs({
maxTs: 50,
limit: 5
}, function(err, txps) {
should.not.exist(err);
2015-02-07 08:13:29 -08:00
var times = _.pluck(txps, 'createdOn');
times.should.deep.equal([50, 40, 30, 20, 10]);
2015-02-06 23:09:45 -08:00
done();
});
});
it('should pull the last 4 txs', function(done) {
server.getTxs({
limit: 4
}, function(err, txps) {
should.not.exist(err);
2015-02-07 08:13:29 -08:00
var times = _.pluck(txps, 'createdOn');
times.should.deep.equal([90, 80, 70, 60]);
2015-02-06 23:09:45 -08:00
done();
});
});
it('should pull all txs', function(done) {
2015-02-07 08:13:29 -08:00
server.getTxs({}, function(err, txps) {
2015-02-06 23:09:45 -08:00
should.not.exist(err);
2015-02-07 08:13:29 -08:00
var times = _.pluck(txps, 'createdOn');
times.should.deep.equal([90, 80, 70, 60, 50, 40, 30, 20, 10]);
2015-02-06 23:09:45 -08:00
done();
});
});
it('should txs from times 50 to 70', function(done) {
server.getTxs({
minTs: 50,
maxTs: 70,
}, function(err, txps) {
should.not.exist(err);
2015-02-07 08:13:29 -08:00
var times = _.pluck(txps, 'createdOn');
times.should.deep.equal([70, 60, 50]);
2015-02-06 23:09:45 -08:00
done();
});
});
});
2015-01-27 05:18:45 -08:00
});