This commit is contained in:
Matias Alejo Garcia 2014-12-31 13:27:35 -03:00
commit e736e52034
9 changed files with 514 additions and 125 deletions

View File

@ -4,10 +4,10 @@
[![Coverage Status](https://img.shields.io/coveralls/bitpay/copay.svg)](https://coveralls.io/r/bitpay/copay?branch=master)
[![Stories in Ready](https://badge.waffle.io/bitpay/copay.svg?label=in progress&title=In progress)](https://waffle.io/bitpay/copay)
Copay is a secure bitcoin wallet for friends and companies.
Copay is an open-source secure bitcoin wallet platform for friends and companies.
Easy-to-use multisignature bitcoin wallet, bringing corporate-level security to ordinary people.
When friends or company executives join a Copay wallet, more than one person must sign every transaction. If your computer is compromised and your private keys are stolen, the bitcoins are still safe. This is in addition to state-of-the-art encrypted storage and communication.
When friends or company executives join a Copay wallet, more than one person must sign every transaction. If your computer is compromised and your private keys are stolen, the bitcoins are still safe if you use the multi-signature feature. This is in addition to state-of-the-art encrypted storage and communication.
## Before you start

View File

@ -5,6 +5,7 @@ angular.module('copayApp.controllers').controller('HomeWalletController', functi
$rootScope.title = 'Home';
var w = $rootScope.wallet;
$scope.isShared = w.isShared();
$scope.requiresMultipleSignatures = w.requiresMultipleSignatures();
if ($scope.isShared)
$scope.copayers = w.getRegisteredPeerIds();
};

View File

@ -52,9 +52,9 @@ angular.module('copayApp.controllers').controller('ProfileController', function(
return;
}
$location.path('/');
setTimeout(function() {
$timeout(function() {
notification.error('Success', 'Profile successfully deleted');
}, 1);
});
});
};
@ -73,10 +73,10 @@ angular.module('copayApp.controllers').controller('ProfileController', function(
identityService.deleteWallet($scope.item, function(err) {
if (err) {
$scope.loading = null;
$scope.error = err.message;
$scope.error = err.message || err;
copay.logger.warn(err);
}
else {
$timeout(function () { $scope.$digest(); });
} else {
$modalInstance.close($scope.item.name || $scope.item.id);
}
});

View File

@ -76,6 +76,10 @@ Identity.getKeyForEmail = function(email) {
return Identity.getStoragePrefix() + bitcore.util.sha256ripe160(email).toString('hex');
};
Identity.prototype.getChecksumForStorage = function() {
return JSON.stringify(_.sortBy(this.walletIds));
};
Identity.prototype.getId = function() {
return Identity.getKeyForEmail(this.email);
};
@ -105,7 +109,7 @@ Identity.create = function(opts, cb) {
});
};
Identity.prototype.resendVerificationEmail = function (cb) {
Identity.prototype.resendVerificationEmail = function(cb) {
var self = this;
preconditions.checkArgument(_.isFunction(cb));
@ -148,6 +152,22 @@ Identity.open = function(opts, cb) {
});
};
Identity.prototype.verifyChecksum = function(cb) {
var self = this;
self.storage.getItem(Identity.getKeyForEmail(self.email), function(err, data, headers) {
var iden;
if (err) return cb(err);
try {
iden = JSON.parse(data);
} catch (e) {
return cb(e);
}
return cb(null, self.getChecksumForStorage() == self.getChecksumForStorage.call(iden));
});
};
/**
* @param {string} walletId
* @returns {Wallet}
@ -182,22 +202,25 @@ Identity.prototype.addWallet = function(w) {
*/
Identity.prototype.deleteWallet = function(walletId, cb) {
preconditions.checkArgument(_.isString(walletId));
var self = this;
var w = this.getWalletById(walletId);
w.close();
delete this.wallets[walletId];
delete this.focusedTimestamps[walletId];
this.walletIds = _.without(this.walletIds, walletId);
this.storage.removeItem(Wallet.getStorageKey(walletId), function(err) {
self.verifyChecksum(function(err, match) {
if (err) return cb(err);
self.emitAndKeepAlive('walletDeleted', walletId);
self.store(null, cb);
if (!match) return cb('The profile is out of sync. Please re-login to get the latest changes.');
var w = self.getWalletById(walletId);
w.close();
delete self.wallets[walletId];
delete self.focusedTimestamps[walletId];
self.walletIds = _.without(self.walletIds, walletId);
self.storage.removeItem(Wallet.getStorageKey(walletId), function(err) {
if (err) return cb(err);
self.emitAndKeepAlive('walletDeleted', walletId);
self.store(null, cb);
});
});
};
@ -346,18 +369,19 @@ Identity.prototype.exportEncryptedWithWalletInfo = function(opts) {
return crypto.encrypt(this.password, this.exportWithWalletInfo(opts));
};
Identity.prototype.setBackupNeeded = function() {
this.backupNeeded = true;
this.store({
noWallets: true
}, function() {});
}
Identity.prototype.setBackupNeeded = function(backupNeeded) {
var self = this;
Identity.prototype.setBackupDone = function() {
this.backupNeeded = false;
this.store({
noWallets: true
}, function() {});
self.backupNeeded = !!backupNeeded;
self.verifyChecksum(function(err, match) {
if (err) return cb(err);
if (!match) return cb('The profile is out of sync. Please re-login to get the latest changes.');
self.store({
noWallets: true
}, function() {});
});
}
Identity.prototype.exportWithWalletInfo = function(opts) {
@ -424,8 +448,11 @@ Identity.prototype.remove = function(opts, cb) {
};
Identity.prototype._cleanUp = function() {
var self = this;
_.each(this.getWallets(), function(w) {
w.close();
delete self.wallets[w.getId()];
});
};
@ -433,7 +460,6 @@ Identity.prototype._cleanUp = function() {
* @desc Closes the wallet and disconnects all services
*/
Identity.prototype.close = function() {
this._cleanUp();
this.emitAndKeepAlive('closed');
};
@ -599,70 +625,76 @@ Identity.prototype.bindWallet = function(w) {
Identity.prototype.createWallet = function(opts, cb) {
preconditions.checkArgument(cb);
opts = opts || {};
opts.networkName = opts.networkName || 'testnet';
log.debug('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey'));
var privOpts = {
networkName: opts.networkName,
};
if (opts.privateKeyHex && opts.privateKeyHex.length > 1) {
privOpts.extendedPrivateKeyString = opts.privateKeyHex;
}
opts.privateKey = opts.privateKey || new PrivateKey(privOpts);
var requiredCopayers = opts.requiredCopayers || this.walletDefaults.requiredCopayers;
var totalCopayers = opts.totalCopayers || this.walletDefaults.totalCopayers;
opts.lockTimeoutMin = this.walletDefaults.idleDurationMin;
opts.publicKeyRing = opts.publicKeyRing || new PublicKeyRing({
networkName: opts.networkName,
requiredCopayers: requiredCopayers,
totalCopayers: totalCopayers,
});
opts.publicKeyRing.addCopayer(
opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(),
opts.nickname || this.getName()
);
log.debug('\t### PublicKeyRing Initialized');
opts.txProposals = opts.txProposals || new TxProposals({
networkName: opts.networkName,
});
var walletClass = opts.walletClass || Wallet;
log.debug('\t### TxProposals Initialized');
opts.networkOpts = this.networkOpts;
opts.blockchainOpts = this.blockchainOpts;
opts.spendUnconfirmed = opts.spendUnconfirmed || this.walletDefaults.spendUnconfirmed;
opts.reconnectDelay = opts.reconnectDelay || this.walletDefaults.reconnectDelay;
opts.requiredCopayers = requiredCopayers;
opts.totalCopayers = totalCopayers;
opts.version = opts.version || this.version;
var self = this;
var w = new walletClass(opts);
if (self.getWalletById(w.getId())) {
return cb('walletAlreadyExists');
}
self.addWallet(w);
self.updateFocusedTimestamp(w.getId());
self.bindWallet(w);
self.storeWallet(w, function(err) {
self.verifyChecksum(function(err, match) {
if (err) return cb(err);
if (!match) return cb('The profile is out of sync. Please re-login to get the latest changes.');
self.backupNeeded = true;
self.store({
noWallets: true,
}, function(err) {
return cb(err, w);
opts = opts || {};
opts.networkName = opts.networkName || 'testnet';
log.debug('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey'));
var privOpts = {
networkName: opts.networkName,
};
if (opts.privateKeyHex && opts.privateKeyHex.length > 1) {
privOpts.extendedPrivateKeyString = opts.privateKeyHex;
}
opts.privateKey = opts.privateKey || new PrivateKey(privOpts);
var requiredCopayers = opts.requiredCopayers || self.walletDefaults.requiredCopayers;
var totalCopayers = opts.totalCopayers || self.walletDefaults.totalCopayers;
opts.lockTimeoutMin = self.walletDefaults.idleDurationMin;
opts.publicKeyRing = opts.publicKeyRing || new PublicKeyRing({
networkName: opts.networkName,
requiredCopayers: requiredCopayers,
totalCopayers: totalCopayers,
});
opts.publicKeyRing.addCopayer(
opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(),
opts.nickname || self.getName()
);
log.debug('\t### PublicKeyRing Initialized');
opts.txProposals = opts.txProposals || new TxProposals({
networkName: opts.networkName,
});
var walletClass = opts.walletClass || Wallet;
log.debug('\t### TxProposals Initialized');
opts.networkOpts = self.networkOpts;
opts.blockchainOpts = self.blockchainOpts;
opts.spendUnconfirmed = opts.spendUnconfirmed || self.walletDefaults.spendUnconfirmed;
opts.reconnectDelay = opts.reconnectDelay || self.walletDefaults.reconnectDelay;
opts.requiredCopayers = requiredCopayers;
opts.totalCopayers = totalCopayers;
opts.version = opts.version || self.version;
var w = new walletClass(opts);
if (self.getWalletById(w.getId())) {
return cb('walletAlreadyExists');
}
self.addWallet(w);
self.updateFocusedTimestamp(w.getId());
self.bindWallet(w);
self.storeWallet(w, function(err) {
if (err) return cb(err);
self.backupNeeded = true;
self.store({
noWallets: true,
}, function(err) {
return cb(err, w);
});
});
});
};

View File

@ -12,7 +12,7 @@ if (!ls || ls.length < 1)
if (window && window.navigator) {
var rxaosp = window.navigator.userAgent.match(/Android.*AppleWebKit\/([\d.]+)/);
var isaosp = (rxaosp && rxaosp[1] < 537);
if (isaosp)
if (!window.cordova && isaosp)
unsupported = true;
}

View File

@ -72,7 +72,7 @@ BackupService.prototype.walletDownload = function(wallet) {
};
BackupService.prototype.profileEncrypted = function(iden) {
iden.setBackupDone();
iden.setBackupNeeded(false);
return iden.exportEncryptedWithWalletInfo(iden.password);
}

View File

@ -168,6 +168,17 @@ describe('Identity model', function() {
});
});
describe('#openWallets', function(done) {
it('should emit noWallets', function() {
var iden = new Identity(getDefaultParams());
sinon.spy(iden, 'emitAndKeepAlive');
iden.openWallets();
iden.emitAndKeepAlive.calledOnce.should.be.true;
iden.emitAndKeepAlive.getCall(0).args[0].should.equal('noWallets');
});
});
describe('#remove', function(done) {
it('should remove empty profile', function(done) {
var storage = sinon.stub();
@ -285,6 +296,48 @@ describe('Identity model', function() {
done();
});
});
it('should return error because the limit has been reached', function(done) {
storage.setItem = sinon.stub().yields('OVERQUOTA');
var w = {
toObj: sinon.stub().returns({
key1: 'val1'
}),
getStorageKey: sinon.stub().returns('storage_key'),
getName: sinon.stub().returns('name'),
setVersion: sinon.spy(),
sizes: sinon.stub().returns(99),
getId: sinon.spy(),
};
iden.storeWallet(w, function(err) {
should.exist(err);
err.should.be.equal('OVERQUOTA');
done();
});
});
it('should return error', function(done) {
storage.setItem = sinon.stub().yields('UNKNOWN');
var w = {
toObj: sinon.stub().returns({
key1: 'val1'
}),
getStorageKey: sinon.stub().returns('storage_key'),
getName: sinon.stub().returns('name'),
setVersion: sinon.spy(),
sizes: sinon.stub().returns(99),
getId: sinon.spy(),
};
iden.storeWallet(w, function(err) {
should.exist(err);
err.should.be.equal('UNKNOWN');
done();
});
});
it('should change wallet version when storing', function(done) {
storage.setItem = sinon.stub().yields(null);
var w = {
@ -327,6 +380,7 @@ describe('Identity model', function() {
it('should be able to create wallets with given pk', function(done) {
var priv = 'tprv8ZgxMBicQKsPdEqHcA7RjJTayxA3gSSqeRTttS1JjVbgmNDZdSk9EHZK5pc52GY5xFmwcakmUeKWUDzGoMLGAhrfr5b3MovMUZUTPqisL2m';
args.storage.getItem = sinon.stub().yields(null, JSON.stringify(iden));
args.storage.setItem = sinon.stub();
args.storage.setItem.onFirstCall().callsArg(2);
args.storage.setItem.onSecondCall().callsArg(2);
@ -342,6 +396,7 @@ describe('Identity model', function() {
});
it('should be able to create wallets with random pk', function(done) {
args.storage.getItem = sinon.stub().yields(null, JSON.stringify(iden));
args.storage.setItem = sinon.stub();
args.storage.setItem.onCall(0).callsArg(2);
args.storage.setItem.onCall(1).callsArg(2);
@ -352,6 +407,7 @@ describe('Identity model', function() {
}, function(err, w1) {
should.exist(w1);
args.storage.getItem = sinon.stub().yields(null, JSON.stringify(iden));
iden.createWallet({
walletClass: walletClass,
}, function(err, w2) {
@ -617,11 +673,10 @@ describe('Identity model', function() {
}).should.deep.equal(w);
});
it('should delete wallet', function(done) {
iden.addWallet(w);
iden.getWalletById('32').getName().should.equal('treintaydos');
iden.storage.getItem = sinon.stub().yields(null, JSON.stringify(iden));
iden.deleteWallet('32', function(err) {
should.not.exist(iden.getWalletById('32'));
iden.walletIds.should.deep.equal([]);
@ -668,26 +723,270 @@ describe('Identity model', function() {
close: sinon.stub(),
};
});
it('should include wallets', function() {
iden.addWallet(w);
var obj = iden.toObj();
_.indexOf(obj.walletIds,'32').should.be.above(-1);
_.indexOf(obj.walletIds, '32').should.be.above(-1);
});
it('should set version to actual version', function() {
var obj = iden.toObj();
obj.version.should.equal(version);
});
it('should include 2 wallets', function() {
iden.addWallet(w);
iden.addWallet(w2);
var obj = iden.toObj();
_.indexOf(obj.walletIds,'32').should.be.above(-1);
_.indexOf(obj.walletIds,'33').should.be.above(-1);
_.indexOf(obj.walletIds, '32').should.be.above(-1);
_.indexOf(obj.walletIds, '33').should.be.above(-1);
});
});
describe('#_cleanUp', function() {
var iden, w, w2;
beforeEach(function() {
var storage = sinon.stub();
storage.setCredentials = sinon.stub();
storage.removeItem = sinon.stub().yields(null);
storage.clear = sinon.stub().yields();
var opts = {
email: 'test@test.com',
password: '123',
network: {
testnet: {
url: 'https://test-insight.bitpay.com:443'
},
livenet: {
url: 'https://insight.bitpay.com:443'
},
},
storage: storage,
};
iden = new Identity(opts);
w = {
getId: sinon.stub().returns('32'),
getName: sinon.stub().returns('treintaydos'),
close: sinon.stub(),
};
w2 = {
getId: sinon.stub().returns('33'),
getName: sinon.stub().returns('treintaytres'),
close: sinon.stub(),
};
iden.addWallet(w);
iden.addWallet(w2);
});
it('should close all wallets', function() {
_.size(iden.wallets).should.be.equal(2);
iden._cleanUp();
_.size(iden.wallets).should.be.equal(0);
});
});
describe('#getLastFocusedWalletId', function() {
var clock;
before(function() {
clock = sinon.useFakeTimers();
});
after(function() {
clock.restore();
});
var iden, w, w2;
beforeEach(function() {
var storage = sinon.stub();
storage.setCredentials = sinon.stub();
storage.removeItem = sinon.stub().yields(null);
storage.clear = sinon.stub().yields();
storage.getItem = sinon.stub();
var opts = {
email: 'test@test.com',
password: '123',
network: {
testnet: {
url: 'https://test-insight.bitpay.com:443'
},
livenet: {
url: 'https://insight.bitpay.com:443'
},
},
storage: storage,
};
iden = new Identity(opts);
w = {
getId: sinon.stub().returns('32'),
getName: sinon.stub().returns('treintaydos'),
close: sinon.stub(),
};
w2 = {
getId: sinon.stub().returns('33'),
getName: sinon.stub().returns('treintaytres'),
close: sinon.stub(),
};
});
it('should return indefined', function() {
expect(iden.getLastFocusedWalletId()).to.be.undefined;
});
it('should return last focused wallet', function() {
iden.addWallet(w);
iden.addWallet(w2);
iden.updateFocusedTimestamp(w.getId());
iden.getLastFocusedWalletId().should.be.equal(w.getId());
clock.tick(1000);
iden.updateFocusedTimestamp(w2.getId());
iden.getLastFocusedWalletId().should.be.equal(w2.getId());
iden.deleteWallet(w2.getId(), function() {
iden.getLastFocusedWalletId().should.be.equal(w.getId());
});
});
});
describe('importFromFullJson', function() {
var opts;
beforeEach(function() {
var storage = sinon.stub();
storage.setCredentials = sinon.stub();
storage.removeItem = sinon.stub().yields(null);
storage.clear = sinon.stub().yields();
opts = {
email: 'test@test.com',
password: '123',
network: {
testnet: {
url: 'https://test-insight.bitpay.com:443'
},
livenet: {
url: 'https://insight.bitpay.com:443'
},
},
storage: storage,
};
});
it('should throw error because json is wrong', function() {
Identity.importFromFullJson('asdfg', '1', {}, function(c) {
c.should.be.equal('BADSTR: Unable to retrieve json from string');
});
});
it('should throw error because json does not have required fields', function() {
Identity.importFromFullJson('{"age":23}', '1', {}, function(c) {
c.should.be.equal('BADSTR');
});
});
it('should create a profile', function() {
var json = '{"networkOpts":{"livenet":{"url":"https://insight.bitpay.com:443","transports":["polling"]},"testnet":{"url":"https://test-insight.bitpay.com:443","transports":["polling"]}},"blockchainOpts":{"livenet":{"url":"https://insight.bitpay.com:443","transports":["polling"]},"testnet":{"url":"https://test-insight.bitpay.com:443","transports":["polling"]}},"fullName":"l@l","email":"l@l","password":"1","storage":{"type":"DB","storeUrl":"https://insight.bitpay.com:443/api/email","iterations":1000,"salt":"jBbYTj8zTrOt6V","email":"l@l","password":"1","_cachedKey":"y4a352k6sM15gGag+PgQwXRdFjzi0yX6aLEGttWaeP+kbU7JeSPDUfbhhzonnQRUicJu/1IMWgDZbDJjWmrKgA=="},"walletDefaults":{"requiredCopayers":2,"totalCopayers":3,"spendUnconfirmed":true,"reconnectDelay":5000,"idleDurationMin":4,"settings":{"unitName":"bits","unitToSatoshi":100,"unitDecimals":2,"alternativeName":"US Dollar","alternativeIsoCode":"USD"}},"version":"0.8.2","walletIds":["15a3ecd34dfb7000","59220d2110461861","bfd6adad419078d9","893dc0c0a776648b","e8ee7218c6ea7f93"],"wallets":{},"focusedTimestamps":{"15a3ecd34dfb7000":1418916813711,"bfd6adad419078d9":1418835855887,"e8ee7218c6ea7f93":1418775999995,"59220d2110461861":1418835858871,"893dc0c0a776648b":1418835763680},"backupNeeded":true,"_events":{}}';
Identity.importFromFullJson(json, '1', opts, function(err, iden) {
expect(err).to.be.null;
iden.should.not.be.null;
});
});
});
describe('#closeWallet', function() {
var iden, w, w2, w3;
beforeEach(function() {
var storage = sinon.stub();
storage.setCredentials = sinon.stub();
storage.removeItem = sinon.stub().yields(null);
storage.clear = sinon.stub().yields();
var opts = {
email: 'test@test.com',
password: '123',
network: {
testnet: {
url: 'https://test-insight.bitpay.com:443'
},
livenet: {
url: 'https://insight.bitpay.com:443'
},
},
storage: storage,
};
iden = new Identity(opts);
w = {
getId: sinon.stub().returns('32'),
getName: sinon.stub().returns('treintaydos'),
close: sinon.stub(),
};
w2 = {
getId: sinon.stub().returns('33'),
getName: sinon.stub().returns('treintaytres'),
close: sinon.stub(),
};
w3 = {
getId: sinon.stub().returns('34'),
getName: sinon.stub().returns('treintaycuatro'),
close: sinon.stub(),
};
iden.addWallet(w);
iden.addWallet(w2);
//do not add w3
});
it('should close a Wallet', function() {
iden.closeWallet(w, function(err) {
expect(err).to.be.null;
});
iden.closeWallet(w3, function(err) {
expect(err).to.be.not.null;
});
});
});
describe('#_checkVersion', function() {
var iden;
beforeEach(function() {
var storage = sinon.stub();
storage.setCredentials = sinon.stub();
storage.removeItem = sinon.stub().yields(null);
storage.clear = sinon.stub().yields();
var opts = {
email: 'test@test.com',
password: '123',
network: {
testnet: {
url: 'https://test-insight.bitpay.com:443'
},
livenet: {
url: 'https://insight.bitpay.com:443'
},
},
storage: storage,
};
iden = new Identity(opts);
});
it('should checkVersion', function() {
expect(iden._checkVersion()).to.be.undefined;
expect(iden._checkVersion('0.0.0')).to.be.undefined;
(function() {
console.log('b', iden._checkVersion('9.9.9'));
}).should.throw('Major difference');
});
});
});

View File

@ -11,6 +11,7 @@ var networks = bitcore.networks;
var TxProposal = copay.TxProposal;
var TxProposals = copay.TxProposals;
var moment = moment || require('moment');
var dummyProposal = new TxProposal({
creator: 1,
@ -34,41 +35,98 @@ describe('TxProposals', function() {
describe('#fromObj', function() {
it('should create an instance from an Object', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
networkName: 'livenet',
walletId: '123a12',
txps: [],
});
should.exist(txps);
txps.network.name.should.equal('livenet');
});
it('should skip Objects with errors', function() {
it('should skip Objects with errors', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
networkName: 'livenet',
walletId: '123a12',
txps: [ { a: 1 }],
txps: [{
a: 1
}],
});
should.exist(txps);
Object.keys(txps.txps).length.should.equal(0);
});
});
describe('#length', function() {
it('should return length', function() {
var txps = new TxProposals();
txps.txps = {
a: 1,
b: 2
};
txps.length().should.equal(2);
});
});
describe('#getNtxidsSince', function() {
it('should throw illegal argument', function() {
var txps = new TxProposals();
txps.txps = {
a: 1,
b: 2
};
(function() {
txps.getNtxidsSince()
}).should.throw('Illegal Argument');
});
it('should return keys since a date', function() {
var today = moment().toDate();
var today_plus_1 = moment().add(1, 'day').toDate();
var today_plus_2 = moment().add(2, 'day').toDate();
var today_plus_3 = moment().add(3, 'day').toDate();
var txps = new TxProposals();
txps.txps = [{
id: 1,
createdTs: today
}, {
id: 2,
createdTs: today_plus_1
}, {
id: 3,
createdTs: today_plus_2
}];
txps.getNtxidsSince(today).length.should.be.equal(3);
txps.getNtxidsSince(today_plus_1).length.should.be.equal(2);
txps.getNtxidsSince(today_plus_2).length.should.be.equal(1);
txps.getNtxidsSince(today_plus_3).length.should.be.equal(0);
});
});
describe('#getNtxids', function() {
it('should return keys', function() {
var txps = new TxProposals();
txps.txps = {a:1, b:2};
txps.getNtxids().should.deep.equal(['a','b']);
txps.txps = {
a: 1,
b: 2
};
txps.getNtxids().should.deep.equal(['a', 'b']);
});
});
describe('#deleteOne', function() {
it('should delete specified ntxid', function() {
var txps = new TxProposals();
txps.txps = {a:1, b:2};
txps.txps = {
a: 1,
b: 2
};
txps.deleteOne('a');
txps.getNtxids().should.deep.equal(['b']);
});
it('should fail on non-existent ntxid', function() {
var txps = new TxProposals();
txps.txps = {a:1, b:2};
(function () {
txps.txps = {
a: 1,
b: 2
};
(function() {
txps.deleteOne('c');
}).should.throw('Unknown TXP: c');
});
@ -76,7 +134,7 @@ describe('TxProposals', function() {
describe('#toObj', function() {
it('should an object', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
networkName: 'livenet',
walletId: '123a12',
txps: [],
});
@ -86,28 +144,28 @@ describe('TxProposals', function() {
});
it('should export txps', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
networkName: 'livenet',
walletId: '123a12',
txps: [],
});
txps.txps = {
'hola' : dummyProposal,
'chau' : dummyProposal,
'hola': dummyProposal,
'chau': dummyProposal,
};
var o = txps.toObj();
o.txps.length.should.equal(2);
});
it('should filter sent txp', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
networkName: 'livenet',
walletId: '123a12',
txps: [],
});
var d = JSON.parse(JSON.stringify(dummyProposal));
d.sent=1;
d.sent = 1;
txps.txps = {
'hola' : dummyProposal,
'chau' : d,
'hola': dummyProposal,
'chau': d,
};
var o = txps.toObj();
o.txps.length.should.equal(1);
@ -117,8 +175,7 @@ describe('TxProposals', function() {
it('should merge', function() {
var txps = new TxProposals();
var d = dummyProposal;
txps.merge(d.toObj(),{});
txps.merge(d.toObj(), {});
});
});
});

View File

@ -50,7 +50,7 @@
</div>
<div ng-show="isShared">
<div ng-show="requiresMultipleSignatures">
<div class="row">
<div class="large-12 columns">
<div class="panel oh">