Fix Conflicts:

views/unsupported.html
This commit is contained in:
Gustavo Maximiliano Cortez 2014-08-06 18:51:04 -03:00
commit 0c2141c380
53 changed files with 1983 additions and 1297 deletions

View file

@ -1,7 +1,7 @@
'use strict';
angular.module('copayApp.controllers').controller('CopayersController',
function($scope, $rootScope, $location, backupService) {
function($scope, $rootScope, $location, backupService, walletFactory, controllerUtils) {
$scope.backup = function() {
var w = $rootScope.wallet;
@ -18,4 +18,12 @@ angular.module('copayApp.controllers').controller('CopayersController',
$location.path('/addresses');
};
$scope.deleteWallet = function() {
var w = $rootScope.wallet;
w.disconnect();
walletFactory.delete(w.id, function() {
controllerUtils.logout();
});
};
});

View file

@ -9,5 +9,5 @@ angular.module('copayApp.controllers').controller('HomeController',
if ($rootScope.pendingPayment) {
notification.info('Login Required', 'Please open wallet to complete payment');
}
$scope.hasWallets = walletFactory.getWallets().length > 0 ? true : false;
$scope.hasWallets = (walletFactory.getWallets() && walletFactory.getWallets().length > 0) ? true : false;
});

View file

@ -11,7 +11,7 @@ angular.module('copayApp.controllers').controller('OpenController',
};
$scope.loading = false;
$scope.wallets = walletFactory.getWallets().sort(cmp);
$scope.selectedWalletId = $scope.wallets.length ? $scope.wallets[0].id : null;
$scope.selectedWalletId = walletFactory.storage.getLastOpened() || ($scope.wallets[0] && $scope.wallets[0].id);
$scope.openPassword = '';
$scope.open = function(form) {

View file

@ -50,7 +50,7 @@ angular.module('copayApp.controllers').controller('SendController',
var w = $rootScope.wallet;
w.createTx(address, amount, commentText, function(ntxid) {
if (w.totalCopayers > 1) {
if (w.isShared()) {
$scope.loading = false;
var message = 'The transaction proposal has been created';
notification.success('Success!', message);

View file

@ -76,6 +76,8 @@ angular.module('copayApp.controllers').controller('SettingsController',
unitToSatoshi: $scope.selectedUnit.value,
}));
window.location.reload();
// Go home reloading the application
var hashIndex = window.location.href.indexOf('#!/');
window.location = window.location.href.substr(0, hashIndex);
};
});

View file

@ -3,8 +3,6 @@
angular.module('copayApp.controllers').controller('SidebarController',
function($scope, $rootScope, $sce, $location, $http, notification, controllerUtils) {
$scope.version = copay.version;
$scope.networkName = config.networkName;
$scope.menu = [{
'title': 'Addresses',
'icon': 'fi-address-book',
@ -62,23 +60,6 @@ angular.module('copayApp.controllers').controller('SidebarController',
return new Array(num);
}
$http.get('https://api.github.com/repos/bitpay/copay/tags').success(function(data) {
var toInt = function(s) {
return parseInt(s);
};
var latestVersion = data[0].name.replace('v', '').split('.').map(toInt);
var currentVersion = copay.version.split('.').map(toInt);
var title = 'Copay ' + data[0].name + ' available.';
var content;
if (currentVersion[0] < latestVersion[0]) {
content = 'It\'s important that you update your wallet at https://copay.io';
notification.version(title, content, true);
} else if (currentVersion[0] == latestVersion[0] && currentVersion[1] < latestVersion[1]) {
var content = 'Please update your wallet at https://copay.io';
notification.version(title, content, false);
}
});
// Init socket handlers (with no wallet yet)
controllerUtils.setSocketHandlers();

26
js/controllers/version.js Normal file
View file

@ -0,0 +1,26 @@
'use strict';
angular.module('copayApp.controllers').controller('VersionController',
function($scope, $rootScope, $http, notification) {
$scope.version = copay.version;
$scope.networkName = config.networkName;
$http.get('https://api.github.com/repos/bitpay/copay/tags').success(function(data) {
var toInt = function(s) {
return parseInt(s);
};
var latestVersion = data[0].name.replace('v', '').split('.').map(toInt);
var currentVersion = copay.version.split('.').map(toInt);
var title = 'Copay ' + data[0].name + ' available.';
var content;
if (currentVersion[0] < latestVersion[0]) {
content = 'It\'s important that you update your wallet at https://copay.io';
notification.version(title, content, true);
} else if (currentVersion[0] == latestVersion[0] && currentVersion[1] < latestVersion[1]) {
var content = 'Please update your wallet at https://copay.io';
notification.version(title, content, false);
}
});
});

View file

@ -2,6 +2,7 @@
var imports = require('soop').imports();
var bitcore = require('bitcore');
var coinUtil = bitcore.util;
var preconditions = require('preconditions').singleton();
var http;
@ -37,33 +38,6 @@ function _asyncForEach(array, fn, callback) {
}
};
function removeRepeatedElements(ar) {
var ya = false,
v = "",
aux = [].concat(ar),
r = Array();
for (var i in aux) { //
v = aux[i];
ya = false;
for (var a in aux) {
if (v == aux[a]) {
if (ya == false) {
ya = true;
} else {
aux[a] = "";
}
}
}
}
for (var a in aux) {
if (aux[a] != "") {
r.push(aux[a]);
}
}
return r;
}
Insight.prototype._getOptions = function(method, path, data) {
return {
host: this.host,
@ -78,6 +52,25 @@ Insight.prototype._getOptions = function(method, path, data) {
};
};
// This is vulneable to txid maneability
// TODO: if ret = false,
// check output address from similar transactions.
//
Insight.prototype.checkSentTx = function(tx, cb) {
var hash = coinUtil.formatHashFull(tx.getHash());
var options = this._getOptions('GET', '/api/tx/' + hash);
this._request(options, function(err, res) {
if (err) return cb(err);
var ret = false;
if (res && res.txid === hash) {
ret = hash;
}
return cb(err, ret);
});
};
Insight.prototype.getTransactions = function(addresses, cb) {
preconditions.shouldBeArray(addresses);
preconditions.shouldBeFunction(cb);
@ -101,8 +94,11 @@ Insight.prototype.getTransactions = function(addresses, cb) {
callback();
});
}, function() {
var clean_txids = removeRepeatedElements(txids);
_asyncForEach(clean_txids, function(txid, callback2) {
var uniqueTxids = {};
for (var k in txids) {
uniqueTxids[txids[k]] = 1;
}
_asyncForEach(Object.keys(uniqueTxids), function(txid, callback2) {
var options = self._getOptions('GET', '/api/tx/' + txid);
self._request(options, function(err, res) {
txs.push(res);
@ -164,8 +160,8 @@ Insight.prototype.checkActivity = function(addresses, cb) {
var getOutputs = function(t) {
return flatArray(
t.vout.map(function(vout) {
return vout.scriptPubKey.addresses;
})
return vout.scriptPubKey.addresses;
})
);
};

View file

@ -33,12 +33,12 @@ HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) {
return BIP45_PUBLIC_PREFIX + '/' + sub;
};
HDPath.indicesForPath = function(path) {
HDPath.indexesForPath = function(path) {
preconditions.shouldBeString(path);
var s = path.split('/');
return {
isChange: s[3] === '1',
index: parseInt(s[4]),
addressIndex: parseInt(s[4]),
copayerIndex: parseInt(s[2])
};
};

View file

@ -23,14 +23,13 @@ function PublicKeyRing(opts) {
this.copayersHK = opts.copayersHK || [];
this.indexes = opts.indexes ? HDParams.fromList(opts.indexes)
: HDParams.init(this.totalCopayers);
this.indexes = opts.indexes ? HDParams.fromList(opts.indexes) : HDParams.init(this.totalCopayers);
this.publicKeysCache = opts.publicKeysCache || {};
this.nicknameFor = opts.nicknameFor || {};
this.copayerIds = [];
this.copayersBackup = opts.copayersBackup || [];
this.addressToPath = {};
this.publicKeysCache = opts.publicKeysCache || {};
this.nicknameFor = opts.nicknameFor || {};
this.copayerIds = [];
this.copayersBackup = opts.copayersBackup || [];
this.addressToPath = {};
}
PublicKeyRing.fromObj = function(data) {
@ -100,14 +99,6 @@ PublicKeyRing.prototype._checkKeys = function() {
throw new Error('dont have required keys yet');
};
PublicKeyRing.prototype._newExtendedPublicKey = function() {
return new PrivateKey({
networkName: this.network.name
})
.deriveBIP45Branch()
.extendedPublicKeyString();
};
PublicKeyRing.prototype._updateBip = function(index) {
var hk = this.copayersHK[index].derive(HDPath.IdBranch);
this.copayerIds[index] = hk.eckey.public.toString('hex');
@ -126,6 +117,8 @@ PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) {
};
PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) {
preconditions.checkArgument(newEpk);
if (this.isComplete())
throw new Error('PKR already has all required key:' + this.totalCopayers);
@ -134,10 +127,6 @@ PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) {
throw new Error('PKR already has that key');
});
if (!newEpk) {
newEpk = this._newExtendedPublicKey();
}
var i = this.copayersHK.length;
var bip = new HK(newEpk);
this.copayersHK.push(bip);
@ -192,7 +181,9 @@ PublicKeyRing.prototype.getAddress = function(index, isChange, id) {
// Overloaded to receive a PubkeyString or a consigner index
PublicKeyRing.prototype.getHDParams = function(id) {
var copayerIndex = this.getCosigner(id);
var index = this.indexes.filter(function(i) { return i.copayerIndex == copayerIndex });
var index = this.indexes.filter(function(i) {
return i.copayerIndex == copayerIndex
});
if (index.length != 1) throw new Error('no index for copayerIndex');
return index[0];
@ -231,9 +222,11 @@ PublicKeyRing.prototype.getCosigner = function(pubKey) {
if (typeof pubKey == 'undefined') return HDPath.SHARED_INDEX;
if (typeof pubKey == 'number') return pubKey;
var sorted = this.copayersHK.map(function(h, i){
var sorted = this.copayersHK.map(function(h, i) {
return h.eckey.public.toString('hex');
}).sort(function(h1, h2){ return h1.localeCompare(h2); });
}).sort(function(h1, h2) {
return h1.localeCompare(h2);
});
var index = sorted.indexOf(pubKey);
if (index == -1) throw new Error('no public key in ring');
@ -255,41 +248,87 @@ PublicKeyRing.prototype.getAddressesInfo = function(opts, pubkey) {
PublicKeyRing.prototype.getAddressesInfoForIndex = function(index, opts, copayerIndex) {
opts = opts || {};
var isOwned = index.copayerIndex == HDPath.SHARED_INDEX
|| index.copayerIndex == copayerIndex;
var isOwned = index.copayerIndex == HDPath.SHARED_INDEX || index.copayerIndex == copayerIndex;
var ret = [];
if (!opts.excludeChange) {
for (var i = 0; i < index.changeIndex; i++) {
var a = this.getAddress(i, true, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: true,
owned: isOwned
});
}
var ret = [];
if (!opts.excludeChange) {
for (var i = 0; i < index.changeIndex; i++) {
var a = this.getAddress(i, true, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: true,
owned: isOwned
});
}
}
if (!opts.excludeMain) {
for (var i = 0; i < index.receiveIndex; i++) {
var a = this.getAddress(i, false, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: false,
owned: isOwned
});
}
if (!opts.excludeMain) {
for (var i = 0; i < index.receiveIndex; i++) {
var a = this.getAddress(i, false, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: false,
owned: isOwned
});
}
}
return ret;
return ret;
};
PublicKeyRing.prototype.getForPath = function(path) {
var p = HDPath.indexesForPath(path);
var pubKeys = this.getPubKeys(p.addressIndex, p.isChange, p.copayerIndex);
return pubKeys;
};
PublicKeyRing.prototype.getForPaths = function(paths) {
preconditions.checkArgument(paths);
return paths.map(this.getForPath.bind(this));
};
PublicKeyRing.prototype.forPaths = function(paths) {
return {
pubKeys: paths.map(this.getForPath.bind(this)),
copayerIds: this.copayerIds,
}
};
// returns pubkey -> copayerId.
PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) {
preconditions.checkArgument(pubkeys);
preconditions.checkArgument(paths);
var inKeyMap = {}, ret = {};
for(var i in pubkeys ){
inKeyMap[pubkeys[i]] = 1;
};
var keys = this.getForPaths(paths);
for(var i in keys ){
for(var copayerIndex in keys[i] ){
var kHex = keys[i][copayerIndex].toString('hex');
if (inKeyMap[kHex]) {
ret[kHex] =this.copayerIds[copayerIndex];
delete inKeyMap[kHex];
}
}
}
for(var i in inKeyMap)
throw new Error('Pubkey not identified')
return ret;
};
// TODO this could be cached
PublicKeyRing.prototype._addScriptMap = function(map, path) {
var p = HDPath.indicesForPath(path);
var script = this.getRedeemScript(p.index, p.isChange, p.copayerIndex);
var p = HDPath.indexesForPath(path);
var script = this.getRedeemScript(p.addressIndex, p.isChange, p.copayerIndex);
map[Address.fromScript(script, this.network.name).toString()] = script.getBuffer().toString('hex');
};

View file

@ -0,0 +1,321 @@
'use strict';
var bitcore = require('bitcore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');;
var TransactionBuilder = bitcore.TransactionBuilder;
var Script = bitcore.Script;
var Key = bitcore.Key;
var buffertools = bitcore.buffertools;
var preconditions = require('preconditions').instance();
var VERSION = 1;
var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment'];
function TxProposal(opts) {
preconditions.checkArgument(opts);
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
preconditions.checkArgument(opts.builder, 'no builder');
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
this.inputChainPaths = opts.inputChainPaths;
this.version = opts.version;
this.builder = opts.builder;
this.createdTs = opts.createdTs;
this.createdTs = opts.createdTs;
this._inputSignatures = [];
// CopayerIds
this.creator = opts.creator;
this.signedBy = opts.signedBy || {};
this.seenBy = opts.seenBy || {};
this.rejectedBy = opts.rejectedBy || {};
this.sentTs = opts.sentTs || null;
this.sentTxid = opts.sentTxid || null;
this.comment = opts.comment || null;
this.readonly = opts.readonly || null;
this._sync();
}
TxProposal.prototype._check = function() {
if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) {
throw new Error('Invalid tx proposal');
}
var tx = this.builder.build();
if (!tx.ins.length)
throw new Error('Invalid tx proposal: no ins');
for (var i in tx.ins) {
var scriptSig = tx.ins[i].s;
if (!scriptSig || !scriptSig.length) {
throw new Error('Invalid tx proposal: no signatures');
}
}
for (var i = 0; i < tx.ins.length; i++) {
var hashType = tx.getHashType(i);
if (hashType && hashType !== Transaction.SIGHASH_ALL)
throw new Error('Invalid tx proposal: bad signatures');
}
};
TxProposal.prototype._updateSignedBy = function() {
this._inputSignatures = [];
var tx = this.builder.build();
for (var i in tx.ins) {
var scriptSig = new Script(tx.ins[i].s);
var signatureCount = scriptSig.countSignatures();
var info = TxProposal._infoFromRedeemScript(scriptSig);
var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL);
var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash);
if (signatureIndexes.length !== signatureCount)
throw new Error('Invalid signature');
this._inputSignatures[i] = signatureIndexes.map(function(i) {
var r = info.keys[i].toString('hex');
return r;
});
};
};
TxProposal.prototype._sync = function() {
this._check();
this._updateSignedBy();
return this;
}
TxProposal.prototype.getId = function() {
preconditions.checkState(this.builder);
return this.builder.build().getNormalizedHash().toString('hex');
};
TxProposal.prototype.toObj = function() {
var o = JSON.parse(JSON.stringify(this));
delete o['builder'];
o.builderObj = this.builder.toObj();
return o;
};
TxProposal._trim = function(o) {
var ret = {};
CORE_FIELDS.forEach(function(k) {
ret[k] = o[k];
});
return ret;
};
TxProposal.fromObj = function(o, forceOpts) {
preconditions.checkArgument(o.builderObj);
delete o['builder'];
try {
// force opts is requested.
for (var k in forceOpts) {
o.builderObj.opts[k] = forceOpts[k];
}
o.builder = TransactionBuilder.fromObj(o.builderObj);
} catch (e) {
// backwards (V0) compatatibility fix.
if (!o.version) {
o.builder = new BuilderMockV0(o.builderObj);
o.readonly = 1;
};
}
return new TxProposal(o);
};
TxProposal.fromUntrustedObj = function(o, forceOpts) {
return TxProposal.fromObj(TxProposal._trim(o), forceOpts);
};
TxProposal.prototype.toObjTrim = function() {
return TxProposal._trim(this.toObj());
};
TxProposal._formatKeys = function(keys) {
var ret = [];
for (var i in keys) {
if (!Buffer.isBuffer(keys[i]))
throw new Error('keys must be buffers');
var k = new Key();
k.public = keys[i];
ret.push(k);
};
return ret;
};
TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) {
preconditions.checkArgument(Buffer.isBuffer(txSigHash));
preconditions.checkArgument(inKeys);
preconditions.checkState(Buffer.isBuffer(inKeys[0]));
if (scriptSig.chunks[0] !== 0)
throw new Error('Invalid scriptSig');
var keys = TxProposal._formatKeys(inKeys);
var ret = [];
for (var i = 1; i <= scriptSig.countSignatures(); i++) {
var chunk = scriptSig.chunks[i];
var sigRaw = new Buffer(chunk.slice(0, chunk.length - 1));
for (var j in keys) {
var k = keys[j];
if (k.verifySignatureSync(txSigHash, sigRaw)) {
ret.push(parseInt(j));
break;
}
}
}
return ret;
};
TxProposal._infoFromRedeemScript = function(s) {
var redeemScript = new Script(s.chunks[s.chunks.length - 1]);
if (!redeemScript)
throw new Error('Bad scriptSig (no redeemscript)');
var pubkeys = redeemScript.capture();
if (!pubkeys || !pubkeys.length)
throw new Error('Bad scriptSig (no pubkeys)');
return {
keys: pubkeys,
script: redeemScript,
};
};
TxProposal.prototype.mergeBuilder = function(incoming) {
var b0 = this.builder;
var b1 = incoming.builder;
var before = JSON.stringify(b0.toObj());
b0.merge(b1);
var after = JSON.stringify(b0.toObj());
return after !== before;
};
TxProposal.prototype.setSeen = function(copayerId) {
if (!this.seenBy[copayerId])
this.seenBy[copayerId] = Date.now();
};
TxProposal.prototype.setRejected = function(copayerId) {
if (this.signedBy[copayerId])
throw new Error('Can not reject a signed TX');
if (!this.rejectedBy[copayerId])
this.rejectedBy[copayerId] = Date.now();
};
TxProposal.prototype.setSent = function(sentTxid) {
this.sentTxid = sentTxid;
this.sentTs = Date.now();
};
TxProposal.prototype._allSignatures = function() {
var ret = {};
for (var i in this._inputSignatures)
for (var j in this._inputSignatures[i])
ret[this._inputSignatures[i][j]] = true;
return ret;
};
TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
var newCopayer = {},
oldCopayers = {},
newSignedBy = {},
readOnlyPeers = {},
isNew = 1;
for (var k in this.signedBy) {
oldCopayers[k] = 1;
isNew = 0;
};
if (isNew == 0) {
if (!this.creator || !this.createdTs)
throw new Error('Existing TX has no creator');
if (!this.signedBy[this.creator])
throw new Error('Existing TX is not signed by creator');
if (Object.keys(this.signedBy).length === 0)
throw new Error('Existing TX has no signatures');
}
var iSig = this._inputSignatures[0];
for (var i in iSig) {
var copayerId = keyMap[iSig[i]];
if (!copayerId)
throw new Error('Found unknown signature')
if (oldCopayers[copayerId]) {
//Already have it. Do nothing
} else {
newCopayer[copayerId] = Date.now();
delete oldCopayers[i];
}
}
// Seems unncessary to check this:
// if (!newCopayer[senderId] && !readOnlyPeers[senderId])
// throw new Error('TX must have a (new) senders signature')
if (Object.keys(newCopayer).length > 1)
throw new Error('New TX must have only 1 new signature');
// Handler creator / createdTs.
// from senderId, and must be signed by senderId
if (isNew) {
this.creator = Object.keys(newCopayer)[0];
this.seenBy[this.creator] = this.createdTs = Date.now();
}
//Ended. Update this.
for (var i in newCopayer) {
this.signedBy[i] = newCopayer[i];
}
// signedBy has preference over rejectedBy
for (var i in this.signedBy) {
delete this.rejectedBy[i];
}
return Object.keys(newCopayer);
};
// merge will not merge any metadata.
TxProposal.prototype.merge = function(incoming) {
var hasChanged = this.mergeBuilder(incoming);
this._sync();
return hasChanged;
};
//This should be on bitcore / Transaction
TxProposal.prototype.countSignatures = function() {
var tx = this.builder.build();
var ret = 0;
for (var i in tx.ins) {
ret += tx.countInputSignatures(i);
}
return ret;
};
module.exports = TxProposal;

View file

@ -1,174 +1,16 @@
'use strict';
var imports = require('soop').imports();
var BuilderMockV0 = require('./BuilderMockV0');;
var bitcore = require('bitcore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');;
var TransactionBuilder = bitcore.TransactionBuilder;
var TxProposal = require('./TxProposal');;
var Script = bitcore.Script;
var Key = bitcore.Key;
var buffertools = bitcore.buffertools;
var preconditions = require('preconditions').instance();
function TxProposal(opts) {
this.creator = opts.creator;
this.createdTs = opts.createdTs;
this.seenBy = opts.seenBy || {};
this.signedBy = opts.signedBy || {};
this.rejectedBy = opts.rejectedBy || {};
this.builder = opts.builder;
this.sentTs = opts.sentTs || null;
this.sentTxid = opts.sentTxid || null;
this.inputChainPaths = opts.inputChainPaths || [];
this.comment = opts.comment || null;
}
TxProposal.prototype.getID = function() {
return this.builder.build().getNormalizedHash().toString('hex');
};
TxProposal.prototype.toObj = function() {
var o = JSON.parse(JSON.stringify(this));
delete o['builder'];
o.builderObj = this.builder.toObj();
return o;
};
TxProposal.prototype.setSent = function(sentTxid) {
this.sentTxid = sentTxid;
this.sentTs = Date.now();
};
TxProposal.fromObj = function(o, forceOpts) {
var t = new TxProposal(o);
try {
// force opts is requested.
for (var k in forceOpts) {
o.builderObj.opts[k] = forceOpts[k];
}
t.builder = TransactionBuilder.fromObj(o.builderObj);
} catch (e) {
if (!o.version) {
t.builder = new BuilderMockV0(o.builderObj);
t.readonly = 1;
};
}
return t;
};
TxProposal.prototype.isValid = function() {
if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) {
return false;
}
var tx = this.builder.build();
for (var i = 0; i < tx.ins.length; i++) {
var hashType = tx.getHashType(i);
if (hashType && hashType !== Transaction.SIGHASH_ALL) {
return false;
}
}
return true;
};
TxProposal.getSentTs = function() {
return this.sentTs;
};
TxProposal.prototype.merge = function(other, author) {
var ret = {};
ret.events = this.mergeMetadata(other, author);
ret.hasChanged = this.mergeBuilder(other);
return ret;
};
TxProposal.prototype.mergeBuilder = function(other) {
var b0 = this.builder;
var b1 = other.builder;
// TODO: improve this comparison
var before = JSON.stringify(b0.toObj());
b0.merge(b1);
var after = JSON.stringify(b0.toObj());
return after !== before;
};
TxProposal.prototype.mergeMetadata = function(v1, author) {
var events = [];
var v0 = this;
var ntxid = this.getID();
Object.keys(v1.seenBy).forEach(function(k) {
if (!v0.seenBy[k]) {
// TODO: uncomment below and change protocol to make this work
//if (k != author) throw new Error('Non authoritative seenBy change by ' + author);
v0.seenBy[k] = v1.seenBy[k];
events.push({
type: 'seen',
cId: k,
txId: ntxid
});
}
});
Object.keys(v1.signedBy).forEach(function(k) {
if (!v0.signedBy[k]) {
// TODO: uncomment below and change protocol to make this work
//if (k != author) throw new Error('Non authoritative signedBy change by ' + author);
v0.signedBy[k] = v1.signedBy[k];
events.push({
type: 'signed',
cId: k,
txId: ntxid
});
}
});
Object.keys(v1.rejectedBy).forEach(function(k) {
if (!v0.rejectedBy[k]) {
// TODO: uncomment below and change protocol to make this work
//if (k != author) throw new Error('Non authoritative rejectedBy change by ' + author);
v0.rejectedBy[k] = v1.rejectedBy[k];
events.push({
type: 'rejected',
cId: k,
txId: ntxid
});
}
});
if (!v0.sentTxid && v1.sentTxid) {
v0.sentTs = v1.sentTs;
v0.sentTxid = v1.sentTxid;
events.push({
type: 'broadcast',
txId: ntxid
});
}
return events;
};
//This should be on bitcore / Transaction
TxProposal.prototype.countSignatures = function() {
var tx = this.builder.build();
var ret = 0;
for (var i in tx.ins) {
ret += tx.countInputSignatures(i);
}
return ret;
};
module.exports = require('soop')(TxProposal);
function TxProposals(opts) {
opts = opts || {};
@ -178,6 +20,7 @@ function TxProposals(opts) {
this.txps = {};
}
// fromObj => from a trusted source
TxProposals.fromObj = function(o, forceOpts) {
var ret = new TxProposals({
networkName: o.networkName,
@ -187,7 +30,7 @@ TxProposals.fromObj = function(o, forceOpts) {
o.txps.forEach(function(o2) {
var t = TxProposal.fromObj(o2, forceOpts);
if (t.builder) {
var id = t.getID();
var id = t.getId();
ret.txps[id] = t;
}
});
@ -198,14 +41,9 @@ TxProposals.prototype.getNtxids = function() {
return Object.keys(this.txps);
};
TxProposals.prototype.toObj = function(onlyThisNtxid) {
if (onlyThisNtxid) throw new Error();
TxProposals.prototype.toObj = function() {
var ret = [];
for (var id in this.txps) {
if (onlyThisNtxid && id != onlyThisNtxid)
continue;
var t = this.txps[id];
if (!t.sent)
ret.push(t.toObj());
@ -217,50 +55,53 @@ TxProposals.prototype.toObj = function(onlyThisNtxid) {
};
};
TxProposals.prototype.merge = function(inTxp, author) {
var myTxps = this.txps;
var ntxid = inTxp.getID();
var ret = {};
ret.events = [];
ret.events.hasChanged = false;
TxProposals.prototype.merge = function(inObj, builderOpts) {
var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts);
incomingTx._sync();
var myTxps = this.txps;
var ntxid = incomingTx.getId();
var ret = {
ntxid: ntxid
};
if (myTxps[ntxid]) {
var v0 = myTxps[ntxid];
var v1 = inTxp;
ret = v0.merge(v1, author);
// Merge an existing txProposal
ret.hasChanged = myTxps[ntxid].merge(incomingTx);
} else {
this.txps[ntxid] = inTxp;
ret.hasChanged = true;
ret.events.push({
type: 'new',
cid: inTxp.creator,
tx: ntxid
});
// Create a new one
ret.new = ret.hasChanged = 1;
this.txps[ntxid] = incomingTx;
}
ret.txp = this.txps[ntxid];
return ret;
};
TxProposals.prototype.add = function(data) {
preconditions.checkArgument(data.inputChainPaths);
preconditions.checkArgument(data.signedBy);
preconditions.checkArgument(data.creator);
preconditions.checkArgument(data.createdTs);
preconditions.checkArgument(data.builder);
var txp = new TxProposal(data);
var ntxid = txp.getID();
// Add a LOCALLY CREATED (trusted) tx proposal
TxProposals.prototype.add = function(txp) {
txp._sync();
var ntxid = txp.getId();
this.txps[ntxid] = txp;
return ntxid;
};
TxProposals.prototype.setSent = function(ntxid, txid) {
//sent TxProposals are local an not broadcasted.
this.txps[ntxid].setSent(txid);
TxProposals.prototype.get = function(ntxid) {
var ret = this.txps[ntxid];
if (!ret)
throw new Error('Unknown TXP: '+ntxid);
return ret;
};
TxProposals.prototype.getTxProposal = function(ntxid, copayers) {
var txp = this.txps[ntxid];
var txp = this.get(ntxid);
var i = JSON.parse(JSON.stringify(txp));
i.builder = txp.builder;
i.ntxid = ntxid;
@ -296,6 +137,17 @@ TxProposals.prototype.getTxProposal = function(ntxid, copayers) {
return i;
};
TxProposals.prototype.reject = function(ntxid, copayerId) {
var txp = this.get(ntxid);
txp.setRejected(copayerId);
};
TxProposals.prototype.seen = function(ntxid, copayerId) {
var txp = this.get(ntxid);
txp.setSeen(copayerId);
};
//returns the unspent txid-vout used in PENDING Txs
TxProposals.prototype.getUsedUnspent = function(maxRejectCount) {
var ret = {};
@ -312,5 +164,4 @@ TxProposals.prototype.getUsedUnspent = function(maxRejectCount) {
return ret;
};
TxProposals.TxProposal = TxProposal;
module.exports = require('soop')(TxProposals);
module.exports = TxProposals;

View file

@ -17,6 +17,7 @@ var Address = bitcore.Address;
var HDParams = require('./HDParams');
var PublicKeyRing = require('./PublicKeyRing');
var TxProposal = require('./TxProposal');
var TxProposals = require('./TxProposals');
var PrivateKey = require('./PrivateKey');
var copayConfig = require('../../../config');
@ -36,7 +37,7 @@ function Wallet(opts) {
});
if (copayConfig.forceNetwork && this.getNetworkName() !== copayConfig.networkName)
throw new Error('Network forced to ' + copayConfig.networkName +
' and tried to create a Wallet with network ' + this.getNetworkName());
' and tried to create a Wallet with network ' + this.getNetworkName());
this.log('creating ' + opts.requiredCopayers + ' of ' + opts.totalCopayers + ' wallet');
@ -58,11 +59,11 @@ function Wallet(opts) {
}
Wallet.builderOpts = {
lockTime: null,
signhash: bitcore.Transaction.SIGNHASH_ALL,
fee: null,
feeSat: null,
Wallet.builderOpts = {
lockTime: null,
signhash: bitcore.Transaction.SIGNHASH_ALL,
fee: null,
feeSat: null,
};
Wallet.parent = EventEmitter;
@ -129,39 +130,158 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) {
};
Wallet.prototype._handleTxProposal = function(senderId, data) {
this.log('RECV TXPROPOSAL: ', data);
var inTxp = TxProposals.TxProposal.fromObj(data.txProposal, Wallet.builderOpts);
var valid = inTxp.isValid();
if (!valid) {
var corruptEvent = {
Wallet.prototype._processProposalEvents = function(senderId, m) {
var ev;
if (m) {
if (m.new) {
ev = {
type: 'new',
cid: senderId
}
} else if (m.newCopayer) {
ev = {
type: 'signed',
cid: m.newCopayer
};
}
} else {
ev = {
type: 'corrupt',
cId: inTxp.creator
cId: senderId,
};
this.emit('txProposalEvent', corruptEvent);
return;
}
var mergeInfo = this.txProposals.merge(inTxp, senderId);
var added = this.addSeenToTxProposals();
if (added) {
this.log('### BROADCASTING txProposals with my seenBy updated.');
this.sendTxProposal(inTxp.getID());
}
this.emit('txProposalsUpdated');
this.store();
for (var i = 0; i < mergeInfo.events.length; i++) {
this.emit('txProposalEvent', mergeInfo.events[i]);
}
if (ev)
this.emit('txProposalEvent', ev);
};
/* OTDO
events.push({
type: 'signed',
cId: k,
txId: ntxid
});
*/
Wallet.prototype._getKeyMap = function(txp) {
preconditions.checkArgument(txp);
var keyMap = this.publicKeyRing.copayersForPubkeys(txp._inputSignatures[0], txp.inputChainPaths);
var inSig = JSON.stringify(txp._inputSignatures[0].sort());
if (JSON.stringify(Object.keys(keyMap).sort()) !== inSig) {
throw new Error('inputSignatures dont match know copayers pubkeys');
}
var keyMapStr = JSON.stringify(keyMap);
// All inputs must be signed with the same copayers
for (var i in txp._inputSignatures) {
if (!i) continue;
var inSigX = JSON.stringify(txp._inputSignatures[i].sort());
if (inSigX !== inSig)
throw new Error('found inputs with different signatures:');
}
return keyMap;
};
Wallet.prototype._checkSentTx = function(ntxid, cb) {
var txp = this.txProposals.get(ntxid);
var tx = txp.builder.build();
this.blockchain.checkSentTx(tx, function(err, txid) {
var ret = false;
if (txid) {
txp.setSent(txid);
ret = txid;
}
return cb(ret);
});
};
Wallet.prototype._handleTxProposal = function(senderId, data) {
var self = this;
this.log('RECV TXPROPOSAL: ', data);
var m;
try {
m = this.txProposals.merge(data.txProposal, Wallet.builderOpts);
var keyMap = this._getKeyMap(m.txp);
ret.newCopayer = m.txp.setCopayers(senderId, keyMap);
} catch (e) {
this.log('Corrupt TX proposal received from:', senderId, e);
}
if (m) {
if (m.hasChanged) {
this.sendSeen(m.ntxid);
var tx = m.txp.builder.build();
if (tx.isComplete()) {
this._checkSentTx(m.ntxid, function(ret) {
if (ret) {
self.emit('txProposalsUpdated');
self.store();
}
});
} else {
this.sendTxProposal(m.ntxid);
}
}
this.emit('txProposalsUpdated');
this.store();
}
this._processProposalEvents(senderId, m);
};
Wallet.prototype._handleReject = function(senderId, data, isInbound) {
preconditions.checkState(data.ntxid);
this.log('RECV REJECT:', data);
var txp = this.txProposals.get(data.ntxid);
if (!txp)
throw new Error('Received Reject for an unknown TX from:' + senderId);
if (txp.signedBy[senderId])
throw new Error('Received Reject for an already signed TX from:' + senderId);
txp.setRejected(senderId);
this.store();
this.emit('txProposalsUpdated');
this.emit('txProposalEvent', {
type: 'rejected',
cId: senderId,
txId: data.ntxid,
});
};
Wallet.prototype._handleSeen = function(senderId, data, isInbound) {
preconditions.checkState(data.ntxid);
this.log('RECV SEEN:', data);
var txp = this.txProposals.get(data.ntxid);
txp.setSeen(senderId);
this.store();
this.emit('txProposalsUpdated');
this.emit('txProposalEvent', {
type: 'seen',
cId: senderId,
txId: data.ntxid,
});
};
Wallet.prototype._handleAddressBook = function(senderId, data, isInbound) {
preconditions.checkState(data.addressBook);
this.log('RECV ADDRESSBOOK:', data);
var rcv = data.addressBook;
var hasChange;
@ -193,24 +313,30 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) {
// This handler is repeaded on WalletFactory (#join). TODO
case 'walletId':
this.sendWalletReady(senderId);
break;
break;
case 'walletReady':
this.sendPublicKeyRing(senderId);
this.sendAddressBook(senderId);
this.sendAllTxProposals(senderId); // send old txps
break;
this.sendAddressBook(senderId);
this.sendAllTxProposals(senderId); // send old txps
break;
case 'publicKeyRing':
this._handlePublicKeyRing(senderId, data, isInbound);
break;
break;
case 'reject':
this._handleReject(senderId, data, isInbound);
break;
case 'seen':
this._handleSeen(senderId, data, isInbound);
break;
case 'txProposal':
this._handleTxProposal(senderId, data, isInbound);
break;
break;
case 'indexes':
this._handleIndexes(senderId, data, isInbound);
break;
break;
case 'addressbook':
this._handleAddressBook(senderId, data, isInbound);
break;
break;
}
};
@ -384,6 +510,7 @@ Wallet.prototype.toObj = function() {
return walletObj;
};
// fromObj => from a trusted source
Wallet.fromObj = function(o, storage, network, blockchain) {
var opts = JSON.parse(JSON.stringify(o.opts));
opts.addressBook = o.addressBook;
@ -418,11 +545,31 @@ Wallet.prototype.sendAllTxProposals = function(recipients) {
Wallet.prototype.sendTxProposal = function(ntxid, recipients) {
preconditions.checkArgument(ntxid);
preconditions.checkState(this.txProposals.txps[ntxid]);
this.log('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals);
this.send(recipients, {
type: 'txProposal',
txProposal: this.txProposals.txps[ntxid].toObj(),
txProposal: this.txProposals.get(ntxid).toObjTrim(),
walletId: this.id,
});
};
Wallet.prototype.sendSeen = function(ntxid) {
preconditions.checkArgument(ntxid);
this.log('### SENDING seen: ' + ntxid + ' TO: All');
this.send(null, {
type: 'seen',
ntxid: ntxid,
walletId: this.id,
});
};
Wallet.prototype.sendReject = function(ntxid) {
preconditions.checkArgument(ntxid);
this.log('### SENDING reject: ' + ntxid + ' TO: All');
this.send(null, {
type: 'reject',
ntxid: ntxid,
walletId: this.id,
});
};
@ -517,30 +664,22 @@ Wallet.prototype.getTxProposals = function() {
Wallet.prototype.reject = function(ntxid) {
var myId = this.getMyCopayerId();
var txp = this.txProposals.txps[ntxid];
if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
throw new Error('Invalid transaction to reject: ' + ntxid);
}
txp.rejectedBy[myId] = Date.now();
this.sendTxProposal(ntxid);
var txp = this.txProposals.reject(ntxid, this.getMyCopayerId());
this.sendReject(ntxid);
this.store();
this.emit('txProposalsUpdated');
};
Wallet.prototype.sign = function(ntxid, cb) {
preconditions.checkState(typeof this.getMyCopayerId() !== 'undefined');
var self = this;
setTimeout(function() {
var myId = self.getMyCopayerId();
var txp = self.txProposals.txps[ntxid];
if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
if (cb) cb(false);
}
var txp = self.txProposals.get(ntxid);
// if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
// if (cb) cb(false);
// }
//
var keys = self.privateKey.getForPaths(txp.inputChainPaths);
var b = txp.builder;
@ -559,14 +698,13 @@ Wallet.prototype.sign = function(ntxid, cb) {
}, 10);
};
Wallet.prototype.sendTx = function(ntxid, cb) {
var txp = this.txProposals.txps[ntxid];
if (!txp) return;
var txp = this.txProposals.get(ntxid);
var tx = txp.builder.build();
if (!tx.isComplete()) return;
if (!tx.isComplete())
throw new Error('Tx is not complete. Can not broadcast');
this.log('Broadcasting Transaction');
var scriptSig = tx.ins[0].getScript();
var size = scriptSig.serialize().length;
@ -577,28 +715,23 @@ Wallet.prototype.sendTx = function(ntxid, cb) {
this.blockchain.sendRawTransaction(txHex, function(txid) {
self.log('BITCOIND txid:', txid);
if (txid) {
self.txProposals.setSent(ntxid, txid);
self.txProposals.get(ntxid).setSent(txid);
self.sendTxProposal(ntxid);
self.store();
return cb(txid);
} else {
self.log('Sent failed. Checking is the TX was sent already');
self._checkSentTx(ntxid, function(txid) {
console.log('[Wallet.js.730:txid:]', txid); //TODO
if (txid)
self.store();
return cb(txid);
});
}
return cb(txid);
});
};
Wallet.prototype.addSeenToTxProposals = function() {
var ret = false;
var myId = this.getMyCopayerId();
for (var k in this.txProposals.txps) {
var txp = this.txProposals.txps[k];
if (!txp.seenBy[myId]) {
txp.seenBy[myId] = Date.now();
ret = true;
}
}
return ret;
};
// TODO: remove this method and use getAddressesInfo everywhere
Wallet.prototype.getAddresses = function(opts) {
@ -719,8 +852,9 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
var priv = this.privateKey;
opts = opts || {};
preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName());
preconditions.checkState(pkr.isComplete());
preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName(), 'networkname mismatch');
preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete');
preconditions.checkState(priv, 'no private key');
if (comment) preconditions.checkArgument(comment.length <= 100);
if (!opts.remainderOut) {
@ -729,16 +863,16 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
};
}
for (var k in Wallet.builderOpts){
for (var k in Wallet.builderOpts) {
opts[k] = Wallet.builderOpts[k];
}
var b = new Builder(opts)
.setUnspent(utxos)
.setOutputs([{
address: toAddress,
amountSatStr: amountSatStr,
}]);
.setUnspent(utxos)
.setOutputs([{
address: toAddress,
amountSatStr: amountSatStr,
}]);
var selectedUtxos = b.getSelectedUnspent();
var inputChainPaths = selectedUtxos.map(function(utxo) {
@ -747,22 +881,23 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths));
if (priv) {
var keys = priv.getForPaths(inputChainPaths);
var signed = b.sign(keys);
}
var keys = priv.getForPaths(inputChainPaths);
var signed = b.sign(keys);
var myId = this.getMyCopayerId();
var now = Date.now();
var me = {};
var tx = b.build();
if (priv && tx.countInputSignatures(0)) me[myId] = now;
if (!tx.countInputSignatures(0))
throw new Error('Could not sign generated tx');
var me = {};
me[myId] = now;
var meSeen = {};
if (priv) meSeen[myId] = now;
var data = {
var ntxid = this.txProposals.add(new TxProposal({
inputChainPaths: inputChainPaths,
signedBy: me,
seenBy: meSeen,
@ -770,9 +905,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
createdTs: now,
builder: b,
comment: comment
};
var ntxid = this.txProposals.add(data);
}));
return ntxid;
};
@ -831,29 +964,29 @@ Wallet.prototype.indexDiscovery = function(start, change, cosigner, gap, cb) {
var self = this;
async.doWhilst(
function _do(next) {
// Optimize window to minimize the derivations.
var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1;
var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner);
self.blockchain.checkActivity(addresses, function(err, actives) {
if (err) throw err;
// Optimize window to minimize the derivations.
var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1;
var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner);
self.blockchain.checkActivity(addresses, function(err, actives) {
if (err) throw err;
// Check for new activities in the newlly scanned addresses
var recentActive = actives.reduce(function(r, e, i) {
return e ? scanIndex + i : r;
}, lastActive);
hasActivity = lastActive != recentActive;
lastActive = recentActive;
scanIndex += scanWindow;
next();
});
},
function _while() {
return hasActivity;
},
function _finnaly(err) {
if (err) return cb(err);
cb(null, lastActive);
}
// Check for new activities in the newlly scanned addresses
var recentActive = actives.reduce(function(r, e, i) {
return e ? scanIndex + i : r;
}, lastActive);
hasActivity = lastActive != recentActive;
lastActive = recentActive;
scanIndex += scanWindow;
next();
});
},
function _while() {
return hasActivity;
},
function _finnaly(err) {
if (err) return cb(err);
cb(null, lastActive);
}
);
}
@ -913,6 +1046,10 @@ Wallet.prototype.toggleAddressBookEntry = function(key) {
this.store();
};
Wallet.prototype.isShared = function() {
return this.totalCopayers > 1;
}
Wallet.prototype.isReady = function() {
var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup();
return ret;

View file

@ -102,10 +102,7 @@ WalletFactory.prototype.read = function(walletId) {
WalletFactory.prototype.create = function(opts) {
opts = opts || {};
this.log('### CREATING NEW WALLET.' +
(opts.id ? ' USING ID: ' + opts.id : ' NEW ID') +
(opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey')
);
this.log('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey'));
opts.privateKey = opts.privateKey || new PrivateKey({
networkName: this.networkName
@ -121,7 +118,8 @@ WalletFactory.prototype.create = function(opts) {
});
opts.publicKeyRing.addCopayer(
opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(),
opts.nickname);
opts.nickname
);
this.log('\t### PublicKeyRing Initialized');
opts.txProposals = opts.txProposals || new TxProposals({
@ -143,6 +141,7 @@ WalletFactory.prototype.create = function(opts) {
opts.version = opts.version || this.version;
var w = new Wallet(opts);
w.store();
this.storage.setLastOpened(w.id);
return w;
};
@ -156,9 +155,9 @@ WalletFactory.prototype._checkVersion = function(inVersion) {
//We only check for major version differences
if (thisV0 < inV0) {
throw new Error('Major difference in software versions' +
'. Received:' + inVersion +
'. Current version:' + this.version +
'. Aborting.');
'. Received:' + inVersion +
'. Current version:' + this.version +
'. Aborting.');
}
};
@ -179,6 +178,8 @@ WalletFactory.prototype.open = function(walletId, opts) {
if (w) {
w.store();
}
this.storage.setLastOpened(walletId);
return w;
};
@ -194,6 +195,7 @@ WalletFactory.prototype.delete = function(walletId, cb) {
var s = this.storage;
this.log('## DELETING WALLET ID:' + walletId); //TODO
s.deleteWallet(walletId);
s.setLastOpened(undefined);
return cb();
};

View file

@ -172,6 +172,13 @@ Storage.prototype.deleteWallet = function(walletId) {
}
};
Storage.prototype.setLastOpened = function(walletId) {
this.setGlobal('lastOpened', walletId);
}
Storage.prototype.getLastOpened = function() {
return this.getGlobal('lastOpened');
}
//obj contains keys to be set
Storage.prototype.setFromObj = function(walletId, obj) {

View file

@ -277,17 +277,28 @@ angular.module('copayApp.services')
i.outs = outs;
i.fee = i.builder.feeSat * satToUnit;
i.missingSignatures = tx.countInputMissingSignatures(0);
i.actionList = getActionList(i.peerActions);
txs.push(i);
}
});
$rootScope.txs = txs; //.some(function(i) {return i.isPending; } );
$rootScope.txs = txs;
if ($rootScope.pendingTxCount < pendingForUs) {
$rootScope.txAlertCount = pendingForUs;
}
$rootScope.pendingTxCount = pendingForUs;
};
function getActionList(actions) {
var peers = Object.keys(actions).map(function(i) {
return {cId: i, actions: actions[i] }
});
return peers.sort(function(a, b) {
return !!b.actions.create - !!a.actions.create;
});
}
$rootScope.$watch('insightError', function(status) {
if (status) {
if (status === -1) {