diff --git a/www/js/services/currency-services.js b/www/js/services/currency-services.js index 828754ca38c7e8b18c9a4ce2ff1f8927d690fc49..a65054d00054a83f2e69969b345bb2c1fb415887 100644 --- a/www/js/services/currency-services.js +++ b/www/js/services/currency-services.js @@ -4,358 +4,345 @@ angular.module('cesium.currency.services', ['ngApi', 'cesium.bma.services']) .factory('csCurrency', function($rootScope, $q, $timeout, BMA, Api, csSettings) { 'ngInject'; - var defaultBMA = BMA; + var + constants = { + // Avoid to many call on well known currencies + WELL_KNOWN_CURRENCIES: { + g1: { + firstBlockTime: 1488987127, + medianTimeOffset: 3600 + } + } + }, - function CsCurrency(id, BMA) { + data = {}, + started = false, + startPromise, + listeners, + api = new Api(this, "csCurrency"); - BMA = BMA || defaultBMA; + function powBase(amount, base) { + return base <= 0 ? amount : amount * Math.pow(10, base); + } - var - constants = { - // Avoid to many call on well known currencies - WELL_KNOWN_CURRENCIES: { - g1: { - firstBlockTime: 1488987127, - medianTimeOffset: 3600 - } - } - }, + function resetData() { + data.name = null; + data.parameters = null; + data.firstBlockTime = null; + data.membersCount = null; + data.cache = {}; + data.node = BMA; + data.currentUD = null; + data.medianTimeOffset = 0; + started = false; + startPromise = undefined; + api.data.raise.reset(data); + } - data = {}, - started = false, - startPromise, - listeners, - api = new Api(this, "csCurrency-" + id); + function loadData() { - function powBase(amount, base) { - return base <= 0 ? amount : amount * Math.pow(10, base); - } + // Load currency from default node + return $q.all([ - function resetData() { - data.name = null; - data.parameters = null; - data.firstBlockTime = null; - data.membersCount = null; - data.cache = {}; - data.node = BMA; - data.currentUD = null; - data.medianTimeOffset = 0; - started = false; - startPromise = undefined; - api.data.raise.reset(data); - } + // get parameters + loadParameters() + .then(function(parameters) { + // load first block info + return loadFirstBlock(parameters.currency); + }), - function loadData() { + // get current UD + loadCurrentUD(), - // Load currency from default node - return $q.all([ + // call extensions + api.data.raisePromise.load(data) + ]) + .catch(function(err) { + resetData(); + throw err; + }); + } - // get parameters - loadParameters() - .then(function(parameters) { - // load first block info - return loadFirstBlock(parameters.currency); - }), + function loadParameters() { + return BMA.blockchain.parameters() + .then(function(res){ + data.name = res.currency; + data.parameters = res; + data.medianTimeOffset = res.avgGenTime * res.medianTimeBlocks / 2; + return res; + }); + } - // get current UD - loadCurrentUD(), + function loadFirstBlock(currencyName) { + // Well known currencies + if (constants.WELL_KNOWN_CURRENCIES[currencyName]){ + angular.merge(data, constants.WELL_KNOWN_CURRENCIES[currencyName]); + return $q.when(); + } - // call extensions - api.data.raisePromise.load(data) - ]) + return BMA.blockchain.block({block:0}) + .then(function(json) { + // Need by graph plugin + data.firstBlockTime = json.medianTime; + }) .catch(function(err) { - resetData(); + // Special case, when currency not started yet + if (err && err.ucode === BMA.errorCodes.BLOCK_NOT_FOUND) { + data.firstBlockTime = 0; + data.initPhase = true; + console.warn('[currency] Blockchain not launched: Enable init phase mode'); + return; + } throw err; }); - } - - function loadParameters() { - return BMA.blockchain.parameters() - .then(function(res){ - data.name = res.currency; - data.parameters = res; - data.medianTimeOffset = res.avgGenTime * res.medianTimeBlocks / 2; - return res; - }); - } + } - function loadFirstBlock(currencyName) { - // Well known currencies - if (constants.WELL_KNOWN_CURRENCIES[currencyName]){ - angular.merge(data, constants.WELL_KNOWN_CURRENCIES[currencyName]); - return $q.when(); - } + function loadCurrentUD() { + return BMA.blockchain.stats.ud() + .then(function(res) { + // Special case for currency init + if (!res.result.blocks.length) { + data.currentUD = data.parameters ? data.parameters.ud0 : -1; + return data.currentUD ; + } + return _safeLoadCurrentUD(res, res.result.blocks.length - 1); + }) + .catch(function(err) { + data.currentUD = null; + throw err; + }); + } - return BMA.blockchain.block({block:0}) - .then(function(json) { - // Need by graph plugin - data.firstBlockTime = json.medianTime; - }) - .catch(function(err) { - // Special case, when currency not started yet - if (err && err.ucode === BMA.errorCodes.BLOCK_NOT_FOUND) { - data.firstBlockTime = 0; - data.initPhase = true; - console.warn('[currency] Blockchain not launched: Enable init phase mode'); - return; - } - throw err; - }); + /** + * Load the last UD, with a workaround if last block with UD is not found in the node + * @param res + * @param blockIndex + * @returns {*} + * @private + */ + function _safeLoadCurrentUD(res, blockIndex) { + // Special case for currency init + if (!res.result.blocks.length || blockIndex < 0) { + data.currentUD = data.parameters ? data.parameters.ud0 : -1; + return data.currentUD ; } - - function loadCurrentUD() { - return BMA.blockchain.stats.ud() - .then(function(res) { - // Special case for currency init - if (!res.result.blocks.length) { - data.currentUD = data.parameters ? data.parameters.ud0 : -1; - return data.currentUD ; - } - return _safeLoadCurrentUD(res, res.result.blocks.length - 1); + else { + var lastBlockWithUD = res.result.blocks[blockIndex]; + return BMA.blockchain.block({ block: lastBlockWithUD }) + .then(function(block){ + data.currentUD = powBase(block.dividend, block.unitbase); + return data.currentUD; }) .catch(function(err) { + console.error("[currency] Unable to load last block with UD, with number {0}".format(lastBlockWithUD)); + if (blockIndex > 0) { + console.error("[currency] Retrying to load UD from a previous block..."); + return _safeLoadCurrentUD(res, blockIndex-1); + } data.currentUD = null; throw err; }); } + } - /** - * Load the last UD, with a workaround if last block with UD is not found in the node - * @param res - * @param blockIndex - * @returns {*} - * @private - */ - function _safeLoadCurrentUD(res, blockIndex) { - // Special case for currency init - if (!res.result.blocks.length || blockIndex < 0) { - data.currentUD = data.parameters ? data.parameters.ud0 : -1; - return data.currentUD ; - } - else { - var lastBlockWithUD = res.result.blocks[blockIndex]; - return BMA.blockchain.block({ block: lastBlockWithUD }) - .then(function(block){ - data.currentUD = powBase(block.dividend, block.unitbase); - return data.currentUD; - }) - .catch(function(err) { - console.error("[currency] Unable to load last block with UD, with number {0}".format(lastBlockWithUD)); - if (blockIndex > 0) { - console.error("[currency] Retrying to load UD from a previous block..."); - return _safeLoadCurrentUD(res, blockIndex-1); - } - data.currentUD = null; - throw err; - }); - } + function getData() { + + if (started) { // load only once + return $q.when(data); } - function getData() { + // Previous load not finished: return the existing promise - fix #452 + return startPromise || start(); + } + function getDataField(field) { + return function() { if (started) { // load only once - return $q.when(data); + return $q.when(data[field]); } // Previous load not finished: return the existing promise - fix #452 - return startPromise || start(); - } - - function getDataField(field) { - return function() { - if (started) { // load only once - return $q.when(data[field]); - } - - // Previous load not finished: return the existing promise - fix #452 - return startPromise || start() // load only once - .then(function(){ - return data[field]; - }); - }; - } - - function onBlock(json) { - var block = new Block(json); - block.cleanData(); // Remove unused content (arrays...) and keep items count + return startPromise || start() // load only once + .then(function(){ + return data[field]; + }); + }; + } - //console.debug('[currency] Received new block', block); - console.debug('[currency] Received new block {' + block.number + '-' + block.hash + '}'); + function onBlock(json) { + var block = new Block(json); + block.cleanData(); // Remove unused content (arrays...) and keep items count - data.currentBlock = block; - data.currentBlock.receivedAt = moment().utc().unix(); + //console.debug('[currency] Received new block', block); + console.debug('[currency] Received new block {' + block.number + '-' + block.hash + '}'); - data.medianTime = block.medianTime; - data.membersCount = block.membersCount; + data.currentBlock = block; + data.currentBlock.receivedAt = moment().utc().unix(); - // Update UD - if (block.dividend) { - data.currentUD = block.dividend; - } + data.medianTime = block.medianTime; + data.membersCount = block.membersCount; - // Dispatch to extensions - api.data.raise.newBlock(block); + // Update UD + if (block.dividend) { + data.currentUD = block.dividend; } - function addListeners() { - listeners = [ - // Listen if node changed - BMA.api.node.on.restart($rootScope, restart, this), - // open web socket on block - BMA.websocket.block().onListener(onBlock) - ]; - } + // Dispatch to extensions + api.data.raise.newBlock(block); + } - function removeListeners() { - _.forEach(listeners, function(remove){ - remove(); - }); - listeners = []; - } + function addListeners() { + listeners = [ + // Listen if node changed + BMA.api.node.on.restart($rootScope, restart, this), + // open web socket on block + BMA.websocket.block().onListener(onBlock) + ]; + } - function ready() { - if (started) return $q.when(data); - return startPromise || start(); - } + function removeListeners() { + _.forEach(listeners, function(remove){ + remove(); + }); + listeners = []; + } - function stop() { - console.debug('[currency] Stopping...'); - removeListeners(); - resetData(); - } + function ready() { + if (started) return $q.when(data); + return startPromise || start(); + } - function restart() { - stop(); - return $timeout(start, 200); - } + function stop() { + console.debug('[currency] Stopping...'); + removeListeners(); + resetData(); + } - function start() { - console.debug('[currency] Starting...'); - var now = Date.now(); + function restart() { + stop(); + return $timeout(start, 200); + } - startPromise = BMA.ready() + function start() { + console.debug('[currency] Starting...'); + var now = Date.now(); - // Load data - .then(loadData) + startPromise = BMA.ready() - // Emit ready event - .then(function() { - addListeners(); + // Load data + .then(loadData) - console.debug('[currency] Started in ' + (Date.now() - now) + 'ms'); + // Emit ready event + .then(function() { + addListeners(); - started = true; - startPromise = null; + console.debug('[currency] Started in ' + (Date.now() - now) + 'ms'); - // Emit event (used by plugins) - api.data.raise.ready(data); - }) - .then(function(){ - return data; - }); + started = true; + startPromise = null; - return startPromise; - } + // Emit event (used by plugins) + api.data.raise.ready(data); + }) + .then(function(){ + return data; + }); - var currentBlockField = getDataField('currentBlock'); + return startPromise; + } - function getCurrent(cache) { - // Get field (and make sure service is started) - return currentBlockField() + var currentBlockField = getDataField('currentBlock'); - .then(function(currentBlock) { + function getCurrent(cache) { + // Get field (and make sure service is started) + return currentBlockField() - var now = moment().utc().unix(); + .then(function(currentBlock) { - if (cache) { - if (currentBlock && currentBlock.receivedAt && (now - currentBlock.receivedAt) < 60/*1min*/) { - //console.debug('[currency] Use current block #'+ currentBlock.number +' from cache (age='+ (now - currentBlock.receivedAt) + 's)'); - return currentBlock; - } + var now = moment().utc().unix(); - if (!currentBlock) { - // Should never occur, if websocket /ws/block works ! - console.warn('[currency] No current block in cache: get it from network. Websocket [/ws/block] may not be started ?'); - } + if (cache) { + if (currentBlock && currentBlock.receivedAt && (now - currentBlock.receivedAt) < 60/*1min*/) { + //console.debug('[currency] Use current block #'+ currentBlock.number +' from cache (age='+ (now - currentBlock.receivedAt) + 's)'); + return currentBlock; } - return BMA.blockchain.current(false) - .catch(function(err){ - // Special case for currency init (root block not exists): use fixed values - if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { - return {number: 0, hash: BMA.constants.ROOT_BLOCK_HASH, medianTime: now}; - } - throw err; - }) - .then(function(current) { - data.currentBlock = current; - data.currentBlock.receivedAt = now; - return current; - }); - }); - } - - function getLastValidBlock() { - if (csSettings.data.blockValidityWindow <= 0) { - return getCurrent(true); - } + if (!currentBlock) { + // Should never occur, if websocket /ws/block works ! + console.warn('[currency] No current block in cache: get it from network. Websocket [/ws/block] may not be started ?'); + } + } - return getCurrent(true) - .then(function(current) { - var number = current.number - csSettings.data.blockValidityWindow; - return (number > 0) ? BMA.blockchain.block({block: number}) : current; - }); - } + return BMA.blockchain.current(false) + .catch(function(err){ + // Special case for currency init (root block not exists): use fixed values + if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { + return {number: 0, hash: BMA.constants.ROOT_BLOCK_HASH, medianTime: now}; + } + throw err; + }) + .then(function(current) { + data.currentBlock = current; + data.currentBlock.receivedAt = now; + return current; + }); + }); + } - // Get time in second (UTC - medianTimeOffset) - function getDateNow() { - return moment().utc().unix() - (data.medianTimeOffset || constants.WELL_KNOWN_CURRENCIES.g1.medianTimeOffset); + function getLastValidBlock() { + if (csSettings.data.blockValidityWindow <= 0) { + return getCurrent(true); } - // TODO register new block event, to get new UD value - - // Register extension points - api.registerEvent('data', 'ready'); - api.registerEvent('data', 'load'); - api.registerEvent('data', 'reset'); - api.registerEvent('data', 'newBlock'); - - // init data - resetData(); + return getCurrent(true) + .then(function(current) { + var number = current.number - csSettings.data.blockValidityWindow; + return (number > 0) ? BMA.blockchain.block({block: number}) : current; + }); + } - // Default action - //start(); - - return { - ready: ready, - start: start, - stop: stop, - data: data, - get: getData, - name: getDataField('name'), - parameters: getDataField('parameters'), - currentUD: getDataField('currentUD'), - medianTimeOffset: getDataField('medianTimeOffset'), - blockchain: { - current: getCurrent, - lastValid: getLastValidBlock - }, - date: { - now: getDateNow - }, - // api extension - api: api, - // deprecated methods - default: function() { - console.warn('[currency] \'csCurrency.default()\' has been DEPRECATED - Please use \'csCurrency.get()\' instead.'); - return getData(); - } - }; + // Get time in second (UTC - medianTimeOffset) + function getDateNow() { + return moment().utc().unix() - (data.medianTimeOffset || constants.WELL_KNOWN_CURRENCIES.g1.medianTimeOffset); } - var service = new CsCurrency('default'); - service.instance = function(id, bma) { - return new CsCurrency(id, bma); + // TODO register new block event, to get new UD value + + // Register extension points + api.registerEvent('data', 'ready'); + api.registerEvent('data', 'load'); + api.registerEvent('data', 'reset'); + api.registerEvent('data', 'newBlock'); + + // init data + resetData(); + + // Default action + //start(); + + return { + ready: ready, + start: start, + stop: stop, + data: data, + get: getData, + name: getDataField('name'), + parameters: getDataField('parameters'), + currentUD: getDataField('currentUD'), + medianTimeOffset: getDataField('medianTimeOffset'), + blockchain: { + current: getCurrent, + lastValid: getLastValidBlock + }, + date: { + now: getDateNow + }, + // api extension + api: api, + // deprecated methods + default: function() { + console.warn('[currency] \'csCurrency.default()\' has been DEPRECATED - Please use \'csCurrency.get()\' instead.'); + return getData(); + } }; - return service; }); diff --git a/www/js/services/device-services.js b/www/js/services/device-services.js index cdb1df2078ec8cb56580c78c93c8a78bf4170bf7..4788424358174a9d8f991fb94b4aadc363e16926 100644 --- a/www/js/services/device-services.js +++ b/www/js/services/device-services.js @@ -2,8 +2,7 @@ var App; angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.settings.services']) - .factory('Device', - function($rootScope, $translate, $ionicPopup, $q, + .factory('Device', function($rootScope, $translate, $ionicPopup, $q, // removeIf(no-device) $cordovaClipboard, $cordovaBarcodeScanner, $cordovaCamera, // endRemoveIf(no-device) diff --git a/www/js/services/network-services.js b/www/js/services/network-services.js index 037345ccfedab352211e1bc857c089111941e87b..7cdee80e5aa80009b025bf4226e32d5627730068 100644 --- a/www/js/services/network-services.js +++ b/www/js/services/network-services.js @@ -4,855 +4,843 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', .factory('csNetwork', function($rootScope, $q, $interval, $timeout, $window, csConfig, BMA, csHttp, csCurrency, Api) { 'ngInject'; - function CsNetwork(id) { - - var - interval, - constants = { - UNKNOWN_BUID: -1, - MAX_BLOCK_OFFSET: 1000 - }, - isHttpsMode = $window.location.protocol === 'https:', - api = new Api(this, "csNetwork-" + id), - - data = { - bma: null, - listeners: [], - loading: true, - peers: [], - filter: { - member: true, - mirror: true, - endpoint: null, - online: false, - bma: false, - ssl: undefined, - tor: undefined - }, - sort:{ - type: null, - asc: true, - compact: true - }, - groupBy: 'pubkey', - expertMode: false, - knownBlocks: [], - mainBlock: null, - minOnlineBlockNumber: 0, - uidsByPubkeys: null, - searchingPeersOnNetwork: false, - difficulties: null, - ws2pHeads: null, - timeout: csConfig.timeout - }, - - // Return the block uid - buid = function(block) { - return block && [block.number, block.hash].join('-'); + var + interval, + constants = { + UNKNOWN_BUID: -1, + MAX_BLOCK_OFFSET: 1000 + }, + isHttpsMode = $window.location.protocol === 'https:', + api = new Api(this, "csNetwork"), + + data = { + bma: null, + listeners: [], + loading: true, + peers: [], + filter: { + member: true, + mirror: true, + endpoint: null, + online: false, + bma: false, + ssl: undefined, + tor: undefined }, - - resetData = function() { - data.bma = null; - data.listeners = []; - data.peers.splice(0); - data.filter = { - member: true, - mirror: true, - endpoint: null, - online: true, - bma: false, - ssl: undefined, - tor: undefined - }; - data.sort = { - type: null, - asc: true - }; - data.groupBy = 'pubkey'; - data.expertMode = false; - data.memberPeersCount = 0; - data.knownBlocks = []; - data.mainBlock = null; - data.minOnlineBlockNumber = 0; - data.uidsByPubkeys = {}; - data.loading = true; - data.searchingPeersOnNetwork = false; - data.difficulties = null; - data.ws2pHeads = null; - data.timeout = csConfig.timeout; - }, - - hasPeers = function() { - return data.peers && data.peers.length > 0; + sort:{ + type: null, + asc: true, + compact: true }, - - getPeers = function() { - return data.peers; - }, - - isBusy = function() { - return data.loading; - }, - - getKnownBlocks = function() { - return data.knownBlocks; - }, - - // Load WS2P heads - loadW2spHeads = function() { - return data.bma.network.ws2p.heads() - .then(function (res) { - data.ws2pHeads = res.heads ? res.heads.reduce(function (res, hit) { - if (hit.message && hit.sig) { - try { - var head = new Ws2pMessage(hit.message); - res[[head.pubkey, head.ws2pid].join('-')] = head; - } - catch(err) { - // just log, then ignore - console.error('[network] Ignoring WS2P head.', err && err.message || err); - } + groupBy: 'pubkey', + expertMode: false, + knownBlocks: [], + mainBlock: null, + minOnlineBlockNumber: 0, + uidsByPubkeys: null, + searchingPeersOnNetwork: false, + difficulties: null, + ws2pHeads: null, + timeout: csConfig.timeout + }, + + // Return the block uid + buid = function(block) { + return block && [block.number, block.hash].join('-'); + }, + + resetData = function() { + data.bma = null; + data.listeners = []; + data.peers.splice(0); + data.filter = { + member: true, + mirror: true, + endpoint: null, + online: true, + bma: false, + ssl: undefined, + tor: undefined + }; + data.sort = { + type: null, + asc: true + }; + data.groupBy = 'pubkey'; + data.expertMode = false; + data.memberPeersCount = 0; + data.knownBlocks = []; + data.mainBlock = null; + data.minOnlineBlockNumber = 0; + data.uidsByPubkeys = {}; + data.loading = true; + data.searchingPeersOnNetwork = false; + data.difficulties = null; + data.ws2pHeads = null; + data.timeout = csConfig.timeout; + }, + + hasPeers = function() { + return data.peers && data.peers.length > 0; + }, + + getPeers = function() { + return data.peers; + }, + + isBusy = function() { + return data.loading; + }, + + getKnownBlocks = function() { + return data.knownBlocks; + }, + + // Load WS2P heads + loadW2spHeads = function() { + return data.bma.network.ws2p.heads() + .then(function (res) { + data.ws2pHeads = res.heads ? res.heads.reduce(function (res, hit) { + if (hit.message && hit.sig) { + try { + var head = new Ws2pMessage(hit.message); + res[[head.pubkey, head.ws2pid].join('-')] = head; + } + catch(err) { + // just log, then ignore + console.error('[network] Ignoring WS2P head.', err && err.message || err); } - return res; - }, {}) : {}; - }) - .catch(function(err) { - // When too many request, retry in 3s - if (err && err.ucode == BMA.errorCodes.HTTP_LIMITATION) { - return $timeout(function() { - return loadW2spHeads(); - }, 3000); } - console.error(err); // can occur on duniter v1.6 - data.ws2pHeads = {}; - }); - }, + return res; + }, {}) : {}; + }) + .catch(function(err) { + // When too many request, retry in 3s + if (err && err.ucode == BMA.errorCodes.HTTP_LIMITATION) { + return $timeout(function() { + return loadW2spHeads(); + }, 3000); + } + console.error(err); // can occur on duniter v1.6 + data.ws2pHeads = {}; + }); + }, + + // Load personal difficulties + loadDifficulties = function() { + return data.bma.blockchain.stats.difficulties() + .then(function (res) { + data.difficulties = res.levels ? res.levels.reduce(function (res, hit) { + if (hit.uid && hit.level) res[hit.uid] = hit.level; + return res; + }, {}) : {}; + }) + .catch(function(err) { + // When too many request, retry in 3s + if (err && err.ucode == BMA.errorCodes.HTTP_LIMITATION) { + return $timeout(function() { + return loadDifficulties(); + }, 3000); + } + console.error(err); + data.difficulties = {}; + }); + }, - // Load personal difficulties - loadDifficulties = function() { - return data.bma.blockchain.stats.difficulties() - .then(function (res) { - data.difficulties = res.levels ? res.levels.reduce(function (res, hit) { - if (hit.uid && hit.level) res[hit.uid] = hit.level; - return res; - }, {}) : {}; - }) - .catch(function(err) { - // When too many request, retry in 3s - if (err && err.ucode == BMA.errorCodes.HTTP_LIMITATION) { - return $timeout(function() { - return loadDifficulties(); - }, 3000); - } - console.error(err); - data.difficulties = {}; - }); - }, + loadPeers = function() { + data.peers = []; + data.searchingPeersOnNetwork = true; + data.loading = true; + data.bma = data.bma || BMA; + var newPeers = []; - loadPeers = function() { - data.peers = []; - data.searchingPeersOnNetwork = true; - data.loading = true; - data.bma = data.bma || BMA; - var newPeers = []; + if (interval) { + $interval.cancel(interval); + } - if (interval) { + interval = $interval(function() { + // not same job instance + if (newPeers.length) { + flushNewPeersAndSort(newPeers); + } + else if (data.loading && !data.searchingPeersOnNetwork) { + data.loading = false; $interval.cancel(interval); + // The peer lookup end, we can make a clean final report + sortPeers(true/*update main buid*/); + + console.debug('[network] Finish: {0} peers found.'.format(data.peers.length)); } + }, 1000); - interval = $interval(function() { - // not same job instance - if (newPeers.length) { - flushNewPeersAndSort(newPeers); - } - else if (data.loading && !data.searchingPeersOnNetwork) { - data.loading = false; - $interval.cancel(interval); - // The peer lookup end, we can make a clean final report - sortPeers(true/*update main buid*/); + var initJobs = [ + // Load uids + data.bma.wot.member.uids() + .then(function(uids) { + data.uidsByPubkeys = uids; + }) + .catch(function(err) { + console.error(err); + data.uidsByPubkeys = {}; + }), - console.debug('[network] Finish: {0} peers found.'.format(data.peers.length)); - } - }, 1000); + // Load WS2P heads + loadW2spHeads() + ]; - var initJobs = [ - // Load uids - data.bma.wot.member.uids() - .then(function(uids) { - data.uidsByPubkeys = uids; - }) - .catch(function(err) { - console.error(err); - data.uidsByPubkeys = {}; - }), + // Get difficulties (expert mode only) + if (data.expertMode) { + initJobs.push(loadDifficulties()); + } - // Load WS2P heads - loadW2spHeads() - ]; + return $q.all(initJobs) + .then(function() { + return data.bma && data.bma.network.peers(); + }) + .then(function(res){ + if (!res || !res.peers || !res.peers.length) return; - // Get difficulties (expert mode only) - if (data.expertMode) { - initJobs.push(loadDifficulties()); - } - - return $q.all(initJobs) - .then(function() { - return data.bma && data.bma.network.peers(); - }) - .then(function(res){ - if (!res || !res.peers || !res.peers.length) return; - - // If filter online peers - if (data.filter.online) { - var jobs = []; - _.forEach(res.peers, function(json) { - // Exclude, if not UP or on a too old block - if (json.status !== 'UP') return; - json.blockNumber = json.block && parseInt(json.block.split('-')[0]); - if (json.blockNumber && json.blockNumber < data.minOnlineBlockNumber) { - console.debug("[network] Exclude a too old peer document, on pubkey {0}".format(json.pubkey.substring(0,6))); - return; - } + // If filter online peers + if (data.filter.online) { + var jobs = []; + _.forEach(res.peers, function(json) { + // Exclude, if not UP or on a too old block + if (json.status !== 'UP') return; + json.blockNumber = json.block && parseInt(json.block.split('-')[0]); + if (json.blockNumber && json.blockNumber < data.minOnlineBlockNumber) { + console.debug("[network] Exclude a too old peer document, on pubkey {0}".format(json.pubkey.substring(0,6))); + return; + } - jobs.push(addOrRefreshPeerFromJson(json, newPeers)); + jobs.push(addOrRefreshPeerFromJson(json, newPeers)); - // Mark WS2P - _.forEach(json.endpoints||[], function(ep) { - if (ep.startsWith('WS2P')) { - var key = json.pubkey + '-' + ep.split(' ')[1]; - if (data.ws2pHeads[key]) { - data.ws2pHeads[key].hasEndpoint = true; - } + // Mark WS2P + _.forEach(json.endpoints||[], function(ep) { + if (ep.startsWith('WS2P')) { + var key = json.pubkey + '-' + ep.split(' ')[1]; + if (data.ws2pHeads[key]) { + data.ws2pHeads[key].hasEndpoint = true; } - }); + } }); + }); - // Add private WS2P endpoints - var privateWs2pHeads = _.values(data.ws2pHeads); - if (privateWs2pHeads && privateWs2pHeads.length) { - var privateEPCount = 0; - //console.debug("[http] Found WS2P endpoints without endpoint:", data.ws2pHeads); - _.forEach(privateWs2pHeads, function(head) { - - if (!head.hasEndPoint) { - var currentNumber = head.buid && parseInt(head.buid.split('-')[0]); - // Exclude if on a too old block - if (currentNumber && currentNumber < data.minOnlineBlockNumber) { - console.debug("[network] Exclude a too old WS2P message, on pubkey {0}".format(head.pubkey.substring(0,6))); - return; - } - - var peer = new Peer({ - buid: head.buid, - currentNumber: currentNumber, - pubkey: head.pubkey, - version: head.version, - powPrefix: head.powPrefix, - online: true, - uid: data.uidsByPubkeys[head.pubkey], - bma: { - useWs2p: true, - private: true, - ws2pid: head.ws2pid - }, - endpoints: [ - // fake endpoint - 'WS2P ' + head.ws2pid - ] - }); - peer.id = peer.keyID(); - if (peer.uid && data.expertMode && data.difficulties) { - peer.difficulty = data.difficulties[peer.uid]; - } - if (applyPeerFilter(peer)) { - newPeers.push(peer); - privateEPCount++; - } + // Add private WS2P endpoints + var privateWs2pHeads = _.values(data.ws2pHeads); + if (privateWs2pHeads && privateWs2pHeads.length) { + var privateEPCount = 0; + //console.debug("[http] Found WS2P endpoints without endpoint:", data.ws2pHeads); + _.forEach(privateWs2pHeads, function(head) { + + if (!head.hasEndPoint) { + var currentNumber = head.buid && parseInt(head.buid.split('-')[0]); + // Exclude if on a too old block + if (currentNumber && currentNumber < data.minOnlineBlockNumber) { + console.debug("[network] Exclude a too old WS2P message, on pubkey {0}".format(head.pubkey.substring(0,6))); + return; } - }); - if (privateEPCount) { - console.debug("[http] Found {0} WS2P endpoints without endpoint (private ?)".format(privateEPCount)); + var peer = new Peer({ + buid: head.buid, + currentNumber: currentNumber, + pubkey: head.pubkey, + version: head.version, + powPrefix: head.powPrefix, + online: true, + uid: data.uidsByPubkeys[head.pubkey], + bma: { + useWs2p: true, + private: true, + ws2pid: head.ws2pid + }, + endpoints: [ + // fake endpoint + 'WS2P ' + head.ws2pid + ] + }); + peer.id = peer.keyID(); + if (peer.uid && data.expertMode && data.difficulties) { + peer.difficulty = data.difficulties[peer.uid]; + } + if (applyPeerFilter(peer)) { + newPeers.push(peer); + privateEPCount++; + } } - } + }); - if (jobs.length) return $q.all(jobs); + if (privateEPCount) { + console.debug("[http] Found {0} WS2P endpoints without endpoint (private ?)".format(privateEPCount)); + } } - // If filter offline peers - else { - return $q.all(_(res && res.peers || []).reduce(function(res, json) { - return res.concat(addOrRefreshPeerFromJson(json, newPeers)); - }, [])); - } - }) - .then(function(){ - data.searchingPeersOnNetwork = false; - }) - .catch(function(err){ - console.error(err); - data.searchingPeersOnNetwork = false; - }); - }, - - /** - * Apply filter on a peer. (peer uid should have been filled BEFORE) - */ - applyPeerFilter = function(peer) { - // no filter - if (!data.filter) return true; - - // Filter member and mirror - if ((data.filter.member && !data.filter.mirror && !peer.uid) || - (data.filter.mirror && !data.filter.member && peer.uid)) { - return false; - } + if (jobs.length) return $q.all(jobs); + } - // Filter on endpoint - if (data.filter.endpoint && !peer.hasEndpoint(data.filter.endpoint)) { - return false; - } + // If filter offline peers + else { + return $q.all(_(res && res.peers || []).reduce(function(res, json) { + return res.concat(addOrRefreshPeerFromJson(json, newPeers)); + }, [])); + } + }) + .then(function(){ + data.searchingPeersOnNetwork = false; + }) + .catch(function(err){ + console.error(err); + data.searchingPeersOnNetwork = false; + }); + }, + + /** + * Apply filter on a peer. (peer uid should have been filled BEFORE) + */ + applyPeerFilter = function(peer) { + // no filter + if (!data.filter) return true; + + // Filter member and mirror + if ((data.filter.member && !data.filter.mirror && !peer.uid) || + (data.filter.mirror && !data.filter.member && peer.uid)) { + return false; + } - // Filter on status - if ((data.filter.online && peer.status !== 'UP' && peer.oldBlock) || (!data.filter.online && peer.status === 'UP' && !peer.oldBlock)) { - return false; - } + // Filter on endpoint + if (data.filter.endpoint && !peer.hasEndpoint(data.filter.endpoint)) { + return false; + } - // Filter on bma - if (angular.isDefined(data.filter.bma) && peer.isBma() != data.filter.bma) { - return false; - } + // Filter on status + if ((data.filter.online && peer.status !== 'UP' && peer.oldBlock) || (!data.filter.online && peer.status === 'UP' && !peer.oldBlock)) { + return false; + } - // Filter on ws2p - if (angular.isDefined(data.filter.ws2p) && peer.isWs2p() != data.filter.ws2p) { - return false; - } + // Filter on bma + if (angular.isDefined(data.filter.bma) && peer.isBma() != data.filter.bma) { + return false; + } - // Filter on ssl - if (angular.isDefined(data.filter.ssl) && peer.isSsl() != data.filter.ssl) { - return false; - } + // Filter on ws2p + if (angular.isDefined(data.filter.ws2p) && peer.isWs2p() != data.filter.ws2p) { + return false; + } - // Filter on tor - if (angular.isDefined(data.filter.tor) && peer.isTor() != data.filter.tor) { - return false; - } + // Filter on ssl + if (angular.isDefined(data.filter.ssl) && peer.isSsl() != data.filter.ssl) { + return false; + } - return true; - }, + // Filter on tor + if (angular.isDefined(data.filter.tor) && peer.isTor() != data.filter.tor) { + return false; + } - addOrRefreshPeerFromJson = function(json, list) { - list = list || data.newPeers; - - // Analyze the peer document, and exclude using the online filter - json.blockNumber = json.block && parseInt(json.block.split('-')[0]); - json.oldBlock = (json.status === 'UP' && json.blockNumber && json.blockNumber < data.minOnlineBlockNumber); - - var peers = createPeerEntities(json); - var hasUpdates = false; - - var jobs = peers.reduce(function(jobs, peer) { - var existingPeer = _.findWhere(data.peers, {id: peer.id}); - var existingMainBuid = existingPeer ? existingPeer.buid : null; - var existingOnline = existingPeer ? existingPeer.online : false; - - return jobs.concat( - refreshPeer(peer) - .then(function (refreshedPeer) { - if (existingPeer) { - // remove existing peers, when reject or offline - if (!refreshedPeer || (refreshedPeer.online !== data.filter.online && data.filter.online !== 'all')) { - var existingIndex = data.peers.indexOf(existingPeer); - if (existingIndex !== -1) { - console.debug('[network] Peer [{0}] removed (cause: {1})'.format(peer.server, !refreshedPeer ? 'filtered' : (refreshedPeer.online ? 'UP' : 'DOWN'))); - data.peers.splice(existingIndex, 1); - hasUpdates = true; - } - } - else if (refreshedPeer.buid !== existingMainBuid){ - console.debug('[network] {0} endpoint [{1}] new current block'.format( - refreshedPeer.bma && (refreshedPeer.bma.useBma ? 'BMA' : 'WS2P') || 'null', - refreshedPeer.server)); - hasUpdates = true; - } - else if (existingOnline !== refreshedPeer.online){ - console.debug('[network] {0} endpoint [{1}] is now {2}'.format( - refreshedPeer.bma && (refreshedPeer.bma.useBma ? 'BMA' : 'WS2P') || 'null', - refreshedPeer.server, - refreshedPeer.online ? 'UP' : 'DOWN')); + return true; + }, + + addOrRefreshPeerFromJson = function(json, list) { + list = list || data.newPeers; + + // Analyze the peer document, and exclude using the online filter + json.blockNumber = json.block && parseInt(json.block.split('-')[0]); + json.oldBlock = (json.status === 'UP' && json.blockNumber && json.blockNumber < data.minOnlineBlockNumber); + + var peers = createPeerEntities(json); + var hasUpdates = false; + + var jobs = peers.reduce(function(jobs, peer) { + var existingPeer = _.findWhere(data.peers, {id: peer.id}); + var existingMainBuid = existingPeer ? existingPeer.buid : null; + var existingOnline = existingPeer ? existingPeer.online : false; + + return jobs.concat( + refreshPeer(peer) + .then(function (refreshedPeer) { + if (existingPeer) { + // remove existing peers, when reject or offline + if (!refreshedPeer || (refreshedPeer.online !== data.filter.online && data.filter.online !== 'all')) { + var existingIndex = data.peers.indexOf(existingPeer); + if (existingIndex !== -1) { + console.debug('[network] Peer [{0}] removed (cause: {1})'.format(peer.server, !refreshedPeer ? 'filtered' : (refreshedPeer.online ? 'UP' : 'DOWN'))); + data.peers.splice(existingIndex, 1); hasUpdates = true; } - else { - console.debug("[network] {0} endpoint [{1}] unchanged".format( - refreshedPeer.bma && (refreshedPeer.bma.useBma ? 'BMA' : 'WS2P') || 'null', - refreshedPeer.server)); - } } - else if (refreshedPeer && (refreshedPeer.online === data.filter.online || data.filter.online === 'all')) { - console.debug("[network] {0} endpoint [{1}] is {2}".format( + else if (refreshedPeer.buid !== existingMainBuid){ + console.debug('[network] {0} endpoint [{1}] new current block'.format( + refreshedPeer.bma && (refreshedPeer.bma.useBma ? 'BMA' : 'WS2P') || 'null', + refreshedPeer.server)); + hasUpdates = true; + } + else if (existingOnline !== refreshedPeer.online){ + console.debug('[network] {0} endpoint [{1}] is now {2}'.format( refreshedPeer.bma && (refreshedPeer.bma.useBma ? 'BMA' : 'WS2P') || 'null', refreshedPeer.server, - refreshedPeer.online ? 'UP' : 'DOWN' - )); - list.push(refreshedPeer); + refreshedPeer.online ? 'UP' : 'DOWN')); hasUpdates = true; } - }) - ); - }, []); - return (jobs.length === 1 ? jobs[0] : $q.all(jobs)) - .then(function() { - return hasUpdates; - }); - }, + else { + console.debug("[network] {0} endpoint [{1}] unchanged".format( + refreshedPeer.bma && (refreshedPeer.bma.useBma ? 'BMA' : 'WS2P') || 'null', + refreshedPeer.server)); + } + } + else if (refreshedPeer && (refreshedPeer.online === data.filter.online || data.filter.online === 'all')) { + console.debug("[network] {0} endpoint [{1}] is {2}".format( + refreshedPeer.bma && (refreshedPeer.bma.useBma ? 'BMA' : 'WS2P') || 'null', + refreshedPeer.server, + refreshedPeer.online ? 'UP' : 'DOWN' + )); + list.push(refreshedPeer); + hasUpdates = true; + } + }) + ); + }, []); + return (jobs.length === 1 ? jobs[0] : $q.all(jobs)) + .then(function() { + return hasUpdates; + }); + }, - createPeerEntities = function(json, ep) { - if (!json) return []; - var peer = new Peer(json); + createPeerEntities = function(json, ep) { + if (!json) return []; + var peer = new Peer(json); - // Read bma endpoints - if (!ep) { - var endpointsAsString = peer.getEndpoints(); - if (!endpointsAsString) return []; // no BMA + // Read bma endpoints + if (!ep) { + var endpointsAsString = peer.getEndpoints(); + if (!endpointsAsString) return []; // no BMA - var endpoints = endpointsAsString.reduce(function (res, epStr) { - var ep = BMA.node.parseEndPoint(epStr); - return ep ? res.concat(ep) : res; - }, []); + var endpoints = endpointsAsString.reduce(function (res, epStr) { + var ep = BMA.node.parseEndPoint(epStr); + return ep ? res.concat(ep) : res; + }, []); - // recursive call, on each endpoint - if (endpoints.length > 1) { - return endpoints.reduce(function (res, ep) { - return res.concat(createPeerEntities(json, ep)); - }, []); - } - else { - // if only one endpoint: use it and continue - ep = endpoints[0]; - } + // recursive call, on each endpoint + if (endpoints.length > 1) { + return endpoints.reduce(function (res, ep) { + return res.concat(createPeerEntities(json, ep)); + }, []); } - peer.bma = ep; - peer.server = peer.getServer(); - peer.dns = peer.getDns(); - peer.buid = peer.buid || peer.block; - peer.blockNumber = peer.buid && parseInt(peer.buid.split('-')[0]); - peer.uid = peer.pubkey && data.uidsByPubkeys[peer.pubkey]; - peer.id = peer.keyID(); - return [peer]; - }, - - refreshPeer = function(peer) { + else { + // if only one endpoint: use it and continue + ep = endpoints[0]; + } + } + peer.bma = ep; + peer.server = peer.getServer(); + peer.dns = peer.getDns(); + peer.buid = peer.buid || peer.block; + peer.blockNumber = peer.buid && parseInt(peer.buid.split('-')[0]); + peer.uid = peer.pubkey && data.uidsByPubkeys[peer.pubkey]; + peer.id = peer.keyID(); + return [peer]; + }, + + refreshPeer = function(peer) { + + // Apply filter + if (!applyPeerFilter(peer)) return $q.when(); + + if (!data.filter.online || (!data.filter.online && peer.status === 'DOWN') || !peer.getHost() /*fix #537*/) { + peer.online = false; + return $q.when(peer); + } - // Apply filter - if (!applyPeerFilter(peer)) return $q.when(); + if (peer.bma.useWs2p && data.ws2pHeads) { + var ws2pHeadKey = [peer.pubkey, peer.bma.ws2pid].join('-'); + var head = data.ws2pHeads[ws2pHeadKey]; + delete data.ws2pHeads[ws2pHeadKey]; + if (head) { + peer.buid = head.buid; + peer.currentNumber=head.buid && parseInt(head.buid.split('-')[0]); + peer.version = head.version; + peer.powPrefix = head.powPrefix; + } + peer.online = !!peer.buid; - if (!data.filter.online || (!data.filter.online && peer.status === 'DOWN') || !peer.getHost() /*fix #537*/) { - peer.online = false; - return $q.when(peer); + if (peer.uid && data.expertMode && data.difficulties) { + peer.difficulty = data.difficulties[peer.uid]; } - if (peer.bma.useWs2p && data.ws2pHeads) { - var ws2pHeadKey = [peer.pubkey, peer.bma.ws2pid].join('-'); - var head = data.ws2pHeads[ws2pHeadKey]; - delete data.ws2pHeads[ws2pHeadKey]; - if (head) { - peer.buid = head.buid; - peer.currentNumber=head.buid && parseInt(head.buid.split('-')[0]); - peer.version = head.version; - peer.powPrefix = head.powPrefix; - } - peer.online = !!peer.buid; + return $q.when(peer); + } - if (peer.uid && data.expertMode && data.difficulties) { - peer.difficulty = data.difficulties[peer.uid]; - } + // Cesium running in SSL: Do not try to access not SSL node, + if (!peer.bma.useWs2p && isHttpsMode && !peer.bma.useSsl) { + peer.online = (peer.status === 'UP'); + peer.buid = constants.UNKNOWN_BUID; + delete peer.version; - return $q.when(peer); + if (peer.uid && data.expertMode && data.difficulties) { + peer.difficulty = data.difficulties[peer.uid]; } - // Cesium running in SSL: Do not try to access not SSL node, - if (!peer.bma.useWs2p && isHttpsMode && !peer.bma.useSsl) { - peer.online = (peer.status === 'UP'); - peer.buid = constants.UNKNOWN_BUID; - delete peer.version; + return $q.when(peer); + } - if (peer.uid && data.expertMode && data.difficulties) { - peer.difficulty = data.difficulties[peer.uid]; - } + // Do not try to access TOR or WS2P endpoints + if (peer.bma.useTor || peer.bma.useWs2p) { + peer.online = (peer.status === 'UP'); + peer.buid = constants.UNKNOWN_BUID; + delete peer.version; - return $q.when(peer); + if (peer.uid && data.expertMode && data.difficulties) { + peer.difficulty = data.difficulties[peer.uid]; } + return $q.when(peer); + } - // Do not try to access TOR or WS2P endpoints - if (peer.bma.useTor || peer.bma.useWs2p) { - peer.online = (peer.status === 'UP'); - peer.buid = constants.UNKNOWN_BUID; - delete peer.version; - - if (peer.uid && data.expertMode && data.difficulties) { - peer.difficulty = data.difficulties[peer.uid]; + peer.api = peer.api || BMA.lightInstance(peer.getHost(), peer.getPort(), peer.isSsl(), data.timeout); + + // Get current block + return peer.api.blockchain.current(false/*no cache*/) + .then(function(block) { + peer.currentNumber = block.number; + peer.online = true; + peer.buid = buid(block); + peer.medianTime = block.medianTime; + if (data.knownBlocks.indexOf(peer.buid) === -1) { + data.knownBlocks.push(peer.buid); } - return $q.when(peer); - } - - peer.api = peer.api || BMA.lightInstance(peer.getHost(), peer.getPort(), peer.isSsl(), data.timeout); - - // Get current block - return peer.api.blockchain.current(false/*no cache*/) - .then(function(block) { - peer.currentNumber = block.number; + return peer; + }) + .catch(function(err) { + // Special case for currency init (root block not exists): use fixed values + if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { peer.online = true; - peer.buid = buid(block); - peer.medianTime = block.medianTime; - if (data.knownBlocks.indexOf(peer.buid) === -1) { - data.knownBlocks.push(peer.buid); - } + peer.buid = buid({number:0, hash: BMA.constants.ROOT_BLOCK_HASH}); + peer.difficulty = 0; return peer; - }) - .catch(function(err) { - // Special case for currency init (root block not exists): use fixed values - if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { - peer.online = true; - peer.buid = buid({number:0, hash: BMA.constants.ROOT_BLOCK_HASH}); - peer.difficulty = 0; - return peer; - } - if (!peer.secondTry) { - var bma = peer.bma || peer.getBMA(); - if (bma.dns && peer.server.indexOf(bma.dns) === -1) { - // try again, using DNS instead of IPv4 / IPV6 - peer.secondTry = true; - peer.api = BMA.lightInstance(bma.dns, bma.port, bma.useSsl); - return refreshPeer(peer); // recursive call - } - } - - peer.buid = null; - peer.blockNumber = null; - peer.currentNumber = null; - peer.online=false; - peer.uid = data.uidsByPubkeys[peer.pubkey]; - return peer; - }) - .then(function(peer) { - // Exit if offline, or not expert mode or too small device - if (!data.filter.online || !peer || !peer.online || !data.expertMode) return peer; - var jobs = []; - - // Get hardship (only for a member peer) - if (peer.uid) { - jobs.push(peer.api.blockchain.stats.hardship({pubkey: peer.pubkey}) - .then(function (res) { - peer.difficulty = res ? res.level : null; - }) - .catch(function() { - peer.difficulty = null; // continue - })); + } + if (!peer.secondTry) { + var bma = peer.bma || peer.getBMA(); + if (bma.dns && peer.server.indexOf(bma.dns) === -1) { + // try again, using DNS instead of IPv4 / IPV6 + peer.secondTry = true; + peer.api = BMA.lightInstance(bma.dns, bma.port, bma.useSsl); + return refreshPeer(peer); // recursive call } + } - // Get Version - jobs.push(peer.api.node.summary() - .then(function(res){ - peer.software = res && res.duniter && res.duniter.software || undefined; - peer.version = res && res.duniter && res.duniter.version || '?'; + peer.buid = null; + peer.blockNumber = null; + peer.currentNumber = null; + peer.online=false; + peer.uid = data.uidsByPubkeys[peer.pubkey]; + return peer; + }) + .then(function(peer) { + // Exit if offline, or not expert mode or too small device + if (!data.filter.online || !peer || !peer.online || !data.expertMode) return peer; + var jobs = []; + + // Get hardship (only for a member peer) + if (peer.uid) { + jobs.push(peer.api.blockchain.stats.hardship({pubkey: peer.pubkey}) + .then(function (res) { + peer.difficulty = res ? res.level : null; }) .catch(function() { - peer.software = undefined; - peer.version = '?'; // continue + peer.difficulty = null; // continue })); - - return $q.all(jobs) - .then(function(){ - return peer; - }); - }); - }, - - flushNewPeersAndSort = function(newPeers, updateMainBuid) { - newPeers = newPeers || data.newPeers; - if (!newPeers.length) return; - var ids = _.map(data.peers, function(peer){ - return peer.id; - }); - var hasUpdates = false; - var newPeersAdded = 0; - _.forEach(newPeers.splice(0), function(peer) { - if (!ids[peer.id]) { - data.peers.push(peer); - ids[peer.id] = peer; - hasUpdates = true; - newPeersAdded++; } - }); - if (hasUpdates) { - console.debug('[network] Flushing {0} new peers...'.format(newPeersAdded)); - sortPeers(updateMainBuid); - } - }, - computeScoreAlphaValue = function(value, nbChars, asc) { - if (!value) return 0; - var score = 0; - value = value.toLowerCase(); - if (nbChars > value.length) { - nbChars = value.length; - } - score += value.charCodeAt(0); - for (var i=1; i < nbChars; i++) { - score += Math.pow(0.001, i) * value.charCodeAt(i); - } - return asc ? (1000 - score) : score; - }, + // Get Version + jobs.push(peer.api.node.summary() + .then(function(res){ + peer.software = res && res.duniter && res.duniter.software || undefined; + peer.version = res && res.duniter && res.duniter.version || '?'; + }) + .catch(function() { + peer.software = undefined; + peer.version = '?'; // continue + })); - sortPeers = function(updateMainBuid) { - // Construct a map of buid, with peer count and medianTime - var buids = {}; - data.memberPeersCount = 0; - _.forEach(data.peers, function(peer){ - if (peer.buid) { - var buid = buids[peer.buid]; - if (!buid || !buid.medianTime) { - buid = { - buid: peer.buid, - medianTime: peer.medianTime, - count: 0 - }; - buids[peer.buid] = buid; - } - // If not already done, try to fill medianTime (need to compute consensusBlockDelta) - else if (!buid.medianTime && peer.medianTime) { - buid.medianTime = peer.medianTime; - } - if (buid.buid !== constants.UNKNOWN_BUID) { - buid.count++; - } - } - data.memberPeersCount += peer.uid ? 1 : 0; + return $q.all(jobs) + .then(function(){ + return peer; + }); }); - var mainBlock = data.mainBlock; - if (data.filter.online) { - // Compute pct of use, per buid - _.forEach(_.values(buids), function(buid) { - buid.pct = buid.count * 100 / data.peers.length; - }); - mainBlock = _.max(buids, function(obj) { - return obj.count; - }); - _.forEach(data.peers, function(peer){ - peer.hasMainConsensusBlock = peer.buid === mainBlock.buid; - peer.hasConsensusBlock = peer.buid && !peer.hasMainConsensusBlock && buids[peer.buid].count > 1; - if (peer.hasConsensusBlock) { - peer.consensusBlockDelta = buids[peer.buid].medianTime - mainBlock.medianTime; - } - }); + }, + + flushNewPeersAndSort = function(newPeers, updateMainBuid) { + newPeers = newPeers || data.newPeers; + if (!newPeers.length) return; + var ids = _.map(data.peers, function(peer){ + return peer.id; + }); + var hasUpdates = false; + var newPeersAdded = 0; + _.forEach(newPeers.splice(0), function(peer) { + if (!ids[peer.id]) { + data.peers.push(peer); + ids[peer.id] = peer; + hasUpdates = true; + newPeersAdded++; } - data.peers = _.uniq(data.peers, false, function(peer) { - return peer.id; - }); - data.peers = _.sortBy(data.peers, function(peer) { - var score = 0; - if (data.sort.type) { - score += (data.sort.type === 'uid' ? computeScoreAlphaValue(peer.uid||peer.pubkey, 3, data.sort.asc) : 0); - score += (data.sort.type === 'api') && - ((peer.isWs2p() && (data.sort.asc ? 1 : -1) || 0) + - (peer.hasEndpoint('ES_USER_API') && (data.sort.asc ? 0.01 : -0.01) || 0) + - (peer.isSsl() && (data.sort.asc ? 0.75 : -0.75) || 0)) || 0; - score += (data.sort.type === 'difficulty' ? (peer.difficulty ? (data.sort.asc ? (10000-peer.difficulty) : peer.difficulty): 0) : 0); - score += (data.sort.type === 'current_block' ? (peer.currentNumber ? (data.sort.asc ? (1000000000 - peer.currentNumber) : peer.currentNumber) : 0) : 0); + }); + if (hasUpdates) { + console.debug('[network] Flushing {0} new peers...'.format(newPeersAdded)); + sortPeers(updateMainBuid); + } + }, + + computeScoreAlphaValue = function(value, nbChars, asc) { + if (!value) return 0; + var score = 0; + value = value.toLowerCase(); + if (nbChars > value.length) { + nbChars = value.length; + } + score += value.charCodeAt(0); + for (var i=1; i < nbChars; i++) { + score += Math.pow(0.001, i) * value.charCodeAt(i); + } + return asc ? (1000 - score) : score; + }, + + sortPeers = function(updateMainBuid) { + // Construct a map of buid, with peer count and medianTime + var buids = {}; + data.memberPeersCount = 0; + _.forEach(data.peers, function(peer){ + if (peer.buid) { + var buid = buids[peer.buid]; + if (!buid || !buid.medianTime) { + buid = { + buid: peer.buid, + medianTime: peer.medianTime, + count: 0 + }; + buids[peer.buid] = buid; } - score = (10000000000 * score); - score += (1000000000 * (peer.online ? 1 : 0)); - score += (100000000 * (peer.hasMainConsensusBlock ? 1 : 0)); - score += (1000000 * (peer.hasConsensusBlock ? buids[peer.buid].pct : 0)); - if (data.expertMode) { - score += (100 * (peer.difficulty ? (10000-peer.difficulty) : 0)); - score += (1 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0)); + // If not already done, try to fill medianTime (need to compute consensusBlockDelta) + else if (!buid.medianTime && peer.medianTime) { + buid.medianTime = peer.medianTime; } - else { - score += (100 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0)); - score += (1 * (!peer.uid ? computeScoreAlphaValue(peer.pubkey, 2, true) : 0)); + if (buid.buid !== constants.UNKNOWN_BUID) { + buid.count++; } - score += (peer.isBma() ? (peer.isSsl() ? 0.01 : 0.001) :0); // If many endpoints: BMAS first, then BMA - return -score; + } + data.memberPeersCount += peer.uid ? 1 : 0; + }); + var mainBlock = data.mainBlock; + if (data.filter.online) { + // Compute pct of use, per buid + _.forEach(_.values(buids), function(buid) { + buid.pct = buid.count * 100 / data.peers.length; }); - - if (data.groupBy) { - var previousPeer; - data.peers.forEach(function(peer) { - peer.compacted = (previousPeer && peer[data.groupBy] && peer[data.groupBy] === previousPeer[data.groupBy]); - previousPeer = peer; - }); + mainBlock = _.max(buids, function(obj) { + return obj.count; + }); + _.forEach(data.peers, function(peer){ + peer.hasMainConsensusBlock = peer.buid === mainBlock.buid; + peer.hasConsensusBlock = peer.buid && !peer.hasMainConsensusBlock && buids[peer.buid].count > 1; + if (peer.hasConsensusBlock) { + peer.consensusBlockDelta = buids[peer.buid].medianTime - mainBlock.medianTime; + } + }); + } + data.peers = _.uniq(data.peers, false, function(peer) { + return peer.id; + }); + data.peers = _.sortBy(data.peers, function(peer) { + var score = 0; + if (data.sort.type) { + score += (data.sort.type === 'uid' ? computeScoreAlphaValue(peer.uid||peer.pubkey, 3, data.sort.asc) : 0); + score += (data.sort.type === 'api') && + ((peer.isWs2p() && (data.sort.asc ? 1 : -1) || 0) + + (peer.hasEndpoint('ES_USER_API') && (data.sort.asc ? 0.01 : -0.01) || 0) + + (peer.isSsl() && (data.sort.asc ? 0.75 : -0.75) || 0)) || 0; + score += (data.sort.type === 'difficulty' ? (peer.difficulty ? (data.sort.asc ? (10000-peer.difficulty) : peer.difficulty): 0) : 0); + score += (data.sort.type === 'current_block' ? (peer.currentNumber ? (data.sort.asc ? (1000000000 - peer.currentNumber) : peer.currentNumber) : 0) : 0); } - - // Raise event on new main block - if (updateMainBuid && mainBlock && mainBlock.buid && (!data.mainBlock || data.mainBlock.buid !== mainBlock.buid)) { - data.mainBlock = mainBlock; - api.data.raise.mainBlockChanged(mainBlock); + score = (10000000000 * score); + score += (1000000000 * (peer.online ? 1 : 0)); + score += (100000000 * (peer.hasMainConsensusBlock ? 1 : 0)); + score += (1000000 * (peer.hasConsensusBlock ? buids[peer.buid].pct : 0)); + if (data.expertMode) { + score += (100 * (peer.difficulty ? (10000-peer.difficulty) : 0)); + score += (1 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0)); } - - // Raise event when changed - api.data.raise.changed(data); // raise event - }, - - removeListeners = function() { - _.forEach(data.listeners, function(remove){ - remove(); + else { + score += (100 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0)); + score += (1 * (!peer.uid ? computeScoreAlphaValue(peer.pubkey, 2, true) : 0)); + } + score += (peer.isBma() ? (peer.isSsl() ? 0.01 : 0.001) :0); // If many endpoints: BMAS first, then BMA + return -score; + }); + + if (data.groupBy) { + var previousPeer; + data.peers.forEach(function(peer) { + peer.compacted = (previousPeer && peer[data.groupBy] && peer[data.groupBy] === previousPeer[data.groupBy]); + previousPeer = peer; }); - data.listeners = []; - }, - - addListeners = function() { - data.listeners = [ - - // Listen for new block - data.bma.websocket.block().onListener(function(block) { - if (!block || data.loading) return; - var buid = [block.number, block.hash].join('-'); - if (data.knownBlocks.indexOf(buid) === -1) { - console.debug('[network] Receiving block: ' + buid.substring(0, 20)); - data.knownBlocks.push(buid); - // If first block: do NOT refresh peers (will be done in start() method) - var skipRefreshPeers = data.knownBlocks.length === 1; - if (!skipRefreshPeers) { - data.loading = true; - // We wait 2s when a new block is received, just to wait for network propagation - $timeout(function() { - console.debug('[network] new block received by WS: will refresh peers'); - loadPeers(); - }, 2000, false /*invokeApply*/); - } - } - }), - - // Listen for new peer - data.bma.websocket.peer().onListener(function(json) { - if (!json || data.loading) return; - var newPeers = []; - addOrRefreshPeerFromJson(json, newPeers) - .then(function(hasUpdates) { - if (!hasUpdates) return; - if (newPeers.length>0) { - flushNewPeersAndSort(newPeers, true); - } - else { - console.debug('[network] [ws] Peers updated received'); - sortPeers(true); - } - }); - }) - ]; - }, + } - sort = function(options) { - options = options || {}; - data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter; - data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; - sortPeers(false); - }, + // Raise event on new main block + if (updateMainBuid && mainBlock && mainBlock.buid && (!data.mainBlock || data.mainBlock.buid !== mainBlock.buid)) { + data.mainBlock = mainBlock; + api.data.raise.mainBlockChanged(mainBlock); + } - start = function(bma, options) { - options = options || {}; - return BMA.ready() - .then(function() { - close(); - - data.bma = bma || BMA; - data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter; - data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; - data.expertMode = angular.isDefined(options.expertMode) ? options.expertMode : data.expertMode; - data.timeout = angular.isDefined(options.timeout) ? options.timeout : csConfig.timeout; - - // Init a min block number - data.minOnlineBlockNumber = data.mainBlock && data.mainBlock.buid && (parseInt(data.mainBlock.buid.split('-')[0]) - constants.MAX_BLOCK_OFFSET) || undefined; - if (data.minOnlineBlockNumber === undefined) { - return csCurrency.blockchain.current(true/*use cache*/) - .then(function(current) { - data.minOnlineBlockNumber = current.number - constants.MAX_BLOCK_OFFSET; - }); + // Raise event when changed + api.data.raise.changed(data); // raise event + }, + + removeListeners = function() { + _.forEach(data.listeners, function(remove){ + remove(); + }); + data.listeners = []; + }, + + addListeners = function() { + data.listeners = [ + + // Listen for new block + data.bma.websocket.block().onListener(function(block) { + if (!block || data.loading) return; + var buid = [block.number, block.hash].join('-'); + if (data.knownBlocks.indexOf(buid) === -1) { + console.debug('[network] Receiving block: ' + buid.substring(0, 20)); + data.knownBlocks.push(buid); + // If first block: do NOT refresh peers (will be done in start() method) + var skipRefreshPeers = data.knownBlocks.length === 1; + if (!skipRefreshPeers) { + data.loading = true; + // We wait 2s when a new block is received, just to wait for network propagation + $timeout(function() { + console.debug('[network] new block received by WS: will refresh peers'); + loadPeers(); + }, 2000, false /*invokeApply*/); } - }) - .then(function() { - console.info('[network] Starting network from [{0}]'.format(bma.server)); - var now = Date.now(); - - addListeners(); - - return loadPeers() - .then(function(peers){ - console.debug('[network] Started in '+(Date.now() - now)+'ms'); - return peers; + } + }), + + // Listen for new peer + data.bma.websocket.peer().onListener(function(json) { + if (!json || data.loading) return; + var newPeers = []; + addOrRefreshPeerFromJson(json, newPeers) + .then(function(hasUpdates) { + if (!hasUpdates) return; + if (newPeers.length>0) { + flushNewPeersAndSort(newPeers, true); + } + else { + console.debug('[network] [ws] Peers updated received'); + sortPeers(true); + } + }); + }) + ]; + }, + + sort = function(options) { + options = options || {}; + data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter; + data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; + sortPeers(false); + }, + + start = function(bma, options) { + options = options || {}; + return BMA.ready() + .then(function() { + close(); + + data.bma = bma || BMA; + data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter; + data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; + data.expertMode = angular.isDefined(options.expertMode) ? options.expertMode : data.expertMode; + data.timeout = angular.isDefined(options.timeout) ? options.timeout : csConfig.timeout; + + // Init a min block number + data.minOnlineBlockNumber = data.mainBlock && data.mainBlock.buid && (parseInt(data.mainBlock.buid.split('-')[0]) - constants.MAX_BLOCK_OFFSET) || undefined; + if (data.minOnlineBlockNumber === undefined) { + return csCurrency.blockchain.current(true/*use cache*/) + .then(function(current) { + data.minOnlineBlockNumber = current.number - constants.MAX_BLOCK_OFFSET; }); - }); - }, - - close = function() { - if (data.bma) { - console.info('[network-service] Stopping...'); - removeListeners(); - resetData(); - } - }, + } + }) + .then(function() { + console.info('[network] Starting network from [{0}]'.format(bma.server)); + var now = Date.now(); - isStarted = function() { - return !data.bma; - }, + addListeners(); - $q_started = function(callback) { - if (!isStarted()) { // start first - return start() - .then(function() { - return $q(callback); + return loadPeers() + .then(function(peers){ + console.debug('[network] Started in '+(Date.now() - now)+'ms'); + return peers; }); - } - else { - return $q(callback); - } - }, - - getMainBlockUid = function() { - return $q_started(function(resolve, reject){ - resolve (data.mainBuid); }); - }, + }, - // Get peers on the main consensus blocks - getTrustedPeers = function() { - return $q_started(function(resolve, reject){ - resolve(data.peers.reduce(function(res, peer){ - return (peer.hasMainConsensusBlock && peer.uid) ? res.concat(peer) : res; - }, [])); - }); + close = function() { + if (data.bma) { + console.info('[network-service] Stopping...'); + removeListeners(); + resetData(); } - ; - - // Register extension points - api.registerEvent('data', 'changed'); - api.registerEvent('data', 'mainBlockChanged'); - api.registerEvent('data', 'rollback'); - - return { - id: id, - data: data, - start: start, - close: close, - hasPeers: hasPeers, - getPeers: getPeers, - sort: sort, - getTrustedPeers: getTrustedPeers, - getKnownBlocks: getKnownBlocks, - getMainBlockUid: getMainBlockUid, - loadPeers: loadPeers, - isBusy: isBusy, - // api extension - api: api - }; - } - - var service = new CsNetwork('default'); - - service.instance = function(id) { - return new CsNetwork(id); - }; + }, + + isStarted = function() { + return !data.bma; + }, - return service; + $q_started = function(callback) { + if (!isStarted()) { // start first + return start() + .then(function() { + return $q(callback); + }); + } + else { + return $q(callback); + } + }, + + getMainBlockUid = function() { + return $q_started(function(resolve, reject){ + resolve (data.mainBuid); + }); + }, + + // Get peers on the main consensus blocks + getTrustedPeers = function() { + return $q_started(function(resolve, reject){ + resolve(data.peers.reduce(function(res, peer){ + return (peer.hasMainConsensusBlock && peer.uid) ? res.concat(peer) : res; + }, [])); + }); + } + ; + + // Register extension points + api.registerEvent('data', 'changed'); + api.registerEvent('data', 'mainBlockChanged'); + api.registerEvent('data', 'rollback'); + + return { + data: data, + start: start, + close: close, + hasPeers: hasPeers, + getPeers: getPeers, + sort: sort, + getTrustedPeers: getTrustedPeers, + getKnownBlocks: getKnownBlocks, + getMainBlockUid: getMainBlockUid, + loadPeers: loadPeers, + isBusy: isBusy, + // api extension + api: api + }; }); diff --git a/www/js/services/tx-services.js b/www/js/services/tx-services.js index 6a45626c79d98a679123ffcdf46303232935907b..67979b0ec1f238aca5878628e9c9541631b7a2e6 100644 --- a/www/js/services/tx-services.js +++ b/www/js/services/tx-services.js @@ -2,476 +2,462 @@ angular.module('cesium.tx.services', ['ngApi', 'cesium.bma.services', 'cesium.settings.services', 'cesium.wot.services' ]) - .factory('csTx', function($q, $timeout, $filter, $translate, FileSaver, UIUtils, BMA, Api, - csConfig, csSettings, csWot, csCurrency) { - 'ngInject'; - - const defaultBMA = BMA; - - function CsTx(id, BMA) { - - BMA = BMA || defaultBMA; - var - api = new Api(this, "csTx-" + id); - - function reduceTxAndPush(pubkey, txArray, result, processedTxMap, allowPendings) { - if (!txArray || !txArray.length) return; // Skip if empty - - _.forEach(txArray, function(tx) { - if (tx.block_number !== null || allowPendings) { - var walletIsIssuer = false; - var otherIssuers = tx.issuers.reduce(function(res, issuer) { - walletIsIssuer = walletIsIssuer || (issuer === pubkey); - return (issuer !== pubkey) ? res.concat(issuer) : res; - }, []); - var otherRecipients = [], - outputBase, - sources = [], - lockedOutputs; - - var amount = tx.outputs.reduce(function(sum, output, noffset) { - // FIXME duniter v1.4.13 - var outputArray = (typeof output === 'string') ? output.split(':',3) : [output.amount,output.base,output.conditions]; - outputBase = parseInt(outputArray[1]); - var outputAmount = powBase(parseInt(outputArray[0]), outputBase); - var outputCondition = outputArray[2]; - var sigMatches = BMA.regexp.TX_OUTPUT_SIG.exec(outputCondition); - - // Simple unlock condition - if (sigMatches) { - var outputPubkey = sigMatches[1]; - if (outputPubkey === pubkey) { // output is for the wallet - if (!walletIsIssuer) { - return sum + outputAmount; - } - // If pending: use output as new sources - else if (tx.block_number === null) { - sources.push({ - amount: parseInt(outputArray[0]), - base: outputBase, - type: 'T', - identifier: tx.hash, - noffset: noffset, - consumed: false, - conditions: outputCondition - }); - } - } - - // The output is for someone else - else { - // Add into recipients list(if not a issuer) - if (outputPubkey !== '' && !_.contains(otherIssuers, outputPubkey)) { - otherRecipients.push(outputPubkey); - } - if (walletIsIssuer) { - // TODO: should be fix, when TX has multiple issuers (need a repartition) - return sum - outputAmount; - } - } +.factory('csTx', function($q, $timeout, $filter, $translate, FileSaver, UIUtils, BMA, Api, + csConfig, csSettings, csWot, csCurrency) { + 'ngInject'; + + var + api = new Api(this, "csTx"); + + function reduceTxAndPush(pubkey, txArray, result, processedTxMap, allowPendings) { + if (!txArray || !txArray.length) return; // Skip if empty + + _.forEach(txArray, function(tx) { + if (tx.block_number !== null || allowPendings) { + var walletIsIssuer = false; + var otherIssuers = tx.issuers.reduce(function(res, issuer) { + walletIsIssuer = walletIsIssuer || (issuer === pubkey); + return (issuer !== pubkey) ? res.concat(issuer) : res; + }, []); + var otherRecipients = [], + outputBase, + sources = [], + lockedOutputs; + + var amount = tx.outputs.reduce(function(sum, output, noffset) { + // FIXME duniter v1.4.13 + var outputArray = (typeof output === 'string') ? output.split(':',3) : [output.amount,output.base,output.conditions]; + outputBase = parseInt(outputArray[1]); + var outputAmount = powBase(parseInt(outputArray[0]), outputBase); + var outputCondition = outputArray[2]; + var sigMatches = BMA.regexp.TX_OUTPUT_SIG.exec(outputCondition); + + // Simple unlock condition + if (sigMatches) { + var outputPubkey = sigMatches[1]; + if (outputPubkey === pubkey) { // output is for the wallet + if (!walletIsIssuer) { + return sum + outputAmount; } - - // Complex unlock condition, on the issuer pubkey - else if (outputCondition.indexOf('SIG('+pubkey+')') !== -1) { - var lockedOutput = BMA.tx.parseUnlockCondition(outputCondition); - if (lockedOutput) { - // Add a source - sources.push(angular.merge({ - amount: parseInt(outputArray[0]), - base: outputBase, - type: 'T', - identifier: tx.hash, - noffset: noffset, - conditions: outputCondition, - consumed: false - }, lockedOutput)); - lockedOutput.amount = outputAmount; - lockedOutputs = lockedOutputs || []; - lockedOutputs.push(lockedOutput); - console.debug('[tx] has locked output:', lockedOutput); - - return sum + outputAmount; - } + // If pending: use output as new sources + else if (tx.block_number === null) { + sources.push({ + amount: parseInt(outputArray[0]), + base: outputBase, + type: 'T', + identifier: tx.hash, + noffset: noffset, + consumed: false, + conditions: outputCondition + }); } - return sum; - }, 0); - - var txPubkeys = amount > 0 ? otherIssuers : otherRecipients; - var time = tx.time || tx.blockstampTime; - - // Avoid duplicated tx, or tx to him self - var txKey = amount + ':' + tx.hash + ':' + time; - if (!processedTxMap[txKey]) { - processedTxMap[txKey] = true; // Mark as processed - var newTx = { - time: time, - amount: amount, - pubkey: txPubkeys.length === 1 ? txPubkeys[0] : undefined, - pubkeys: txPubkeys.length > 1 ? txPubkeys : undefined, - comment: tx.comment, - isUD: false, - hash: tx.hash, - locktime: tx.locktime, - block_number: tx.block_number - }; - - // If pending: store sources and inputs for a later use - see method processTransactionsAndSources() - if (walletIsIssuer && tx.block_number === null) { - newTx.inputs = tx.inputs; - newTx.sources = sources; + } + + // The output is for someone else + else { + // Add into recipients list(if not a issuer) + if (outputPubkey !== '' && !_.contains(otherIssuers, outputPubkey)) { + otherRecipients.push(outputPubkey); } - if (lockedOutputs) { - newTx.lockedOutputs = lockedOutputs; + if (walletIsIssuer) { + // TODO: should be fix, when TX has multiple issuers (need a repartition) + return sum - outputAmount; } - result.push(newTx); } } - }); - } - - function loadTx(pubkey, fromTime) { - return $q(function(resolve, reject) { - - var nowInSec = moment().utc().unix(); - fromTime = fromTime || (nowInSec - csSettings.data.walletHistoryTimeSecond); - var tx = { - pendings: [], - validating: [], - history: [], - errors: [] + // Complex unlock condition, on the issuer pubkey + else if (outputCondition.indexOf('SIG('+pubkey+')') !== -1) { + var lockedOutput = BMA.tx.parseUnlockCondition(outputCondition); + if (lockedOutput) { + // Add a source + sources.push(angular.merge({ + amount: parseInt(outputArray[0]), + base: outputBase, + type: 'T', + identifier: tx.hash, + noffset: noffset, + conditions: outputCondition, + consumed: false + }, lockedOutput)); + lockedOutput.amount = outputAmount; + lockedOutputs = lockedOutputs || []; + lockedOutputs.push(lockedOutput); + console.debug('[tx] has locked output:', lockedOutput); + + return sum + outputAmount; + } + } + return sum; + }, 0); + + var txPubkeys = amount > 0 ? otherIssuers : otherRecipients; + var time = tx.time || tx.blockstampTime; + + // Avoid duplicated tx, or tx to him self + var txKey = amount + ':' + tx.hash + ':' + time; + if (!processedTxMap[txKey]) { + processedTxMap[txKey] = true; // Mark as processed + var newTx = { + time: time, + amount: amount, + pubkey: txPubkeys.length === 1 ? txPubkeys[0] : undefined, + pubkeys: txPubkeys.length > 1 ? txPubkeys : undefined, + comment: tx.comment, + isUD: false, + hash: tx.hash, + locktime: tx.locktime, + block_number: tx.block_number }; - var processedTxMap = {}; + // If pending: store sources and inputs for a later use - see method processTransactionsAndSources() + if (walletIsIssuer && tx.block_number === null) { + newTx.inputs = tx.inputs; + newTx.sources = sources; + } + if (lockedOutputs) { + newTx.lockedOutputs = lockedOutputs; + } + result.push(newTx); + } + } + }); + } - var jobs = [ - // get current block - csCurrency.blockchain.current(true), - // get pending tx - BMA.tx.history.pending({pubkey: pubkey}) - .then(function (res) { - reduceTxAndPush(pubkey, res.history.sending, tx.pendings, processedTxMap, true /*allow pendings*/); - reduceTxAndPush(pubkey, res.history.pending, tx.pendings, processedTxMap, true /*allow pendings*/); - }) - ]; - - // get TX history since - if (fromTime !== 'pending') { - var reduceTxFn = function (res) { - reduceTxAndPush(pubkey, res.history.sent, tx.history, processedTxMap, false); - reduceTxAndPush(pubkey, res.history.received, tx.history, processedTxMap, false); - }; - - // get TX from a given time - if (fromTime > 0) { - // Use slice, to be able to cache requests result - var sliceTime = csSettings.data.walletHistorySliceSecond; - fromTime = fromTime - (fromTime % sliceTime); - for(var i = fromTime; i - sliceTime < nowInSec; i += sliceTime) { - jobs.push(BMA.tx.history.times({pubkey: pubkey, from: i, to: i+sliceTime-1}, true /*with cache*/) - .then(reduceTxFn) - ); - } + function loadTx(pubkey, fromTime) { + return $q(function(resolve, reject) { - // Last slide: no cache - jobs.push(BMA.tx.history.times({pubkey: pubkey, from: nowInSec - (nowInSec % sliceTime), to: nowInSec+999999999}, false/*no cache*/) - .then(reduceTxFn)); - } + var nowInSec = moment().utc().unix(); + fromTime = fromTime || (nowInSec - csSettings.data.walletHistoryTimeSecond); + var tx = { + pendings: [], + validating: [], + history: [], + errors: [] + }; - // get all TX - else { - jobs.push(BMA.tx.history.all({pubkey: pubkey}) - .then(reduceTxFn) - ); - } + var processedTxMap = {}; - // get UD history - if (csSettings.data.showUDHistory && fromTime > 0) { - /*jobs.push( - BMA.ud.history({pubkey: pubkey}) - .then(function(res){ - udHistory = !res.history || !res.history.history ? [] : - _.forEach(res.history.history, function(ud){ - if (ud.time < fromTime) return res; // skip to old UD - var amount = powBase(ud.amount, ud.base); - udHistory.push({ - time: ud.time, - amount: amount, - isUD: true, - block_number: ud.block_number - }); - }); - }));*/ - // API extension - jobs.push( - api.data.raisePromise.loadUDs({ - pubkey: pubkey, - fromTime: fromTime - }) - .then(function(res) { - if (!res || !res.length) return; - _.forEach(res, function(hits) { - tx.history.push(hits); - }); - }) + var jobs = [ + // get current block + csCurrency.blockchain.current(true), - .catch(function(err) { - console.debug('Error while loading UDs history, on extension point.'); - console.error(err); - }) - ); - } + // get pending tx + BMA.tx.history.pending({pubkey: pubkey}) + .then(function (res) { + reduceTxAndPush(pubkey, res.history.sending, tx.pendings, processedTxMap, true /*allow pendings*/); + reduceTxAndPush(pubkey, res.history.pending, tx.pendings, processedTxMap, true /*allow pendings*/); + }) + ]; + + // get TX history since + if (fromTime !== 'pending') { + var reduceTxFn = function (res) { + reduceTxAndPush(pubkey, res.history.sent, tx.history, processedTxMap, false); + reduceTxAndPush(pubkey, res.history.received, tx.history, processedTxMap, false); + }; + + // get TX from a given time + if (fromTime > 0) { + // Use slice, to be able to cache requests result + var sliceTime = csSettings.data.walletHistorySliceSecond; + fromTime = fromTime - (fromTime % sliceTime); + for(var i = fromTime; i - sliceTime < nowInSec; i += sliceTime) { + jobs.push(BMA.tx.history.times({pubkey: pubkey, from: i, to: i+sliceTime-1}, true /*with cache*/) + .then(reduceTxFn) + ); } - // Execute jobs - $q.all(jobs) - .then(function(res){ - var current = res[0]; - - // sort by time desc - tx.history.sort(function(tx1, tx2) { - return (tx2.time - tx1.time); - }); - var firstValidatedTxIndex = tx.history.findIndex(function(tx){ - return (tx.block_number <= current.number - csSettings.data.blockValidityWindow); - }); - // remove validating from history - tx.validating = firstValidatedTxIndex > 0 ? tx.history.splice(0, firstValidatedTxIndex) : []; - - tx.fromTime = fromTime !== 'pending' && fromTime || undefined; - tx.toTime = tx.history.length ? tx.history[0].time /*=max(tx.time)*/: tx.fromTime; - - resolve(tx); - }) - .catch(reject); - }); - } - - function powBase(amount, base) { - return base <= 0 ? amount : amount * Math.pow(10, base); - } + // Last slide: no cache + jobs.push(BMA.tx.history.times({pubkey: pubkey, from: nowInSec - (nowInSec % sliceTime), to: nowInSec+999999999}, false/*no cache*/) + .then(reduceTxFn)); + } - function addSource(src, sources, sourcesIndexByKey) { - var srcKey = src.type+':'+src.identifier+':'+src.noffset; - if (angular.isUndefined(sourcesIndexByKey[srcKey])) { - sources.push(src); - sourcesIndexByKey[srcKey] = sources.length - 1; + // get all TX + else { + jobs.push(BMA.tx.history.all({pubkey: pubkey}) + .then(reduceTxFn) + ); } - } - function addSources(result, sources) { - _(sources).forEach(function(src) { - addSource(src, result.sources, result.sourcesIndexByKey); - }); - } + // get UD history + if (csSettings.data.showUDHistory && fromTime > 0) { + /*jobs.push( + BMA.ud.history({pubkey: pubkey}) + .then(function(res){ + udHistory = !res.history || !res.history.history ? [] : + _.forEach(res.history.history, function(ud){ + if (ud.time < fromTime) return res; // skip to old UD + var amount = powBase(ud.amount, ud.base); + udHistory.push({ + time: ud.time, + amount: amount, + isUD: true, + block_number: ud.block_number + }); + }); + }));*/ + // API extension + jobs.push( + api.data.raisePromise.loadUDs({ + pubkey: pubkey, + fromTime: fromTime + }) + .then(function(res) { + if (!res || !res.length) return; + _.forEach(res, function(hits) { + tx.history.push(hits); + }); + }) - function loadSourcesAndBalance(pubkey) { - return BMA.tx.sources({pubkey: pubkey}) - .then(function(res){ - var data = { - sources: [], - sourcesIndexByKey: [], - balance: 0 - }; - if (res.sources && res.sources.length) { - _.forEach(res.sources, function(src) { - src.consumed = false; - data.balance += powBase(src.amount, src.base); - }); - addSources(data, res.sources); - } - return data; - }) - .catch(function(err) { - console.warn("[tx] Error while getting sources...", err); - throw err; - }); + .catch(function(err) { + console.debug('Error while loading UDs history, on extension point.'); + console.error(err); + }) + ); + } } - function loadData(pubkey, fromTime) { - var now = Date.now(); - - return $q.all([ - - // Load Sources - loadSourcesAndBalance(pubkey), - - // Load Tx - loadTx(pubkey, fromTime) - ]) + // Execute jobs + $q.all(jobs) + .then(function(res){ + var current = res[0]; - .then(function(res) { - // Copy sources and balance - var data = res[0]; - data.tx = res[1]; - - var txPendings = []; - var txErrors = []; - var balanceFromSource = data.balance; - var balanceWithPending = data.balance; - - function _processPendingTx(tx) { - var consumedSources = []; - var valid = true; - if (tx.amount > 0) { // do not check sources from received TX - valid = false; - // TODO get sources from the issuer ? - } - else { - _.find(tx.inputs, function(input) { - var inputKey = input.split(':').slice(2).join(':'); - var srcIndex = data.sourcesIndexByKey[inputKey]; - if (angular.isDefined(srcIndex)) { - consumedSources.push(data.sources[srcIndex]); - } - else { - valid = false; - return true; // break - } - }); - if (tx.sources) { // add source output - addSources(data, tx.sources); - } - delete tx.sources; - delete tx.inputs; - } - if (valid) { - balanceWithPending += tx.amount; // update balance - txPendings.push(tx); - _.forEach(consumedSources, function(src) { - src.consumed=true; - }); - } - else { - txErrors.push(tx); - } - } - - var txs = data.tx.pendings; - var retry = true; - while(txs && txs.length) { - // process TX pendings - _.forEach(txs, _processPendingTx); - - // Retry once (TX could be chained and processed in a wrong order) - if (txErrors.length > 0 && txPendings.length > 0 && retry) { - txs = txErrors; - txErrors = []; - retry = false; + // sort by time desc + tx.history.sort(function(tx1, tx2) { + return (tx2.time - tx1.time); + }); + var firstValidatedTxIndex = tx.history.findIndex(function(tx){ + return (tx.block_number <= current.number - csSettings.data.blockValidityWindow); + }); + // remove validating from history + tx.validating = firstValidatedTxIndex > 0 ? tx.history.splice(0, firstValidatedTxIndex) : []; + + tx.fromTime = fromTime !== 'pending' && fromTime || undefined; + tx.toTime = tx.history.length ? tx.history[0].time /*=max(tx.time)*/: tx.fromTime; + + resolve(tx); + }) + .catch(reject); + }); + } + + function powBase(amount, base) { + return base <= 0 ? amount : amount * Math.pow(10, base); + } + + function addSource(src, sources, sourcesIndexByKey) { + var srcKey = src.type+':'+src.identifier+':'+src.noffset; + if (angular.isUndefined(sourcesIndexByKey[srcKey])) { + sources.push(src); + sourcesIndexByKey[srcKey] = sources.length - 1; + } + } + + function addSources(result, sources) { + _(sources).forEach(function(src) { + addSource(src, result.sources, result.sourcesIndexByKey); + }); + } + + function loadSourcesAndBalance(pubkey) { + return BMA.tx.sources({pubkey: pubkey}) + .then(function(res){ + var data = { + sources: [], + sourcesIndexByKey: [], + balance: 0 + }; + if (res.sources && res.sources.length) { + _.forEach(res.sources, function(src) { + src.consumed = false; + data.balance += powBase(src.amount, src.base); + }); + addSources(data, res.sources); + } + return data; + }) + .catch(function(err) { + console.warn("[tx] Error while getting sources...", err); + throw err; + }); + } + + function loadData(pubkey, fromTime) { + var now = Date.now(); + + return $q.all([ + + // Load Sources + loadSourcesAndBalance(pubkey), + + // Load Tx + loadTx(pubkey, fromTime) + ]) + + .then(function(res) { + // Copy sources and balance + var data = res[0]; + data.tx = res[1]; + + var txPendings = []; + var txErrors = []; + var balanceFromSource = data.balance; + var balanceWithPending = data.balance; + + function _processPendingTx(tx) { + var consumedSources = []; + var valid = true; + if (tx.amount > 0) { // do not check sources from received TX + valid = false; + // TODO get sources from the issuer ? + } + else { + _.find(tx.inputs, function(input) { + var inputKey = input.split(':').slice(2).join(':'); + var srcIndex = data.sourcesIndexByKey[inputKey]; + if (angular.isDefined(srcIndex)) { + consumedSources.push(data.sources[srcIndex]); } else { - txs = null; + valid = false; + return true; // break } - } - - data.tx = data.tx || {}; - data.tx.pendings = txPendings.sort(function(tx1, tx2) { - return (tx2.time - tx1.time); }); - data.tx.errors = txErrors.sort(function(tx1, tx2) { - return (tx2.time - tx1.time); + if (tx.sources) { // add source output + addSources(data, tx.sources); + } + delete tx.sources; + delete tx.inputs; + } + if (valid) { + balanceWithPending += tx.amount; // update balance + txPendings.push(tx); + _.forEach(consumedSources, function(src) { + src.consumed=true; }); - // Negative balance not allow (use only source's balance) - fix #769 - data.balance = (balanceWithPending < 0) ? balanceFromSource : balanceWithPending; - - // Will add uid (+ plugin will add name, avatar, etc. if enable) - var allTx = (data.tx.history || []).concat(data.tx.validating||[], data.tx.pendings||[], data.tx.errors||[]); - return csWot.extendAll(allTx, 'pubkey') - .then(function() { - console.debug('[tx] TX and sources loaded in '+ (Date.now()-now) +'ms'); - return data; - }); - }) - .catch(function(err) { - console.warn("[tx] Error while getting sources and tx...", err); - throw err; - }); - } - - function loadSources(pubkey) { - console.debug("[tx] Loading sources for " + pubkey.substring(0,8)); - return loadData(pubkey, 'pending'); - } + } + else { + txErrors.push(tx); + } + } - // Download TX history file - function downloadHistoryFile(pubkey, options) { - - options = options || {}; - options.fromTime = options.fromTime || -1; - - console.debug("[tx] Exporting TX history for pubkey [{0}]".format(pubkey.substr(0,8))); - - return $q.all([ - $translate(['ACCOUNT.HEADERS.TIME', - 'COMMON.UID', - 'COMMON.PUBKEY', - 'COMMON.UNIVERSAL_DIVIDEND', - 'ACCOUNT.HEADERS.AMOUNT', - 'ACCOUNT.HEADERS.COMMENT']), - csCurrency.blockchain.current(true/*withCache*/), - loadData(pubkey, options.fromTime) - ]) - .then(function(result){ - var translations = result[0]; - var currentBlock = result[1]; - var currentTime = (currentBlock && currentBlock.medianTime) || moment().utc().unix(); - var currency = currentBlock && currentBlock.currency; - - var data = result[2]; - - // no TX - if (!data || !data.tx || !data.tx.history) { - return UIUtils.toast.show('INFO.EMPTY_TX_HISTORY'); - } + var txs = data.tx.pendings; + var retry = true; + while(txs && txs.length) { + // process TX pendings + _.forEach(txs, _processPendingTx); + + // Retry once (TX could be chained and processed in a wrong order) + if (txErrors.length > 0 && txPendings.length > 0 && retry) { + txs = txErrors; + txErrors = []; + retry = false; + } + else { + txs = null; + } + } - return $translate('ACCOUNT.FILE_NAME', {currency: currency, pubkey: pubkey, currentTime : currentTime}) - .then(function(filename){ - - var formatDecimal = $filter('formatDecimal'); - var medianDate = $filter('medianDate'); - var formatSymbol = $filter('currencySymbolNoHtml'); - - var headers = [ - translations['ACCOUNT.HEADERS.TIME'], - translations['COMMON.UID'], - translations['COMMON.PUBKEY'], - translations['ACCOUNT.HEADERS.AMOUNT'] + ' (' + formatSymbol(currency) + ')', - translations['ACCOUNT.HEADERS.COMMENT'] - ]; - var content = data.tx.history.concat(data.tx.validating).reduce(function(res, tx){ - return res.concat([ - medianDate(tx.time), - tx.uid, - tx.pubkey, - formatDecimal(tx.amount/100), - '"' + (tx.isUD ? translations['COMMON.UNIVERSAL_DIVIDEND'] : tx.comment) + '"' - ].join(';') + '\n'); - }, [headers.join(';') + '\n']); - - var file = new Blob(content, {type: 'text/plain; charset=utf-8'}); - FileSaver.saveAs(file, filename); - }); + data.tx = data.tx || {}; + data.tx.pendings = txPendings.sort(function(tx1, tx2) { + return (tx2.time - tx1.time); + }); + data.tx.errors = txErrors.sort(function(tx1, tx2) { + return (tx2.time - tx1.time); + }); + // Negative balance not allow (use only source's balance) - fix #769 + data.balance = (balanceWithPending < 0) ? balanceFromSource : balanceWithPending; + + // Will add uid (+ plugin will add name, avatar, etc. if enable) + var allTx = (data.tx.history || []).concat(data.tx.validating||[], data.tx.pendings||[], data.tx.errors||[]); + return csWot.extendAll(allTx, 'pubkey') + .then(function() { + console.debug('[tx] TX and sources loaded in '+ (Date.now()-now) +'ms'); + return data; }); - } - - // Register extension points - api.registerEvent('data', 'loadUDs'); - - return { - id: id, - load: loadData, - loadSources: loadSources, - downloadHistoryFile: downloadHistoryFile, - // api extension - api: api - }; - } - - var service = new CsTx('default'); + }) + .catch(function(err) { + console.warn("[tx] Error while getting sources and tx...", err); + throw err; + }); + } + + function loadSources(pubkey) { + console.debug("[tx] Loading sources for " + pubkey.substring(0,8)); + return loadData(pubkey, 'pending'); + } + + // Download TX history file + function downloadHistoryFile(pubkey, options) { + + options = options || {}; + options.fromTime = options.fromTime || -1; + + console.debug("[tx] Exporting TX history for pubkey [{0}]".format(pubkey.substr(0,8))); + + return $q.all([ + $translate(['ACCOUNT.HEADERS.TIME', + 'COMMON.UID', + 'COMMON.PUBKEY', + 'COMMON.UNIVERSAL_DIVIDEND', + 'ACCOUNT.HEADERS.AMOUNT', + 'ACCOUNT.HEADERS.COMMENT']), + csCurrency.blockchain.current(true/*withCache*/), + loadData(pubkey, options.fromTime) + ]) + .then(function(result){ + var translations = result[0]; + var currentBlock = result[1]; + var currentTime = (currentBlock && currentBlock.medianTime) || moment().utc().unix(); + var currency = currentBlock && currentBlock.currency; + + var data = result[2]; + + // no TX + if (!data || !data.tx || !data.tx.history) { + return UIUtils.toast.show('INFO.EMPTY_TX_HISTORY'); + } - service.instance = function(id, bma) { - return new CsTx(id, bma); - }; - return service; - }); + return $translate('ACCOUNT.FILE_NAME', {currency: currency, pubkey: pubkey, currentTime : currentTime}) + .then(function(filename){ + + var formatDecimal = $filter('formatDecimal'); + var medianDate = $filter('medianDate'); + var formatSymbol = $filter('currencySymbolNoHtml'); + + var headers = [ + translations['ACCOUNT.HEADERS.TIME'], + translations['COMMON.UID'], + translations['COMMON.PUBKEY'], + translations['ACCOUNT.HEADERS.AMOUNT'] + ' (' + formatSymbol(currency) + ')', + translations['ACCOUNT.HEADERS.COMMENT'] + ]; + var content = data.tx.history.concat(data.tx.validating).reduce(function(res, tx){ + return res.concat([ + medianDate(tx.time), + tx.uid, + tx.pubkey, + formatDecimal(tx.amount/100), + '"' + (tx.isUD ? translations['COMMON.UNIVERSAL_DIVIDEND'] : tx.comment) + '"' + ].join(';') + '\n'); + }, [headers.join(';') + '\n']); + + var file = new Blob(content, {type: 'text/plain; charset=utf-8'}); + FileSaver.saveAs(file, filename); + }); + }); + } + + // Register extension points + api.registerEvent('data', 'loadUDs'); + + return { + load: loadData, + loadSources: loadSources, + downloadHistoryFile: downloadHistoryFile, + // api extension + api: api + }; +}); diff --git a/www/js/services/wallet-services.js b/www/js/services/wallet-services.js index b1f9b5aa0683396a14aaacf7e1daa150428a4222..ba6f8cf37583550548737b0c86edc59fd8abeb5f 100644 --- a/www/js/services/wallet-services.js +++ b/www/js/services/wallet-services.js @@ -11,7 +11,7 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se var defaultBMA = BMA; var service; - function csWallet(id, BMA) { + function CsWallet(id, BMA) { BMA = BMA || defaultBMA; var @@ -2450,8 +2450,8 @@ angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.se return exports; } - service = csWallet('default', BMA); - service.instance = csWallet; + service = CsWallet('default', BMA); + service.instance = CsWallet; return service; }); diff --git a/www/js/services/wot-services.js b/www/js/services/wot-services.js index 4ad2f3f9850ed7a233e7b90a2aaa2a13cc036caa..98511b3e13a2c1b35fdcf8e2e1aa415d0f8327a8 100644 --- a/www/js/services/wot-services.js +++ b/www/js/services/wot-services.js @@ -5,1212 +5,1204 @@ angular.module('cesium.wot.services', ['ngApi', 'cesium.bma.services', 'cesium.c .factory('csWot', function($rootScope, $q, $timeout, BMA, Api, CacheFactory, csConfig, csCurrency, csSettings, csCache) { 'ngInject'; - function csWot(id) { - - var - api = new Api(this, "csWot-" + id), - cachePrefix = 'csWot-', - identityCache = csCache.get(cachePrefix + 'idty-', csCache.constants.MEDIUM), - requirementsCache = csCache.get(cachePrefix + 'requirements-', csCache.constants.MEDIUM), - - // Add id, and remove duplicated id - _addUniqueIds = function(idties) { - var idtyKeys = {}; - return idties.reduce(function(res, idty) { - idty.id = idty.id || idty.uid + '-' + idty.pubkey; - if (!idtyKeys[idty.id]) { - idtyKeys[idty.id] = true; - return res.concat(idty); - } - return res; - }, []); - }, - - _sortAndSliceIdentities = function(idties, offset, size) { - offset = offset || 0; - - // Add unique ids - idties = _addUniqueIds(idties); - - // Sort by block and - idties = _.sortBy(idties, function(idty){ - var score = 1; - score += (1000000 * (idty.block)); - score += (10 * (900 - idty.uid.toLowerCase().charCodeAt(0))); - return -score; - }); - if (angular.isDefined(size) && idties.length > size) { - idties = idties.slice(offset, offset+size); // limit if more than expected size - } - - return idties; - }, - - _sortCertifications = function(certifications) { - certifications = _.sortBy(certifications, function(cert){ - var score = 1; - score += (1000000000000 * (cert.expiresIn ? cert.expiresIn : 0)); - score += (10000000 * (cert.isMember ? 1 : 0)); - score += (10 * (cert.block ? cert.block : 0)); - return -score; - }); - return certifications; - }, - - _resetRequirements = function(data) { - data.requirements = { - loaded: false, - meta: {}, - hasSelf: false, - needSelf: true, - needMembership: true, - canMembershipOut: false, - needRenew: false, - pendingMembership: false, - isMember: false, - wasMember: false, - certificationCount: 0, - needCertifications: false, - needCertificationCount: 0, - willNeedCertificationCount: 0, - alternatives: undefined - }; - data.blockUid = null; - data.isMember = false; - data.sigDate = null; - data.hasSelf = false; - }, - - _fillRequirements = function(requirements, currencyParameters) { - // Add useful custom fields - requirements.hasSelf = !!requirements.meta.timestamp; - requirements.needSelf = !requirements.hasSelf || requirements.meta.invalid; - requirements.wasMember = angular.isDefined(requirements.wasMember) ? requirements.wasMember : false; // Compat with Duniter 0.9 - requirements.needMembership = (!requirements.revoked && requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn <= 0 && !requirements.wasMember); - requirements.needRenew = (!requirements.needMembership && !requirements.revoked && - requirements.membershipExpiresIn <= csSettings.data.timeWarningExpireMembership && - requirements.membershipPendingExpiresIn <= 0) || - (requirements.wasMember && !requirements.revoked && requirements.membershipExpiresIn === 0 && - requirements.membershipPendingExpiresIn === 0); - requirements.canMembershipOut = (!requirements.revoked && requirements.membershipExpiresIn > 0); - requirements.pendingMembership = (!requirements.revoked && requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn > 0); - requirements.isMember = (!requirements.revoked && requirements.membershipExpiresIn > 0); - requirements.blockUid = requirements.meta.timestamp; - // Force certification count to 0, is not a member yet - fix #269 - requirements.certificationCount = ((requirements.isMember || (requirements.wasMember && !requirements.expired)) && requirements.certifications) ? requirements.certifications.length : 0; - requirements.willExpireCertificationCount = requirements.certifications ? requirements.certifications.reduce(function(count, cert){ - return count + (cert.expiresIn <= csSettings.data.timeWarningExpire ? 1 : 0); - }, 0) : 0; - requirements.willExpire = requirements.willExpireCertificationCount > 0; - requirements.pendingRevocation = !requirements.revoked && !!requirements.revocation_sig; - //requirements.outdistanced = requirements.outdistanced; // outdistanced is always present in requirement - see #777 - - // Fix pending certifications count - Fix #624 - if (!requirements.isMember && !requirements.wasMember) { - var certifiers = _.union( - _.pluck(requirements.pendingCerts || [], 'from'), - _.pluck(requirements.certifications || [], 'from') - ); - requirements.pendingCertificationCount = _.size(certifiers); - } - else { - requirements.pendingCertificationCount = angular.isDefined(requirements.pendingCerts) ? requirements.pendingCerts.length : 0 ; + var + api = new Api(this, "csWot"), + cachePrefix = 'csWot-', + identityCache = csCache.get(cachePrefix + 'idty-', csCache.constants.MEDIUM), + requirementsCache = csCache.get(cachePrefix + 'requirements-', csCache.constants.MEDIUM), + + // Add id, and remove duplicated id + _addUniqueIds = function(idties) { + var idtyKeys = {}; + return idties.reduce(function(res, idty) { + idty.id = idty.id || idty.uid + '-' + idty.pubkey; + if (!idtyKeys[idty.id]) { + idtyKeys[idty.id] = true; + return res.concat(idty); } + return res; + }, []); + }, + + _sortAndSliceIdentities = function(idties, offset, size) { + offset = offset || 0; + + // Add unique ids + idties = _addUniqueIds(idties); + + // Sort by block and + idties = _.sortBy(idties, function(idty){ + var score = 1; + score += (1000000 * (idty.block)); + score += (10 * (900 - idty.uid.toLowerCase().charCodeAt(0))); + return -score; + }); + if (angular.isDefined(size) && idties.length > size) { + idties = idties.slice(offset, offset+size); // limit if more than expected size + } - // Compute - requirements.needCertificationCount = (!requirements.needSelf && (requirements.certificationCount < currencyParameters.sigQty)) ? - (currencyParameters.sigQty - requirements.certificationCount) : 0; - requirements.willNeedCertificationCount = (!requirements.needMembership && !requirements.needCertificationCount && - (requirements.certificationCount - requirements.willExpireCertificationCount) < currencyParameters.sigQty) ? - (currencyParameters.sigQty - requirements.certificationCount + requirements.willExpireCertificationCount) : 0; - - // Mark as loaded - need by csWallet.isDataLoaded() - requirements.loaded = true; - - - return requirements; - }, - _fillIdentitiesMeta = function(identities) { - if (!identities) return $q.when(identities); + return idties; + }, + + _sortCertifications = function(certifications) { + certifications = _.sortBy(certifications, function(cert){ + var score = 1; + score += (1000000000000 * (cert.expiresIn ? cert.expiresIn : 0)); + score += (10000000 * (cert.isMember ? 1 : 0)); + score += (10 * (cert.block ? cert.block : 0)); + return -score; + }); + return certifications; + }, + + _resetRequirements = function(data) { + data.requirements = { + loaded: false, + meta: {}, + hasSelf: false, + needSelf: true, + needMembership: true, + canMembershipOut: false, + needRenew: false, + pendingMembership: false, + isMember: false, + wasMember: false, + certificationCount: 0, + needCertifications: false, + needCertificationCount: 0, + willNeedCertificationCount: 0, + alternatives: undefined + }; + data.blockUid = null; + data.isMember = false; + data.sigDate = null; + data.hasSelf = false; + }, + + _fillRequirements = function(requirements, currencyParameters) { + // Add useful custom fields + requirements.hasSelf = !!requirements.meta.timestamp; + requirements.needSelf = !requirements.hasSelf || requirements.meta.invalid; + requirements.wasMember = angular.isDefined(requirements.wasMember) ? requirements.wasMember : false; // Compat with Duniter 0.9 + requirements.needMembership = (!requirements.revoked && requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn <= 0 && !requirements.wasMember); + requirements.needRenew = (!requirements.needMembership && !requirements.revoked && + requirements.membershipExpiresIn <= csSettings.data.timeWarningExpireMembership && + requirements.membershipPendingExpiresIn <= 0) || + (requirements.wasMember && !requirements.revoked && requirements.membershipExpiresIn === 0 && + requirements.membershipPendingExpiresIn === 0); + requirements.canMembershipOut = (!requirements.revoked && requirements.membershipExpiresIn > 0); + requirements.pendingMembership = (!requirements.revoked && requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn > 0); + requirements.isMember = (!requirements.revoked && requirements.membershipExpiresIn > 0); + requirements.blockUid = requirements.meta.timestamp; + // Force certification count to 0, is not a member yet - fix #269 + requirements.certificationCount = ((requirements.isMember || (requirements.wasMember && !requirements.expired)) && requirements.certifications) ? requirements.certifications.length : 0; + requirements.willExpireCertificationCount = requirements.certifications ? requirements.certifications.reduce(function(count, cert){ + return count + (cert.expiresIn <= csSettings.data.timeWarningExpire ? 1 : 0); + }, 0) : 0; + requirements.willExpire = requirements.willExpireCertificationCount > 0; + requirements.pendingRevocation = !requirements.revoked && !!requirements.revocation_sig; + //requirements.outdistanced = requirements.outdistanced; // outdistanced is always present in requirement - see #777 + + // Fix pending certifications count - Fix #624 + if (!requirements.isMember && !requirements.wasMember) { + var certifiers = _.union( + _.pluck(requirements.pendingCerts || [], 'from'), + _.pluck(requirements.certifications || [], 'from') + ); + requirements.pendingCertificationCount = _.size(certifiers); + } + else { + requirements.pendingCertificationCount = angular.isDefined(requirements.pendingCerts) ? requirements.pendingCerts.length : 0 ; + } - var blocks = []; - _.forEach(identities, function(identity) { - var blockUid = identity.meta.timestamp.split('-', 2); - identity.meta.number = parseInt(blockUid[0]); - identity.meta.hash = blockUid[1]; - identity.meta.sig = identity.meta.sig || identity.sig; - delete identity.sig; - blocks.push(identity.meta.number); - }); + // Compute + requirements.needCertificationCount = (!requirements.needSelf && (requirements.certificationCount < currencyParameters.sigQty)) ? + (currencyParameters.sigQty - requirements.certificationCount) : 0; + requirements.willNeedCertificationCount = (!requirements.needMembership && !requirements.needCertificationCount && + (requirements.certificationCount - requirements.willExpireCertificationCount) < currencyParameters.sigQty) ? + (currencyParameters.sigQty - requirements.certificationCount + requirements.willExpireCertificationCount) : 0; + + // Mark as loaded - need by csWallet.isDataLoaded() + requirements.loaded = true; + + + return requirements; + }, + + _fillIdentitiesMeta = function(identities) { + if (!identities) return $q.when(identities); + + var blocks = []; + _.forEach(identities, function(identity) { + var blockUid = identity.meta.timestamp.split('-', 2); + identity.meta.number = parseInt(blockUid[0]); + identity.meta.hash = blockUid[1]; + identity.meta.sig = identity.meta.sig || identity.sig; + delete identity.sig; + blocks.push(identity.meta.number); + }); + + // Get identities blocks, to fill self and revocation time + return BMA.blockchain.blocks(_.uniq(blocks)) + .then(function(blocks) { + _.forEach(identities, function(identity) { + var block = _.findWhere(blocks, {number: identity.meta.number}); + identity.meta.time = block && block.medianTime; + + // Check if self has been done on a valid block + if (block && identity.meta.number !== 0 && identity.meta.hash !== block.hash) { + identity.meta.invalid = true; + } + }); - // Get identities blocks, to fill self and revocation time - return BMA.blockchain.blocks(_.uniq(blocks)) - .then(function(blocks) { + return identities; + }) + .catch(function(err){ + // Special case for currency init (root block not exists): use now + if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND) { _.forEach(identities, function(identity) { - var block = _.findWhere(blocks, {number: identity.meta.number}); - identity.meta.time = block && block.medianTime; - - // Check if self has been done on a valid block - if (block && identity.meta.number !== 0 && identity.meta.hash !== block.hash) { - identity.meta.invalid = true; + if (identity.number === 0) { + identity.meta.time = moment().utc().unix(); } }); - return identities; - }) - .catch(function(err){ - // Special case for currency init (root block not exists): use now - if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND) { - _.forEach(identities, function(identity) { - if (identity.number === 0) { - identity.meta.time = moment().utc().unix(); - } - }); - return identities; - } - else { - throw err; - } - }); - }, - - loadRequirements = function(inputData, withCache) { - if (!inputData || (!inputData.pubkey && !inputData.uid)) return $q.when(inputData); - - var cacheKey = inputData.pubkey||inputData.uid; - var data = (withCache !== false) ? requirementsCache.get(cacheKey) : null; - if (data) { - console.debug("[wot] Requirements " + cacheKey + " found in cache"); - // Update data with cache - angular.merge(inputData, data); - return $q.when(data); - } - data = {pubkey: inputData.pubkey, uid: inputData.uid}; + } + else { + throw err; + } + }); + }, + + loadRequirements = function(inputData, withCache) { + if (!inputData || (!inputData.pubkey && !inputData.uid)) return $q.when(inputData); + + var cacheKey = inputData.pubkey||inputData.uid; + var data = (withCache !== false) ? requirementsCache.get(cacheKey) : null; + if (data) { + console.debug("[wot] Requirements " + cacheKey + " found in cache"); + // Update data with cache + angular.merge(inputData, data); + return $q.when(data); + } + data = {pubkey: inputData.pubkey, uid: inputData.uid}; - var now = Date.now(); + var now = Date.now(); - return $q.all([ - // Get currency - csCurrency.get(), + return $q.all([ + // Get currency + csCurrency.get(), - // Get requirements - BMA.wot.requirements({pubkey: data.pubkey||data.uid}, false/*no cache*/) - .then(function(res) { - return _fillIdentitiesMeta(res && res.identities); - }) - ]) - .then(function(res){ - var currency = res[0]; - var identities = res[1]; - - if (!identities || !identities.length) return; - - // Sort to select the best identity - if (identities.length > 1) { - // Select the best identity, by sorting using this order - // - same wallet uid - // - is member - // - has a pending membership - // - is not expired (in sandbox) - // - is not outdistanced - // - if has certifications - // max(count(certification) - // else - // max(membershipPendingExpiresIn) = must recent membership - identities = _.sortBy(identities, function(idty) { - var score = 0; - score += (1000000000000* ((data.uid && idty.uid === data.uid) ? 1 : 0)); - score += (100000000000 * (!idty.meta.invalid ? 1 : 0)); - score += (10000000000 * ((data.blockUid && idty.meta.timestamp && idty.meta.timestamp === data.blockUid) ? 1 : 0)); - score += (1000000000 * (idty.membershipExpiresIn > 0 ? 1 : 0)); - score += (100000000 * (idty.membershipPendingExpiresIn > 0 ? 1 : 0)); - score += (10000000 * (!idty.expired ? 1 : 0)); - score += (1000000 * (!idty.outdistanced ? 1 : 0)); - score += (100000 * (idty.wasMember ? 1 : 0)); - var certCount = !idty.expired && idty.certifications ? idty.certifications.length : 0; - score += (1 * (certCount ? certCount : 0)); - score += (1 * (!certCount && idty.membershipPendingExpiresIn > 0 ? idty.membershipPendingExpiresIn/1000 : 0)); - return -score; - }); - console.debug('[wot] Found {0} identities (in requirements). Will selected the best one'.format(identities.length)); - } + // Get requirements + BMA.wot.requirements({pubkey: data.pubkey||data.uid}, false/*no cache*/) + .then(function(res) { + return _fillIdentitiesMeta(res && res.identities); + }) + ]) + .then(function(res){ + var currency = res[0]; + var identities = res[1]; + + if (!identities || !identities.length) return; + + // Sort to select the best identity + if (identities.length > 1) { + // Select the best identity, by sorting using this order + // - same wallet uid + // - is member + // - has a pending membership + // - is not expired (in sandbox) + // - is not outdistanced + // - if has certifications + // max(count(certification) + // else + // max(membershipPendingExpiresIn) = must recent membership + identities = _.sortBy(identities, function(idty) { + var score = 0; + score += (1000000000000* ((data.uid && idty.uid === data.uid) ? 1 : 0)); + score += (100000000000 * (!idty.meta.invalid ? 1 : 0)); + score += (10000000000 * ((data.blockUid && idty.meta.timestamp && idty.meta.timestamp === data.blockUid) ? 1 : 0)); + score += (1000000000 * (idty.membershipExpiresIn > 0 ? 1 : 0)); + score += (100000000 * (idty.membershipPendingExpiresIn > 0 ? 1 : 0)); + score += (10000000 * (!idty.expired ? 1 : 0)); + score += (1000000 * (!idty.outdistanced ? 1 : 0)); + score += (100000 * (idty.wasMember ? 1 : 0)); + var certCount = !idty.expired && idty.certifications ? idty.certifications.length : 0; + score += (1 * (certCount ? certCount : 0)); + score += (1 * (!certCount && idty.membershipPendingExpiresIn > 0 ? idty.membershipPendingExpiresIn/1000 : 0)); + return -score; + }); + console.debug('[wot] Found {0} identities (in requirements). Will selected the best one'.format(identities.length)); + } - // Select the first identity - data.requirements = _fillRequirements(identities[0], currency.parameters); - - // Copy some useful properties into data - data.pubkey = data.requirements.pubkey; - data.uid = data.requirements.uid; - data.isMember = data.requirements.isMember; - data.blockUid = data.requirements.meta && data.requirements.meta.timestamp; - data.hasSelf = data.requirements.hasSelf; - data.sigDate = data.requirements.meta && data.requirements.meta.time; - - // Prepare alternatives identities if any - if (!data.requirements.isMember && !data.requirements.wasMember && identities.length > 1) { - data.requirements.alternatives = identities.splice(1); - _.forEach(data.requirements.alternatives, function(requirements) { - _fillRequirements(requirements, currency.parameters); - }); - } + // Select the first identity + data.requirements = _fillRequirements(identities[0], currency.parameters); + + // Copy some useful properties into data + data.pubkey = data.requirements.pubkey; + data.uid = data.requirements.uid; + data.isMember = data.requirements.isMember; + data.blockUid = data.requirements.meta && data.requirements.meta.timestamp; + data.hasSelf = data.requirements.hasSelf; + data.sigDate = data.requirements.meta && data.requirements.meta.time; + + // Prepare alternatives identities if any + if (!data.requirements.isMember && !data.requirements.wasMember && identities.length > 1) { + data.requirements.alternatives = identities.splice(1); + _.forEach(data.requirements.alternatives, function(requirements) { + _fillRequirements(requirements, currency.parameters); + }); + } - /// Save to cache - requirementsCache.put(cacheKey, data); + /// Save to cache + requirementsCache.put(cacheKey, data); - angular.merge(inputData, data); // Update the input data + angular.merge(inputData, data); // Update the input data - console.debug("[wot] Requirements for '{0}' loaded in {1}ms".format((data.pubkey && data.pubkey.substring(0,8))||data.uid, Date.now() - now)); + console.debug("[wot] Requirements for '{0}' loaded in {1}ms".format((data.pubkey && data.pubkey.substring(0,8))||data.uid, Date.now() - now)); + return inputData; + }) + .catch(function(err) { + _resetRequirements(inputData); + // If not a member: continue + if (!!err && + (err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER || + err.ucode == BMA.errorCodes.NO_IDTY_MATCHING_PUB_OR_UID)) { + inputData.requirements.loaded = true; return inputData; - }) - .catch(function(err) { - _resetRequirements(inputData); - // If not a member: continue - if (!!err && - (err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER || - err.ucode == BMA.errorCodes.NO_IDTY_MATCHING_PUB_OR_UID)) { - inputData.requirements.loaded = true; - return inputData; - } - throw err; - }); - }, - - - - loadIdentityByLookup = function(pubkey, uid) { - var data = { - pubkey: pubkey, - uid: uid, - hasSelf: false - }; - return BMA.wot.lookup({ search: pubkey||uid }) - .then(function(res){ - var identities = res.results.reduce(function(idties, res) { - return idties.concat(res.uids.reduce(function(uids, idty) { - var blockUid = idty.meta.timestamp.split('-', 2); - var blockNumber = parseInt(blockUid[0]); - return uids.concat({ - uid: idty.uid, - pubkey: res.pubkey, - meta: { - timestamp: idty.meta.timestamp, - number: blockNumber, - hash: blockUid[1], - sig: idty.self - }, - revoked: idty.revoked, - revoked_on: idty.revoked_on - }); - }, [])); - }, []); - - // Fill identities meta (self) - return _fillIdentitiesMeta(identities) - .then(function(identities) { - return { - identities: identities, - results: res.results - }; - }); - }) - .then(function(res){ - var identities = res.identities; - - // Sort identities if need - if (identities.length > 1) { - // Select the best identity, by sorting using this order - // - valid block - // - same given uid - // - not revoked - // - max(block_number) - res.identities = _.sortBy(identities, function(idty) { - var score = 0; - score += (100000000000 * ((data.uid && idty.uid === data.uid) ? 1 : 0)); - score += (10000000000 * (!idty.meta.invalid ? 1 : 0)); - score += (1000000000 * ((data.blockUid && idty.meta.timestamp && idty.meta.timestamp === data.blockUid) ? 1 : 0)); - score += (100000000 * (!idty.revoked ? 1 : 0)); - score += (1 * (idty.meta.number ? idty.meta.number : 0) / 1000); - return -score; + } + throw err; + }); + }, + + + + loadIdentityByLookup = function(pubkey, uid) { + var data = { + pubkey: pubkey, + uid: uid, + hasSelf: false + }; + return BMA.wot.lookup({ search: pubkey||uid }) + .then(function(res){ + var identities = res.results.reduce(function(idties, res) { + return idties.concat(res.uids.reduce(function(uids, idty) { + var blockUid = idty.meta.timestamp.split('-', 2); + var blockNumber = parseInt(blockUid[0]); + return uids.concat({ + uid: idty.uid, + pubkey: res.pubkey, + meta: { + timestamp: idty.meta.timestamp, + number: blockNumber, + hash: blockUid[1], + sig: idty.self + }, + revoked: idty.revoked, + revoked_on: idty.revoked_on }); - console.debug('[wot] Found {0} identities (in lookup). Will selected the best one'.format(identities.length)); - } + }, [])); + }, []); - // Prepare alternatives identities - _.forEach(identities, function(idty) { - idty.hasSelf = !!(idty.uid && idty.meta.timestamp && idty.meta.sig); + // Fill identities meta (self) + return _fillIdentitiesMeta(identities) + .then(function(identities) { + return { + identities: identities, + results: res.results + }; + }); + }) + .then(function(res){ + var identities = res.identities; + + // Sort identities if need + if (identities.length > 1) { + // Select the best identity, by sorting using this order + // - valid block + // - same given uid + // - not revoked + // - max(block_number) + res.identities = _.sortBy(identities, function(idty) { + var score = 0; + score += (100000000000 * ((data.uid && idty.uid === data.uid) ? 1 : 0)); + score += (10000000000 * (!idty.meta.invalid ? 1 : 0)); + score += (1000000000 * ((data.blockUid && idty.meta.timestamp && idty.meta.timestamp === data.blockUid) ? 1 : 0)); + score += (100000000 * (!idty.revoked ? 1 : 0)); + score += (1 * (idty.meta.number ? idty.meta.number : 0) / 1000); + return -score; }); + console.debug('[wot] Found {0} identities (in lookup). Will selected the best one'.format(identities.length)); + } - // Select the first identity - data.requirements = identities[0]; + // Prepare alternatives identities + _.forEach(identities, function(idty) { + idty.hasSelf = !!(idty.uid && idty.meta.timestamp && idty.meta.sig); + }); - // Copy some useful properties into data - data.pubkey = data.requirements.pubkey; - data.uid = data.requirements.uid; - data.blockUid = data.requirements.meta && data.requirements.meta.timestamp; - data.hasSelf = data.requirements.hasSelf; - data.sigDate = data.requirements.meta && data.requirements.meta.time; + // Select the first identity + data.requirements = identities[0]; - if (identities.length > 1) { - data.requirements.alternatives = identities.splice(1); - } + // Copy some useful properties into data + data.pubkey = data.requirements.pubkey; + data.uid = data.requirements.uid; + data.blockUid = data.requirements.meta && data.requirements.meta.timestamp; + data.hasSelf = data.requirements.hasSelf; + data.sigDate = data.requirements.meta && data.requirements.meta.time; - // Store additional data (e.g. certs) - data.lookup = {}; - - // Store received certifications (can be usefull later) - var certPubkeys = {}; - data.lookup.certifications = (res.results || []).reduce(function(certsMap, res) { - return res.uids.reduce(function(certsMap, idty) { - var idtyFullKey = idty.uid + '-' + (idty.meta ? idty.meta.timestamp : ''); - certsMap[idtyFullKey] = (idty.others||[]).reduce(function(certs, cert) { - var certFullKey = idtyFullKey + '-' + cert.pubkey; - var result = { - pubkey: cert.pubkey, - uid: cert.uids[0], - cert_time: { - block: (cert.meta && cert.meta.block_number) ? cert.meta.block_number : 0, - block_hash: (cert.meta && cert.meta.block_hash) ? cert.meta.block_hash : null - }, - isMember: cert.isMember, - wasMember: cert.wasMember, - }; - if (!certPubkeys[certFullKey]) { - certPubkeys[certFullKey] = result; - } - else { // if duplicated cert: keep the most recent - if (result.cert_time.block > certPubkeys[certFullKey].cert_time.block) { - certPubkeys[certFullKey] = result; - certs.splice(_.findIndex(certs, {pubkey: cert.pubkey}), 1, result); - return certs; - } - else { - return certs; // skip this cert - } - } - return certs.concat(result); - }, []); - return certsMap; - }, certsMap); - }, {}); - - // Store given certifications - certPubkeys = {}; - data.lookup.givenCertifications = (res.results || []).reduce(function(certs, res) { - return (res.signed || []).reduce(function(certs, cert) { + if (identities.length > 1) { + data.requirements.alternatives = identities.splice(1); + } + + // Store additional data (e.g. certs) + data.lookup = {}; + + // Store received certifications (can be usefull later) + var certPubkeys = {}; + data.lookup.certifications = (res.results || []).reduce(function(certsMap, res) { + return res.uids.reduce(function(certsMap, idty) { + var idtyFullKey = idty.uid + '-' + (idty.meta ? idty.meta.timestamp : ''); + certsMap[idtyFullKey] = (idty.others||[]).reduce(function(certs, cert) { + var certFullKey = idtyFullKey + '-' + cert.pubkey; var result = { pubkey: cert.pubkey, - uid: cert.uid, + uid: cert.uids[0], cert_time: { - block: (cert.cert_time && cert.cert_time.block) ? cert.cert_time.block : 0, - block_hash: (cert.cert_time && cert.cert_time.block_hash) ? cert.cert_time.block_hash : null + block: (cert.meta && cert.meta.block_number) ? cert.meta.block_number : 0, + block_hash: (cert.meta && cert.meta.block_hash) ? cert.meta.block_hash : null }, - sigDate: cert.meta ? cert.meta.timestamp : null, isMember: cert.isMember, - wasMember: cert.wasMember + wasMember: cert.wasMember, }; - if (!certPubkeys[cert.pubkey]) { - certPubkeys[cert.pubkey] = result; + if (!certPubkeys[certFullKey]) { + certPubkeys[certFullKey] = result; } else { // if duplicated cert: keep the most recent - if (result.block > certPubkeys[cert.pubkey].block) { - certPubkeys[cert.pubkey] = result; - // TODO: Replace the existing one ? May be not, to be able to see renewal - // (see issue #806) - // If yes (need to replace), check this code works: - //certs.splice(_.findIndex(certs, {pubkey: cert.pubkey}), 1, result); - //return certs; + if (result.cert_time.block > certPubkeys[certFullKey].cert_time.block) { + certPubkeys[certFullKey] = result; + certs.splice(_.findIndex(certs, {pubkey: cert.pubkey}), 1, result); + return certs; } else { return certs; // skip this cert } } return certs.concat(result); - }, certs); - }, []); - + }, []); + return certsMap; + }, certsMap); + }, {}); + + // Store given certifications + certPubkeys = {}; + data.lookup.givenCertifications = (res.results || []).reduce(function(certs, res) { + return (res.signed || []).reduce(function(certs, cert) { + var result = { + pubkey: cert.pubkey, + uid: cert.uid, + cert_time: { + block: (cert.cert_time && cert.cert_time.block) ? cert.cert_time.block : 0, + block_hash: (cert.cert_time && cert.cert_time.block_hash) ? cert.cert_time.block_hash : null + }, + sigDate: cert.meta ? cert.meta.timestamp : null, + isMember: cert.isMember, + wasMember: cert.wasMember + }; + if (!certPubkeys[cert.pubkey]) { + certPubkeys[cert.pubkey] = result; + } + else { // if duplicated cert: keep the most recent + if (result.block > certPubkeys[cert.pubkey].block) { + certPubkeys[cert.pubkey] = result; + // TODO: Replace the existing one ? May be not, to be able to see renewal + // (see issue #806) + // If yes (need to replace), check this code works: + //certs.splice(_.findIndex(certs, {pubkey: cert.pubkey}), 1, result); + //return certs; + } + else { + return certs; // skip this cert + } + } + return certs.concat(result); + }, certs); + }, []); + + return data; + }) + .catch(function(err) { + if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) { // Identity not found (if no self) + _resetRequirements(data); return data; - }) - .catch(function(err) { - if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) { // Identity not found (if no self) - _resetRequirements(data); - return data; - } - else { - throw err; - } - }); - }, + } + else { + throw err; + } + }); + }, - loadCertifications = function(getFunction, pubkey, lookupCertifications, parameters, medianTime, certifiersOf) { + loadCertifications = function(getFunction, pubkey, lookupCertifications, parameters, medianTime, certifiersOf) { - function _certId(pubkey, block) { - return pubkey + '-' + block; - } + function _certId(pubkey, block) { + return pubkey + '-' + block; + } - // TODO : remove this later (when all node will use duniter v0.50+) - var lookupHasCertTime = true; // Will be set ti FALSE before Duniter v0.50 - var lookupCerticationsByCertId = lookupCertifications ? lookupCertifications.reduce(function(res, cert){ - var certId = _certId(cert.pubkey, cert.cert_time ? cert.cert_time.block : cert.sigDate); - if (!cert.cert_time) lookupHasCertTime = false; - res[certId] = cert; - return res; - }, {}) : {}; + // TODO : remove this later (when all node will use duniter v0.50+) + var lookupHasCertTime = true; // Will be set ti FALSE before Duniter v0.50 + var lookupCerticationsByCertId = lookupCertifications ? lookupCertifications.reduce(function(res, cert){ + var certId = _certId(cert.pubkey, cert.cert_time ? cert.cert_time.block : cert.sigDate); + if (!cert.cert_time) lookupHasCertTime = false; + res[certId] = cert; + return res; + }, {}) : {}; - var isMember = true; + var isMember = true; - return getFunction({ pubkey: pubkey }) - .then(function(res) { - return (res && res.certifications || []).reduce(function (res, cert) { - // Rappel : - // cert.sigDate = blockstamp de l'identité - // cert.cert_time.block : block au moment de la certification - // cert.written.number : block où la certification est écrite - - var pending = !cert.written; - var certTime = cert.cert_time ? cert.cert_time.medianTime : null; - var expiresIn = (!certTime) ? 0 : (pending ? - (certTime + parameters.sigWindow - medianTime) : - (certTime + parameters.sigValidity - medianTime)); - expiresIn = (expiresIn < 0) ? 0 : expiresIn; - // Remove from lookup certs - var certId = _certId(cert.pubkey, lookupHasCertTime && cert.cert_time ? cert.cert_time.block : cert.sigDate); - delete lookupCerticationsByCertId[certId]; - - // Add to result list + return getFunction({ pubkey: pubkey }) + .then(function(res) { + return (res && res.certifications || []).reduce(function (res, cert) { + // Rappel : + // cert.sigDate = blockstamp de l'identité + // cert.cert_time.block : block au moment de la certification + // cert.written.number : block où la certification est écrite + + var pending = !cert.written; + var certTime = cert.cert_time ? cert.cert_time.medianTime : null; + var expiresIn = (!certTime) ? 0 : (pending ? + (certTime + parameters.sigWindow - medianTime) : + (certTime + parameters.sigValidity - medianTime)); + expiresIn = (expiresIn < 0) ? 0 : expiresIn; + // Remove from lookup certs + var certId = _certId(cert.pubkey, lookupHasCertTime && cert.cert_time ? cert.cert_time.block : cert.sigDate); + delete lookupCerticationsByCertId[certId]; + + // Add to result list + return res.concat({ + pubkey: cert.pubkey, + uid: cert.uid, + time: certTime, + isMember: cert.isMember, + wasMember: cert.wasMember, + expiresIn: expiresIn, + willExpire: (expiresIn && expiresIn <= csSettings.data.timeWarningExpire), + pending: pending, + block: (cert.written !== null) ? cert.written.number : + (cert.cert_time ? cert.cert_time.block : null), + valid: (expiresIn > 0) + }); + }, []); + }) + .catch(function(err) { + if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER) { // member not found + isMember = false; + return []; // continue (append pendings cert if exists in lookup) + } + /*FIXME: workaround for Duniter issue #1309 */ + else if (!!err && err.ucode == 1002) { + console.warn("[wallet-service] Detecting Duniter issue #1309 ! Applying workaround... "); + isMember = false; + return []; // not found + } + else { + throw err; + } + }) + + // Add pending certs (found in lookup - see loadIdentityByLookup()) + .then(function(certifications) { + var pendingCertifications = _.values(lookupCerticationsByCertId); + if (!pendingCertifications.length) return certifications; // No more pending continue + + // Special case for initPhase - issue # + if (csCurrency.data.initPhase) { + return pendingCertifications.reduce(function(res, cert) { return res.concat({ pubkey: cert.pubkey, uid: cert.uid, - time: certTime, isMember: cert.isMember, wasMember: cert.wasMember, - expiresIn: expiresIn, - willExpire: (expiresIn && expiresIn <= csSettings.data.timeWarningExpire), - pending: pending, - block: (cert.written !== null) ? cert.written.number : - (cert.cert_time ? cert.cert_time.block : null), - valid: (expiresIn > 0) + time: null, + expiresIn: parameters.sigWindow, + willExpire: false, + pending: true, + block: 0, + valid: true }); - }, []); - }) - .catch(function(err) { - if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER) { // member not found - isMember = false; - return []; // continue (append pendings cert if exists in lookup) - } - /*FIXME: workaround for Duniter issue #1309 */ - else if (!!err && err.ucode == 1002) { - console.warn("[wallet-service] Detecting Duniter issue #1309 ! Applying workaround... "); - isMember = false; - return []; // not found - } - else { - throw err; - } - }) - - // Add pending certs (found in lookup - see loadIdentityByLookup()) - .then(function(certifications) { - var pendingCertifications = _.values(lookupCerticationsByCertId); - if (!pendingCertifications.length) return certifications; // No more pending continue + }, certifications); + } - // Special case for initPhase - issue # - if (csCurrency.data.initPhase) { - return pendingCertifications.reduce(function(res, cert) { + var pendingCertByBlocks = pendingCertifications.reduce(function(res, cert){ + var block = lookupHasCertTime && cert.cert_time ? cert.cert_time.block : + (cert.sigDate ? cert.sigDate.split('-')[0] : null); + if (angular.isDefined(block)) { + if (!res[block]) { + res[block] = [cert]; + } + else { + res[block].push(cert); + } + } + return res; + }, {}); + + // Set time to pending cert, from blocks + return BMA.blockchain.blocks(_.keys(pendingCertByBlocks)).then(function(blocks){ + certifications = blocks.reduce(function(res, block){ + return res.concat(pendingCertByBlocks[block.number].reduce(function(res, cert) { + var certTime = block.medianTime; + var expiresIn = Math.max(0, certTime + parameters.sigWindow - medianTime); + var validBuid = (!cert.cert_time || !cert.cert_time.block_hash || cert.cert_time.block_hash == block.hash); + if (!validBuid) { + console.debug("[wot] Invalid cert {0}: block hash changed".format(cert.pubkey.substring(0,8))); + } + var valid = (expiresIn > 0) && (!certifiersOf || cert.isMember) && validBuid; return res.concat({ pubkey: cert.pubkey, uid: cert.uid, isMember: cert.isMember, wasMember: cert.wasMember, - time: null, - expiresIn: parameters.sigWindow, - willExpire: false, + time: certTime, + expiresIn: expiresIn, + willExpire: (expiresIn && expiresIn <= csSettings.data.timeWarningExpire), pending: true, - block: 0, - valid: true + block: lookupHasCertTime && cert.cert_time ? cert.cert_time.block : + (cert.sigDate ? cert.sigDate.split('-')[0] : null), + valid: valid }); - }, certifications); - } - - var pendingCertByBlocks = pendingCertifications.reduce(function(res, cert){ - var block = lookupHasCertTime && cert.cert_time ? cert.cert_time.block : - (cert.sigDate ? cert.sigDate.split('-')[0] : null); - if (angular.isDefined(block)) { - if (!res[block]) { - res[block] = [cert]; - } - else { - res[block].push(cert); - } - } - return res; - }, {}); - - // Set time to pending cert, from blocks - return BMA.blockchain.blocks(_.keys(pendingCertByBlocks)).then(function(blocks){ - certifications = blocks.reduce(function(res, block){ - return res.concat(pendingCertByBlocks[block.number].reduce(function(res, cert) { - var certTime = block.medianTime; - var expiresIn = Math.max(0, certTime + parameters.sigWindow - medianTime); - var validBuid = (!cert.cert_time || !cert.cert_time.block_hash || cert.cert_time.block_hash == block.hash); - if (!validBuid) { - console.debug("[wot] Invalid cert {0}: block hash changed".format(cert.pubkey.substring(0,8))); - } - var valid = (expiresIn > 0) && (!certifiersOf || cert.isMember) && validBuid; - return res.concat({ - pubkey: cert.pubkey, - uid: cert.uid, - isMember: cert.isMember, - wasMember: cert.wasMember, - time: certTime, - expiresIn: expiresIn, - willExpire: (expiresIn && expiresIn <= csSettings.data.timeWarningExpire), - pending: true, - block: lookupHasCertTime && cert.cert_time ? cert.cert_time.block : - (cert.sigDate ? cert.sigDate.split('-')[0] : null), - valid: valid - }); - }, [])); - }, certifications); - return certifications; - }); - }) + }, [])); + }, certifications); + return certifications; + }); + }) - // Sort and return result - .then(function(certifications) { + // Sort and return result + .then(function(certifications) { - // Remove pending cert duplicated with a written & valid cert - var writtenCertByPubkey = certifications.reduce(function(res, cert) { - if (!cert.pending && cert.valid && cert.expiresIn >= parameters.sigWindow) { - res[cert.pubkey] = true; + // Remove pending cert duplicated with a written & valid cert + var writtenCertByPubkey = certifications.reduce(function(res, cert) { + if (!cert.pending && cert.valid && cert.expiresIn >= parameters.sigWindow) { + res[cert.pubkey] = true; + } + return res; + }, {}); + + // Final sort + certifications = _sortCertifications(certifications); + + // Split into valid/pending/error + var pendingCertifications = []; + var errorCertifications = []; + var validCertifications = certifications.reduce(function(res, cert) { + if (cert.pending) { + if (cert.valid && !writtenCertByPubkey[cert.pubkey]) { + pendingCertifications.push(cert); } - return res; - }, {}); - - // Final sort - certifications = _sortCertifications(certifications); - - // Split into valid/pending/error - var pendingCertifications = []; - var errorCertifications = []; - var validCertifications = certifications.reduce(function(res, cert) { - if (cert.pending) { - if (cert.valid && !writtenCertByPubkey[cert.pubkey]) { - pendingCertifications.push(cert); - } - else if (!cert.valid && !writtenCertByPubkey[cert.pubkey]){ - errorCertifications.push(cert); - } - return res; + else if (!cert.valid && !writtenCertByPubkey[cert.pubkey]){ + errorCertifications.push(cert); } - return res.concat(cert); - }, []); - - return { - valid: validCertifications, - pending: pendingCertifications, - error: errorCertifications - }; - }) - ; - }, - - // Add events on given account - addEvents = function(data) { + return res; + } + return res.concat(cert); + }, []); - if (data.requirements.revoked) { - delete data.requirements.meta.invalid; - if (data.requirements.revoked_on) { - addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_REVOKED_WITH_TIME', messageParams: {revocationTime: data.requirements.revoked_on}}); - console.debug("[wot] Identity [{0}] has been revoked on {1}".format(data.uid, data.requirements.revoked_on)); - } - else { - addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_REVOKED'}); - console.debug("[wot] Identity [{0}] has been revoked".format(data.uid)); - } - } - else if (data.requirements.pendingRevocation) { - delete data.requirements.meta.invalid; - addEvent(data, {type:'error', message: 'ERROR.IDENTITY_PENDING_REVOCATION'}); - console.debug("[wot] Identity [{0}] has pending revocation".format(data.uid)); - } - else if (data.requirements.meta && data.requirements.meta.invalid) { - if (!data.isMember) { - addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_INVALID_BLOCK_HASH'}); - console.debug("[wot] Invalid membership for uid {0}: block hash changed".format(data.uid)); - } - } - else if (data.requirements.expired) { - addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_EXPIRED'}); - console.debug("[wot] Identity {0} expired (in sandbox)".format(data.uid)); - } - else if (data.requirements.willNeedCertificationCount > 0) { - addEvent(data, {type: 'error', message: 'INFO.IDENTITY_WILL_MISSING_CERTIFICATIONS', messageParams: data.requirements}); - console.debug("[wot] Identity {0} will need {1} certification(s)".format(data.uid, data.requirements.willNeedCertificationCount)); + return { + valid: validCertifications, + pending: pendingCertifications, + error: errorCertifications + }; + }) + ; + }, + + // Add events on given account + addEvents = function(data) { + + if (data.requirements.revoked) { + delete data.requirements.meta.invalid; + if (data.requirements.revoked_on) { + addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_REVOKED_WITH_TIME', messageParams: {revocationTime: data.requirements.revoked_on}}); + console.debug("[wot] Identity [{0}] has been revoked on {1}".format(data.uid, data.requirements.revoked_on)); } - else if (!data.requirements.needSelf && data.requirements.needMembership) { - addEvent(data, {type: 'error', message: 'INFO.IDENTITY_NEED_MEMBERSHIP'}); - console.debug("[wot] Identity {0} has a self but no membership".format(data.uid)); + else { + addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_REVOKED'}); + console.debug("[wot] Identity [{0}] has been revoked".format(data.uid)); } - if (!data.isMember && data.requirements.alternatives) { - addEvent(data, {type: 'info', message: 'INFO.HAS_ALTERNATIVE_IDENTITIES'}); + } + else if (data.requirements.pendingRevocation) { + delete data.requirements.meta.invalid; + addEvent(data, {type:'error', message: 'ERROR.IDENTITY_PENDING_REVOCATION'}); + console.debug("[wot] Identity [{0}] has pending revocation".format(data.uid)); + } + else if (data.requirements.meta && data.requirements.meta.invalid) { + if (!data.isMember) { + addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_INVALID_BLOCK_HASH'}); + console.debug("[wot] Invalid membership for uid {0}: block hash changed".format(data.uid)); } - }, + } + else if (data.requirements.expired) { + addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_EXPIRED'}); + console.debug("[wot] Identity {0} expired (in sandbox)".format(data.uid)); + } + else if (data.requirements.willNeedCertificationCount > 0) { + addEvent(data, {type: 'error', message: 'INFO.IDENTITY_WILL_MISSING_CERTIFICATIONS', messageParams: data.requirements}); + console.debug("[wot] Identity {0} will need {1} certification(s)".format(data.uid, data.requirements.willNeedCertificationCount)); + } + else if (!data.requirements.needSelf && data.requirements.needMembership) { + addEvent(data, {type: 'error', message: 'INFO.IDENTITY_NEED_MEMBERSHIP'}); + console.debug("[wot] Identity {0} has a self but no membership".format(data.uid)); + } + if (!data.isMember && data.requirements.alternatives) { + addEvent(data, {type: 'info', message: 'INFO.HAS_ALTERNATIVE_IDENTITIES'}); + } + }, - loadData = function(pubkey, uid, options) { + loadData = function(pubkey, uid, options) { - options = options || {}; - var data; + options = options || {}; + var data; - if (!pubkey && uid && !options.force) { - return BMA.wot.member.getByUid(uid) - .then(function(member) { - if (member) return loadData(member.pubkey, member.uid, options); // recursive call, with a pubkey - //throw {message: 'NOT_A_MEMBER'}; - var options = angular.copy(options || {}); - options.force = true; - return loadData(pubkey, uid, options); // Loop with force=true - }); - } + if (!pubkey && uid && !options.force) { + return BMA.wot.member.getByUid(uid) + .then(function(member) { + if (member) return loadData(member.pubkey, member.uid, options); // recursive call, with a pubkey + //throw {message: 'NOT_A_MEMBER'}; + var options = angular.copy(options || {}); + options.force = true; + return loadData(pubkey, uid, options); // Loop with force=true + }); + } - // Check cached data - if (pubkey) { - data = (options.cache !== false) ? identityCache.get(pubkey) : null; - if (data && (!uid || data.uid === uid) && (!options.blockUid || data.blockUid === options.blockUid)) { - console.debug("[wot] Identity " + pubkey.substring(0, 8) + " found in cache"); - return $q.when(data); - } - console.debug("[wot] Loading identity " + pubkey.substring(0, 8) + "..."); - data = { - pubkey: pubkey, - uid: uid - }; - } - else { - console.debug("[wot] Loading identity from uid " + uid); - data = { - uid: uid - }; - } - if (options.blockUid) { - data.blockUid = options.blockUid; + // Check cached data + if (pubkey) { + data = (options.cache !== false) ? identityCache.get(pubkey) : null; + if (data && (!uid || data.uid === uid) && (!options.blockUid || data.blockUid === options.blockUid)) { + console.debug("[wot] Identity " + pubkey.substring(0, 8) + " found in cache"); + return $q.when(data); } + console.debug("[wot] Loading identity " + pubkey.substring(0, 8) + "..."); + data = { + pubkey: pubkey, + uid: uid + }; + } + else { + console.debug("[wot] Loading identity from uid " + uid); + data = { + uid: uid + }; + } + if (options.blockUid) { + data.blockUid = options.blockUid; + } - var now = Date.now(); - var parameters; - var medianTime; + var now = Date.now(); + var parameters; + var medianTime; - return $q.all([ + return $q.all([ - // Get parameters - csCurrency.parameters() - .then(function(res) { - parameters = res; + // Get parameters + csCurrency.parameters() + .then(function(res) { + parameters = res; + }), + + // Get current time + csCurrency.blockchain.current(true) + .then(function(current) { + medianTime = current.medianTime; + }) + .catch(function(err){ + // Special case for currency init (root block not exists): use now + if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { + medianTime = moment.utc().unix(); + } + else { + throw err; + } + }), + + // Get requirements + loadRequirements(data, options.cache !== false), + + // Get identity using lookup + loadIdentityByLookup(pubkey, uid) + + ]) + .then(function(res) { + var dataByLookup = res[3]; + + // If no requirements found: copy from lookup data + if (!data.requirements.uid) { + console.debug("[wot] No requirements found: using data from lookup"); + angular.merge(data, dataByLookup); + delete data.lookup; // not need + return; + } + + var idtyFullKey = data.requirements.uid + '-' + data.requirements.meta.timestamp; + + return $q.all([ + // Get received certifications + loadCertifications(BMA.wot.certifiersOf, data.pubkey, dataByLookup.lookup ? dataByLookup.lookup.certifications[idtyFullKey] : null, parameters, medianTime, true /*certifiersOf*/) + .then(function (res) { + data.received_cert = res.valid; + data.received_cert_pending = res.pending; + data.received_cert_error = res.error; }), - // Get current time - csCurrency.blockchain.current(true) - .then(function(current) { - medianTime = current.medianTime; + // Get given certifications + loadCertifications(BMA.wot.certifiedBy, data.pubkey, dataByLookup.lookup ? dataByLookup.lookup.givenCertifications : null, parameters, medianTime, false/*certifiersOf*/) + .then(function (res) { + data.given_cert = res.valid; + data.given_cert_pending = res.pending; + data.given_cert_error = res.error; }) - .catch(function(err){ - // Special case for currency init (root block not exists): use now - if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { - medianTime = moment.utc().unix(); - } - else { - throw err; - } - }), + ]); + }) + .then(function() { - // Get requirements - loadRequirements(data, options.cache !== false), + // Add compute some additional requirements (that required all data like certifications) + data.requirements.pendingCertificationCount = data.received_cert_pending ? data.received_cert_pending.length : data.requirements.pendingCertificationCount; + // Use /wot/lookup.revoked when requirements not filled + data.requirements.revoked = angular.isDefined(data.requirements.revoked) ? data.requirements.revoked : data.revoked; - // Get identity using lookup - loadIdentityByLookup(pubkey, uid) + // Add account events + addEvents(data); - ]) - .then(function(res) { - var dataByLookup = res[3]; - - // If no requirements found: copy from lookup data - if (!data.requirements.uid) { - console.debug("[wot] No requirements found: using data from lookup"); - angular.merge(data, dataByLookup); - delete data.lookup; // not need - return; - } + // API extension + return api.data.raisePromise.load(data) + .catch(function(err) { + console.debug('Error while loading identity data, on extension point.'); + console.error(err); + }); + }) + .then(function() { + if (!data.pubkey) return undefined; // not found + identityCache.put(data.pubkey, data); // add to cache + console.debug('[wot] Identity '+ data.pubkey.substring(0, 8) +' loaded in '+ (Date.now()-now) +'ms'); + return data; + }); + }, - var idtyFullKey = data.requirements.uid + '-' + data.requirements.meta.timestamp; - - return $q.all([ - // Get received certifications - loadCertifications(BMA.wot.certifiersOf, data.pubkey, dataByLookup.lookup ? dataByLookup.lookup.certifications[idtyFullKey] : null, parameters, medianTime, true /*certifiersOf*/) - .then(function (res) { - data.received_cert = res.valid; - data.received_cert_pending = res.pending; - data.received_cert_error = res.error; - }), - - // Get given certifications - loadCertifications(BMA.wot.certifiedBy, data.pubkey, dataByLookup.lookup ? dataByLookup.lookup.givenCertifications : null, parameters, medianTime, false/*certifiersOf*/) - .then(function (res) { - data.given_cert = res.valid; - data.given_cert_pending = res.pending; - data.given_cert_error = res.error; - }) - ]); - }) - .then(function() { + search = function(text, options) { + if (!text || text.trim() !== text) { + return $q.when(undefined); + } - // Add compute some additional requirements (that required all data like certifications) - data.requirements.pendingCertificationCount = data.received_cert_pending ? data.received_cert_pending.length : data.requirements.pendingCertificationCount; - // Use /wot/lookup.revoked when requirements not filled - data.requirements.revoked = angular.isDefined(data.requirements.revoked) ? data.requirements.revoked : data.revoked; + // Remove first special characters (to avoid request error) + var safeText = text.replace(/(^|\s)#\w+/g, ''); // remove tags + safeText = safeText.replace(/[^a-zA-Z0-9_-\s]+/g, ''); + safeText = safeText.replace(/\s+/g, ' ').trim(); - // Add account events - addEvents(data); + options = options || {}; + options.addUniqueId = angular.isDefined(options.addUniqueId) ? options.addUniqueId : true; + options.allowExtension = angular.isDefined(options.allowExtension) ? options.allowExtension : true; + options.excludeRevoked = angular.isDefined(options.excludeRevoked) ? options.excludeRevoked : false; - // API extension - return api.data.raisePromise.load(data) - .catch(function(err) { - console.debug('Error while loading identity data, on extension point.'); - console.error(err); - }); + var promise; + if (!safeText) { + promise = $q.when([]); + } + else { + promise = $q.all( + safeText.split(' ').reduce(function(res, text) { + console.debug('[wot] Will search on: \'' + text + '\''); + return res.concat(BMA.wot.lookup({ search: text })); + }, []) + ).then(function(res){ + return res.reduce(function(idties, res) { + return idties.concat(res.results.reduce(function(idties, res) { + return idties.concat(res.uids.reduce(function(uids, idty) { + var blocUid = idty.meta.timestamp.split('-', 2); + var revoked = !idty.revoked && idty.revocation_sig; + if (!options.excludeRevoked || !revoked) { + return uids.concat({ + uid: idty.uid, + pubkey: res.pubkey, + number: blocUid[0], + hash: blocUid[1], + revoked: revoked + }); + } + return uids; + }, [])); + }, [])); + }, []); }) - .then(function() { - if (!data.pubkey) return undefined; // not found - identityCache.put(data.pubkey, data); // add to cache - console.debug('[wot] Identity '+ data.pubkey.substring(0, 8) +' loaded in '+ (Date.now()-now) +'ms'); - return data; + .catch(function(err) { + if (err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) { + return []; + } + else { + throw err; + } }); - }, - - search = function(text, options) { - if (!text || text.trim() !== text) { - return $q.when(undefined); - } - - // Remove first special characters (to avoid request error) - var safeText = text.replace(/(^|\s)#\w+/g, ''); // remove tags - safeText = safeText.replace(/[^a-zA-Z0-9_-\s]+/g, ''); - safeText = safeText.replace(/\s+/g, ' ').trim(); + } - options = options || {}; - options.addUniqueId = angular.isDefined(options.addUniqueId) ? options.addUniqueId : true; - options.allowExtension = angular.isDefined(options.allowExtension) ? options.allowExtension : true; - options.excludeRevoked = angular.isDefined(options.excludeRevoked) ? options.excludeRevoked : false; + return promise + .then(function(idties) { + if (!options.allowExtension) { + // Add unique id (if enable) + return options.addUniqueId ? _addUniqueIds(idties) : idties; + } + var lookupResultCount = idties.length; + // call extension point + return api.data.raisePromise.search(text, idties, 'pubkey') + .then(function() { - var promise; - if (!safeText) { - promise = $q.when([]); - } - else { - promise = $q.all( - safeText.split(' ').reduce(function(res, text) { - console.debug('[wot] Will search on: \'' + text + '\''); - return res.concat(BMA.wot.lookup({ search: text })); - }, []) - ).then(function(res){ - return res.reduce(function(idties, res) { - return idties.concat(res.results.reduce(function(idties, res) { - return idties.concat(res.uids.reduce(function(uids, idty) { - var blocUid = idty.meta.timestamp.split('-', 2); - var revoked = !idty.revoked && idty.revocation_sig; - if (!options.excludeRevoked || !revoked) { - return uids.concat({ - uid: idty.uid, - pubkey: res.pubkey, - number: blocUid[0], - hash: blocUid[1], - revoked: revoked + // Make sure to add uid to new results - fix #488 + if (idties.length > lookupResultCount) { + var idtiesWithoutUid = _.filter(idties, function(idty) { + return !idty.uid && idty.pubkey; + }); + if (idtiesWithoutUid.length) { + return BMA.wot.member.uids() + .then(function(uids) { + _.forEach(idties, function(idty) { + if (!idty.uid && idty.pubkey) { + idty.uid = uids[idty.pubkey]; + } }); - } - return uids; - }, [])); - }, [])); - }, []); - }) - .catch(function(err) { - if (err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) { - return []; - } - else { - throw err; + }); + } } - }); - } - - return promise - .then(function(idties) { - if (!options.allowExtension) { + }) + .then(function() { // Add unique id (if enable) return options.addUniqueId ? _addUniqueIds(idties) : idties; - } - var lookupResultCount = idties.length; - // call extension point - return api.data.raisePromise.search(text, idties, 'pubkey') - .then(function() { - - // Make sure to add uid to new results - fix #488 - if (idties.length > lookupResultCount) { - var idtiesWithoutUid = _.filter(idties, function(idty) { - return !idty.uid && idty.pubkey; + }); + }); + }, + + getNewcomers = function(offset, size) { + offset = offset || 0; + size = size || 20; + var total; + return $q.all([ + csCurrency.blockchain.current(true) + .then(function(block) { + total = block.membersCount || 0; + }), + BMA.blockchain.stats.newcomers() + ]) + .then(function(res) { + res = res[1]; + if (!res || !res.result || !res.result.blocks || !res.result.blocks.length) return null; // no result + var blocks = _.sortBy(res.result.blocks, function (n) { + return -n; + }); + return getNewcomersRecursive(blocks, 0, 5, offset+size); + }) + .then(function(idties){ + if (!idties || !idties.length) { + return null; + } + idties = _sortAndSliceIdentities(idties, offset, size); + + // Extension point + return extendAll(idties, 'pubkey', true/*skipAddUid*/); + }) + .then(function(idties) { + return { + hits: idties, + total: total + }; + }); + }, + + + getNewcomersRecursive = function(blocks, offset, size, maxResultSize) { + return $q(function(resolve, reject) { + var result = []; + var jobs = []; + _.each(blocks.slice(offset, offset+size), function(number) { + jobs.push( + BMA.blockchain.block({block: number}) + .then(function(block){ + if (!block || !block.joiners) return; + _.each(block.joiners, function(joiner){ + var parts = joiner.split(':'); + var idtyKey = parts[parts.length-1]/*uid*/ + '-' + parts[0]/*pubkey*/; + result.push({ + id: idtyKey, + uid: parts[parts.length-1], + pubkey:parts[0], + memberDate: block.medianTime, + block: block.number }); - if (idtiesWithoutUid.length) { - return BMA.wot.member.uids() - .then(function(uids) { - _.forEach(idties, function(idty) { - if (!idty.uid && idty.pubkey) { - idty.uid = uids[idty.pubkey]; - } - }); - }); - } - } + }); }) - .then(function() { - // Add unique id (if enable) - return options.addUniqueId ? _addUniqueIds(idties) : idties; - }); - }); - }, - - getNewcomers = function(offset, size) { - offset = offset || 0; - size = size || 20; - var total; - return $q.all([ - csCurrency.blockchain.current(true) - .then(function(block) { - total = block.membersCount || 0; - }), - BMA.blockchain.stats.newcomers() - ]) - .then(function(res) { - res = res[1]; - if (!res || !res.result || !res.result.blocks || !res.result.blocks.length) return null; // no result - var blocks = _.sortBy(res.result.blocks, function (n) { - return -n; - }); - return getNewcomersRecursive(blocks, 0, 5, offset+size); - }) - .then(function(idties){ - if (!idties || !idties.length) { - return null; - } - idties = _sortAndSliceIdentities(idties, offset, size); + ); + }); - // Extension point - return extendAll(idties, 'pubkey', true/*skipAddUid*/); - }) - .then(function(idties) { - return { - hits: idties, - total: total - }; - }); - }, - - - getNewcomersRecursive = function(blocks, offset, size, maxResultSize) { - return $q(function(resolve, reject) { - var result = []; - var jobs = []; - _.each(blocks.slice(offset, offset+size), function(number) { - jobs.push( - BMA.blockchain.block({block: number}) - .then(function(block){ - if (!block || !block.joiners) return; - _.each(block.joiners, function(joiner){ - var parts = joiner.split(':'); - var idtyKey = parts[parts.length-1]/*uid*/ + '-' + parts[0]/*pubkey*/; - result.push({ - id: idtyKey, - uid: parts[parts.length-1], - pubkey:parts[0], - memberDate: block.medianTime, - block: block.number - }); + $q.all(jobs) + .then(function() { + if (result.length < maxResultSize && offset < blocks.length - 1) { + $timeout(function() { + getNewcomersRecursive(blocks, offset+size, size, maxResultSize - result.length) + .then(function(res) { + resolve(result.concat(res)); + }) + .catch(function(err) { + reject(err); }); - }) - ); + }, 1000); + } + else { + resolve(result); + } + }) + .catch(function(err){ + if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) { + resolve(result); + } + else { + reject(err); + } }); - - $q.all(jobs) - .then(function() { - if (result.length < maxResultSize && offset < blocks.length - 1) { - $timeout(function() { - getNewcomersRecursive(blocks, offset+size, size, maxResultSize - result.length) - .then(function(res) { - resolve(result.concat(res)); - }) - .catch(function(err) { - reject(err); - }); - }, 1000); - } - else { - resolve(result); + }); + }, + + getPending = function(offset, size) { + offset = offset || 0; + size = size || 20; + var now = Date.now(); + return $q.all([ + BMA.wot.member.uids(), + BMA.wot.member.pending() + .then(function(res) { + return (res.memberships && res.memberships.length) ? res.memberships : undefined; + }) + ]) + .then(function(res) { + var uids = res[0]; + var memberships = res[1]; + if (!memberships) return; + + var idtiesByBlock = {}; + var idtiesByPubkey = {}; + _.forEach(memberships, function(ms){ + if (ms.membership == 'IN' && !uids[ms.pubkey]) { + var idty = { + uid: ms.uid, + pubkey: ms.pubkey, + block: ms.blockNumber, + blockHash: ms.blockHash + }; + var otherIdtySamePubkey = idtiesByPubkey[ms.pubkey]; + if (otherIdtySamePubkey && idty.block > otherIdtySamePubkey.block) { + return; // skip } - }) - .catch(function(err){ - if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) { - resolve(result); + idtiesByPubkey[idty.pubkey] = idty; + if (!idtiesByBlock[idty.block]) { + idtiesByBlock[idty.block] = [idty]; } else { - reject(err); + idtiesByBlock[idty.block].push(idty); } - }); - }); - }, - - getPending = function(offset, size) { - offset = offset || 0; - size = size || 20; - var now = Date.now(); - return $q.all([ - BMA.wot.member.uids(), - BMA.wot.member.pending() - .then(function(res) { - return (res.memberships && res.memberships.length) ? res.memberships : undefined; - }) - ]) - .then(function(res) { - var uids = res[0]; - var memberships = res[1]; - if (!memberships) return; - - var idtiesByBlock = {}; - var idtiesByPubkey = {}; - _.forEach(memberships, function(ms){ - if (ms.membership == 'IN' && !uids[ms.pubkey]) { - var idty = { - uid: ms.uid, - pubkey: ms.pubkey, - block: ms.blockNumber, - blockHash: ms.blockHash - }; - var otherIdtySamePubkey = idtiesByPubkey[ms.pubkey]; - if (otherIdtySamePubkey && idty.block > otherIdtySamePubkey.block) { - return; // skip - } - idtiesByPubkey[idty.pubkey] = idty; - if (!idtiesByBlock[idty.block]) { - idtiesByBlock[idty.block] = [idty]; - } - else { - idtiesByBlock[idty.block].push(idty); - } - // Remove previous idty from map - if (otherIdtySamePubkey) { - idtiesByBlock[otherIdtySamePubkey.block] = idtiesByBlock[otherIdtySamePubkey.block].reduce(function(res, aidty){ - if (aidty.pubkey == otherIdtySamePubkey.pubkey) return res; // if match idty to remove, to NOT add - return (res||[]).concat(aidty); - }, null); - if (idtiesByBlock[otherIdtySamePubkey.block] === null) { - delete idtiesByBlock[otherIdtySamePubkey.block]; - } + // Remove previous idty from map + if (otherIdtySamePubkey) { + idtiesByBlock[otherIdtySamePubkey.block] = idtiesByBlock[otherIdtySamePubkey.block].reduce(function(res, aidty){ + if (aidty.pubkey == otherIdtySamePubkey.pubkey) return res; // if match idty to remove, to NOT add + return (res||[]).concat(aidty); + }, null); + if (idtiesByBlock[otherIdtySamePubkey.block] === null) { + delete idtiesByBlock[otherIdtySamePubkey.block]; } } - }); - - var idties = _.values(idtiesByPubkey); - var total = idties.length; // get total BEFORE slice - - idties = _sortAndSliceIdentities(idties, offset, size); - var blocks = idties.reduce(function(res, aidty) { - return res.concat(aidty.block); - }, []); + } + }); - return $q.all([ - // Get time from blocks - BMA.blockchain.blocks(_.uniq(blocks)) - .then(function(blocks) { - - _.forEach(blocks, function(block){ - _.forEach(idtiesByBlock[block.number], function(idty) { - idty.sigDate = block.medianTime; - if (block.number !== 0 && idty.blockHash !== block.hash) { - addEvent(idty, {type:'error', message: 'ERROR.WOT_PENDING_INVALID_BLOCK_HASH'}); - console.debug("Invalid membership for uid={0}: block hash changed".format(idty.uid)); - } - }); + var idties = _.values(idtiesByPubkey); + var total = idties.length; // get total BEFORE slice + + idties = _sortAndSliceIdentities(idties, offset, size); + var blocks = idties.reduce(function(res, aidty) { + return res.concat(aidty.block); + }, []); + + return $q.all([ + // Get time from blocks + BMA.blockchain.blocks(_.uniq(blocks)) + .then(function(blocks) { + + _.forEach(blocks, function(block){ + _.forEach(idtiesByBlock[block.number], function(idty) { + idty.sigDate = block.medianTime; + if (block.number !== 0 && idty.blockHash !== block.hash) { + addEvent(idty, {type:'error', message: 'ERROR.WOT_PENDING_INVALID_BLOCK_HASH'}); + console.debug("Invalid membership for uid={0}: block hash changed".format(idty.uid)); + } }); - }), - - // Extension point - extendAll(idties, 'pubkey', true/*skipAddUid*/) - ]) - .then(function() { - console.debug("[ES] [wot] Loaded {0}/{1} pending identities in {2} ms".format(idties && idties.length || 0, total, Date.now() - now)); - return { - hits: idties, - total: total - }; - }); - }); - }, + }); + }), - getAll = function() { - var letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','u','v','w','x','y','z']; - return getAllRecursive(letters, 0, BMA.constants.LIMIT_REQUEST_COUNT) - .then(function(idties) { - return extendAll(idties, 'pubkey', true/*skipAddUid*/); - }) - .then(_addUniqueIds) + // Extension point + extendAll(idties, 'pubkey', true/*skipAddUid*/) + ]) .then(function() { + console.debug("[ES] [wot] Loaded {0}/{1} pending identities in {2} ms".format(idties && idties.length || 0, total, Date.now() - now)); return { hits: idties, - total: idties.length + total: total }; }); - }, - - getAllRecursive = function(letters, offset, size) { - return $q(function(resolve, reject) { - var result = []; - var pubkeys = {}; - var jobs = []; - _.each(letters.slice(offset, offset+size), function(letter) { - jobs.push( - search(letter, { - addUniqueId: false, // will be done in parent method - allowExtension: false // extension point will be called in parent method - }) - .then(function(idties){ - if (!idties || !idties.length) return; - result = idties.reduce(function(res, idty) { - if (!pubkeys[idty.pubkey]) { - pubkeys[idty.pubkey] = true; - return res.concat(idty); - } - return res; - }, result); - }) - ); - }); + }); + }, + + getAll = function() { + var letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','u','v','w','x','y','z']; + return getAllRecursive(letters, 0, BMA.constants.LIMIT_REQUEST_COUNT) + .then(function(idties) { + return extendAll(idties, 'pubkey', true/*skipAddUid*/); + }) + .then(_addUniqueIds) + .then(function() { + return { + hits: idties, + total: idties.length + }; + }); + }, - $q.all(jobs) - .then(function() { - if (offset < letters.length - 1) { - $timeout(function() { - getAllRecursive(letters, offset+size, size) - .then(function(idties) { - if (!idties || !idties.length) { - resolve(result); - return; - } - resolve(idties.reduce(function(res, idty) { - if (!pubkeys[idty.pubkey]) { - pubkeys[idty.pubkey] = true; - return res.concat(idty); - } - return res; - }, result)); - }) - .catch(function(err) { - reject(err); - }); - }, BMA.constants.LIMIT_REQUEST_DELAY); - } - else { - resolve(result); - } + getAllRecursive = function(letters, offset, size) { + return $q(function(resolve, reject) { + var result = []; + var pubkeys = {}; + var jobs = []; + _.each(letters.slice(offset, offset+size), function(letter) { + jobs.push( + search(letter, { + addUniqueId: false, // will be done in parent method + allowExtension: false // extension point will be called in parent method }) - .catch(function(err){ - if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) { - resolve(result); - } - else { - reject(err); - } - }); + .then(function(idties){ + if (!idties || !idties.length) return; + result = idties.reduce(function(res, idty) { + if (!pubkeys[idty.pubkey]) { + pubkeys[idty.pubkey] = true; + return res.concat(idty); + } + return res; + }, result); + }) + ); }); - }, - extend = function(idty, pubkeyAttributeName, skipAddUid) { - return extendAll([idty], pubkeyAttributeName, skipAddUid) - .then(function(res) { - return res[0]; + $q.all(jobs) + .then(function() { + if (offset < letters.length - 1) { + $timeout(function() { + getAllRecursive(letters, offset+size, size) + .then(function(idties) { + if (!idties || !idties.length) { + resolve(result); + return; + } + resolve(idties.reduce(function(res, idty) { + if (!pubkeys[idty.pubkey]) { + pubkeys[idty.pubkey] = true; + return res.concat(idty); + } + return res; + }, result)); + }) + .catch(function(err) { + reject(err); + }); + }, BMA.constants.LIMIT_REQUEST_DELAY); + } + else { + resolve(result); + } + }) + .catch(function(err){ + if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) { + resolve(result); + } + else { + reject(err); + } }); - }, - - extendAll = function(idties, pubkeyAttributeName, skipAddUid) { - - pubkeyAttributeName = pubkeyAttributeName || 'pubkey'; - - var jobs = []; - if (!skipAddUid) jobs.push(BMA.wot.member.uids()); - - jobs.push(api.data.raisePromise.search(null, idties, pubkeyAttributeName) - .catch(function(err) { - console.debug('Error while search identities, on extension point.'); - console.error(err); - })); + }); + }, - return $q.all(jobs) + extend = function(idty, pubkeyAttributeName, skipAddUid) { + return extendAll([idty], pubkeyAttributeName, skipAddUid) .then(function(res) { - if (!skipAddUid) { - var uidsByPubkey = res[0]; - // Set uid (on every data) - _.forEach(idties, function(data) { - if (!data.uid && data[pubkeyAttributeName]) { - data.uid = uidsByPubkey[data[pubkeyAttributeName]]; - // Remove name if redundant with uid - if (data.uid && data.uid == data.name) { - delete data.name; - } + return res[0]; + }); + }, + + extendAll = function(idties, pubkeyAttributeName, skipAddUid) { + + pubkeyAttributeName = pubkeyAttributeName || 'pubkey'; + + var jobs = []; + if (!skipAddUid) jobs.push(BMA.wot.member.uids()); + + jobs.push(api.data.raisePromise.search(null, idties, pubkeyAttributeName) + .catch(function(err) { + console.debug('Error while search identities, on extension point.'); + console.error(err); + })); + + return $q.all(jobs) + .then(function(res) { + if (!skipAddUid) { + var uidsByPubkey = res[0]; + // Set uid (on every data) + _.forEach(idties, function(data) { + if (!data.uid && data[pubkeyAttributeName]) { + data.uid = uidsByPubkey[data[pubkeyAttributeName]]; + // Remove name if redundant with uid + if (data.uid && data.uid == data.name) { + delete data.name; } - }); - } + } + }); + } - return idties; - }); - }, - - addEvent = function(data, event) { - event = event || {}; - event.type = event.type || 'info'; - event.message = event.message || ''; - event.messageParams = event.messageParams || {}; - data.events = data.events || []; - data.events.push(event); - }, - - cleanCache = function() { - console.debug("[wot] Cleaning cache..."); - csCache.clear(cachePrefix); - } - ; - - // Register extension points - api.registerEvent('data', 'load'); - api.registerEvent('data', 'search'); - - // Listen if node changed - BMA.api.node.on.stop($rootScope, cleanCache, this); - - return { - id: id, - load: loadData, - loadRequirements: loadRequirements, - search: search, - newcomers: getNewcomers, - pending: getPending, - all: getAll, - extend: extend, - extendAll: extendAll, - // api extension - api: api - }; - } - - var service = csWot('default', BMA); - - service.instance = csWot; - return service; + return idties; + }); + }, + + addEvent = function(data, event) { + event = event || {}; + event.type = event.type || 'info'; + event.message = event.message || ''; + event.messageParams = event.messageParams || {}; + data.events = data.events || []; + data.events.push(event); + }, + + cleanCache = function() { + console.debug("[wot] Cleaning cache..."); + csCache.clear(cachePrefix); + } + ; + + // Register extension points + api.registerEvent('data', 'load'); + api.registerEvent('data', 'search'); + + // Listen if node changed + BMA.api.node.on.stop($rootScope, cleanCache, this); + + return { + load: loadData, + loadRequirements: loadRequirements, + search: search, + newcomers: getNewcomers, + pending: getPending, + all: getAll, + extend: extend, + extendAll: extendAll, + // api extension + api: api + }; }); diff --git a/www/plugins/es/js/services/network-services.js b/www/plugins/es/js/services/network-services.js index de4da6d2d06158e05b0b00a6894f34e1ab0232c6..6b653b1b6fb4fc128ddf807b470242edad086f92 100644 --- a/www/plugins/es/js/services/network-services.js +++ b/www/plugins/es/js/services/network-services.js @@ -220,13 +220,13 @@ angular.module('cesium.es.network.services', ['ngApi', 'cesium.es.http.services' hasUpdates = true; } else { - console.debug("[network] {0} endpoint [{1}] unchanged".format( + console.debug("[ES] [network] {0} endpoint [{1}] unchanged".format( refreshedPeer.ep && refreshedPeer.ep.api || '', refreshedPeer.server)); } } else if (refreshedPeer && (refreshedPeer.online === data.filter.online || data.filter.online === 'all')) { - console.debug("[network] {0} endpoint [{1}] is {2}".format( + console.debug("[ES] [network] {0} endpoint [{1}] is {2}".format( refreshedPeer.ep && refreshedPeer.ep.api || '', refreshedPeer.server, refreshedPeer.online ? 'UP' : 'DOWN' @@ -328,10 +328,10 @@ angular.module('cesium.es.network.services', ['ngApi', 'cesium.es.http.services' } if (!peer.secondTry) { var ep = peer.ep || peer.getEP(); - if (ep.dns && peer.server.indexOf(ep.dns) == -1) { + if (ep.dns && peer.server.indexOf(ep.dns) === -1) { // try again, using DNS instead of IPv4 / IPV6 peer.secondTry = true; - peer.api = esHttp.lightInstance(ep.dns, ep.port, ep.useSsl); + peer.api = esHttp.lightInstance(ep.dns, peer.getPort(), peer.isSsl(), data.timeout); return refreshPeer(peer); // recursive call } } @@ -574,14 +574,14 @@ angular.module('cesium.es.network.services', ['ngApi', 'cesium.es.http.services' data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; data.expertMode = angular.isDefined(options.expertMode) ? options.expertMode : data.expertMode; data.timeout = angular.isDefined(options.timeout) ? options.timeout : csConfig.timeout; - console.info('[network] Starting network from [{0}]'.format(data.pod.server)); + console.info('[ES] [network] Starting network from [{0}]'.format(data.pod.server)); var now = Date.now(); addListeners(); return loadPeers() .then(function(peers){ - console.debug('[network] Started in '+(Date.now() - now)+'ms'); + console.debug('[ES] [network] Started in '+(Date.now() - now)+'ms'); return peers; }); }); @@ -589,7 +589,7 @@ angular.module('cesium.es.network.services', ['ngApi', 'cesium.es.http.services' close = function() { if (data.pod) { - console.info('[network-service] Stopping...'); + console.info('[ES] [network-service] Stopping...'); removeListeners(); resetData(); }