angular.module('cesium.settings.services', ['ngApi', 'cesium.config']) .factory('csSettings', ['$rootScope', '$q', '$window', 'Api', 'localStorage', '$translate', 'csConfig', function($rootScope, $q, $window, Api, localStorage, $translate, csConfig) { 'ngInject'; // Define app locales var locales = [ {id:'en', label:'English', country: 'us'}, {id:'en-GB', label:'English (UK)', country: 'gb'}, {id:'eo-EO', label:'Esperanto'}, {id:'fr-FR', label:'Français', country: 'fr'} ]; var fallbackLocale = csConfig.fallbackLanguage ? fixLocale(csConfig.fallbackLanguage) : 'en'; // Convert browser locale to app locale (fix #140) function fixLocale (locale) { if (!locale) return fallbackLocale; // exists in app locales: use it if (_.findWhere(locales, {id: locale})) return locale; // not exists: reiterate with the root (e.g. 'fr-XX' -> 'fr') var localeParts = locale.split('-'); if (localeParts.length > 1) { return fixLocale(localeParts[0]); } // If another locale exists with the same root: use it var similarLocale = _.find(locales, function(l) { return String.prototype.startsWith.call(l.id, locale); }); if (similarLocale) return similarLocale.id; return fallbackLocale; } // Convert browser locale to app locale (fix #140) function fixLocaleWithLog (locale) { var fixedLocale = fixLocale(locale); if (locale !== fixedLocale) { console.debug('[settings] Fix locale [{0}] -> [{1}]'.format(locale, fixedLocale)); } return fixedLocale; } var constants = { STORAGE_KEY: 'GCHANGE_SETTINGS', KEEP_AUTH_IDLE_SESSION: 9999 }, // Settings that user cannot change himself (only config can override this values) fixedSettings = { timeout : 4000, cacheTimeMs: 60000, /*1 min*/ timeWarningExpireMembership: 2592000 * 2 /*=2 mois*/, timeWarningExpire: 2592000 * 3 /*=3 mois*/, minVersion: '1.7.0', // min duniter version newIssueUrl: "https://github.com/duniter-gchange/gchange-client/issues/new?labels=bug", //userForumUrl: "https://forum.gchange.fr", userForumUrl: "https://forum.monnaie-libre.fr", latestReleaseUrl: "https://api.github.com/repos/duniter-gchange/gchange-client/releases/latest", httpsMode: false }, defaultSettings = angular.merge({ useRelative: false, useLocalStorage: !!$window.localStorage, // override to false if no device useLocalStorageEncryption: false, walletHistoryTimeSecond: 30 * 24 * 60 * 60 /*30 days*/, walletHistorySliceSecond: 5 * 24 * 60 * 60 /*download using 5 days slice*/, walletHistoryAutoRefresh: true, // override to false if device rememberMe: true, keepAuthIdle: 10 * 60, showUDHistory: true, showLoginSalt: false, expertMode: false, decimalCount: 2, uiEffects: true, blockValidityWindow: 6, helptip: { enable: false, installDocUrl: "https://github.com/duniter-gchange/gchange-client/blob/master/README.md", currency: 0, network: 0, wotLookup: 0, wot: 0, wotCerts: 0, wallet: 0, walletCerts: 0, header: 0, settings: 0 }, currency: { allRules: false, allWotRules: false }, wallet: { showPubkey: true, alertIfUnusedWallet: true, notificationReadTime: 0 }, locale: { id: fixLocaleWithLog(csConfig.defaultLanguage || $translate.use()) // use config locale if set, or browser default } }, fixedSettings, csConfig), data = {}, previousData, started = false, startPromise, api = new Api(this, "csSettings"); var reset = function() { _.keys(data).forEach(function(key){ delete data[key]; }); applyData(defaultSettings); return api.data.raisePromise.reset(data) .then(store); }, getByPath = function(path, defaultValue) { var obj = data; _.each(path.split('.'), function(key) { obj = obj[key]; if (angular.isUndefined(obj)) { obj = defaultValue; return; // stop } }); return obj; }, emitChangedEvent = function() { var hasChanged = angular.isUndefined(previousData) || !angular.equals(previousData, data); if (hasChanged) { previousData = angular.copy(data); return api.data.raise.changed(data); } }, store = function() { if (!started) { console.debug('[setting] Waiting start finished...'); return (startPromise || start()).then(store); } var promise; if (data.useLocalStorage) { // When node is temporary (fallback node): keep previous node address - issue #476 if (data.node.temporary === true) { promise = localStorage.getObject(constants.STORAGE_KEY) .then(function(previousSettings) { var savedData = angular.copy(data); savedData.node = previousSettings && previousSettings.node || {}; delete savedData.temporary; // never store temporary flag return localStorage.setObject(constants.STORAGE_KEY, savedData); }); } else { promise = localStorage.setObject(constants.STORAGE_KEY, data); } } else { promise = localStorage.setObject(constants.STORAGE_KEY, null); } return promise .then(function() { if (data.useLocalStorage) { console.debug('[setting] Saved locally'); } // Emit event on store return api.data.raisePromise.store(data); }) // Emit event on store .then(emitChangedEvent); }, /** * Apply new settings (can be partial) * @param newData */ applyData = function(newData) { if (!newData) return; // skip empty var localeChanged = false; if (newData.locale && newData.locale.id) { // Fix previously stored locale (could use bad format) var localeId = fixLocale(newData.locale.id); newData.locale = _.findWhere(locales, {id: localeId}); localeChanged = !data.locale || newData.locale.id !== data.locale.id || newData.locale.id !== $translate.use(); } // Force some fixed settings, before merging _.keys(fixedSettings).forEach(function(key) { newData[key] = defaultSettings[key]; // This will apply fixed value (override by config.js file) }); // Apply new settings angular.merge(data, newData); // Delete temporary properties, if false if (newData && newData.node && !newData.node.temporary || !data.node.temporary) delete data.node.temporary; // Gchange workaround: Replace OLD default duniter node, by gchange pod if ((data.plugins && data.plugins.es.host && data.plugins.es.port) && (!data.node || (data.node.host !== data.plugins.es.host))) { var oldBmaNode = data.node.host; var newBmaNode = data.plugins.es.host; console.warn("[settings] Replacing duniter node {{0}} with gchange pod {{1}}".format(oldBmaNode, newBmaNode)); data.node = { host: newBmaNode, port: data.plugins.es.port, useSsl: data.plugins.es.useSsl }; } // Apply the new locale (only if need) // will produce an event cached by onLocaleChange(); if (localeChanged) { $translate.use(data.locale.id); } }, restore = function() { var now = Date.now(); return localStorage.getObject(constants.STORAGE_KEY) .then(function(storedData) { // No settings stored if (!storedData) { console.debug("[settings] No settings in local storage. Using defaults."); applyData(defaultSettings); emitChangedEvent(); return; } // Apply stored data applyData(storedData); console.debug('[settings] Loaded from local storage in '+(Date.now()-now)+'ms'); emitChangedEvent(); }); }, // Detect locale successful changes, then apply to vendor libs onLocaleChange = function() { var locale = $translate.use(); console.debug('[settings] Locale ['+locale+']'); // config moment lib try { moment.locale(locale.toLowerCase()); } catch(err) { try { moment.locale(locale.substr(0,2)); } catch(err) { moment.locale('en-gb'); console.warn('[settings] Unknown local for moment lib. Using default [en]'); } } // config numeral lib try { numeral.language(locale.toLowerCase()); } catch(err) { try { numeral.language(locale.substring(0, 2)); } catch(err) { numeral.language('en-gb'); console.warn('[settings] Unknown local for numeral lib. Using default [en]'); } } // Emit event api.locale.raise.changed(locale); }, ready = function() { if (started) return $q.when(); return startPromise || start(); }, start = function() { console.debug('[settings] Starting...'); startPromise = localStorage.ready() // Restore .then(restore) // Emit ready event .then(function() { console.debug('[settings] Started'); started = true; startPromise = null; // Emit event (used by plugins) api.data.raise.ready(data); }); return startPromise; }; $rootScope.$on('$translateChangeSuccess', onLocaleChange); api.registerEvent('data', 'reset'); api.registerEvent('data', 'changed'); api.registerEvent('data', 'store'); api.registerEvent('data', 'ready'); api.registerEvent('locale', 'changed'); // Apply default settings. This is required on some browser (web or mobile - see #361) applyData(defaultSettings); // Default action //start(); return { ready: ready, start: start, data: data, apply: applyData, getByPath: getByPath, reset: reset, store: store, restore: restore, defaultSettings: defaultSettings, // api extension api: api, locales: locales, constants: constants }; }]); //var Base58, Base64, scrypt_module_factory = null, nacl_factory = null; angular.module('cesium.crypto.services', ['cesium.utils.services']) .factory('CryptoUtils', ['$q', '$timeout', 'ionicReady', function($q, $timeout, ionicReady) { 'ngInject'; /** * CryptoAbstract, abstract class with useful methods * @type {number} */ function CryptoAbstractService() { this.loaded = false; var that = this; this.copy = function(source) { _.forEach(_.keys(source), function(key) { that[key] = source[key]; }); }; this.isLoaded = function() { return this.loaded; }; this.util = this.util || {}; /** * Converts an array buffer to a string * * @private * @param {ArrayBuffer} buf The buffer to convert * @param {Function} callback The function to call when conversion is complete */ this.util.array_to_string = function(buf, callback) { var bb = new Blob([new Uint8Array(buf)]); var f = new FileReader(); f.onload = function(e) { callback(e.target.result); }; f.readAsText(bb); }; } CryptoAbstractService.prototype.constants = { crypto_sign_BYTES: 64, crypto_secretbox_NONCEBYTES: 24, crypto_box_MACBYTES: 16, SEED_LENGTH: 32, // Length of the key SCRYPT_PARAMS:{ SIMPLE: { N: 2048, r: 8, p: 1, memory: -1 // default }, DEFAULT: { N: 4096, r: 16, p: 1, memory: -1 // default }, } }; CryptoAbstractService.prototype.async_load_base58 = function(on_ready) { var that = this; if (Base58 !== null){return on_ready(Base58);} else {$timeout(function(){that.async_load_base58(on_ready);}, 100);} }; CryptoAbstractService.prototype.async_load_scrypt = function(on_ready, options) { var that = this; if (scrypt_module_factory !== null){scrypt_module_factory(on_ready, options);} else {$timeout(function(){that.async_load_scrypt(on_ready, options);}, 100);} }; CryptoAbstractService.prototype.async_load_nacl_js = function(on_ready, options) { var that = this; if (nacl_factory !== null) {nacl_factory.instantiate(on_ready, options);} else {$timeout(function(){that.async_load_nacl_js(on_ready, options);}, 100);} }; CryptoAbstractService.prototype.async_load_base64 = function(on_ready) { var that = this; if (Base64 !== null) {on_ready(Base64);} else {$timeout(function(){that.async_load_base64(on_ready);}, 100);} }; CryptoAbstractService.prototype.async_load_sha256 = function(on_ready) { var that = this; if (sha256 !== null){return on_ready(sha256);} else {$timeout(function(){that.async_load_sha256(on_ready);}, 100);} }; CryptoAbstractService.prototype.seed_from_signSk = function(signSk) { var seed = new Uint8Array(that.constants.SEED_LENGTH); for (var i = 0; i < seed.length; i++) seed[i] = signSk[i]; return seed; }; CryptoAbstractService.prototype.seed_from_signSk = function(signSk) { var seed = new Uint8Array(that.constants.SEED_LENGTH); for (var i = 0; i < seed.length; i++) seed[i] = signSk[i]; return seed; }; // Web crypto API - see https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API var crypto = window.crypto || window.msCrypto || window.Crypto; if (crypto && crypto.getRandomValues) { CryptoAbstractService.prototype.crypto = crypto; CryptoAbstractService.prototype.util = {}; CryptoAbstractService.prototype.util.random_nonce = function() { var nonce = new Uint8Array(crypto_secretbox_NONCEBYTES); this.crypto.getRandomValues(nonce); return $q.when(nonce); }; } function FullJSServiceFactory() { this.id = 'FullJS'; // libraries handlers this.scrypt = null; this.nacl = null; this.base58 = null; this.base64 = null; var that = this; this.util = this.util || {}; this.util.decode_utf8 = function (s) { var i, d = unescape(encodeURIComponent(s)), b = new Uint8Array(d.length); for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); return b; }; this.util.encode_utf8 = function (s) { return that.nacl.encode_utf8(s); }; this.util.encode_base58 = function (a) { // TODO : move to abstract factory return that.base58.encode(a); }; this.util.decode_base58 = function (a) { // TODO : move to abstract factory var i; var d = that.base58.decode(a); var b = new Uint8Array(d.length); for (i = 0; i < d.length; i++) b[i] = d[i]; return b; }; this.util.decode_base64 = function (a) { return that.base64.decode(a); }; this.util.encode_base64 = function (b) { return that.base64.encode(b); }; this.util.hash_sha256 = function (message) { return $q(function (resolve) { var msg = that.util.decode_utf8(message); var hash = that.nacl.to_hex(that.nacl.crypto_hash_sha256(msg)); resolve(hash.toUpperCase()); }); }; this.util.random_nonce = function () { if (that.crypto && that.crypto.getRandomValues) { var nonce = new Uint8Array(that.constants.crypto_secretbox_NONCEBYTES); that.crypto.getRandomValues(nonce); return $q.when(nonce); } else { return $q.when(that.nacl.crypto_box_random_nonce()); } }; this.util.crypto_hash_sha256 = function(msg_int8) { return that.nacl.crypto_hash_sha256(msg_int8); }; this.util.crypto_scrypt = function(password, salt, N, r, p, seedLength) { return $q(function(resolve, reject) { try { var seed = that.scrypt.crypto_scrypt( password, salt, N, r, p, seedLength); resolve(seed); } catch(err) { reject(err); } }); }; /** * Compute the box key pair, from a sign key pair */ this.box_keypair_from_sign = function (signKeyPair) { if (signKeyPair.boxSk && signKeyPair.boxPk) return $q.when(signKeyPair); return $q(function (resolve, reject) { try { // TODO: waiting for a new version of js-nacl, with missing functions expose //resolve(that.nacl.crypto_box_keypair_from_sign_sk(signKeyPair.signPk); resolve(crypto_box_keypair_from_sign_sk(signKeyPair.signSk)); } catch(err) { reject(err); } }); }; /** * Compute the box public key, from a sign public key */ this.box_pk_from_sign = function (signPk) { return $q(function(resolve, reject) { try { // TODO: waiting for a new version of js-nacl, with missing functions expose //resolve(that.nacl.crypto_box_pk_from_sign_pk(signPk)); resolve(crypto_box_pk_from_sign_pk(signPk)); } catch(err) { reject(err); } }); }; this.box_sk_from_sign = function (signSk) { return $q(function(resolve, reject) { try { // TODO: waiting for a new version of js-nacl, with missing functions expose //resolve(that.nacl.crypto_box_sk_from_sign_sk(signSk)); resolve(crypto_box_sk_from_sign_sk(signSk)); } catch(err) { reject(err); } }); }; /** * Encrypt a message, from a key pair */ this.box = function(message, nonce, recipientPk, senderSk) { return $q(function (resolve, reject) { if (!message) { resolve(message); return; } var messageBin = that.nacl.encode_utf8(message); if (typeof recipientPk === "string") { recipientPk = that.util.decode_base58(recipientPk); } try { var ciphertextBin = that.nacl.crypto_box(messageBin, nonce, recipientPk, senderSk); var ciphertext = that.util.encode_base64(ciphertextBin); resolve(ciphertext); } catch (err) { reject(err); } }); }; /** * Decrypt a message, from a key pair */ this.box_open = function (cypherText, nonce, senderPk, recipientSk) { return $q(function (resolve, reject) { if (!cypherText) { resolve(cypherText); return; } var ciphertextBin = that.util.decode_base64(cypherText); if (typeof senderPk === "string") { senderPk = that.util.decode_base58(senderPk); } try { var message = that.nacl.crypto_box_open(ciphertextBin, nonce, senderPk, recipientSk); resolve(that.nacl.decode_utf8(message)); } catch (err) { reject(err); } }); }; /** * Create key pairs (sign and box), from salt+password (Scrypt auth) */ this.scryptKeypair = function(salt, password, scryptParams) { return that.util.crypto_scrypt( that.util.encode_utf8(password), that.util.encode_utf8(salt), scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N, scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r, scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p, that.constants.SEED_LENGTH) .then(function(seed){ var signKeypair = that.nacl.crypto_sign_seed_keypair(seed); var boxKeypair = that.nacl.crypto_box_seed_keypair(seed); return { signPk: signKeypair.signPk, signSk: signKeypair.signSk, boxPk: boxKeypair.boxPk, boxSk: boxKeypair.boxSk }; }); }; /** * Create key pairs from a seed */ this.seedKeypair = function(seed) { return $q(function(resolve, reject) { var signKeypair = that.nacl.crypto_sign_seed_keypair(seed); var boxKeypair = that.nacl.crypto_box_seed_keypair(seed); resolve({ signPk: signKeypair.signPk, signSk: signKeypair.signSk, boxPk: boxKeypair.boxPk, boxSk: boxKeypair.boxSk }); }); }; /** * Get sign pk from salt+password (scrypt auth) */ this.scryptSignPk = function(salt, password, scryptParams) { return $q(function(resolve, reject) { try { var seed = that.scrypt.crypto_scrypt( that.util.encode_utf8(password), that.util.encode_utf8(salt), scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N, scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r, scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p, that.constants.SEED_LENGTH); var signKeypair = that.nacl.crypto_sign_seed_keypair(seed); resolve(signKeypair.signPk); } catch(err) { reject(err); } }); }; /** * Verify a signature of a message, for a pubkey */ this.verify = function (message, signature, pubkey) { return $q(function(resolve, reject) { var msg = that.util.decode_utf8(message); var sig = that.util.decode_base64(signature); var pub = that.util.decode_base58(pubkey); var sm = new Uint8Array(that.constants.crypto_sign_BYTES + msg.length); var i; for (i = 0; i < that.constants.crypto_sign_BYTES; i++) sm[i] = sig[i]; for (i = 0; i < msg.length; i++) sm[i+that.constants.crypto_sign_BYTES] = msg[i]; // Call to verification lib... var verified = that.nacl.crypto_sign_open(sm, pub) !== null; resolve(verified); }); }; /** * Sign a message, from a key pair */ this.sign = function(message, keypair) { return $q(function(resolve, reject) { var m = that.util.decode_utf8(message); var sk = keypair.signSk; var signedMsg = that.nacl.crypto_sign(m, sk); var sig = new Uint8Array(that.constants.crypto_sign_BYTES); for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; var signature = that.base64.encode(sig); resolve(signature); }); }; this.load = function() { var deferred = $q.defer(); var naclOptions = {}; var scryptOptions = {}; if (ionic.Platform.grade.toLowerCase()!='a') { console.info('Reduce NaCl memory to 16mb, because plateform grade is not [a] but [{0}]'.format(ionic.Platform.grade)); naclOptions.requested_total_memory = 16 * 1048576; // 16 Mo } var loadedLib = 0; var checkAllLibLoaded = function() { loadedLib++; if (loadedLib === 4) { that.loaded = true; deferred.resolve(); } }; this.async_load_nacl_js(function(lib) { that.nacl = lib; checkAllLibLoaded(); }, naclOptions); this.async_load_scrypt(function(lib) { that.scrypt = lib; that.scrypt.requested_total_memory = scryptOptions.requested_total_memory; checkAllLibLoaded(); }, scryptOptions); this.async_load_base58(function(lib) { that.base58 = lib; checkAllLibLoaded(); }); that.async_load_base64(function(lib) { that.base64 = lib; checkAllLibLoaded(); }); return deferred.promise; }; // Shortcuts this.util.hash = that.util.hash_sha256; this.box = { keypair: { fromSignKeypair: that.box_keypair_from_sign, skFromSignSk: that.box_sk_from_sign, pkFromSignPk: that.box_pk_from_sign }, pack: that.box, open: that.box_open }; /*-- start WORKAROUND - Publish missing functions (see PR js-nacl: https://github.com/tonyg/js-nacl/pull/54) -- */ function crypto_box_keypair_from_sign_sk(sk) { var ska = check_injectBytes("crypto_box_keypair_from_sign_sk", "sk", sk, that.nacl.nacl_raw._crypto_sign_secretkeybytes()); var skb = new Target(that.nacl.nacl_raw._crypto_box_secretkeybytes()); check("_crypto_sign_ed25519_sk_to_curve25519", that.nacl.nacl_raw._crypto_sign_ed25519_sk_to_curve25519(skb.address, ska)); FREE(ska); return that.nacl.crypto_box_keypair_from_raw_sk(skb.extractBytes()); } function crypto_box_pk_from_sign_pk(pk) { var pka = check_injectBytes("crypto_box_pk_from_sign_pk", "pk", pk, that.nacl.nacl_raw._crypto_sign_publickeybytes()); var pkb = new Target(that.nacl.nacl_raw._crypto_box_publickeybytes()); check("_crypto_sign_ed25519_pk_to_curve25519", that.nacl.nacl_raw._crypto_sign_ed25519_pk_to_curve25519(pkb.address, pka)); FREE(pka); return pkb.extractBytes(); } function crypto_box_sk_from_sign_sk(sk) { var ska = check_injectBytes("crypto_box_sk_from_sign_sk", "sk", sk, that.nacl.nacl_raw._crypto_sign_secretkeybytes()); var skb = new Target(that.nacl.nacl_raw._crypto_box_secretkeybytes()); check("_crypto_sign_ed25519_sk_to_curve25519", that.nacl.nacl_raw._crypto_sign_ed25519_sk_to_curve25519(skb.address, ska)); FREE(ska); return skb.extractBytes(); } function check_length(function_name, what, thing, expected_length) { if (thing.length !== expected_length) { throw {message: "nacl." + function_name + " expected " + expected_length + "-byte " + what + " but got length " + thing.length}; } } function check(function_name, result) { if (result !== 0) { throw {message: "nacl_raw." + function_name + " signalled an error"}; } } function check_injectBytes(function_name, what, thing, expected_length, leftPadding) { check_length(function_name, what, thing, expected_length); return injectBytes(thing, leftPadding); } function injectBytes(bs, leftPadding) { var p = leftPadding || 0; var address = MALLOC(bs.length + p); that.nacl.nacl_raw.HEAPU8.set(bs, address + p); for (var i = address; i < address + p; i++) { that.nacl.nacl_raw.HEAPU8[i] = 0; } return address; } function MALLOC(nbytes) { var result = that.nacl.nacl_raw._malloc(nbytes); if (result === 0) { throw {message: "malloc() failed", nbytes: nbytes}; } return result; } function FREE(pointer) { that.nacl.nacl_raw._free(pointer); } function free_all(addresses) { for (var i = 0; i < addresses.length; i++) { FREE(addresses[i]); } } function extractBytes(address, length) { var result = new Uint8Array(length); result.set(that.nacl.nacl_raw.HEAPU8.subarray(address, address + length)); return result; } function Target(length) { this.length = length; this.address = MALLOC(length); } Target.prototype.extractBytes = function (offset) { var result = extractBytes(this.address + (offset || 0), this.length - (offset || 0)); FREE(this.address); this.address = null; return result; }; /*-- end of WORKAROUND -- */ } FullJSServiceFactory.prototype = new CryptoAbstractService(); /* ----------------------------------------------------------------------------------------------------------------- * Service that use Cordova MiniSodium plugin * ----------------------------------------------------------------------------------------------------------------*/ /*** * Factory for crypto, using Cordova plugin */ function CordovaServiceFactory() { this.id = 'MiniSodium'; // libraries handlers this.nacl = null; // the cordova plugin this.base58= null; this.sha256= null; var that = this; // functions this.util = this.util || {}; this.util.decode_utf8 = function(s) { return that.nacl.to_string(s); }; this.util.encode_utf8 = function(s) { return that.nacl.from_string(s); }; this.util.encode_base58 = function(a) { return that.base58.encode(a); }; this.util.decode_base58 = function(a) { var i; var d = that.base58.decode(a); var b = new Uint8Array(d.length); for (i = 0; i < d.length; i++) b[i] = d[i]; return b; }; this.util.decode_base64 = function (a) { return that.nacl.from_base64(a); }; this.util.encode_base64 = function (b) { return that.nacl.to_base64(b); }; this.util.hash_sha256 = function(message) { return $q.when(that.sha256(message).toUpperCase()); }; this.util.random_nonce = function() { var nonce = new Uint8Array(that.constants.crypto_secretbox_NONCEBYTES); that.crypto.getRandomValues(nonce); return $q.when(nonce); }; this.util.crypto_hash_sha256 = function (message) { return that.nacl.from_hex(that.sha256(message)); }; this.util.crypto_scrypt = function(password, salt, N, r, p, seedLength) { var deferred = $q.defer(); that.nacl.crypto_pwhash_scryptsalsa208sha256_ll( password, salt, N, r, p, seedLength, function (err, seed) { if (err) { deferred.reject(err); return;} deferred.resolve(seed); } ); return deferred.promise; }; /** * Create key pairs (sign and box), from salt+password (Scrypt), using cordova */ this.scryptKeypair = function(salt, password, scryptParams) { var deferred = $q.defer(); that.nacl.crypto_pwhash_scryptsalsa208sha256_ll( that.nacl.from_string(password), that.nacl.from_string(salt), scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N, scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r, scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p, that.constants.SEED_LENGTH, function (err, seed) { if (err) { deferred.reject(err); return;} that.nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) { if (err) { deferred.reject(err); return;} var result = { signPk: signKeypair.pk, signSk: signKeypair.sk }; that.box_keypair_from_sign(result) .then(function(boxKeypair) { result.boxPk = boxKeypair.pk; result.boxSk = boxKeypair.sk; deferred.resolve(result); }) .catch(function(err) { deferred.reject(err); }); }); } ); return deferred.promise; }; /** * Create key pairs from a seed */ this.seedKeypair = function(seed) { var deferred = $q.defer(); that.nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) { if (err) { deferred.reject(err); return;} deferred.resolve({ signPk: signKeypair.pk, signSk: signKeypair.sk }); }); return deferred.promise; }; /** * Get sign PK from salt+password (Scrypt), using cordova */ this.scryptSignPk = function(salt, password, scryptParams) { var deferred = $q.defer(); that.nacl.crypto_pwhash_scryptsalsa208sha256_ll( that.nacl.from_string(password), that.nacl.from_string(salt), scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N, scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r, scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p, that.constants.SEED_LENGTH, function (err, seed) { if (err) { deferred.reject(err); return;} that.nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) { if (err) { deferred.reject(err); return;} deferred.resolve(signKeypair.pk); }); } ); return deferred.promise; }; /** * Verify a signature of a message, for a pubkey */ this.verify = function (message, signature, pubkey) { var deferred = $q.defer(); that.nacl.crypto_sign_verify_detached( that.nacl.from_base64(signature), that.nacl.from_string(message), that.nacl.from_base64(pubkey), function(err, verified) { if (err) { deferred.reject(err); return;} deferred.resolve(verified); }); return deferred.promise; }; /** * Sign a message, from a key pair */ this.sign = function(message, keypair) { var deferred = $q.defer(); that.nacl.crypto_sign( that.nacl.from_string(message), // message keypair.signSk, // sk function(err, signedMsg) { if (err) { deferred.reject(err); return;} var sig; if (signedMsg.length > that.constants.crypto_sign_BYTES) { sig = new Uint8Array(that.constants.crypto_sign_BYTES); for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; } else { sig = signedMsg; } var signature = that.nacl.to_base64(sig); deferred.resolve(signature); }); return deferred.promise; }; /** * Compute the box key pair, from a sign key pair */ this.box_keypair_from_sign = function(signKeyPair) { if (signKeyPair.boxSk && signKeyPair.boxPk) return $q.when(signKeyPair); var deferred = $q.defer(); var result = {}; that.nacl.crypto_sign_ed25519_pk_to_curve25519(signKeyPair.signPk, function(err, boxPk) { if (err) { deferred.reject(err); return;} result.boxPk = boxPk; if (result.boxSk) deferred.resolve(result); }); that.nacl.crypto_sign_ed25519_sk_to_curve25519(signKeyPair.signSk, function(err, boxSk) { if (err) { deferred.reject(err); return;} result.boxSk = boxSk; if (result.boxPk) deferred.resolve(result); }); return deferred.promise; }; /** * Compute the box public key, from a sign public key */ this.box_pk_from_sign = function(signPk) { var deferred = $q.defer(); that.nacl.crypto_sign_ed25519_pk_to_curve25519(signPk, function(err, boxPk) { if (err) { deferred.reject(err); return;} deferred.resolve(boxPk); }); return deferred.promise; }; /** * Compute the box secret key, from a sign secret key */ this.box_sk_from_sign = function(signSk) { var deferred = $q.defer(); that.nacl.crypto_sign_ed25519_sk_to_curve25519(signSk, function(err, boxSk) { if (err) { deferred.reject(err); return;} deferred.resolve(boxSk); }); return deferred.promise; }; /** * Encrypt a message, from a key pair */ this.box = function(message, nonce, recipientPk, senderSk) { if (!message) { return $q.reject('No message'); } var deferred = $q.defer(); var messageBin = that.nacl.from_string(message); if (typeof recipientPk === "string") { recipientPk = that.util.decode_base58(recipientPk); } that.nacl.crypto_box_easy(messageBin, nonce, recipientPk, senderSk, function(err, ciphertextBin) { if (err) { deferred.reject(err); return;} var ciphertext = that.util.encode_base64(ciphertextBin); //console.debug('Encrypted message: ' + ciphertext); deferred.resolve(ciphertext); }); return deferred.promise; }; /** * Decrypt a message, from a key pair */ this.box_open = function(cypherText, nonce, senderPk, recipientSk) { if (!cypherText) { return $q.reject('No cypherText'); } var deferred = $q.defer(); var ciphertextBin = that.nacl.from_base64(cypherText); if (typeof senderPk === "string") { senderPk = that.util.decode_base58(senderPk); } // Avoid crash if content has not the minimal length - Fix #346 if (ciphertextBin.length < that.constants.crypto_box_MACBYTES) { deferred.reject('Invalid cypher content length'); return; } that.nacl.crypto_box_open_easy(ciphertextBin, nonce, senderPk, recipientSk, function(err, message) { if (err) { deferred.reject(err); return;} that.util.array_to_string(message, function(result) { //console.debug('Decrypted text: ' + result); deferred.resolve(result); }); }); return deferred.promise; }; this.load = function() { var deferred = $q.defer(); if (!window.plugins || !window.plugins.MiniSodium) { deferred.reject("Cordova plugin 'MiniSodium' not found. Please load Full JS implementation instead."); } else { that.nacl = window.plugins.MiniSodium; var loadedLib = 0; var checkAllLibLoaded = function() { loadedLib++; if (loadedLib == 2) { that.loaded = true; deferred.resolve(); } }; that.async_load_base58(function(lib) { that.base58 = lib; checkAllLibLoaded(); }); that.async_load_sha256(function(lib) { that.sha256 = lib; checkAllLibLoaded(); }); } return deferred.promise; }; // Shortcuts this.util.hash = that.util.hash_sha256; this.box = { keypair: { fromSignKeypair: that.box_keypair_from_sign, skFromSignSk: that.box_sk_from_sign, pkFromSignPk: that.box_pk_from_sign }, pack: that.box, open: that.box_open }; } CordovaServiceFactory.prototype = new CryptoAbstractService(); /* ----------------------------------------------------------------------------------------------------------------- * Create service instance * ----------------------------------------------------------------------------------------------------------------*/ var service = new CryptoAbstractService(); var isDevice = true; // removeIf(android) // removeIf(ios) isDevice = false; // endRemoveIf(ios) // endRemoveIf(android) //console.debug("[crypto] Created CryptotUtils service. device=" + isDevice); ionicReady().then(function() { console.debug('[crypto] Starting...'); var now = Date.now(); var serviceImpl; // Use Cordova plugin implementation, when exists if (isDevice && window.plugins && window.plugins.MiniSodium && crypto && crypto.getRandomValues) { console.debug('[crypto] Loading \'MiniSodium\' implementation...'); serviceImpl = new CordovaServiceFactory(); } else { console.debug('[crypto] Loading \'FullJS\' implementation...'); serviceImpl = new FullJSServiceFactory(); } // Load (async lib) serviceImpl.load() .catch(function(err) { console.error(err); throw err; }) .then(function() { service.copy(serviceImpl); console.debug('[crypto] Loaded \'{0}\' implementation in {1}ms'.format(service.id, Date.now() - now)); }); }); return service; }]) /* ----------------------------- Crypto advanced service for Cesium */ .factory('csCrypto', ['$q', '$rootScope', '$timeout', 'CryptoUtils', 'UIUtils', 'Modals', function($q, $rootScope, $timeout, CryptoUtils, UIUtils, Modals) { 'ngInject'; function test(regexpContent) { return new RegExp(regexpContent); } function xor(a, b) { var length = Math.max(a.length, b.length); var buffer = new Uint8Array(length); for (var i = 0; i < length; ++i) { buffer[i] = a[i] ^ b[i]; } return buffer; } function concat_Uint8Array( buffer1, buffer2 ) { var tmp = new Uint8Array( buffer1.byteLength + buffer2.byteLength ); tmp.set( new Uint8Array( buffer1 ), 0 ); tmp.set( new Uint8Array( buffer2 ), buffer1.byteLength ); return tmp; } var constants = { WIF: { DATA_LENGTH: 35 }, EWIF: { SALT_LENGTH: 4, DERIVED_HALF_LENGTH: 16, DATA_LENGTH: 39, SCRYPT_PARAMS: { N: 16384, r: 8, p: 8 } }, REGEXP: { PUBKEY: '[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}', SECKEY: '[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{86,88}', FILE: { TYPE_LINE: '^Type: ([a-zA-Z0-9]+)\n', VERSION: 'Version: ([0-9]+)\n', PUB: '[Pp]ub: ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44})\n', SEC: '[Ss]ec: ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{86,88})(\n|$)', DATA: '[Dd]ata: ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)(\n|$)' } } }, regexp = { FILE: { TYPE_LINE: test(constants.REGEXP.FILE.TYPE_LINE), VERSION: test(constants.REGEXP.FILE.VERSION), PUB: test(constants.REGEXP.FILE.PUB), SEC: test(constants.REGEXP.FILE.SEC), DATA: test(constants.REGEXP.FILE.DATA) } }, errorCodes = { BAD_PASSWORD: 3001, BAD_CHECKSUM: 3002 }; /* -- keyfile -- */ function readKeyFile(file, options) { if (file && file.content) { return parseKeyFileContent(file.content, options); } return $q(function(resolve, reject) { if (!file) { return reject('Argument [file] is missing'); } //console.debug('[crypto] [keypair] reading file: ', file); var reader = new FileReader(); reader.onload = function (event) { parseKeyFileContent(event.target.result, options) .then(resolve) .catch(reject); }; reader.readAsText(file, 'utf8'); }); } function parseKeyFileContent(content, options) { if (!content) return $q.reject('Argument [content] is missing'); options = options || {}; options.withSecret = angular.isDefined(options.withSecret) ? options.withSecret : false; options.defaultType = options.defaultType || 'PubSec'; var matches; var typeMatch = regexp.FILE.TYPE_LINE.exec(content); // If no Type field: add default type var type = typeMatch && typeMatch[1]; if (!type && options.defaultType) { return parseKeyFileContent('Type: {0}\n{1}'.format(options.defaultType, content), options); } // Type: PubSec if (type == 'PubSec') { // Read Pub field matches = regexp.FILE.PUB.exec(content); if (!matches) return $q.reject('Missing [pub] field in file, or invalid public key value'); var signKeypair = { signPk: CryptoUtils.base58.decode(matches[1]) }; if (!options.withSecret) return $q.resolve(signKeypair); // Read Sec field matches= regexp.FILE.SEC.exec(content); if (!matches) return $q.reject('Missing [sec] field in file, or invalid secret key value'); signKeypair.signSk = CryptoUtils.base58.decode(matches[1]); return $q.resolve(signKeypair); } // Type: WIF or EWIF else if (type == 'WIF' || type == 'EWIF') { matches = regexp.FILE.DATA.exec(content); if (!matches) { return $q.reject('Missing [Data] field in file. This is required for WIF or EWIF format'); } return parseWIF_or_EWIF(matches[1], { type: type, password: options.password }) .then(function(signKeypair) { return signKeypair && !options.withSecret ? {signPk: signKeypair.signPk} : signKeypair; }); } // Type: unknown if (options.defaultType) { return $q.reject('Bad file format: missing Type field'); } else { return $q.reject('Bad file format, unknown type [' + type + ']'); } } /** * * @param data_base58 * @param options * @returns {*} */ function parseWIF_or_EWIF(data_base58, options) { options = options || {}; var data_int8 = data_base58 && CryptoUtils.base58.decode(data_base58); if (!data_int8 || data_int8.length != constants.EWIF.DATA_LENGTH && data_int8.length != constants.WIF.DATA_LENGTH) { return $q.reject('Invalid WIF or EWIF format (invalid bytes count).'); } // Detect the type from the first byte options.type = options.type || (data_int8[0] == 1 && 'WIF') || (data_int8[0] == 2 && 'EWIF'); // Type: WIF if (options.type == 'WIF') { return parseWIF_v1(data_base58); } // Type: EWIF if (options.type == 'EWIF') { // If not set, resolve password using the given callback if (typeof options.password == "function") { //console.debug("[crypto] [EWIF] Executing 'options.password()' to resolve the password..."); options.password = options.password(); if (!options.password) { return $q.reject({message: "Invalid callback result for 'options.password()': must return a promise or a string."}); } } // If password is a promise, get the result then read data if (typeof options.password === "object" && options.password.then) { return options.password.then(function(password) { if (!password) throw 'CANCELLED'; return parseEWIF_v1(data_base58, password); }); } // If password is a valid string, read data if (typeof options.password == "string") { return parseEWIF_v1(data_base58, options.password); } return $q.reject({message: 'Invalid EWIF options.password. Waiting a callback function, a promise or a string.'}); } // Unknown type return $q.reject({message: 'Invalid WIF or EWIF format: unknown first byte identifier.'}); } function parseWIF_v1(wif_base58) { var wif_int8 = CryptoUtils.util.decode_base58(wif_base58); // Check identifier byte = 0x01 if (wif_int8[0] != 1) { return $q.reject({message: 'Invalid WIF v1 format: expected [0x01] as first byte'}); } // Check length if (wif_int8.length != constants.WIF.DATA_LENGTH) { return $q.reject({message: 'Invalid WIF v1 format: Data must be a '+constants.WIF.DATA_LENGTH+' bytes array, encoded in base 58.'}); } var wif_int8_no_checksum = wif_int8.slice(0, -2), seed = wif_int8.slice(1, -2), checksum = wif_int8.slice(-2); // Compute expected checksum var expectedChecksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(wif_int8_no_checksum)).slice(0,2); if (CryptoUtils.util.encode_base58(checksum) != CryptoUtils.util.encode_base58(expectedChecksum)) { $q.reject({message: 'Invalid WIF format: bad checksum'}); } // Generate keypair from seed return CryptoUtils.seedKeypair(seed); } function parseEWIF_v1(ewif_base58, password) { var ewif_int8 = CryptoUtils.util.decode_base58(ewif_base58); // Check identifier byte = 0x02 if (ewif_int8[0] != 2) { return $q.reject({message: 'Invalid EWIF v1 format: Expected [0x02] as first byte'}); } // Check length if (ewif_int8.length != constants.EWIF.DATA_LENGTH) { return $q.reject({message: 'Invalid EWIF v1 format: Expected {0} bytes, encoded in base 58.'.format(constants.EWIF.DATA_LENGTH)}); } var ewif_int8_no_checksum = ewif_int8.slice(0,-2); var checksum = ewif_int8.slice(-2); var salt = ewif_int8.slice(1,5); var encryptedhalf1 = ewif_int8.slice(5,21); var encryptedhalf2 = ewif_int8.slice(21,37); // Retrieve the scrypt_seed return CryptoUtils.util.crypto_scrypt( CryptoUtils.util.encode_utf8(password), salt, constants.EWIF.SCRYPT_PARAMS.N, constants.EWIF.SCRYPT_PARAMS.r, constants.EWIF.SCRYPT_PARAMS.p, 64) // Compute the final seed .then(function(scrypt_seed) { var derivedhalf1 = scrypt_seed.slice(0, 32); var derivedhalf2 = scrypt_seed.slice(32, 64); //AES var aesEcb = new aesjs.ModeOfOperation.ecb(derivedhalf2); var decryptedhalf1 = aesEcb.decrypt(encryptedhalf1); var decryptedhalf2 = aesEcb.decrypt(encryptedhalf2); decryptedhalf1 = new Uint8Array(decryptedhalf1); decryptedhalf2 = new Uint8Array(decryptedhalf2); //xor var seed1 = xor(decryptedhalf1, derivedhalf1.slice(0, 16)); var seed2 = xor(decryptedhalf2, derivedhalf1.slice(16, 32)); var seed = concat_Uint8Array(seed1, seed2); return seed; }) // Get the keypair, from the seed .then(CryptoUtils.seedKeypair) // Do some controls .then(function(keypair) { // Check salt var expectedSalt = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(keypair.signPk)).slice(0,4); if(CryptoUtils.util.encode_base58(salt) !== CryptoUtils.util.encode_base58(expectedSalt)) { throw {ucode: errorCodes.BAD_PASSWORD, message: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_PASSWORD'}; } // Check checksum var expectedChecksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(ewif_int8_no_checksum)).slice(0,2); if (CryptoUtils.util.encode_base58(checksum) != CryptoUtils.util.encode_base58(expectedChecksum)) { throw {ucode: errorCodes.BAD_CHECKSUM, message: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_CHECKSUM'}; } return keypair; }); } function wif_v1_from_keypair(keypair) { var seed = CryptoUtils.seed_from_signSk(keypair.signSk); if (!seed || seed.byteLength !== CryptoUtils.constants.SEED_LENGTH) throw "Bad see format. Expected {0} bytes".format(CryptoUtils.constants.SEED_LENGTH); var fi = new Uint8Array(1); fi[0] = 0x01; var seed_fi = concat_Uint8Array(fi, seed); // checksum var checksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(seed_fi)).slice(0,2); var wif_int8 = concat_Uint8Array(seed_fi, checksum); return $q.when(CryptoUtils.util.encode_base58(wif_int8)); } function ewif_v1_from_keypair(keypair, password) { var seed = CryptoUtils.seed_from_signSk(keypair.signSk); if (!seed || seed.byteLength !== CryptoUtils.constants.SEED_LENGTH) return $q.reject({message: "Bad see format. Expected {0} bytes".format(CryptoUtils.constants.SEED_LENGTH)}); // salt var salt = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(keypair.signPk)).slice(0,4); // scrypt_seed return CryptoUtils.util.crypto_scrypt( CryptoUtils.util.encode_utf8(password), salt, constants.EWIF.SCRYPT_PARAMS.N, constants.EWIF.SCRYPT_PARAMS.r, constants.EWIF.SCRYPT_PARAMS.p, 64) .then(function(scrypt_seed) { var derivedhalf1 = scrypt_seed.slice(0,32); var derivedhalf2 = scrypt_seed.slice(32,64); //XOR & AES var seed1_xor_derivedhalf1_1 = xor(seed.slice(0,16), derivedhalf1.slice(0,16)); var seed2_xor_derivedhalf1_2 = xor(seed.slice(16,32), derivedhalf1.slice(16,32)); var aesEcb = new aesjs.ModeOfOperation.ecb(derivedhalf2); var encryptedhalf1 = aesEcb.encrypt(seed1_xor_derivedhalf1_1); var encryptedhalf2 = aesEcb.encrypt(seed2_xor_derivedhalf1_2); encryptedhalf1 = new Uint8Array(encryptedhalf1); encryptedhalf2 = new Uint8Array(encryptedhalf2); // concatenate ewif var ewif_int8 = new Uint8Array(1); ewif_int8[0] = 0x02; ewif_int8 = concat_Uint8Array(ewif_int8,salt); ewif_int8 = concat_Uint8Array(ewif_int8,encryptedhalf1); ewif_int8 = concat_Uint8Array(ewif_int8,encryptedhalf2); var checksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(ewif_int8)).slice(0,2); ewif_int8 = concat_Uint8Array(ewif_int8,checksum); return CryptoUtils.util.encode_base58(ewif_int8); }); } function generateKeyFileContent(keypair, options) { options = options || {}; options.type = options.type || "PubSec"; switch(options.type) { // PubSec case "PubSec" : return $q.resolve( "Type: PubSec\n" + "Version: 1\n" + "pub: " + CryptoUtils.base58.encode(keypair.signPk) + "\n" + "sec: " + CryptoUtils.base58.encode(keypair.signSk) + "\n"); // WIF - v1 case "WIF" : return wif_v1_from_keypair(keypair) .then(function(data) { return "Type: WIF\n" + "Version: 1\n" + "Data: " + data + "\n"; }); // EWIF - v1 case "EWIF" : if (!options.password) return $q.reject({message: 'Missing EWIF options.password.'}); // If not set, resolve password using the given callback if (options.password && typeof options.password == "function") { console.debug("[crypto] [EWIF] Executing 'options.password()' to resolve the password..."); options.password = options.password(); if (!options.password) { return $q.reject({message: "Invalid callback result for 'options.password()': must return a promise or a string."}); } } // If password is a promise, get the result then read data if (options.password && typeof options.password == "object" && options.password.then) { return options.password.then(function(password) { if (!password) throw 'CANCELLED'; // Recursive call, with the string password in options return generateKeyFileContent(keypair, angular.merge({}, options, {password: password})); }); } // If password is a valid string, read data if (options.password && typeof options.password == "string") { return ewif_v1_from_keypair(keypair, options.password) .then(function(data) { return "Type: EWIF\n" + "Version: 1\n" + "Data: " + data + "\n"; }); } return $q.reject({message: 'Invalid EWIF options.password. Waiting a callback function, a promise or a string.'}); default: return $q.reject({message: "Unknown keyfile format: " + options.type}); } } /* -- usefull methods -- */ function pkChecksum(pubkey) { var signPk_int8 = CryptoUtils.util.decode_base58(pubkey); return CryptoUtils.util.encode_base58(CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(signPk_int8))).substring(0,3); } /* -- box (pack/unpack a record) -- */ function getBoxKeypair(keypair) { if (!keypair) { throw new Error('Missing keypair'); } if (keypair.boxPk && keypair.boxSk) { return $q.when(keypair); } return $q.all([ CryptoUtils.box.keypair.skFromSignSk(keypair.signSk), CryptoUtils.box.keypair.pkFromSignPk(keypair.signPk) ]) .then(function(res) { return { boxSk: res[0], boxPk: res[1] }; }); } function packRecordFields(record, keypair, recipientFieldName, cypherFieldNames, nonce) { recipientFieldName = recipientFieldName || 'recipient'; if (!record[recipientFieldName]) { return $q.reject({message:'ES_WALLET.ERROR.RECIPIENT_IS_MANDATORY'}); } cypherFieldNames = cypherFieldNames || 'content'; if (typeof cypherFieldNames == 'string') { cypherFieldNames = [cypherFieldNames]; } // Work on a copy, to keep the original record (as it could be use again - fix #382) record = angular.copy(record); // Get recipient var recipientPk = CryptoUtils.util.decode_base58(record[recipientFieldName]); return $q.all([ getBoxKeypair(keypair), CryptoUtils.box.keypair.pkFromSignPk(recipientPk), nonce ? $q.when(nonce) : CryptoUtils.util.random_nonce() ]) .then(function(res) { //var senderSk = res[0]; var boxKeypair = res[0]; var senderSk = boxKeypair.boxSk; var boxRecipientPk = res[1]; var nonce = res[2]; return $q.all( cypherFieldNames.reduce(function(res, fieldName) { if (!record[fieldName]) return res; // skip undefined fields return res.concat( CryptoUtils.box.pack(record[fieldName], nonce, boxRecipientPk, senderSk) ); }, [])) .then(function(cypherTexts){ // Replace field values with cypher texts var i = 0; _.forEach(cypherFieldNames, function(cypherFieldName) { if (!record[cypherFieldName]) { // Force undefined fields to be present in object // This is better for ES storage, that always works on lazy update mode record[cypherFieldName] = null; } else { record[cypherFieldName] = cypherTexts[i++]; } }); // Set nonce record.nonce = CryptoUtils.util.encode_base58(nonce); return record; }); }); } function openRecordFields(records, keypair, issuerFieldName, cypherFieldNames) { issuerFieldName = issuerFieldName || 'issuer'; cypherFieldNames = cypherFieldNames || 'content'; if (typeof cypherFieldNames == 'string') { cypherFieldNames = [cypherFieldNames]; } var now = Date.now(); var issuerBoxPks = {}; // a map used as cache var jobs = [getBoxKeypair(keypair)]; return $q.all(records.reduce(function(jobs, message) { var issuer = message[issuerFieldName]; if (!issuer) {throw 'Record has no ' + issuerFieldName;} if (issuerBoxPks[issuer]) return res; return jobs.concat( CryptoUtils.box.keypair.pkFromSignPk(CryptoUtils.util.decode_base58(issuer)) .then(function(issuerBoxPk) { issuerBoxPks[issuer] = issuerBoxPk; // fill box pk cache })); }, jobs)) .then(function(res){ var boxKeypair = res[0]; return $q.all(records.reduce(function(jobs, record) { var issuerBoxPk = issuerBoxPks[record[issuerFieldName]]; var nonce = CryptoUtils.util.decode_base58(record.nonce); record.valid = true; return jobs.concat( cypherFieldNames.reduce(function(res, cypherFieldName) { if (!record[cypherFieldName]) return res; return res.concat(CryptoUtils.box.open(record[cypherFieldName], nonce, issuerBoxPk, boxKeypair.boxSk) .then(function(text) { record[cypherFieldName] = text; }) .catch(function(err){ console.error(err); console.warn('[ES] [crypto] a record may have invalid cypher ' + cypherFieldName); record.valid = false; })); }, [])); }, [])); }) .then(function() { console.debug('[ES] [crypto] All record decrypted in ' + (Date.now() - now) + 'ms'); return records; }); } function parseKeyFileData(data, options){ options = options || {}; options.withSecret = angular.isDefined(options.withSecret) ? options.withSecret : true; options.silent = angular.isDefined(options.withSecret) ? options.silent : false; options.password = function() { return UIUtils.loading.hide(100) .then(function() { return Modals.showPassword({ title: 'ACCOUNT.SECURITY.KEYFILE.PASSWORD_POPUP.TITLE', subTitle: 'ACCOUNT.SECURITY.KEYFILE.PASSWORD_POPUP.HELP', error: options.error, scope: options.scope }); }) .then(function(password) { // Timeout is need to force popup to be hide return $timeout(function() { if (password) UIUtils.loading.show(); return password; }, 150); }); }; if (!options.silent) { UIUtils.loading.show(); } return parseWIF_or_EWIF(data, options) .then(function(res){ return res; }) .catch(function(err) { if (err && err === 'CANCELLED') return; if (err && err.ucode == errorCodes.BAD_PASSWORD) { // recursive call return parseKeyFileData(data, {withSecret: options.withSecret, error: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_PASSWORD'}); } console.error("[crypto] Unable to parse as WIF or EWIF format: " + (err && err.message || err)); throw err; // rethrow }); } // exports return { errorCodes: errorCodes, constants: constants, // copy CryptoUtils util: angular.extend({ pkChecksum: pkChecksum }, CryptoUtils.util), keyfile: { read: readKeyFile, parseData: parseKeyFileData, generateContent: generateKeyFileContent }, box: { getKeypair: getBoxKeypair, pack: packRecordFields, open: openRecordFields } }; }]) ; angular.module('cesium.utils.services', []) // Replace the '$ionicPlatform.ready()', to enable multiple calls // See http://stealthcode.co/multiple-calls-to-ionicplatform-ready/ .factory('ionicReady', ['$ionicPlatform', function($ionicPlatform) { 'ngInject'; var readyPromise; return function () { if (!readyPromise) { readyPromise = $ionicPlatform.ready(); } return readyPromise; }; }]) .factory('UIUtils', ['$ionicLoading', '$ionicPopup', '$ionicConfig', '$ionicHistory', '$translate', '$q', 'ionicMaterialInk', 'ionicMaterialMotion', '$window', '$timeout', 'Fullscreen', '$ionicPopover', '$state', '$rootScope', 'screenmatch', function($ionicLoading, $ionicPopup, $ionicConfig, $ionicHistory, $translate, $q, ionicMaterialInk, ionicMaterialMotion, $window, $timeout, Fullscreen, $ionicPopover, $state, $rootScope, screenmatch) { 'ngInject'; var loadingTextCache=null, CONST = { MAX_HEIGHT: 480, MAX_WIDTH: 640, THUMB_MAX_HEIGHT: 200, THUMB_MAX_WIDTH: 200 }, data = { smallscreen: screenmatch.bind('xs, sm', $rootScope) }, exports, raw = {} ; function alertError(err, subtitle) { if (!err) { return $q.when(); } return $q(function(resolve) { $translate([err, subtitle, 'ERROR.POPUP_TITLE', 'ERROR.UNKNOWN_ERROR', 'COMMON.BTN_OK']) .then(function (translations) { var message = err.message || translations[err]; return $ionicPopup.show({ template: '

' + (message || translations['ERROR.UNKNOWN_ERROR']) + '

', title: translations['ERROR.POPUP_TITLE'], subTitle: translations[subtitle], buttons: [ { text: ''+translations['COMMON.BTN_OK']+'', type: 'button-assertive', onTap: function(e) { resolve(e); } } ] }); }); }); } function alertInfo(message, subtitle) { return $q(function(resolve) { $translate([message, subtitle, 'INFO.POPUP_TITLE', 'COMMON.BTN_OK']) .then(function (translations) { $ionicPopup.show({ template: '

' + translations[message] + '

', title: translations['INFO.POPUP_TITLE'], subTitle: translations[subtitle], buttons: [ { text: translations['COMMON.BTN_OK'], type: 'button-positive', onTap: function(e) { resolve(e); } } ] }); }); }); } function alertNotImplemented() { return alertInfo('INFO.FEATURES_NOT_IMPLEMENTED'); } function askConfirm(message, title, options) { title = title || 'CONFIRM.POPUP_TITLE'; options = options || {}; options.cssClass = options.cssClass || 'confirm'; options.okText = options.okText || 'COMMON.BTN_OK'; options.cancelText = options.cancelText || 'COMMON.BTN_CANCEL'; return $translate([message, title, options.cancelText, options.okText]) .then(function (translations) { return $ionicPopup.confirm({ template: translations[message], cssClass: options.cssClass, title: translations[title], cancelText: translations[options.cancelText], cancelType: options.cancelType, okText: translations[options.okText], okType: options.okType }); }); } function hideLoading(timeout){ if (timeout) { return $timeout(function(){ return $ionicLoading.hide(); }, timeout); } else { return $ionicLoading.hide(); } } function showLoading(options) { if (!loadingTextCache) { return $translate('COMMON.LOADING') .then(function(translation){ loadingTextCache = translation; return showLoading(options); }); } options = options || {}; options.template = options.template||loadingTextCache; return $ionicLoading.show(options); } function updateLoading(options) { return $ionicLoading._getLoader().then(function(loader) { if (!loader || !loader.isShown) return; // Translate template (if exists) if (options && options.template) { return $translate(options && options.template) .then(function(template) { options.template = template; return loader; }); } }) .then(function(loader) { if (loader && loader.isShown) return showLoading(options); }); } function showToast(message, duration, position) { duration = duration || 'short'; position = position || 'bottom'; return $translate([message]) .then(function(translations){ // removeIf(device) // Use the $ionicLoading toast. // First, make sure to convert duration in number if (typeof duration == 'string') { if (duration == 'short') { duration = 2000; } else { duration = 5000; } } return $ionicLoading.show({ template: translations[message], noBackdrop: true, duration: duration }); // endRemoveIf(device) }); } function onError(msg, reject/*optional*/) { return function(err) { var fullMsg = msg; var subtitle; if (!!err && !!err.message) { fullMsg = err.message; subtitle = msg; } else if (!msg){ fullMsg = err; } // If reject has been given, use it if (!!reject) { reject(fullMsg); } // If just a user cancellation: silent else if (fullMsg == 'CANCELLED') { return hideLoading(10); // timeout, to avoid bug on transfer (when error on reference) } // Otherwise, log to console and display error else { hideLoading(10); // timeout, to avoid bug on transfer (when error on reference) return alertError(fullMsg, subtitle); } }; } function isSmallScreen() { return data.smallscreen.active; } function selectElementText(el) { if (el.value || el.type == "text" || el.type == "textarea") { // Source: http://stackoverflow.com/questions/14995884/select-text-on-input-focus if ($window.getSelection && !$window.getSelection().toString()) { el.setSelectionRange(0, el.value.length); } } else { if (el.childNodes && el.childNodes.length > 0) { selectElementText(el.childNodes[0]); } else { // See http://www.javascriptkit.com/javatutors/copytoclipboard.shtml var range = $window.document.createRange(); // create new range object range.selectNodeContents(el); // set range to encompass desired element text var selection = $window.getSelection(); // get Selection object from currently user selected text selection.removeAllRanges(); // unselect any user selected text (if any) selection.addRange(range); // add range to Selection object to select it } } } function getSelectionText(){ var selectedText = ""; if (window.getSelection){ // all modern browsers and IE9+ selectedText = $window.getSelection().toString(); } return selectedText; } function imageOnLoadResize(resolve, reject, thumbnail) { return function(event) { var width = event.target.width, height = event.target.height, maxWidth = (thumbnail ? CONST.THUMB_MAX_WIDTH : CONST.MAX_WIDTH), maxHeight = (thumbnail ? CONST.THUMB_MAX_HEIGHT : CONST.MAX_HEIGHT) ; var canvas = document.createElement("canvas"); var ctx; // Thumbnail: resize and crop (to the expected size) if (thumbnail) { // landscape if (width > height) { width *= maxHeight / height; height = maxHeight; } // portrait else { height *= maxWidth / width; width = maxWidth; } canvas.width = maxWidth; canvas.height = maxHeight; ctx = canvas.getContext("2d"); var xoffset = Math.trunc((maxWidth - width) / 2 + 0.5); var yoffset = Math.trunc((maxHeight - height) / 2 + 0.5); ctx.drawImage(event.target, xoffset, // x1 yoffset, // y1 maxWidth + -2 * xoffset, // x2 maxHeight + -2 * yoffset // y2 ); } // Resize, but keep the full image else { // landscape if (width > height) { if (width > maxWidth) { height *= maxWidth / width; width = maxWidth; } } // portrait else { if (height > maxHeight) { width *= maxHeight / height; height = maxHeight; } } canvas.width = width; canvas.height = height; ctx = canvas.getContext("2d"); // Resize the whole image ctx.drawImage(event.target, 0, 0, canvas.width, canvas.height); } var dataurl = canvas.toDataURL(); canvas.remove(); resolve(dataurl); }; } function resizeImageFromFile(file, thumbnail) { var img = document.createElement("img"); return $q(function(resolve, reject) { if (file) { var reader = new FileReader(); reader.onload = function(event){ img.onload = imageOnLoadResize(resolve, reject, thumbnail); img.src = event.target.result; }; reader.readAsDataURL(file); } else { reject('no file to resize'); } }) .then(function(dataurl) { img.remove(); return dataurl; }) ; } function resizeImageFromSrc(imageSrc, thumbnail) { var img = document.createElement("img"); return $q(function(resolve, reject) { img.onload = imageOnLoadResize(resolve, reject, thumbnail); img.src = imageSrc; }) .then(function(data){ img.remove(); return data; }); } function imageOnLoadRotate(resolve, reject) { var deg = Math.PI / 180; var angle = 90 * deg; return function(event) { var width = event.target.width; var height = event.target.height; var maxWidth = CONST.MAX_WIDTH; var maxHeight = CONST.MAX_HEIGHT; if (width > height) { if (width > maxWidth) { height *= maxWidth / width; width = maxWidth; } } else { if (height > maxHeight) { width *= maxHeight / height; height = maxHeight; } } var canvas = document.createElement("canvas"); canvas.width = height; canvas.height = width; var ctx = canvas.getContext("2d"); ctx.rotate(angle); ctx.drawImage(event.target, 0, (-1) * canvas.width); var dataurl = canvas.toDataURL(); canvas.remove(); resolve(dataurl); }; } function rotateFromSrc(imageSrc, angle) { var img = document.createElement("img"); return $q(function(resolve, reject) { img.onload = imageOnLoadRotate(resolve, reject, angle); img.src = imageSrc; }) .then(function(data){ img.remove(); return data; }); } function showPopover(event, options) { var deferred = $q.defer(); options = options || {}; options.templateUrl = options.templateUrl ? options.templateUrl : 'templates/common/popover_copy.html'; options.scope = options.scope || $rootScope; options.scope.popovers = options.scope.popovers || {}; options.autoselect = options.autoselect || false; options.autoremove = angular.isDefined(options.autoremove) ? options.autoremove : true; options.backdropClickToClose = angular.isDefined(options.backdropClickToClose) ? options.backdropClickToClose : true; options.focusFirstInput = angular.isDefined(options.focusFirstInput) ? options.focusFirstInput : false; var _show = function(popover) { popover = popover || options.scope.popovers[options.templateUrl]; popover.isResolved=false; popover.deferred=deferred; popover.options=options; // Fill the popover scope if (options.bindings) { angular.merge(popover.scope, options.bindings); } $timeout(function() { // This is need for Firefox popover.show(event) .then(function() { var element; // Auto select text if (options.autoselect) { element = document.querySelectorAll(options.autoselect)[0]; if (element) { if ($window.getSelection && !$window.getSelection().toString()) { element.setSelectionRange(0, element.value.length); element.focus(); } else { element.focus(); } } } else { // Auto focus on a element if (options.autofocus) { element = document.querySelectorAll(options.autofocus)[0]; if (element) element.focus(); } } popover.scope.$parent.$emit('popover.shown'); // Callback 'afterShow' if (options.afterShow) options.afterShow(popover); }); }); }; var _cleanup = function(popover) { popover = popover || options.scope.popovers[options.templateUrl]; if (popover) { delete options.scope.popovers[options.templateUrl]; // Remove the popover popover.remove() // Workaround for issue #244 // See also https://github.com/driftyco/ionic-v1/issues/71 // and https://github.com/driftyco/ionic/issues/9069 .then(function() { var bodyEl = angular.element($window.document.querySelectorAll('body')[0]); bodyEl.removeClass('popover-open'); }); } }; var popover = options.scope.popovers[options.templateUrl]; if (!popover) { $ionicPopover.fromTemplateUrl(options.templateUrl, { scope: options.scope, backdropClickToClose: options.backdropClickToClose }) .then(function (popover) { popover.isResolved = false; popover.scope.closePopover = function(result) { var autoremove = popover.options && popover.options.autoremove; if (popover.options) delete popover.options.autoremove; // remove to avoid to trigger 'popover.hidden' popover.hide() .then(function() { if (autoremove) { return _cleanup(popover); } }) .then(function() { if (popover.deferred) { popover.deferred.resolve(result); } delete popover.deferred; delete popover.options; }); }; // Execute action on hidden popover popover.scope.$on('popover.hidden', function() { if (popover.options && popover.options.afterHidden) { popover.options.afterHidden(); } if (popover.options && popover.options.autoremove) { _cleanup(popover); } }); // Cleanup the popover when hidden options.scope.$on('$remove', function() { if (popover.deferred) { popover.deferred.resolve(); } _cleanup(); }); options.scope.popovers[options.templateUrl] = popover; _show(popover); }); } else { _show(popover); } return deferred.promise; } function showCopyPopover(event, value) { var rows = value && value.indexOf('\n') >= 0 ? value.split('\n').length : 1; return showPopover(event, { templateUrl: 'templates/common/popover_copy.html', bindings: { value: value, rows: rows }, autoselect: '.popover-copy ' + (rows <= 1 ? 'input' : 'textarea') }); } function showSharePopover(event, options) { options = options || {}; options.templateUrl = options.templateUrl ? options.templateUrl : 'templates/common/popover_share.html'; options.autoselect = options.autoselect || '.popover-share input'; options.bindings = options.bindings || {}; options.bindings.value = options.bindings.value || options.bindings.url || $state.href($state.current, $state.params, {absolute: true}); options.bindings.postUrl = options.bindings.postUrl || options.bindings.value; options.bindings.postMessage = options.bindings.postMessage || ''; options.bindings.titleKey = options.bindings.titleKey || 'COMMON.POPOVER_SHARE.TITLE'; return showPopover(event, options); } function showHelptip(id, options) { var element = (typeof id == 'string') && id ? $window.document.getElementById(id) : id; if (!id && !element && options.selector) { element = $window.document.querySelector(options.selector); } options = options || {}; var deferred = options.deferred || $q.defer(); if(element && !options.timeout) { if (options.preAction) { element[options.preAction](); } options.templateUrl = options.templateUrl ? options.templateUrl : 'templates/common/popover_helptip.html'; options.autofocus = options.autofocus || '#helptip-btn-ok'; options.bindings = options.bindings || {}; options.bindings.icon = options.bindings.icon || {}; options.bindings.icon.position = options.bindings.icon.position || false; options.bindings.icon.glyph = options.bindings.icon.glyph || (options.bindings.icon.position && options.bindings.icon.position.startsWith('bottom-') ? 'ion-arrow-down-c' :'ion-arrow-up-c'); options.bindings.icon.class = options.bindings.icon.class || 'calm icon ' + options.bindings.icon.glyph; options.bindings.tour = angular.isDefined(options.bindings.tour) ? options.bindings.tour : false; showPopover(element, options) .then(function(result){ if (options.postAction) { element[options.postAction](); } deferred.resolve(result); }) .catch(function(err){ if (options.postAction) { element[options.postAction](); } deferred.reject(err); }); } else { // Do timeout if ask if (options.timeout) { var timeout = options.timeout; options.retryTimeout = options.retryTimeout || timeout; delete options.timeout; options.deferred = deferred; $timeout(function () { showHelptip(id, options); }, timeout); } // No element: reject else if (angular.isDefined(options.retry) && !options.retry) { if (options.onError === 'continue') { $timeout(function () { deferred.resolve(true); }); } else { $timeout(function () { deferred.reject("[helptip] element now found: " + id); }); } } // Retry until element appears else { options.retry = angular.isUndefined(options.retry) ? 2 : (options.retry-1); options.deferred = deferred; $timeout(function() { showHelptip(id, options); }, options.timeout || options.retryTimeout || 100); } } return deferred.promise; } function showFab(id, timeout) { if (!timeout) { timeout = 900; } $timeout(function () { // Could not use 'getElementById', because it return only once element, // but many fabs can have the same id (many view could be loaded at the same time) var fabs = document.getElementsByClassName('button-fab'); _.forEach(fabs, function(fab){ if (fab.id == id) { fab.classList.toggle('on', true); } }); }, timeout); } function hideFab(id, timeout) { if (!timeout) { timeout = 10; } $timeout(function () { // Could not use 'getElementById', because it return only once element, // but many fabs can have the same id (many view could be loaded at the same time) var fabs = document.getElementsByClassName('button-fab'); _.forEach(fabs, function(fab){ if (fab.id == id) { fab.classList.toggle('on', false); } }); }, timeout); } function motionDelegate(delegate, ionListClass) { var motionTimeout = isSmallScreen() ? 100 : 10; var defaultSelector = '.list.{0} .item, .list .{0} .item'.format(ionListClass, ionListClass); return { ionListClass: ionListClass, show: function(options) { options = options || {}; options.selector = options.selector || defaultSelector; options.ink = angular.isDefined(options.ink) ? options.ink : true; options.startVelocity = options.startVelocity || (isSmallScreen() ? 1100 : 3000); return $timeout(function(){ // Display ink effect (no selector need) if (options.ink) exports.ink(); // Display the delegated motion effect delegate(options); }, options.timeout || motionTimeout); } }; } function setEffects(enable) { if (exports.motion.enable === enable) return; // same console.debug('[UI] [effects] ' + (enable ? 'Enable' : 'Disable')); exports.motion.enable = enable; if (enable) { $ionicConfig.views.transition('platform'); angular.merge(exports.motion, raw.motion); } else { $ionicConfig.views.transition('none'); var nothing = { class: undefined, show: function(){} }; angular.merge(exports.motion, { enable : false, default: nothing, fadeSlideIn: nothing, fadeSlideInRight: nothing, panInLeft: nothing, pushDown: nothing, ripple: nothing, slideUp: nothing, fadeIn: nothing, toggleOn: toggleOn, toggleOff: toggleOff }); $rootScope.motion = nothing; } $ionicHistory.clearCache(); } raw.motion = { enable: true, default: motionDelegate(ionicMaterialMotion.ripple, 'animate-ripple'), blinds: motionDelegate(ionicMaterialMotion.blinds, 'animate-blinds'), fadeSlideIn: motionDelegate(ionicMaterialMotion.fadeSlideIn, 'animate-fade-slide-in'), fadeSlideInRight: motionDelegate(ionicMaterialMotion.fadeSlideInRight, 'animate-fade-slide-in-right'), panInLeft: motionDelegate(ionicMaterialMotion.panInLeft, 'animate-pan-in-left'), pushDown: motionDelegate(ionicMaterialMotion.pushDown, 'push-down'), ripple: motionDelegate(ionicMaterialMotion.ripple, 'animate-ripple'), slideUp: motionDelegate(ionicMaterialMotion.slideUp, 'slide-up'), fadeIn: motionDelegate(function(options) { toggleOn(options); }, 'fade-in'), toggleOn: toggleOn, toggleOff: toggleOff }; function toggleOn(options, timeout) { // We have a single option, so it may be passed as a string or property if (typeof options === 'string') { options = { selector: options }; } // Fail early & silently log var isInvalidSelector = typeof options.selector === 'undefined' || options.selector === ''; if (isInvalidSelector) { console.error('invalid toggleOn selector'); return false; } $timeout(function () { var elements = document.querySelectorAll(options.selector); if (elements) _.forEach(elements, function(element){ element.classList.toggle('on', true); }); }, timeout || 100); } function toggleOff(options, timeout) { // We have a single option, so it may be passed as a string or property if (typeof options === 'string') { options = { selector: options }; } // Fail early & silently log var isInvalidSelector = typeof options.selector === 'undefined' || options.selector === ''; if (isInvalidSelector) { console.error('invalid toggleOff selector'); return false; } $timeout(function () { var elements = document.querySelectorAll(options.selector); if (elements) _.forEach(elements, function(element){ element.classList.toggle('on', false); }); }, timeout || 900); } exports = { alert: { error: alertError, info: alertInfo, confirm: askConfirm, notImplemented: alertNotImplemented }, loading: { show: showLoading, hide: hideLoading, update: updateLoading }, toast: { show: showToast }, onError: onError, screen: { isSmall: isSmallScreen, fullscreen: Fullscreen }, ink: ionicMaterialInk.displayEffect, motion: raw.motion, setEffects: setEffects, fab: { show: showFab, hide: hideFab }, popover: { show: showPopover, copy: showCopyPopover, share: showSharePopover, helptip: showHelptip }, selection: { select: selectElementText, get: getSelectionText }, image: { resizeFile: resizeImageFromFile, resizeSrc: resizeImageFromSrc, rotateSrc: rotateFromSrc }, raw: raw }; return exports; }]) // See http://plnkr.co/edit/vJQXtsZiX4EJ6Uvw9xtG?p=preview .factory('$focus', ['$timeout', '$window', function($timeout, $window) { 'ngInject'; return function(id) { // timeout makes sure that it is invoked after any other event has been triggered. // e.g. click events that need to run before the focus or // inputs elements that are in a disabled state but are enabled when those events // are triggered. $timeout(function() { var element = $window.document.getElementById(id); if(element) element.focus(); }); }; }]) ; angular.module('cesium.cache.services', ['ngResource', 'angular-cache']) .factory('csCache', ['$http', 'csSettings', 'CacheFactory', function($http, csSettings, CacheFactory) { 'ngInject'; var constants = { LONG: 1 * 60 * 60 * 1000 /*5 min*/, SHORT: csSettings.defaultSettings.cacheTimeMs }, cacheNames = [] ; function getOrCreateCache(prefix, maxAge, onExpire){ prefix = prefix || 'csCache-'; maxAge = maxAge || constants.SHORT; var cacheName = prefix + maxAge; if (!onExpire) { if (!cacheNames[cacheName]) { cacheNames[cacheName] = true; } return CacheFactory.get(cacheName) || CacheFactory.createCache(cacheName, { maxAge: maxAge, deleteOnExpire: 'aggressive', //cacheFlushInterval: 60 * 60 * 1000, // clear itself every hour recycleFreq: Math.max(maxAge - 1000, 5 * 60 * 1000 /*5min*/), storageMode: 'memory' // FIXME : enable this when cache is cleaning on rollback //csSettings.data.useLocalStorage ? 'localStorage' : 'memory' }); } else { var counter = 1; while(CacheFactory.get(cacheName + counter)) { counter++; } cacheName = cacheName + counter; if (!cacheNames[cacheName]) { cacheNames[cacheName] = true; } return CacheFactory.createCache(cacheName, { maxAge: maxAge, deleteOnExpire: 'aggressive', //cacheFlushInterval: 60 * 60 * 1000, // This cache will clear itself every hour recycleFreq: maxAge, onExpire: onExpire, storageMode: 'memory' // FIXME : enable this when cache is cleaning on rollback //csSettings.data.useLocalStorage ? 'localStorage' : 'memory' }); } } function clearAllCaches() { console.debug("[cache] cleaning all caches"); _.forEach(_.keys(cacheNames), function(cacheName) { var cache = CacheFactory.get(cacheName); if (cache) { cache.removeAll(); } }); } function clearFromPrefix(cachePrefix) { _.forEach(_.keys(cacheNames), function(cacheName) { if (cacheName.startsWith(cachePrefix)) { var cache = CacheFactory.get(cacheNames); if (cache) { cache.removeAll(); } } }); } return { get: getOrCreateCache, clear: clearFromPrefix, clearAll: clearAllCaches, constants: { LONG : constants.LONG, SHORT: constants.SHORT } }; }]) ; angular.module('cesium.modal.services', []) // Useful for modal with no controller .controller('EmptyModalCtrl', function () { 'ngInject'; }) .controller('AboutModalCtrl', ['$scope', 'UIUtils', 'csHttp', function ($scope, UIUtils, csHttp) { 'ngInject'; $scope.openLink = function(event, uri, options) { options = options || {}; // If unable to open, just copy value options.onError = function() { return UIUtils.popover.copy(event, uri); }; return csHttp.uri.open(uri, options); }; }]) .factory('ModalUtils', ['$ionicModal', '$rootScope', '$q', '$injector', '$controller', '$timeout', function($ionicModal, $rootScope, $q, $injector, $controller, $timeout) { 'ngInject'; function _evalController(ctrlName) { var result = { isControllerAs: false, controllerName: '', propName: '' }; var fragments = (ctrlName || '').trim().split(/\s+/); result.isControllerAs = fragments.length === 3 && (fragments[1] || '').toLowerCase() === 'as'; if (result.isControllerAs) { result.controllerName = fragments[0]; result.propName = fragments[2]; } else { result.controllerName = ctrlName; } return result; } function DefaultModalController($scope, deferred, parameters) { $scope.deferred = deferred || $q.defer(); $scope.resolved = false; $scope.openModal = function () { return $scope.modal.show(); }; $scope.closeModal = function (result) { $scope.resolved = true; return $scope.modal.remove() .then(function() { $scope.deferred.resolve(result); return result; }); }; // Useful method for modal with forms $scope.setForm = function (form, propName) { if (propName) { $scope[propName] = form; } else { $scope.form = form; } }; // Useful method for modal to get input parameters $scope.getParameters = function () { return parameters; }; $scope.$on('modal.hidden', function () { // If not resolved yet: send result // (after animation out) if (!$scope.resolved) { $scope.resolved = true; $timeout(function() { $scope.deferred.resolve(); return $scope.modal.remove(); }, ($scope.modal.hideDelay || 320) + 20); } }); } function show(templateUrl, controller, parameters, options) { var deferred = $q.defer(); options = options ? options : {} ; options.animation = options.animation || 'slide-in-up'; // If modal has a controller if (controller) { // If a controller defined, always use a new scope options.scope = options.scope ? options.scope.$new() : $rootScope.$new(); DefaultModalController.call({}, options.scope, deferred, parameters); // Invoke the controller on this new scope var locals = { '$scope': options.scope, 'parameters': parameters }; var ctrlEval = _evalController(controller); var ctrlInstance = $controller(controller, locals); if (ctrlEval.isControllerAs) { ctrlInstance.openModal = options.scope.openModal; ctrlInstance.closeModal = options.scope.closeModal; } } $ionicModal.fromTemplateUrl(templateUrl, options) .then(function (modal) { if (controller) { // Set modal into the controller's scope modal.scope.$parent.modal = modal; } else { var scope = modal.scope; // Define default scope functions DefaultModalController.call({}, scope, deferred, parameters); // Set modal scope.modal = modal; } // Show the modal return modal.show(); }, function (err) { deferred.reject(err); }); return deferred.promise; } return { show: show }; }]) .factory('Modals', ['ModalUtils', function(ModalUtils) { 'ngInject'; function showTransfer(parameters) { return ModalUtils.show('templates/wallet/modal_transfer.html','TransferModalCtrl', parameters, {focusFirstInput: true}); } function showLogin(options) { var parameters = angular.copy(options||{}); delete parameters.templateUrl; delete parameters.controller; return ModalUtils.show( options && options.templateUrl || 'templates/login/modal_login.html', options && options.controller || 'LoginModalCtrl', parameters, {focusFirstInput: true}); } function showWotLookup(parameters) { return ModalUtils.show('templates/wot/modal_lookup.html','WotLookupModalCtrl', parameters || {}, {focusFirstInput: true}); } function showNetworkLookup(parameters) { return ModalUtils.show('templates/network/modal_network.html', 'NetworkLookupModalCtrl', parameters, {focusFirstInput: true}); } function showAbout(parameters) { return ModalUtils.show('templates/modal_about.html','AboutModalCtrl', parameters); } function showAccountSecurity(parameters) { return ModalUtils.show('templates/wallet/modal_security.html', 'WalletSecurityModalCtrl', parameters); } function showJoin(parameters) { return ModalUtils.show('templates/join/modal_join.html','JoinModalCtrl', parameters); } function showHelp(parameters) { return ModalUtils.show('templates/help/modal_help.html','HelpModalCtrl', parameters); } return { showTransfer: showTransfer, showLogin: showLogin, showWotLookup: showWotLookup, showNetworkLookup: showNetworkLookup, showAbout: showAbout, showJoin: showJoin, showHelp: showHelp, showAccountSecurity: showAccountSecurity }; }]); angular.module('cesium.http.services', ['cesium.cache.services']) .factory('csHttp', ['$http', '$q', '$timeout', '$window', 'csSettings', 'csCache', 'Device', function($http, $q, $timeout, $window, csSettings, csCache, Device) { 'ngInject'; var timeout = csSettings.data.timeout; var sockets = [], cachePrefix = 'csHttp-' ; if (!timeout) { timeout=4000; // default } function getServer(host, port) { // Remove port if 80 or 443 return !host ? null : (host + (port && port != 80 && port != 443 ? ':' + port : '')); } function getUrl(host, port, path, useSsl) { var protocol = (port == 443 || useSsl) ? 'https' : 'http'; return protocol + '://' + getServer(host, port) + (path ? path : ''); } function getWsUrl(host, port, path, useSsl) { var protocol = (port == 443 || useSsl) ? 'wss' : 'ws'; return protocol + '://' + getServer(host, port) + (path ? path : ''); } function processError(reject, data, url, status) { if (data && data.message) { reject(data); } else { if (status == 404) { reject({ucode: 404, message: 'Resource not found' + (url ? ' ('+url+')' : '')}); } else if (url) { reject('Error while requesting [' + url + ']'); } else { reject('Unknown error from node'); } } } function prepare(url, params, config, callback) { var pkeys = [], queryParams = {}, newUri = url; if (typeof params === 'object') { pkeys = _.keys(params); } _.forEach(pkeys, function(pkey){ var prevURI = newUri; newUri = newUri.replace(':' + pkey, params[pkey]); if (prevURI === newUri) { queryParams[pkey] = params[pkey]; } }); config.params = queryParams; return callback(newUri, config); } function getResource(host, port, path, useSsl, forcedTimeout) { // Make sure host is defined - fix #537 if (!host) { return $q.reject('[http] invalid URL from host: ' + host); } var url = getUrl(host, port, path, useSsl); return function(params) { return $q(function(resolve, reject) { var config = { timeout: forcedTimeout || timeout, responseType: 'json' }; prepare(url, params, config, function(url, config) { $http.get(url, config) .success(function(data, status, headers, config) { resolve(data); }) .error(function(data, status, headers, config) { processError(reject, data, url, status); }); }); }); }; } function getResourceWithCache(host, port, path, useSsl, maxAge, autoRefresh, forcedTimeout, cachePrefix) { var url = getUrl(host, port, path, useSsl); maxAge = maxAge || csCache.constants.LONG; //console.debug('[http] will cache ['+url+'] ' + maxAge + 'ms' + (autoRefresh ? ' with auto-refresh' : '')); return function(params) { return $q(function(resolve, reject) { var config = { timeout: forcedTimeout || timeout, responseType: 'json' }; if (autoRefresh) { // redo the request if need config.cache = csCache.get(cachePrefix, maxAge, function (key, value, done) { console.debug('[http] Refreshing cache for ['+key+'] '); $http.get(key, config) .success(function (data) { config.cache.put(key, data); if (done) done(key, data); }); }); } else { config.cache = csCache.get(cachePrefix, maxAge); } prepare(url, params, config, function(url, config) { $http.get(url, config) .success(function(data) { resolve(data); }) .error(function(data, status) { processError(reject, data, url, status); }); }); }); }; } function postResource(host, port, path, useSsl, forcedTimeout) { var url = getUrl(host, port, path, useSsl); return function(data, params) { return $q(function(resolve, reject) { var config = { timeout: forcedTimeout || timeout, headers : {'Content-Type' : 'application/json;charset=UTF-8'} }; prepare(url, params, config, function(url, config) { $http.post(url, data, config) .success(function(data) { resolve(data); }) .error(function(data, status) { processError(reject, data, url, status); }); }); }); }; } function ws(host, port, path, useSsl, timeout) { if (!path) { console.error('calling csHttp.ws without path argument'); throw 'calling csHttp.ws without path argument'; } var uri = getWsUrl(host, port, path, useSsl); timeout = timeout || csSettings.data.timeout; function _waitOpen(self) { if (!self.delegate) throw new Error('Websocket not opened'); if (self.delegate.readyState == 1) { return $q.when(self.delegate); } if (self.delegate.readyState == 3) { return $q.reject('Unable to connect to websocket ['+self.delegate.url+']'); } if (self.waitDuration >= timeout) { self.waitRetryDelay = self.waitRetryDelay && Math.min(self.waitRetryDelay + 2000, 30000) || 2000; // add 2 seconds, until 30s) console.debug("[http] Will retry websocket [{0}] in {1}s...".format(self.path, Math.round(self.waitRetryDelay/1000))); } else if (Math.round(self.waitDuration / 1000) % 10 === 0){ console.debug('[http] Waiting websocket ['+self.path+']...'); } return $timeout(function(){ self.waitDuration += self.waitRetryDelay; return _waitOpen(self); }, self.waitRetryDelay); } function _open(self, callback, params) { if (!self.delegate) { self.path = path; self.callbacks = []; self.waitDuration = 0; self.waitRetryDelay = 200; prepare(uri, params, {}, function(uri) { self.delegate = new WebSocket(uri); self.delegate.onerror = function(e) { self.delegate.readyState=3; }; self.delegate.onmessage = function(e) { var obj = JSON.parse(e.data); _.forEach(self.callbacks, function(callback) { callback(obj); }); }; self.delegate.onopen = function(e) { console.debug('[http] Listening on websocket ['+self.path+']...'); sockets.push(self); self.delegate.openTime = Date.now(); }; self.delegate.onclose = function(closeEvent) { // Remove from sockets arrays var index = _.findIndex(sockets, function(socket){return socket.path === self.path;}); if (index >= 0) { sockets.splice(index,1); } // If close event comes from Cesium if (self.delegate.closing) { self.delegate = null; } // If unexpected close event, reopen the socket (fix #535) else { if (self.delegate.openTime) { console.debug('[http] Unexpected close of websocket [{0}] (open {1} ms ago): re-opening...', path, (Date.now() - self.delegate.openTime)); // Force new connection self.delegate = null; // Loop, but without the already registered callback _open(self, null, params); } else if (closeEvent) { console.debug('[http] TODO -- Unexpected close of websocket [{0}]: error code: '.format(path), closeEvent); // Force new connection self.delegate = null; // Loop, but without the already registered callback _open(self, null, params); } } }; }); } if (callback) self.callbacks.push(callback); return _waitOpen(self); } function _close(self) { if (self.delegate) { self.delegate.closing = true; console.debug('[http] Closing websocket ['+self.path+']...'); self.delegate.close(); self.callbacks = []; if (self.onclose) self.onclose(); } } function _remove(self, callback) { self.callbacks = _.reject(self.callbacks, function(item) { return item === callback; }); if (!self.callbacks.length) { _close(self); } } return { open: function(params) { return _open(this, null, params); }, on: function(callback, params) { return _open(this, callback, params); }, onListener: function(callback, params) { var self = this; _open(self, callback, params); return function() { _remove(self, callback); }; }, send: function(data) { var self = this; return _waitOpen(self) .then(function(){ if (self.delegate) self.delegate.send(data); }); }, close: function() { var self = this; _close(self); }, isClosed: function() { var self = this; return !self.delegate || self.delegate.closing; } }; } function closeAllWs() { if (sockets.length > 0) { console.debug('[http] Closing all websocket...'); _.forEach(sockets, function(sock) { sock.close(); }); sockets = []; // Reset socks list } } // See doc : https://gist.github.com/jlong/2428561 function parseUri(uri) { var protocol; if (uri.startsWith('duniter://')) { protocol = 'duniter'; uri = uri.replace('duniter://', 'http://'); } var parser = document.createElement('a'); parser.href = uri; var pathname = parser.pathname; if (pathname && pathname.startsWith('/')) { pathname = pathname.substring(1); } var result = { protocol: protocol ? protocol : parser.protocol, hostname: parser.hostname, host: parser.host, port: parser.port, username: parser.username, password: parser.password, pathname: pathname, search: parser.search, hash: parser.hash }; parser.remove(); return result; } /** * Open a URI (url, email, phone, ...) * @param event * @param link * @param type */ function openUri(uri, options) { options = options || {}; if (!uri.startsWith('http://') && !uri.startsWith('https://')) { var parts = parseUri(uri); if (!parts.protocol && options.type) { parts.protocol = (options.type == 'email') ? 'mailto:' : ((options.type == 'phone') ? 'tel:' : ''); uri = parts.protocol + uri; } // On desktop, open into external tool if (parts.protocol == 'mailto:' && Device.isDesktop()) { try { nw.Shell.openExternal(uri); return; } catch(err) { console.error("[http] Failed not open 'mailto:' URI into external tool."); } } // Check if device is enable, on special tel: or mailto: protocole var validProtocol = (parts.protocol == 'mailto:' || parts.protocol == 'tel:') && Device.enable; if (!validProtocol) { if (options.onError && typeof options.onError == 'function') { options.onError(uri); } return; } } // Note: If device enable, then target=_system will use InAppBrowser cordova plugin var openTarget = (options.target || (Device.enable ? '_system' : '_blank')); // If desktop, try to open into external browser if (openTarget === '_blank' || openTarget === '_system' && Device.isDesktop()) { try { nw.Shell.openExternal(uri); return; } catch(err) { console.error("[http] Failed not open URI into external browser."); } } // If desktop, should always open in new window (no tabs) var openOptions; if (openTarget === '_blank' && Device.isDesktop()) { if (nw && nw.Shell) { nw.Shell.openExternal(uri); return false; } // Override default options openOptions= "location=1,titlebar=1,status=1,menubar=1,toolbar=1,resizable=1,scrollbars=1"; // Add width/height if ($window.screen && $window.screen.width && $window.screen.height) { openOptions += ",width={0},height={1}".format(Math.trunc($window.screen.width/2), Math.trunc($window.screen.height/2)); } } var win = $window.open(uri, openTarget, openOptions); // Center the opened window if (openOptions && $window.screen && $window.screen.width && $window.screen.height) { win.moveTo($window.screen.width/2/2, $window.screen.height/2/2); win.focus(); } } // Get time in second (UTC) function getDateNow() { return moment().utc().unix(); } function isPositiveInteger(x) { // http://stackoverflow.com/a/1019526/11236 return /^\d+$/.test(x); } /** * Compare two software version numbers (e.g. 1.7.1) * Returns: * * 0 if they're identical * negative if v1 < v2 * positive if v1 > v2 * Nan if they in the wrong format * * E.g.: * * assert(version_number_compare("1.7.1", "1.6.10") > 0); * assert(version_number_compare("1.7.1", "1.7.10") < 0); * * "Unit tests": http://jsfiddle.net/ripper234/Xv9WL/28/ * * Taken from http://stackoverflow.com/a/6832721/11236 */ function compareVersionNumbers(v1, v2){ var v1parts = v1.split('.'); var v2parts = v2.split('.'); // First, validate both numbers are true version numbers function validateParts(parts) { for (var i = 0; i < parts.length; ++i) { if (!isPositiveInteger(parts[i])) { return false; } } return true; } if (!validateParts(v1parts) || !validateParts(v2parts)) { return NaN; } for (var i = 0; i < v1parts.length; ++i) { if (v2parts.length === i) { return 1; } if (v1parts[i] === v2parts[i]) { continue; } if (v1parts[i] > v2parts[i]) { return 1; } return -1; } if (v1parts.length != v2parts.length) { return -1; } return 0; } function isVersionCompatible(minVersion, actualVersion) { console.debug('[http] Checking actual version [{0}] is compatible with min expected version [{1}]'.format(actualVersion, minVersion)); return compareVersionNumbers(minVersion, actualVersion) <= 0; } var cache = angular.copy(csCache.constants); cache.clear = function() { console.debug('[http] Cleaning cache...'); csCache.clear(cachePrefix); }; return { get: getResource, getWithCache: getResourceWithCache, post: postResource, ws: ws, closeAllWs: closeAllWs, getUrl : getUrl, getServer: getServer, uri: { parse: parseUri, open: openUri }, date: { now: getDateNow }, version: { compare: compareVersionNumbers, isCompatible: isVersionCompatible }, cache: cache }; }]) ; angular.module('cesium.storage.services', [ 'cesium.config']) .factory('sessionStorage', ['$window', '$q', function($window, $q) { 'ngInject'; var exports = { storage: $window.sessionStorage || {} }; exports.put = function(key, value) { exports.storage[key] = value; return $q.when(); }; exports.get = function(key, defaultValue) { return $q.when(exports.storage[key] || defaultValue); }; exports.setObject = function(key, value) { exports.storage[key] = JSON.stringify(value); return $q.when(); }; exports.getObject = function(key) { return $q.when(JSON.parse(exports.storage[key] || 'null')); }; return exports; }]) .factory('localStorage', ['$window', '$q', '$log', 'sessionStorage', function($window, $q, $log, sessionStorage) { 'ngInject'; var appName = "Cesium", started = false, startPromise, isDevice = true, // default for device (override later) exports = { standard: { storage: null }, secure: { storage: null } }; // removeIf(device) // Use this workaround to avoid to wait ionicReady() event isDevice = false; // endRemoveIf(device) /* -- Use standard browser implementation -- */ exports.standard.put = function(key, value) { if (angular.isDefined(value) && value != null) { exports.standard.storage[key] = value; } else { exports.standard.storage.removeItem(key); } return $q.when(); }; exports.standard.get = function(key, defaultValue) { return $q.when(exports.standard.storage[key] || defaultValue); }; exports.standard.setObject = function(key, value) { exports.standard.storage[key] = JSON.stringify(value); return $q.when(); }; exports.standard.getObject = function(key) { return $q.when(JSON.parse(exports.standard.storage[key] || 'null')); }; /* -- Use secure storage (using a cordova plugin) -- */ // Set a value to the secure storage (or remove if value is not defined) exports.secure.put = function(key, value) { return $q(function(resolve, reject) { if (value !== undefined && value !== null) { exports.secure.storage.set( function (key) { resolve(); }, function (err) { $log.error(err); reject(err); }, key, value); } // Remove else { exports.secure.storage.remove( function () { resolve(); }, function (err) { $log.error(err); resolve(); // Error = not found }, key); } }); }; // Get a value from the secure storage exports.secure.get = function(key, defaultValue) { return $q(function(resolve, reject) { exports.secure.storage.get( function (value) { if (!value && defaultValue) { resolve(defaultValue); } else { resolve(value); } }, function (err) { $log.error(err); resolve(); // Error = not found }, key); }); }; // Set a object to the secure storage exports.secure.setObject = function(key, value) { $log.debug("[storage] Setting object into secure storage, using key=" + key); return $q(function(resolve, reject){ exports.secure.storage.set( resolve, reject, key, value ? JSON.stringify(value) : undefined); }); }; // Get a object from the secure storage exports.secure.getObject = function(key) { $log.debug("[storage] Getting object from secure storage, using key=" + key); return $q(function(resolve, reject){ exports.secure.storage.get( function(value) {resolve(JSON.parse(value||'null'));}, function(err) { $log.error(err); resolve(); // Error = not found }, key); }); }; function initStandardStorage() { // use local browser storage if ($window.localStorage) { console.debug('[storage] Starting {local} storage...'); exports.standard.storage = $window.localStorage; // Set standard storage as default _.forEach(_.keys(exports.standard), function(key) { exports[key] = exports.standard[key]; }); } // Fallback to session storage (locaStorage could have been disabled on some browser) else { console.debug('[storage] Starting {session} storage...'); // Set standard storage as default _.forEach(_.keys(sessionStorage), function(key) { exports[key] = sessionStorage[key]; }); } return $q.when(); } function initSecureStorage() { console.debug('[storage] Starting {secure} storage...'); // Set secure storage as default _.forEach(_.keys(exports.secure), function(key) { exports[key] = exports.secure[key]; }); var deferred = $q.defer(); // No secure storage plugin: fall back to standard storage if (!cordova.plugins || !cordova.plugins.SecureStorage) { initStandardStorage(); deferred.resolve(); } else { exports.secure.storage = new cordova.plugins.SecureStorage( function () { deferred.resolve(); }, function (err) { console.error('[storage] Could not use secure storage. Will use standard.', err); initStandardStorage(); deferred.resolve(); }, appName); } return deferred.promise; } exports.isStarted = function() { return started; }; exports.ready = function() { if (started) return $q.when(); return startPromise || start(); }; function start() { if (startPromise) return startPromise; var now = Date.now(); // Use Cordova secure storage plugin if (isDevice) { startPromise = initSecureStorage(); } // Use default browser local storage else { startPromise = initStandardStorage(); } return startPromise .then(function() { console.debug('[storage] Started in ' + (Date.now() - now) + 'ms'); started = true; startPromise = null; }); } // default action start(); return exports; }]) ; angular.module('cesium.device.services', ['ngResource', 'cesium.utils.services', 'cesium.settings.services']) .factory('Device', ['$translate', '$ionicPopup', '$q', 'ionicReady', function($translate, $ionicPopup, $q, ionicReady) { 'ngInject'; var CONST = { MAX_HEIGHT: 400, MAX_WIDTH: 400 }, exports = { // workaround to quickly no is device or not (even before the ready() event) enable: true }, cache = {}, started = false, startPromise; // removeIf(device) // workaround to quickly no is device or not (even before the ready() event) exports.enable = false; // endRemoveIf(device) function getPicture(options) { if (!exports.camera.enable) { return $q.reject("Camera not enable. Please call 'Device.ready()' once before use (e.g in app.js)."); } // Options is the sourceType by default if (options && (typeof options === "string")) { options = { sourceType: options }; } options = options || {}; // Make sure a source type has been given (if not, ask user) if (angular.isUndefined(options.sourceType)) { return $translate(['SYSTEM.PICTURE_CHOOSE_TYPE', 'SYSTEM.BTN_PICTURE_GALLERY', 'SYSTEM.BTN_PICTURE_CAMERA']) .then(function(translations){ return $ionicPopup.show({ title: translations['SYSTEM.PICTURE_CHOOSE_TYPE'], buttons: [ { text: translations['SYSTEM.BTN_PICTURE_GALLERY'], type: 'button', onTap: function(e) { return navigator.camera.PictureSourceType.PHOTOLIBRARY; } }, { text: translations['SYSTEM.BTN_PICTURE_CAMERA'], type: 'button button-positive', onTap: function(e) { return navigator.camera.PictureSourceType.CAMERA; } } ] }) .then(function(sourceType){ console.info('[camera] User select sourceType:' + sourceType); options.sourceType = sourceType; return exports.camera.getPicture(options); }); }); } options.quality = options.quality || 50; options.destinationType = options.destinationType || navigator.camera.DestinationType.DATA_URL; options.encodingType = options.encodingType || navigator.camera.EncodingType.PNG; options.targetWidth = options.targetWidth || CONST.MAX_WIDTH; options.targetHeight = options.targetHeight || CONST.MAX_HEIGHT; return $cordovaCamera.getPicture(options); } function scan(n) { if (!exports.enable) { return $q.reject("Barcode scanner not enable. Please call 'Device.ready()' once before use (e.g in app.js)."); } var deferred = $q.defer(); cordova.plugins.barcodeScanner.scan( function(result) { console.debug('[device] bar code result', result); if (!result.cancelled) { deferred.resolve(result.text); // make sure to convert into String } else { deferred.resolve(); } }, function(err) { console.error('[device] Error while using barcode scanner -> ' + err); deferred.reject(err); }, n); return deferred.promise; } function copy(text, callback) { if (!exports.enable) { return $q.reject('Device disabled'); } var deferred = $q.defer(); $cordovaClipboard .copy(text) .then(function () { // success if (callback) { callback(); } deferred.resolve(); }, function () { // error deferred.reject({message: 'ERROR.COPY_CLIPBOARD'}); }); return deferred.promise; } exports.clipboard = {copy: copy}; exports.camera = { getPicture : getPicture, scan: function(n){ console.warn('Deprecated use of Device.camera.scan(). Use Device.barcode.scan() instead'); return scan(n); } }; exports.barcode = { enable : false, scan: scan }; exports.keyboard = { enable: false, close: function() { if (!exports.keyboard.enable) return; cordova.plugins.Keyboard.close(); } }; // Numerical keyboard - fix #30 exports.keyboard.digit = { settings: { bindModel: function(modelScope, modelPath, settings) { settings = settings || {}; modelScope = modelScope || $rootScope; var getModelValue = function() { return (modelPath||'').split('.').reduce(function(res, path) { return res ? res[path] : undefined; }, modelScope); }; var setModelValue = function(value) { var paths = (modelPath||'').split('.'); var property = paths.length && paths[paths.length-1]; paths.reduce(function(res, path) { if (path == property) { res[property] = value; return; } return res[path]; }, modelScope); }; settings.action = settings.action || function(number) { setModelValue((getModelValue() ||'') + number); }; if (settings.decimal) { settings.decimalSeparator = settings.decimalSeparator || '.'; settings.leftButton = settings.leftButton = { html: '.', action: function () { var text = getModelValue() || ''; // only one '.' allowed if (text.indexOf(settings.decimalSeparator) >= 0) return; // Auto add zero when started with '.' if (!text.trim().length) { text = '0'; } setModelValue(text + settings.decimalSeparator); } }; } settings.rightButton = settings.rightButton || { html: '', action: function() { var text = getModelValue(); if (text && text.length) { text = text.slice(0, -1); setModelValue(text); } } }; return settings; } } }; exports.isIOS = function() { return !!navigator.userAgent.match(/iPhone | iPad | iPod/i) || ionic.Platform.isIOS(); }; exports.isDesktop = function() { if (!angular.isDefined(cache.isDesktop)) { try { // Should have NodeJs and NW cache.isDesktop = !exports.enable && !!process && !!nw && !!nw.App; } catch (err) { cache.isDesktop = false; } } return cache.isDesktop; }; exports.isWeb = function() { return !exports.enable && !exports.isDesktop(); }; exports.ready = function() { if (started) return $q.when(); return startPromise || exports.start(); }; exports.start = function() { startPromise = ionicReady() .then(function(){ exports.enable = window.cordova && cordova && cordova.plugins; if (exports.enable){ exports.camera.enable = !!navigator.camera; exports.keyboard.enable = cordova && cordova.plugins && !!cordova.plugins.Keyboard; exports.barcode.enable = cordova && cordova.plugins && !!cordova.plugins.barcodeScanner; exports.clipboard.enable = cordova && cordova.plugins && !!cordova.plugins.clipboard; if (exports.keyboard.enable) { angular.extend(exports.keyboard, cordova.plugins.Keyboard); } console.debug('[device] Ionic platform ready, with [camera: {0}] [barcode scanner: {1}] [keyboard: {2}] [clipboard: {3}]' .format(exports.camera.enable, exports.barcode.enable, exports.keyboard.enable, exports.clipboard.enable)); } else { console.debug('[device] Ionic platform ready - no device detected.'); } started = true; startPromise = null; }); return startPromise; }; return exports; }]) ; angular.module('cesium.currency.services', ['ngApi', 'cesium.bma.services']) .factory('csCurrency', ['$rootScope', '$q', '$timeout', 'BMA', 'Api', 'csSettings', function($rootScope, $q, $timeout, BMA, Api, csSettings) { 'ngInject'; function factory(id, BMA) { var constants = { // Avoid to many call on well known currencies WELL_KNOWN_CURRENCIES: { g1: { firstBlockTime: 1488987127, medianTimeOffset: 3600 } } }, data = {}, started = false, startPromise, listeners, api = new Api(this, "csCurrency-" + id); function powBase(amount, base) { return base <= 0 ? amount : amount * Math.pow(10, base); } 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); } function loadData() { // Load currency from default node return $q.all([ // get parameters loadParameters() .then(function(parameters) { // load first block info return loadFirstBlock(parameters.currency); }), // get current UD loadCurrentUD(), // call extensions api.data.raisePromise.load(data) ]) .catch(function(err) { resetData(); 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(); } 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; }); } 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; }); } /** * 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); } // 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 console.debug('[currency] Received new block [' + block.number + '-' + block.hash + ']'); data.currentBlock = block; data.currentBlock.receivedAt = moment().utc().unix(); data.medianTime = block.medianTime; data.membersCount = block.membersCount; // Update UD if (block.dividend) { data.currentUD = block.dividend; } // Dispatch to extensions api.data.raise.newBlock(block); } 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 removeListeners() { _.forEach(listeners, function(remove){ remove(); }); listeners = []; } function ready() { if (started) return $q.when(data); return startPromise || start(); } function stop() { console.debug('[currency] Stopping...'); removeListeners(); resetData(); } function restart() { stop(); return $timeout(start, 200); } function start() { console.debug('[currency] Starting...'); var now = Date.now(); startPromise = BMA.ready() // Load data .then(loadData) // Emit ready event .then(function() { addListeners(); console.debug('[currency] Started in ' + (Date.now() - now) + 'ms'); started = true; startPromise = null; // Emit event (used by plugins) api.data.raise.ready(data); }) .then(function(){ return data; }); return startPromise; } var currentBlockField = getDataField('currentBlock'); function getCurrent(cache) { // Get field (and make sure service is started) return currentBlockField() .then(function(currentBlock) { var now = moment().utc().unix(); if (cache) { if (currentBlock && (now - currentBlock.receivedAt) < 60/*1min*/) { //console.debug('[currency] Use current block #'+ currentBlock.number +' from cache (age='+ (now - currentBlock.receivedAt) + 's)'); return currentBlock; } 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 BMA.blockchain.current() .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: moment().utc().unix()}; } throw err; }) .then(function(current) { data.currentBlock = current; data.currentBlock.receivedAt = now; return current; }); }); } function getLastValidBlock() { if (csSettings.data.blockValidityWindow <= 0) { return getCurrent(true); } return getCurrent(true) .then(function(current) { var number = current.number - csSettings.data.blockValidityWindow; return (number > 0) ? BMA.blockchain.block({block: number}) : current; }); } // Get time in second (UTC - medianTimeOffset) function getDateNow() { return moment().utc().unix() - (data.medianTimeOffset || constants.WELL_KNOWN_CURRENCIES.g1.medianTimeOffset); } // 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(); } }; } var service = factory('default', BMA); service.instance = factory; return service; }]); //var Base58, Base64, scrypt_module_factory = null, nacl_factory = null; angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.settings.services']) .factory('BMA', ['$q', '$window', '$rootScope', '$timeout', 'Api', 'Device', 'csConfig', 'csSettings', 'csHttp', function($q, $window, $rootScope, $timeout, Api, Device, csConfig, csSettings, csHttp) { 'ngInject'; function BMA(host, port, useSsl, useCache) { var pubkey = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}"; var // TX output conditions SIG = "SIG\\(([0-9a-zA-Z]{43,44})\\)", XHX = 'XHX\\(([A-F0-9]{1,64})\\)', CSV = 'CSV\\(([0-9]{1,8})\\)', CLTV = 'CLTV\\(([0-9]{1,10})\\)', OUTPUT_FUNCTION = SIG+'|'+XHX+'|'+CSV+'|'+CLTV, OUTPUT_OPERATOR = '(&&)|(\\|\\|)', OUTPUT_FUNCTIONS = OUTPUT_FUNCTION+'([ ]*' + OUTPUT_OPERATOR + '[ ]*' + OUTPUT_FUNCTION +')*', OUTPUT_OBJ = 'OBJ\\(([0-9]+)\\)', OUTPUT_OBJ_OPERATOR = OUTPUT_OBJ + '[ ]*' + OUTPUT_OPERATOR + '[ ]*' + OUTPUT_OBJ, REGEX_ENDPOINT_PARAMS = "( ([a-z_][a-z0-9-_.ğĞ]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( (.+))?", api = { BMA: 'BASIC_MERKLED_API', BMAS: 'BMAS', WS2P: 'WS2P', BMATOR: 'BMATOR', WS2PTOR: 'WS2PTOR' }, regexp = { USER_ID: "[0-9a-zA-Z-_]+", CURRENCY: "[0-9a-zA-Z-_]+", PUBKEY: pubkey, PUBKEY_WITH_CHECKSUM: "(" + pubkey +"):([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{3})", COMMENT: "[ a-zA-Z0-9-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]*", INVALID_COMMENT_CHARS: "[^ a-zA-Z0-9-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]*", // duniter://[uid]:[pubkey]@[host]:[port] URI_WITH_AT: "duniter://(?:([A-Za-z0-9_-]+):)?("+pubkey+"@([a-zA-Z0-9-.]+.[ a-zA-Z0-9-_:/;*?!^\\+=@&~#|<>%.]+)", URI_WITH_PATH: "duniter://([a-zA-Z0-9-.]+.[a-zA-Z0-9-_:.]+)/("+pubkey+")(?:/([A-Za-z0-9_-]+))?", BMA_ENDPOINT: api.BMA + REGEX_ENDPOINT_PARAMS, BMAS_ENDPOINT: api.BMAS + REGEX_ENDPOINT_PARAMS, WS2P_ENDPOINT: api.WS2P + " ([a-f0-9]{8})"+ REGEX_ENDPOINT_PARAMS, BMATOR_ENDPOINT: api.BMATOR + " ([a-z0-9-_.]*|[0-9.]+|[0-9a-f:]+.onion)(?: ([0-9]+))?", WS2PTOR_ENDPOINT: api.WS2PTOR + " ([a-f0-9]{8}) ([a-z0-9-_.]*|[0-9.]+|[0-9a-f:]+.onion)(?: ([0-9]+))?(?: (.+))?" }, errorCodes = { REVOCATION_ALREADY_REGISTERED: 1002, HTTP_LIMITATION: 1006, IDENTITY_SANDBOX_FULL: 1007, NO_MATCHING_IDENTITY: 2001, UID_ALREADY_USED: 2003, NO_MATCHING_MEMBER: 2004, NO_IDTY_MATCHING_PUB_OR_UID: 2021, WRONG_SIGNATURE_MEMBERSHIP: 2006, MEMBERSHIP_ALREADY_SEND: 2007, NO_CURRENT_BLOCK: 2010, BLOCK_NOT_FOUND: 2011, SOURCE_ALREADY_CONSUMED: 2015, TX_INPUTS_OUTPUTS_NOT_EQUAL: 2024, TX_OUTPUT_SUM_NOT_EQUALS_PREV_DELTAS: 2025, TX_ALREADY_PROCESSED: 2030 }, constants = { PROTOCOL_VERSION: 10, ROOT_BLOCK_HASH: 'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', LIMIT_REQUEST_COUNT: 5, // simultaneous async request to a Duniter node LIMIT_REQUEST_DELAY: 1000, // time (in second) to wait between to call of a rest request regexp: regexp, api: api }, listeners, that = this; that.api = new Api(this, 'BMA-' + that.server); that.started = false; that.init = init; // Allow to force SSL connection with port different from 443 that.forceUseSsl = (csConfig.httpsMode === 'true' || csConfig.httpsMode === true || csConfig.httpsMode === 'force') || ($window.location && $window.location.protocol === 'https:') ? true : false; if (that.forceUseSsl) { console.debug('[BMA] Enable SSL (forced by config or detected in URL)'); } if (host) { init(host, port, useSsl, useCache); } that.useCache = useCache; // need here because used in get() function function init(host, port, useSsl, useCache) { if (that.started) that.stop(); that.alive = false; that.cache = _emptyCache(); // Use settings as default, if exists if (csSettings.data && csSettings.data.node) { host = host || csSettings.data.node.host; port = port || csSettings.data.node.port; useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443 || csSettings.data.node.useSsl || that.forceUseSsl); useCache = angular.isDefined(useCache) ? useCache : true; } if (!host) { return; // could not init yet } that.host = host; that.port = port || 80; that.useSsl = angular.isDefined(useSsl) ? useSsl : (that.port == 443 || that.forceUseSsl); that.useCache = angular.isDefined(useCache) ? useCache : false; that.server = csHttp.getServer(host, port); that.url = csHttp.getUrl(host, port, ''/*path*/, useSsl); } function exact(regexpContent) { return new RegExp("^" + regexpContent + "$"); } function test(regexpContent) { return new RegExp(regexpContent); } function _emptyCache() { return { getByPath: {}, postByPath: {}, wsByPath: {} }; } function closeWs() { if (!that.cache) return; console.warn('[BMA] Closing all websockets...'); _.keys(that.cache.wsByPath||{}).forEach(function(key) { var sock = that.cache.wsByPath[key]; sock.close(); }); that.cache.wsByPath = {}; } that.cleanCache = function() { console.debug('[BMA] Cleaning requests cache...'); closeWs(); that.cache = _emptyCache(); }; get = function (path, cacheTime) { cacheTime = that.useCache && cacheTime; var cacheKey = path + (cacheTime ? ('#'+cacheTime) : ''); var getRequestFn = function(params) { if (!that.started) { if (!that._startPromise) { console.warn('[BMA] Trying to get [{0}] before start(). Waiting...'.format(path)); } return that.ready().then(function() { return getRequestFn(params); }); } var request = that.cache.getByPath[cacheKey]; if (!request) { if (cacheTime) { request = csHttp.getWithCache(that.host, that.port, path, that.useSsl, cacheTime); } else { request = csHttp.get(that.host, that.port, path, that.useSsl); } that.cache.getByPath[cacheKey] = request; } var execCount = 1; return request(params) .catch(function(err){ // If node return too many requests error if (err && err.ucode == exports.errorCodes.HTTP_LIMITATION) { // If max number of retry not reach if (execCount <= exports.constants.LIMIT_REQUEST_COUNT) { if (execCount === 1) { console.warn("[BMA] Too many HTTP requests: Will wait then retry..."); // Update the loading message (if exists) UIUtils.loading.update({template: "COMMON.LOADING_WAIT"}); } // Wait 1s then retry return $timeout(function() { execCount++; return request(params); }, exports.constants.LIMIT_REQUEST_DELAY); } } throw err; }); }; return getRequestFn; }; post = function(path) { var postRequest = function(obj, params) { if (!that.started) { if (!that._startPromise) { console.error('[BMA] Trying to post [{0}] before start()...'.format(path)); } return that.ready().then(function() { return postRequest(obj, params); }); } var request = that.cache.postByPath[path]; if (!request) { request = csHttp.post(that.host, that.port, path, that.useSsl); that.cache.postByPath[path] = request; } return request(obj, params); }; return postRequest; }; ws = function(path) { return function() { var sock = that.cache.wsByPath[path]; if (!sock || sock.isClosed()) { sock = csHttp.ws(that.host, that.port, path, that.useSsl); // When close, remove from cache sock.onclose = function() { delete that.cache.wsByPath[path]; }; that.cache.wsByPath[path] = sock; } return sock; }; }; that.isAlive = function() { return csHttp.get(that.host, that.port, '/node/summary', that.useSsl)() .then(function(json) { var software = json && json.duniter && json.duniter.software; var isCompatible = true; // Check duniter min version if (software === 'duniter' && json.duniter.version && csSettings.data.minVersion && true) { isCompatible = csHttp.version.isCompatible(csSettings.data.minVersion, json.duniter.version); } // gchange-pod else if (software === 'gchange-pod' && json.duniter.version && csSettings.data.plugins.es.minVersion && true) { isCompatible = csHttp.version.isCompatible(csSettings.data.plugins.es.minVersion, json.duniter.version); } // TODO: check version of other software (DURS, Juniter, etc.) else { console.debug('[BMA] Unknown node software [{0} v{1}]: could not check compatibility.'.format(software || '?', json.duniter.version || '?')); } if (!isCompatible) { console.error('[BMA] Incompatible node [{0} v{1}]: expected at least v{2}'.format(software, json.duniter.version, csSettings.data.minVersion)); } return isCompatible; }) .catch(function() { return false; }); }; function removeListeners() { _.forEach(listeners, function(remove){ remove(); }); listeners = []; } function addListeners() { listeners = [ // Listen if node changed csSettings.api.data.on.changed($rootScope, onSettingsChanged, this) ]; } function onSettingsChanged(settings) { var server = csHttp.getUrl(settings.node.host, settings.node.port, ''/*path*/, settings.node.useSsl); var hasChanged = (server != that.url); if (hasChanged) { init(settings.node.host, settings.node.port, settings.node.useSsl, that.useCache); that.restart(); } } that.isStarted = function() { return that.started; }; that.ready = function() { if (that.started) return $q.when(true); return that._startPromise || that.start(); }; that.start = function() { if (that._startPromise) return that._startPromise; if (that.started) return $q.when(that.alive); if (!that.host) { return csSettings.ready() .then(function() { that.init(); // Always enable cache that.useCache = true; return that.start(); // recursive call }); } if (that.useSsl) { console.debug('[BMA] Starting [{0}] (SSL on)...'.format(that.server)); } else { console.debug('[BMA] Starting [{0}]...'.format(that.server)); } var now = Date.now(); that._startPromise = $q.all([ csSettings.ready, that.isAlive() ]) .then(function(res) { that.alive = res[1]; if (!that.alive) { console.error('[BMA] Could not start [{0}]: node unreachable'.format(that.server)); that.started = true; delete that._startPromise; return false; } // Add listeners if (!listeners || listeners.length === 0) { addListeners(); } console.debug('[BMA] Started in '+(Date.now()-now)+'ms'); that.api.node.raise.start(); that.started = true; delete that._startPromise; return true; }); return that._startPromise; }; that.stop = function() { console.debug('[BMA] Stopping...'); removeListeners(); csHttp.cache.clear(); that.cleanCache(); that.alive = false; that.started = false; delete that._startPromise; that.api.node.raise.stop(); }; that.restart = function() { that.stop(); return $timeout(that.start, 200) .then(function(alive) { if (alive) { that.api.node.raise.restart(); } return alive; }); }; that.api.registerEvent('node', 'start'); that.api.registerEvent('node', 'stop'); that.api.registerEvent('node', 'restart'); var exports = { errorCodes: errorCodes, constants: constants, regexp: { USER_ID: exact(regexp.USER_ID), COMMENT: exact(regexp.COMMENT), PUBKEY: exact(regexp.PUBKEY), PUBKEY_WITH_CHECKSUM: exact(regexp.PUBKEY_WITH_CHECKSUM), CURRENCY: exact(regexp.CURRENCY), URI: exact(regexp.URI), BMA_ENDPOINT: exact(regexp.BMA_ENDPOINT), BMAS_ENDPOINT: exact(regexp.BMAS_ENDPOINT), WS2P_ENDPOINT: exact(regexp.WS2P_ENDPOINT), BMATOR_ENDPOINT: exact(regexp.BMATOR_ENDPOINT), WS2PTOR_ENDPOINT: exact(regexp.WS2PTOR_ENDPOINT), // TX output conditions TX_OUTPUT_SIG: exact(SIG), TX_OUTPUT_FUNCTION: test(OUTPUT_FUNCTION), TX_OUTPUT_OBJ_OPERATOR_AND: test(OUTPUT_OBJ + '([ ]*&&[ ]*(' + OUTPUT_OBJ + '))+'), TX_OUTPUT_OBJ_OPERATOR_OR: test(OUTPUT_OBJ + '([ ]*\\|\\|[ ]*(' + OUTPUT_OBJ + '))+'), TX_OUTPUT_OBJ: test(OUTPUT_OBJ), TX_OUTPUT_OBJ_OPERATOR: test(OUTPUT_OBJ_OPERATOR), TX_OUTPUT_OBJ_PARENTHESIS: test('\\(('+OUTPUT_OBJ+')\\)'), TX_OUTPUT_FUNCTIONS: test(OUTPUT_FUNCTIONS) }, node: { summary: get('/node/summary', csHttp.cache.LONG), same: function(host2, port2) { return host2 === that.host && ((!that.port && !port2) || (that.port == port2||80)) && (that.useSsl == (port2 && port2 === 443)); }, forceUseSsl: that.forceUseSsl }, network: { peering: { self: get('/network/peering'), peers: get('/network/peering/peers') }, peers: get('/network/peers'), ws2p: { info: get('/network/ws2p/info'), heads: get('/network/ws2p/heads') } }, wot: { lookup: get('/wot/lookup/:search', csHttp.cache.MEDIUM), certifiedBy: get('/wot/certified-by/:pubkey'), certifiersOf: get('/wot/certifiers-of/:pubkey'), member: { all: get('/wot/members', csHttp.cache.LONG), pending: get('/wot/pending', csHttp.cache.SHORT) }, requirements: function(params, withCache) { // No cache by default if (withCache !== true) return exports.raw.wot.requirements(params); return exports.raw.wot.requirementsWithCache(params); }, add: post('/wot/add'), certify: post('/wot/certify'), revoke: post('/wot/revoke') }, blockchain: { parameters: get('/blockchain/parameters', csHttp.cache.VERY_LONG), block: get('/blockchain/block/:block', csHttp.cache.SHORT), blocksSlice: get('/blockchain/blocks/:count/:from'), current: get('/blockchain/current', csHttp.cache.SHORT), membership: post('/blockchain/membership'), stats: { ud: get('/blockchain/with/ud', csHttp.cache.MEDIUM), tx: get('/blockchain/with/tx'), newcomers: get('/blockchain/with/newcomers', csHttp.cache.MEDIUM), hardship: get('/blockchain/hardship/:pubkey'), difficulties: get('/blockchain/difficulties') } }, tx: { sources: get('/tx/sources/:pubkey', csHttp.cache.SHORT), process: post('/tx/process'), history: { all: function(params) { return exports.raw.tx.history.all(params) .then(function(res) { res.history = res.history || {}; // Clean sending and pendings, because already returned by tx/history/:pubkey/pending res.history.sending = []; res.history.pendings = []; return res; }); }, times: function(params, withCache) { // No cache by default return ((withCache !== true) ? exports.raw.tx.history.times(params) : exports.raw.tx.history.timesWithCache(params)) .then(function(res) { res.history = res.history || {}; // Clean sending and pendings, because already returned by tx/history/:pubkey/pending res.history.sending = []; res.history.pendings = []; return res; }); }, blocks: get('/tx/history/:pubkey/blocks/:from/:to', csHttp.cache.LONG), pending: get('/tx/history/:pubkey/pending') } }, ud: { history: get('/ud/history/:pubkey') }, uri: {}, version: {}, raw: { wot: { requirementsWithCache: get('/wot/requirements/:pubkey', csHttp.cache.LONG), requirements: get('/wot/requirements/:pubkey') }, tx: { history: { timesWithCache: get('/tx/history/:pubkey/times/:from/:to', csHttp.cache.LONG), times: get('/tx/history/:pubkey/times/:from/:to'), all: get('/tx/history/:pubkey') } }, } }; exports.tx.parseUnlockCondition = function(unlockCondition) { //console.debug('[BMA] Parsing unlock condition: {0}.'.format(unlockCondition)); var convertedOutput = unlockCondition; var treeItems = []; var treeItem; var treeItemId; var childrenContent; var childrenMatches; var functions = {}; // Parse functions, then replace with an 'OBJ()' generic function, used to build a object tree var matches = exports.regexp.TX_OUTPUT_FUNCTION.exec(convertedOutput); while(matches) { treeItem = {}; treeItemId = 'OBJ(' + treeItems.length + ')'; treeItem.type = convertedOutput.substr(matches.index, matches[0].indexOf('(')); treeItem.value = matches[1] || matches[2] || matches[3] || matches[4]; // get value from regexp OUTPUT_FUNCTION treeItems.push(treeItem); functions[treeItem.type] = functions[treeItem.type]++ || 1; convertedOutput = convertedOutput.replace(matches[0], treeItemId); matches = exports.regexp.TX_OUTPUT_FUNCTION.exec(convertedOutput); } var loop = true; while(loop) { // Parse AND operators matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_AND.exec(convertedOutput); loop = !!matches; while (matches) { treeItem = {}; treeItemId = 'OBJ(' + treeItems.length + ')'; treeItem.type = 'AND'; treeItem.children = []; treeItems.push(treeItem); childrenContent = matches[0]; childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent); while(childrenMatches) { treeItem.children.push(treeItems[childrenMatches[1]]); childrenContent = childrenContent.replace(childrenMatches[0], ''); childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent); } convertedOutput = convertedOutput.replace(matches[0], treeItemId); matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_AND.exec(childrenContent); } // Parse OR operators matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_OR.exec(convertedOutput); loop = loop || !!matches; while (matches) { treeItem = {}; treeItemId = 'OBJ(' + treeItems.length + ')'; treeItem.type = 'OR'; treeItem.children = []; treeItems.push(treeItem); childrenContent = matches[0]; childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent); while(childrenMatches) { treeItem.children.push(treeItems[childrenMatches[1]]); childrenContent = childrenContent.replace(childrenMatches[0], ''); childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent); } convertedOutput = convertedOutput.replace(matches[0], treeItemId); matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_AND.exec(convertedOutput); } // Remove parenthesis matches = exports.regexp.TX_OUTPUT_OBJ_PARENTHESIS.exec(convertedOutput); loop = loop || !!matches; while (matches) { convertedOutput = convertedOutput.replace(matches[0], matches[1]); matches = exports.regexp.TX_OUTPUT_OBJ_PARENTHESIS.exec(convertedOutput); } } functions = _.keys(functions); if (functions.length === 0) { console.error('[BMA] Unparseable unlock condition: ', output); return; } console.debug('[BMA] Unlock conditions successfully parsed:', treeItem); return { unlockFunctions: functions, unlockTree: treeItem }; }; exports.node.parseEndPoint = function(endpoint, epPrefix) { // Try BMA var matches = exports.regexp.BMA_ENDPOINT.exec(endpoint); if (matches) { return { "dns": matches[2] || '', "ipv4": matches[4] || '', "ipv6": matches[6] || '', "port": matches[8] || 80, "useSsl": matches[8] && matches[8] == 443, "path": matches[10], "useBma": true }; } // Try BMAS matches = exports.regexp.BMAS_ENDPOINT.exec(endpoint); if (matches) { return { "dns": matches[2] || '', "ipv4": matches[4] || '', "ipv6": matches[6] || '', "port": matches[8] || 80, "useSsl": true, "path": matches[10], "useBma": true }; } // Try BMATOR matches = exports.regexp.BMATOR_ENDPOINT.exec(endpoint); if (matches) { return { "dns": matches[1] || '', "port": matches[2] || 80, "useSsl": false, "useTor": true, "useBma": true }; } // Try WS2P matches = exports.regexp.WS2P_ENDPOINT.exec(endpoint); if (matches) { return { "ws2pid": matches[1] || '', "dns": matches[3] || '', "ipv4": matches[5] || '', "ipv6": matches[7] || '', "port": matches[9] || 80, "useSsl": matches[9] && matches[9] == 443, "path": matches[11] || '', "useWs2p": true }; } // Try WS2PTOR matches = exports.regexp.WS2PTOR_ENDPOINT.exec(endpoint); if (matches) { return { "ws2pid": matches[1] || '', "dns": matches[2] || '', "port": matches[3] || 80, "path": matches[4] || '', "useSsl": false, "useTor": true, "useWs2p": true }; } // Use generic match if (epPrefix) { matches = exact(epPrefix + REGEX_ENDPOINT_PARAMS).exec(endpoint); if (matches) { return { "dns": matches[2] || '', "ipv4": matches[4] || '', "ipv6": matches[6] || '', "port": matches[8] || 80, "useSsl": matches[8] && matches[8] == 443, "path": matches[10], "useBma": false }; } } }; exports.copy = function(otherNode) { var wasStarted = that.started; var server = csHttp.getUrl(otherNode.host, otherNode.port, ''/*path*/, otherNode.useSsl); var hasChanged = (server !== that.url); if (hasChanged) { that.init(otherNode.host, otherNode.port, otherNode.useSsl, that.useCache/*keep original value*/); // Restart (only if was already started) return wasStarted ? that.restart() : $q.when(); } }; exports.wot.member.uids = function() { return exports.wot.member.all() .then(function(res){ return res.results.reduce(function(res, member){ res[member.pubkey] = member.uid; return res; }, {}); }); }; exports.wot.member.get = function(pubkey) { return exports.wot.member.uids() .then(function(memberUidsByPubkey){ var uid = memberUidsByPubkey[pubkey]; return { pubkey: pubkey, uid: (uid ? uid : null) }; }); }; exports.wot.member.getByUid = function(uid) { return exports.wot.member.all() .then(function(res){ return _.findWhere(res.results, {uid: uid}); }); }; /** * Return all expected blocks * @param blockNumbers a rray of block number */ exports.blockchain.blocks = function(blockNumbers){ return exports.raw.getHttpRecursive(exports.blockchain.block, 'block', blockNumbers); }; /** * Return all expected blocks * @param blockNumbers a rray of block number */ exports.network.peering.peersByLeaves = function(leaves){ return exports.raw.getHttpRecursive(exports.network.peering.peers, 'leaf', leaves, 0, 10); }; exports.raw.getHttpRecursive = function(httpGetRequest, paramName, paramValues, offset, size) { offset = angular.isDefined(offset) ? offset : 0; size = size || exports.constants.LIMIT_REQUEST_COUNT; return $q(function(resolve, reject) { var result = []; var jobs = []; _.each(paramValues.slice(offset, offset+size), function(paramValue) { var requestParams = {}; requestParams[paramName] = paramValue; jobs.push( httpGetRequest(requestParams) .then(function(res){ if (!res) return; result.push(res); }) ); }); $q.all(jobs) .then(function() { if (offset < paramValues.length - 1) { $timeout(function() { exports.raw.getHttpRecursive(httpGetRequest, paramName, paramValues, offset+size, size) .then(function(res) { if (!res || !res.length) { resolve(result); return; } resolve(result.concat(res)); }) .catch(function(err) { reject(err); }); }, exports.constants.LIMIT_REQUEST_DELAY); } else { resolve(result); } }) .catch(function(err){ if (err && err.ucode === exports.errorCodes.HTTP_LIMITATION) { resolve(result); } else { reject(err); } }); }); }; exports.raw.getHttpWithRetryIfLimitation = function(exec) { return exec() .catch(function(err){ // When too many request, retry in 3s if (err && err.ucode == exports.errorCodes.HTTP_LIMITATION) { return $timeout(function() { // retry return exports.raw.getHttpWithRetryIfLimitation(exec); }, exports.constants.LIMIT_REQUEST_DELAY); } }); }; exports.blockchain.lastUd = function() { return exports.blockchain.stats.ud() .then(function(res) { if (!res.result.blocks || !res.result.blocks.length) { return null; } var lastBlockWithUD = res.result.blocks[res.result.blocks.length - 1]; return exports.blockchain.block({block: lastBlockWithUD}) .then(function(block){ return (block.unitbase > 0) ? block.dividend * Math.pow(10, block.unitbase) : block.dividend; }); }); }; exports.uri.parse = function(uri) { return $q(function(resolve, reject) { var pubkey; // If pubkey: not need to parse if (exact(regexp.PUBKEY).test(uri)) { resolve({ pubkey: uri }); } // If pubkey+checksum else if (exact(regexp.PUBKEY_WITH_CHECKSUM).test(uri)) { console.debug("[BMA.parse] Detecting a pubkey with checksum: " + uri); var matches = exports.regexp.PUBKEY_WITH_CHECKSUM.exec(uri); pubkey = matches[1]; var checksum = matches[2]; console.debug("[BMA.parse] Detecting a pubkey {"+pubkey+"} with checksum {" + checksum + "}"); var expectedChecksum = csCrypto.util.pkChecksum(pubkey); console.debug("[BMA.parse] Expecting checksum for pubkey is {" + expectedChecksum + "}"); if (checksum != expectedChecksum) { reject( {message: 'ERROR.PUBKEY_INVALID_CHECKSUM'}); } else { resolve({ pubkey: pubkey }); } } else if(uri.startsWith('duniter://')) { var parser = csHttp.uri.parse(uri), uid, currency = parser.host.indexOf('.') === -1 ? parser.host : null, host = parser.host.indexOf('.') !== -1 ? parser.host : null; if (parser.username) { if (parser.password) { uid = parser.username; pubkey = parser.password; } else { pubkey = parser.username; } } if (parser.pathname) { var paths = parser.pathname.split('/'); var pathCount = !paths ? 0 : paths.length; var index = 0; if (!currency && pathCount > index) { currency = paths[index++]; } if (!pubkey && pathCount > index) { pubkey = paths[index++]; } if (!uid && pathCount > index) { uid = paths[index++]; } if (pathCount > index) { reject( {message: 'Bad Duniter URI format. Invalid path (incomplete or redundant): '+ parser.pathname}); return; } } if (!currency){ if (host) { csHttp.get(host + '/blockchain/parameters')() .then(function(parameters){ resolve({ uid: uid, pubkey: pubkey, host: host, currency: parameters.currency }); }) .catch(function(err) { console.error(err); reject({message: 'Could not get node parameter. Currency could not be retrieve'}); }); } else { reject({message: 'Bad Duniter URI format. Missing currency name (or node address).'}); return; } } else { if (!host) { resolve({ uid: uid, pubkey: pubkey, currency: currency }); } // Check if currency are the same (between node and uri) return csHttp.get(host + '/blockchain/parameters')() .then(function(parameters){ if (parameters.currency !== currency) { reject( {message: "Node's currency ["+parameters.currency+"] does not matched URI's currency ["+currency+"]."}); return; } resolve({ uid: uid, pubkey: pubkey, host: host, currency: currency }); }) .catch(function(err) { console.error(err); reject({message: 'Could not get node parameter. Currency could not be retrieve'}); }); } } else { console.debug("[BMA.parse] Could not parse URI: " + uri); reject({message: 'ERROR.UNKNOWN_URI_FORMAT'}); } }) // Check values against regex .then(function(result) { if (!result) return; if (result.pubkey && !(exact(regexp.PUBKEY).test(result.pubkey))) { throw {message: "Invalid pubkey format [" + result.pubkey + "]"}; } if (result.uid && !(exact(regexp.USER_ID).test(result.uid))) { throw {message: "Invalid uid format [" + result.uid + "]"}; } if (result.currency && !(exact(regexp.CURRENCY).test(result.currency))) { throw {message: "Invalid currency format ["+result.currency+"]"}; } return result; }); }; // Define get latest release (or fake function is no URL defined) var duniterLatestReleaseUrl = csSettings.data.duniterLatestReleaseUrl && csHttp.uri.parse(csSettings.data.duniterLatestReleaseUrl); exports.raw.getLatestRelease = duniterLatestReleaseUrl ? csHttp.getWithCache(duniterLatestReleaseUrl.host, duniterLatestReleaseUrl.port, "/" + duniterLatestReleaseUrl.pathname, /*useSsl*/ (+(duniterLatestReleaseUrl.port) === 443 || duniterLatestReleaseUrl.protocol === 'https:' || that.forceUseSsl), csHttp.cache.LONG ) : // No URL define: use a fake function function() { return $q.when(); }; exports.version.latest = function() { return exports.raw.getLatestRelease() .then(function (json) { if (!json) return; if (json.name && json.html_url) { return { version: json.name, url: json.html_url }; } if (json.tag_name && json.html_url) { return { version: json.tag_name.substring(1), url: json.html_url }; } }) .catch(function(err) { // silent (just log it) console.error('[BMA] Failed to get Duniter latest version', err); }); }; exports.websocket = { block: ws('/ws/block'), peer: ws('/ws/peer'), close : closeWs }; angular.merge(that, exports); } var service = new BMA(undefined, undefined, undefined, true); service.instance = function(host, port, useSsl, useCache) { return new BMA(host, port, useSsl, useCache); }; service.lightInstance = function(host, port, useSsl, timeout) { port = port || 80; useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443); return { host: host, port: port, useSsl: useSsl, url: csHttp.getUrl(host, port, ''/*path*/, useSsl), node: { summary: csHttp.getWithCache(host, port, '/node/summary', useSsl, csHttp.cache.LONG, false, timeout) }, network: { peering: { self: csHttp.get(host, port, '/network/peering', useSsl, timeout) }, peers: csHttp.get(host, port, '/network/peers', useSsl, timeout) }, blockchain: { current: csHttp.get(host, port, '/blockchain/current', useSsl, timeout), stats: { hardship: csHttp.get(host, port, '/blockchain/hardship/:pubkey', useSsl, timeout) } } }; }; // default action //service.start(); return service; }]) ; angular.module('cesium.wot.services', ['ngApi', 'cesium.bma.services', 'cesium.crypto.services', 'cesium.utils.services', 'cesium.settings.services']) .factory('csWot', ['$q', '$timeout', 'BMA', 'Api', 'CacheFactory', 'csConfig', 'csCurrency', 'csSettings', 'csCache', function($q, $timeout, BMA, Api, CacheFactory, csConfig, csCurrency, csSettings, csCache) { 'ngInject'; function factory(id) { var api = new Api(this, "csWot-" + id), identityCache = csCache.get('csWot-idty-', csCache.constants.SHORT), // 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; }, loadRequirements = function(pubkey, uid) { if (!pubkey) return $q.when({}); // Get requirements return BMA.wot.requirements({pubkey: pubkey}) .then(function(res){ if (!res.identities || !res.identities.length) return; // Sort to select the best identity if (res.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 res.identities = _.sortBy(res.identities, function(idty) { var score = 0; score += (10000000000 * ((uid && idty.uid === uid) ? 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)); 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('Found {0} identities. Will selected the best one'.format(res.identities.length)); } var requirements = res.identities[0]; // Add useful custom fields requirements.hasSelf = true; requirements.needMembership = (requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn <= 0 ); requirements.needRenew = (!requirements.needMembership && requirements.membershipExpiresIn <= csSettings.data.timeWarningExpire && requirements.membershipPendingExpiresIn <= 0 ); requirements.canMembershipOut = (requirements.membershipExpiresIn > 0); requirements.pendingMembership = (requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn > 0); requirements.isMember = (requirements.membershipExpiresIn > 0); // Force certification count to 0, is not a member yet - fix #269 requirements.certificationCount = (requirements.isMember && requirements.certifications) ? requirements.certifications.length : 0; requirements.willExpireCertificationCount = requirements.certifications ? requirements.certifications.reduce(function(count, cert){ if (cert.expiresIn <= csSettings.data.timeWarningExpire) { cert.willExpire = true; return count + 1; } return count; }, 0) : 0; requirements.pendingRevocation = !requirements.revoked && !!requirements.revocation_sig; return requirements; }) .catch(function(err) { // If not a member: continue if (!!err && (err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER || err.ucode == BMA.errorCodes.NO_IDTY_MATCHING_PUB_OR_UID)) { return { hasSelf: false, needMembership: true, canMembershipOut: false, needRenew: false, pendingMembership: false, needCertifications: false, needCertificationCount: 0, willNeedCertificationCount: 0 }; } throw err; }); }, loadIdentityByLookup = function(pubkey, uid) { return BMA.wot.lookup({ search: pubkey||uid }) .then(function(res) { if (!res || !res.results || !res.results.length) { return { uid: null, pubkey: pubkey, hasSelf: false }; } var identities = res.results.reduce(function(idties, res) { return idties.concat(res.uids.reduce(function(uids, idty) { var blockUid = idty.meta.timestamp.split('-', 2); return uids.concat({ uid: idty.uid, pubkey: res.pubkey, timestamp: idty.meta.timestamp, number: parseInt(blockUid[0]), hash: blockUid[1], revoked: idty.revoked, revocationNumber: idty.revoked_on, sig: idty.self }); }, [])); }, []); // Sort identities if need if (identities.length) { // Select the best identity, by sorting using this order // - same given uid // - not revoked // - max(block_number) identities = _.sortBy(identities, function(idty) { var score = 0; score += (10000000000 * ((uid && idty.uid === uid) ? 1 : 0)); score += (1000000000 * (!idty.revoked ? 1 : 0)); score += (1 * (idty.number ? idty.number : 0)); return -score; }); } var identity = identities[0]; // Retrieve time (self and revocation) var blocks = [identity.number]; if (identity.revocationNumber) { blocks.push(identity.revocationNumber); } return BMA.blockchain.blocks(blocks) .then(function(blocks){ identity.sigDate = blocks[0].medianTime; // Check if self has been done on a valid block if (identity.number !== 0 && identity.hash !== blocks[0].hash) { identity.hasBadSelfBlock = true; } // Set revocation time if (identity.revocationNumber) { identity.revocationTime = blocks[1].medianTime; } return identity; }) .catch(function(err){ // Special case for currency init (root block not exists): use now if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND && identity.number === 0) { identity.sigDate = csCurrency.date.now(); return identity; } else { throw err; } }); }) .catch(function(err) { if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) { // Identity not found (if no self) var identity = { uid: null, pubkey: pubkey, hasSelf: false }; return identity; } else { throw err; } }); }, loadCertifications = function(getFunction, pubkey, lookupCertifications, parameters, medianTime, certifiersOf) { 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; }, {}) : {}; var isMember = true; return getFunction({ pubkey: pubkey }) .then(function(res) { return 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) } 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, isMember: cert.isMember, wasMember: cert.wasMember, time: null, expiresIn: parameters.sigWindow, willExpire: false, pending: true, block: 0, valid: true }); }, 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; }); }) // 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; } 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; } return res.concat(cert); }, []); return { valid: validCertifications, pending: pendingCertifications, error: errorCertifications }; }) ; }, finishLoadRequirements = function(data) { data.requirements.needCertificationCount = (!data.requirements.needMembership && (data.requirements.certificationCount < data.sigQty)) ? (data.sigQty - data.requirements.certificationCount) : 0; data.requirements.willNeedCertificationCount = (!data.requirements.needMembership && !data.requirements.needCertificationCount && (data.requirements.certificationCount - data.requirements.willExpireCertificationCount) < data.sigQty) ? (data.sigQty - data.requirements.certificationCount + data.requirements.willExpireCertificationCount) : 0; data.requirements.pendingCertificationCount = data.received_cert_pending ? data.received_cert_pending.length : 0; // Use /wot/lookup.revoked when requirements not filled data.requirements.revoked = angular.isDefined(data.requirements.revoked) ? data.requirements.revoked : data.revoked; // Add events if (data.requirements.revoked) { delete data.hasBadSelfBlock; addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_REVOKED', messageParams: {revocationTime: data.revocationTime}}); console.debug("[wot] Identity [{0}] has been revoked".format(data.uid)); } else if (data.requirements.pendingRevocation) { addEvent(data, {type:'error', message: 'ERROR.IDENTITY_PENDING_REVOCATION'}); console.debug("[wot] Identity [{0}] has pending revocation".format(data.uid)); } else if (data.hasBadSelfBlock) { delete data.hasBadSelfBlock; if (!data.isMember) { addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_INVALID_BLOCK_HASH'}); console.debug("[wot] Invalid membership for {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)); } }, loadData = function(pubkey, withCache, uid, force) { var data; if (!pubkey && uid && !force) { return BMA.wot.member.getByUid(uid) .then(function(member) { if (member) return loadData(member.pubkey, withCache, member.uid); // recursive call //throw {message: 'NOT_A_MEMBER'}; return loadData(pubkey, withCache, uid, true/*force*/); }); } // Check cached data if (pubkey) { data = withCache ? identityCache.get(pubkey) : null; if (data && (!uid || data.uid == uid)) { 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}; } else { console.debug("[wot] Loading identity from uid " + uid); data = {}; } var now = Date.now(); var parameters; var medianTime; return $q.all([ // Get parameters csCurrency.parameters() .then(function(res) { parameters = res; data.sigQty = parameters.sigQty; data.sigStock = parameters.sigStock; }), // Get current time csCurrency.blockchain.current() .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 = Math.trunc(new Date().getTime()/1000); } else { throw err; } }), // Get requirements $q.when() .then(function () { data.requirements = {}; data.isMember = false; }), // loadRequirements(pubkey, uid) // .then(function (requirements) { // data.requirements = requirements; // data.isMember = requirements.isMember; // }), // Get identity using lookup loadIdentityByLookup(pubkey, uid) .then(function (identity) { angular.merge(data, identity); }) ]) .then(function() { if (!data.requirements.uid) { return; } var idtyFullKey = data.requirements.uid + '-' + data.requirements.meta.timestamp; return $q.all([ // Get received certifications loadCertifications(BMA.wot.certifiersOf, data.pubkey, data.lookup ? data.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, data.lookup ? data.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() { // Add compute some additional requirements (that required all data like certifications) finishLoadRequirements(data); // 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 delete data.lookup; // not need anymore 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; }); }, 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; 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; }, [])); }, [])); }, []); }) .catch(function(err) { if (err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) { return []; } else { throw err; } }); } 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() { // 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]; } }); }); } } }) .then(function() { // Add unique id (if enable) return options.addUniqueId ? _addUniqueIds(idties) : idties; }); }); }, getNewcomers = function(offset, size) { offset = offset || 0; size = size || 20; return BMA.blockchain.stats.newcomers() .then(function(res) { if (!res.result.blocks || !res.result.blocks.length) { return null; } 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*/); }); }, 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); } }); }); }, getPending = function(offset, size) { offset = offset || 0; size = size || 20; 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]; } } } }); var idties = _sortAndSliceIdentities(_.values(idtiesByPubkey), 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() { return idties; }); }); }, 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(function() { return _addUniqueIds(idties); }); }); }, 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); }) ); }); $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); } }); }); }, extend = function(idty, pubkeyAttributeName, skipAddUid) { return extendAll([idty], pubkeyAttributeName, skipAddUid) .then(function(res) { 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); } ; // Register extension points api.registerEvent('data', 'load'); api.registerEvent('data', 'search'); return { id: id, load: loadData, search: search, newcomers: getNewcomers, pending: getPending, all: getAll, extend: extend, extendAll: extendAll, // api extension api: api }; } var service = factory('default', BMA); service.instance = factory; return service; }]); angular.module('cesium.tx.services', ['ngApi', 'cesium.bma.services', 'cesium.settings.services', 'cesium.wot.services' ]) .factory('csTx', ['$q', '$timeout', '$filter', '$translate', 'UIUtils', 'BMA', 'Api', 'csConfig', 'csSettings', 'csWot', 'FileSaver', function($q, $timeout, $filter, $translate, UIUtils, BMA, Api, csConfig, csSettings, csWot, FileSaver) { 'ngInject'; function factory(id, BMA) { var api = new Api(this, "csTx-" + id), _reduceTxAndPush = function(pubkey, txArray, result, processedTxMap, allowPendings) { if (!txArray || txArray.length === 0) { return; } _.forEach(txArray, function(tx) { if (tx.block_number || allowPendings) { var walletIsIssuer = false; var otherIssuer = tx.issuers.reduce(function(issuer, res) { walletIsIssuer = (res === pubkey) ? true : walletIsIssuer; return issuer + ((res !== pubkey) ? ', ' + res : ''); }, ''); if (otherIssuer.length > 0) { otherIssuer = otherIssuer.substring(2); } var otherReceiver; var outputBase; var sources = []; var lockedOutputs; var amount = tx.outputs.reduce(function(sum, output, noffset) { var outputArray = output.split(':',3); 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 }); } } else { // output is for someone else if (outputPubkey !== '' && outputPubkey != otherIssuer) { otherReceiver = outputPubkey; } 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 // FIXME: should be uncomment when filtering source on transfer() /*sources.push(angular.merge({ amount: parseInt(outputArray[0]), base: outputBase, type: 'T', identifier: tx.hash, noffset: noffset, 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 txPubkey = amount > 0 ? otherIssuer : otherReceiver; var time = tx.time || tx.blockstampTime; // Avoid duplicated tx, or tx to him self var txKey = amount + ':' + tx.hash + ':' + time; if (!processedTxMap[txKey] && amount !== 0) { processedTxMap[txKey] = true; var newTx = { time: time, amount: amount, pubkey: txPubkey, 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; } if (lockedOutputs) { newTx.lockedOutputs = lockedOutputs; } result.push(newTx); } } }); }, loadTx = function(pubkey, fromTime, existingPendings) { return $q(function(resolve, reject) { var txHistory = []; var udHistory = []; var txPendings = []; var nowInSec = Math.trunc(new Date().getTime() / 1000); // TODO test to replace using moment().utc().unix() fromTime = fromTime || (nowInSec - csSettings.data.walletHistoryTimeSecond); var processedTxMap = {}; var tx = { pendings: [] }; var _reduceTx = function(res){ _reduceTxAndPush(pubkey, res.history.sent, txHistory, processedTxMap); _reduceTxAndPush(pubkey, res.history.received, txHistory, processedTxMap); _reduceTxAndPush(pubkey, res.history.sending, txHistory, processedTxMap); _reduceTxAndPush(pubkey, res.history.pending, txPendings, processedTxMap, true /*allow pendings*/); }; var jobs = [ // get pendings history BMA.tx.history.pending({pubkey: pubkey}) .then(_reduceTx) ]; // get TX history since if (fromTime !== -1) { var sliceTime = csSettings.data.walletHistorySliceSecond; for(var i = fromTime - (fromTime % sliceTime); i - sliceTime < nowInSec; i += sliceTime) { jobs.push(BMA.tx.history.times({pubkey: pubkey, from: i, to: i+sliceTime-1}) .then(_reduceTx) ); } jobs.push(BMA.tx.history.timesNoCache({pubkey: pubkey, from: nowInSec - (nowInSec % sliceTime), to: nowInSec+999999999}) .then(_reduceTx)); } // get all TX else { jobs.push(BMA.tx.history.all({pubkey: pubkey}) .then(_reduceTx) ); } // get UD history // FIXME issue#232 /* if (csSettings.data.showUDHistory) { jobs.push( BMA.ud.history({pubkey: pubkey}) .then(function(res){ udHistory = !res.history || !res.history.history ? [] : res.history.history.reduce(function(res, ud){ if (ud.time < fromTime) return res; // skip to old UD var amount = powBase(ud.amount, ud.base); return res.concat({ time: ud.time, amount: amount, isUD: true, block_number: ud.block_number }); }, []); })); } */ // Execute jobs $q.all(jobs) .then(function(){ // sort by time desc tx.history = txHistory.concat(udHistory).sort(function(tx1, tx2) { return (tx2.time - tx1.time); }); tx.pendings = txPendings; tx.fromTime = fromTime; tx.toTime = tx.history.length ? tx.history[0].time /*=max(tx.time)*/: fromTime; resolve(tx); }) .catch(function(err) { tx.history = []; tx.pendings = []; tx.errors = []; delete tx.fromTime; delete tx.toTime; reject(err); }); }); }, powBase = function(amount, base) { return base <= 0 ? amount : amount * Math.pow(10, base); }, addSource = function(src, sources, sourcesIndexByKey) { var srcKey = src.type+':'+src.identifier+':'+src.noffset; if (angular.isUndefined(sourcesIndexByKey[srcKey])) { sources.push(src); sourcesIndexByKey[srcKey] = sources.length - 1; } }, addSources = function(result, sources) { _(sources).forEach(function(src) { addSource(src, result.sources, result.sourcesIndexByKey); }); }, loadSourcesAndBalance = function(pubkey) { return BMA.tx.sources({pubkey: pubkey}) .then(function(res){ var result = { sources: [], sourcesIndexByKey: [], balance: 0 }; if (res.sources && res.sources.length) { _.forEach(res.sources, function(src) { src.consumed = false; result.balance += powBase(src.amount, src.base); }); addSources(result, res.sources); } return result; }); }, loadData = function(pubkey, fromTime) { var now = new Date().getTime(); var data = {}; return $q.all([ // Load Sources loadSourcesAndBalance(pubkey), // Load Tx loadTx(pubkey, fromTime) ]) .then(function(res) { angular.merge(data, res[0]); data.tx = res[1]; var txPendings = []; var txErrors = []; var balance = 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 { _.forEach(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 false; // break } }); if (tx.sources) { // add source output addSources(data, tx.sources); } delete tx.sources; delete tx.inputs; } if (valid) { balance += 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 > 0) { // 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; } } data.tx.pendings = txPendings; data.tx.errors = txErrors; data.balance = balance; // Will add uid (+ plugin will add name, avatar, etc. if enable) return csWot.extendAll((data.tx.history || []).concat(data.tx.pendings||[]), 'pubkey'); }) .then(function() { console.debug('[tx] TX and sources loaded in '+ (new Date().getTime()-now) +'ms'); return data; }); }; // Download TX history file downloadHistoryFile = function(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', 'ACCOUNT.HEADERS.AMOUNT', 'ACCOUNT.HEADERS.COMMENT']), //TODO : Utiliser plutôt csCurency pour avoir le bloc courant BMA.blockchain.current(), 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; result = result[2]; // no TX if (!result || !result.tx || !result.tx.history) { return UIUtils.toast.show('INFO.EMPTY_TX_HISTORY'); } return $translate('ACCOUNT.FILE_NAME', {currency: currency, pubkey: pubkey, currentTime : currentTime}) .then(function(result){ var formatDecimal = $filter('formatDecimal'); var formatPubkey = $filter('formatPubkey'); var formatDate = $filter('formatDate'); var formatDateForFile = $filter('formatDateForFile'); 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 = result.tx.history.reduce(function(res, tx){ return res.concat([ formatDate(tx.time), tx.uid, tx.pubkey, formatDecimal(tx.amount/100), '"' + tx.comment + '"' ].join(';') + '\n'); }, [headers.join(';') + '\n']); var file = new Blob(content, {type: 'text/plain; charset=utf-8'}); FileSaver.saveAs(file, result); }); }); }; return { id: id, load: loadData, downloadHistoryFile: downloadHistoryFile, // api extension api: api }; } var service = factory('default', BMA); service.instance = factory; return service; }]); angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.services', 'cesium.crypto.services', 'cesium.utils.services', 'cesium.settings.services']) .factory('csWallet', ['$q', '$rootScope', '$timeout', '$translate', '$filter', 'Api', 'localStorage', 'CryptoUtils', 'BMA', 'csConfig', 'csSettings', 'FileSaver', 'Blob', 'csWot', 'csTx', 'csCurrency', function($q, $rootScope, $timeout, $translate, $filter, Api, localStorage, CryptoUtils, BMA, csConfig, csSettings, FileSaver, Blob, csWot, csTx, csCurrency) { 'ngInject'; function factory(id, BMA) { var constants = { OLD_STORAGE_KEY: 'CESIUM_DATA', STORAGE_KEY: 'GCHANGE_DATA' }, data = {}, listeners, started, startPromise, api = new Api(this, 'csWallet-' + id), resetData = function(init) { data.loaded = false; data.pubkey= null; data.keypair = { signSk: null, signPk: null }; data.uid = null; data.isNew = null; data.events = []; resetKeypair(); started = false; startPromise = undefined; if (init) { api.data.raise.init(data); } else { if (!csSettings.data.useLocalStorage) { csSettings.reset(); } api.data.raise.reset(data); } }, resetKeypair = function(){ data.keypair = { signSk: null, signPk: null }; }, login = function(salt, password) { return CryptoUtils.scryptKeypair(salt, password) .then(function(keypair) { // Copy result to properties data.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); // FOR DEV ONLY - on crosschain // console.error('TODO REMOVE this code - dev only'); data.pubkey = '36j6pCNzKDPo92m7UXJLFpgDbcLFAZBgThD2TCwTwGrd'; data.keypair = keypair; // Call login check (can stop the login process) return api.data.raisePromise.loginCheck(data) // reset data then stop process .catch(function(err) { resetData(); throw err; }) // Call extend api (cannot stop login process) .then(function() { return api.data.raisePromise.login(data); }); }) // store if need .then(function() { if (csSettings.data.useLocalStorage) { store(); } return data; }); }, logout = function() { return $q(function(resolve, reject) { resetData(); // will reset keypair store(); // store (if local storage enable) // Send logout event api.data.raise.logout(); // Send unauth event (compat with new Cesium auth) api.data.raise.unauth(); resolve(); }); }, isLogin = function() { return !!data.pubkey; }, getKeypair = function(options) { if (!started) { return (startPromise || start()) .then(function () { return getKeypair(options); // loop }); } if (isLogin()) { return $q.when(data.keypair); } return $q.reject('Not auth'); }, isDataLoaded = function(options) { if (options && options.minData) return data.loaded; return data.loaded; // && data.sources; -- Gchange not use sources }, isNeverUsed = function() { if (!data.loaded) return undefined; // undefined if not full loaded return !data.pubkey || !( // Check extended data (name, profile, avatar) data.name || data.profile || data.avatar ); }, isNew = function() {return !!data.isNew;}, // If connected and same pubkey isUserPubkey = function(pubkey) { return isLogin() && data.pubkey === pubkey; }, store = function() { if (csSettings.data.useLocalStorage) { if (isLogin() && csSettings.data.rememberMe) { var dataToStore = { keypair: data.keypair, pubkey: data.pubkey, version: csConfig.version }; localStorage.setObject(constants.STORAGE_KEY, dataToStore); } else { localStorage.setObject(constants.STORAGE_KEY, null); } } else { localStorage.setObject(constants.STORAGE_KEY, null); } // Remove old storage key localStorage.setObject(constants.OLD_STORAGE_KEY, null); }, restore = function() { return $q.all([ localStorage.get(constants.STORAGE_KEY), localStorage.get(constants.OLD_STORAGE_KEY) ]) .then(function(res) { var dataStr = res[0] || res[1]; if (!dataStr) return; return fromJson(dataStr, false) .then(function(storedData){ if (storedData && storedData.keypair && storedData.pubkey) { data.keypair = storedData.keypair; data.pubkey = storedData.pubkey; data.loaded = false; // Call extend api return api.data.raisePromise.login(data); } }) .then(function(){ return data; }); }); }, getData = function() { return data; }, loadData = function(options) { if (options && options.minData) { return loadMinData(options); } if (options || data.loaded) { return refreshData(options); } return loadFullData(); }, loadFullData = function() { data.loaded = false; return $q.all([ // API extension api.data.raisePromise.load(data, null) .catch(function(err) { console.error('Error while loading wallet data, on extension point. Try to continue'); console.error(err); }) ]) .then(function() { return api.data.raisePromise.finishLoad(data) .catch(function(err) { console.error('Error while finishing wallet data load, on extension point. Try to continue'); console.error(err); }); }) .then(function() { data.loaded = true; return data; }) .catch(function(err) { data.loaded = false; throw err; }); }, loadMinData = function(options) { options = options || {}; return refreshData(options); }, refreshData = function(options) { options = options || { api: true }; // Force some load (parameters & requirements) if not already loaded var jobs = []; // Reset events cleanEventsByContext('requirements'); // API extension (force if no other jobs) if (!jobs.length || options.api) jobs.push(api.data.raisePromise.load(data, options)); return $q.all(jobs) .then(function(){ return api.data.raisePromise.finishLoad(data); }) .then(function(){ return data; }); }, addEvent = function(event, insertAtFirst) { event = event || {}; event.type = event.type || 'info'; event.message = event.message || ''; event.messageParams = event.messageParams || {}; event.context = event.context || 'undefined'; if (event.message.trim().length) { if (!insertAtFirst) { data.events.push(event); } else { data.events.splice(0, 0, event); } } else { console.debug('Event without message. Skipping this event'); } }, getkeypairSaveId = function(record) { var nbCharSalt = Math.round(record.answer.length / 2); var salt = record.answer.substr(0, nbCharSalt); var pwd = record.answer.substr(nbCharSalt); return CryptoUtils.scryptKeypair(salt, pwd) .then(function (keypair) { record.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); record.keypair = keypair; return record; }); }, getCryptedId = function(record){ return getkeypairSaveId(record) .then(function() { return CryptoUtils.util.random_nonce(); }) .then(function(nonce) { record.nonce = nonce; return CryptoUtils.box.pack(record.salt, record.nonce, record.keypair.boxPk, record.keypair.boxSk); }) .then(function (cypherSalt) { record.salt = cypherSalt; return CryptoUtils.box.pack(record.pwd, record.nonce, record.keypair.boxPk, record.keypair.boxSk); }) .then(function (cypherPwd) { record.pwd = cypherPwd; record.nonce = CryptoUtils.util.encode_base58(record.nonce); return record; }); }, recoverId = function(recover) { var nonce = CryptoUtils.util.decode_base58(recover.cypherNonce); return getkeypairSaveId(recover) .then(function (recover) { return CryptoUtils.box.open(recover.cypherSalt, nonce, recover.keypair.boxPk, recover.keypair.boxSk); }) .then(function (salt) { recover.salt = salt; return CryptoUtils.box.open(recover.cypherPwd, nonce, recover.keypair.boxPk, recover.keypair.boxSk); }) .then(function (pwd) { recover.pwd = pwd; return recover; }) .catch(function(err){ console.warn('Incorrect answers - Unable to recover passwords'); }); }, getSaveIDDocument = function(record) { var saveId = 'Version: 10 \n' + 'Type: SaveID\n' + 'Questions: ' + '\n' + record.questions + 'Issuer: ' + data.pubkey + '\n' + 'Crypted-Nonce: '+ record.nonce + '\n'+ 'Crypted-Pubkey: '+ record.pubkey +'\n' + 'Crypted-Salt: '+ record.salt + '\n' + 'Crypted-Pwd: '+ record.pwd + '\n'; // Sign SaveId document return CryptoUtils.sign(saveId, data.keypair) .then(function(signature) { saveId += signature + '\n'; console.debug('Has generate an SaveID document:\n----\n' + saveId + '----'); return saveId; }); }, downloadSaveId = function(record){ return getSaveIDDocument(record) .then(function(saveId) { var saveIdFile = new Blob([saveId], {type: 'text/plain; charset=utf-8'}); FileSaver.saveAs(saveIdFile, 'saveID.txt'); }); }, cleanEventsByContext = function(context){ data.events = data.events.reduce(function(res, event) { if (event.context && event.context == context) return res; return res.concat(event); },[]); }, /** * De-serialize from JSON string */ fromJson = function(json, failIfInvalid) { failIfInvalid = angular.isUndefined(failIfInvalid) ? true : failIfInvalid; return $q(function(resolve, reject) { var obj = JSON.parse(json || '{}'); // FIXME #379 /*if (obj && obj.pubkey) { resolve({ pubkey: obj.pubkey }); } else */ if (obj && obj.keypair && obj.keypair.signPk && obj.keypair.signSk) { var keypair = {}; var i; // sign Pk : Convert to Uint8Array type var signPk = new Uint8Array(32); for (i = 0; i < 32; i++) signPk[i] = obj.keypair.signPk[i]; keypair.signPk = signPk; var signSk = new Uint8Array(64); for (i = 0; i < 64; i++) signSk[i] = obj.keypair.signSk[i]; keypair.signSk = signSk; // box Pk : Convert to Uint8Array type if (obj.version && obj.keypair.boxPk) { var boxPk = new Uint8Array(32); for (i = 0; i < 32; i++) boxPk[i] = obj.keypair.boxPk[i]; keypair.boxPk = boxPk; } if (obj.version && obj.keypair.boxSk) { var boxSk = new Uint8Array(32); for (i = 0; i < 64; i++) boxSk[i] = obj.keypair.boxSk[i]; keypair.boxSk = boxSk; } resolve({ pubkey: obj.pubkey, keypair: keypair, tx: obj.tx }); } else if (failIfInvalid) { reject('Not a valid Wallet.data object'); } else { resolve(); } }); } ; function addListeners() { listeners = [ // Listen if settings changed csSettings.api.data.on.changed($rootScope, store, this), // Listen if node changed BMA.api.node.on.restart($rootScope, restart, this) ]; } function removeListeners() { _.forEach(listeners, function(remove){ remove(); }); listeners = []; } function ready() { if (started) return $q.when(); return startPromise || start(); } function stop() { console.debug('[wallet] Stopping...'); removeListeners(); resetData(); } function restart() { stop(); return $timeout(start, 200); } function start() { console.debug('[wallet] Starting...'); var now = new Date().getTime(); startPromise = $q.all([ csSettings.ready(), csCurrency.ready(), BMA.ready() ]) // Restore .then(restore) // Load data (if a wallet restored) .then(function(data) { if (data && data.pubkey) { return loadData({minData: true}); } }) // Emit ready event .then(function() { addListeners(); console.debug('[wallet] Started in ' + (new Date().getTime() - now) + 'ms'); started = true; startPromise = null; // Emit event (used by plugins) api.data.raise.ready(data); }) .then(function(){ return data; }); return startPromise; } // Register extension points api.registerEvent('data', 'ready'); api.registerEvent('data', 'init'); api.registerEvent('data', 'loginCheck'); // allow to stop the login process api.registerEvent('data', 'login'); // executed after login check (cannot stop the login process) api.registerEvent('data', 'load'); api.registerEvent('data', 'finishLoad'); api.registerEvent('data', 'logout'); api.registerEvent('data', 'unauth'); api.registerEvent('data', 'reset'); api.registerEvent('error', 'send'); api.registerEvent('action', 'certify'); // init data resetData(true); return { id: id, data: data, ready: ready, start: start, stop: stop, // auth login: login, logout: logout, isLogin: isLogin, getKeypair: getKeypair, isDataLoaded: isDataLoaded, isNeverUsed: isNeverUsed, isNew: function() {return !!data.isNew;}, isUserPubkey: isUserPubkey, getData: getData, loadData: loadData, refreshData: refreshData, downloadSaveId: downloadSaveId, getCryptedId: getCryptedId, recoverId: recoverId, events: { add: addEvent, cleanByContext: cleanEventsByContext }, api: api }; } var service = factory('default', BMA); service.instance = factory; return service; }]); angular.module('cesium.plugin.services', []) .provider('PluginService', function PluginServiceProvider() { 'ngInject'; var eagerLoadingServices = []; var extensionByStates = {}; this.registerEagerLoadingService = function(serviceName) { eagerLoadingServices.push(serviceName); return this; }; this.extendState = function(stateName, extension) { if (angular.isDefined(stateName) && angular.isDefined(extension)) { if (!extensionByStates[stateName]) { extensionByStates[stateName] = []; } extensionByStates[stateName].push(extension); } return this; }; this.extendStates = function(stateNames, extension) { var that = this; stateNames.forEach(function(stateName) { that.extendState(stateName, extension); }); return this; }; this.$get = ['$injector', '$state', function($injector, $state) { var currentExtensionPointName; function start() { if (eagerLoadingServices.length>0) { _.forEach(eagerLoadingServices, function(name) { $injector.get(name); }); } } function getActiveExtensionPointsByName(extensionPointName) { var extensions = _.keys(extensionByStates).reduce(function(res, stateName){ return $state.includes(stateName) ? res.concat(extensionByStates[stateName]) : res; }, []); return extensions.reduce(function(res, extension){ return extension.points && extension.points[extensionPointName] ? res.concat(extension.points[extensionPointName]) : res; }, []); } function setCurrentExtensionPointName(extensionPointName) { currentExtensionPointName = extensionPointName; } function getCurrentExtensionPointName() { return currentExtensionPointName; } return { start: start, extensions: { points: { getActivesByName: getActiveExtensionPointsByName, current: { get: getCurrentExtensionPointName, set: setCurrentExtensionPointName } } } }; }]; }) ; angular.module('cesium.services', [ 'cesium.config', 'cesium.settings.services', 'cesium.http.services', 'cesium.bma.services', 'cesium.crypto.services', 'cesium.utils.services', 'cesium.modal.services', 'cesium.storage.services', 'cesium.device.services', 'cesium.currency.services', 'cesium.wallet.services', 'cesium.tx.services', 'cesium.wot.services', 'cesium.plugin.services' ]) ; /** * Created by blavenie on 01/02/17. */ function Block(json, attributes) { "use strict"; var that = this; // Copy default fields if (!attributes || !attributes.length) { ["currency", "issuer", "medianTime", "number", "version", "powMin", "dividend", "membersCount", "hash", "identities", "joiners", "actives", "leavers", "revoked", "excluded", "certifications", "transactions"] .forEach(function (key) { that[key] = json[key]; }); } // or just given else { _.forEach(attributes, function (key) { that[key] = json[key]; }); } that.identitiesCount = that.identities ? that.identities.length : 0; that.joinersCount = that.joiners ? that.joiners.length : 0; that.activesCount = that.actives ? that.actives.length : 0; that.leaversCount = that.leavers ? that.leavers.length : 0; that.revokedCount = that.revoked ? that.revoked.length : 0; that.excludedCount = that.excluded ? that.excluded.length : 0; that.certificationsCount = that.certifications ? that.certifications.length : 0; that.transactionsCount = that.transactions ? that.transactions.length : 0; that.empty = that.isEmpty(); } Block.prototype.isEmpty = function(){ "use strict"; return !this.transactionsCount && !this.certificationsCount && !this.joinersCount && !this.dividend && !this.activesCount && !this.identitiesCount && !this.leaversCount && !this.excludedCount && !this.revokedCount; }; Block.prototype.parseData = function() { this.identities = this.parseArrayValues(this.identities, ['pubkey', 'signature', 'buid', 'uid']); this.joiners = this.parseArrayValues(this.joiners, ['pubkey', 'signature', 'mBuid', 'iBuid', 'uid']); this.actives = this.parseArrayValues(this.actives, ['pubkey', 'signature', 'mBuid', 'iBuid', 'uid']); this.leavers = this.parseArrayValues(this.leavers, ['pubkey', 'signature', 'mBuid', 'iBuid', 'uid']); this.revoked = this.parseArrayValues(this.revoked, ['pubkey', 'signature']); this.excluded = this.parseArrayValues(this.excluded, ['pubkey']); // certifications this.certifications = this.parseArrayValues(this.certifications, ['from', 'to', 'block', 'signature']); //this.certifications = _.groupBy(this.certifications, 'to'); // TX this.transactions = this.parseTransactions(this.transactions); delete this.raw; // not need }; Block.prototype.cleanData = function() { delete this.identities; delete this.joiners; delete this.actives; delete this.leavers; delete this.revoked; delete this.excluded; delete this.certifications; delete this.transactions; delete this.raw; // not need }; Block.prototype.parseArrayValues = function(array, itemObjProperties){ if (!array || !array.length) return []; return array.reduce(function(res, raw) { var parts = raw.split(':'); if (parts.length != itemObjProperties.length) { console.debug('[block] Bad format for \'{0}\': [{1}]. Expected {1} parts. Skipping'.format(arrayProperty, raw, itemObjProperties.length)); return res; } var obj = {}; for (var i=0; i= 21)) { kind = 'winter'; } else if ((month == 3 && day >= 21) || (month < 6) || (month == 6 && day < 21)) { kind = 'spring'; } else if ((month == 6 && day >= 21) || (month < 9) || (month == 9 && day < 21)) { kind = 'summer'; } else { kind = 'autumn'; } } var imageCount = imageCountByKind[kind]; var imageIndex = Math.floor(Math.random()*imageCount)+1; return './img/bg/{0}-{1}.jpg'.format(kind, imageIndex); } $scope.bgImage = getRandomImage(); $scope.enter = function(e, state) { if (ionic.Platform.isIOS()) { if(window.StatusBar) { // needed to fix Xcode 9 / iOS 11 issue with blank space at bottom of webview // https://github.com/meteor/meteor/issues/9041 StatusBar.overlaysWebView(false); StatusBar.overlaysWebView(true); } } if (state && state.stateParams && state.stateParams.error) { // Error query parameter $scope.error = state.stateParams.error; $scope.node = csCurrency.data.node; $scope.loading = false; $ionicHistory.nextViewOptions({ disableAnimate: true, disableBack: true, historyRoot: true }); $state.go('app.home', {error: undefined}, { reload: false, inherit: true, notify: false}); } else { // Start platform csPlatform.ready() .then(function() { $scope.loading = false; }) .catch(function(err) { $scope.node = csCurrency.data.node; $scope.loading = false; $scope.error = err; }); } }; $scope.$on('$ionicView.enter', $scope.enter); $scope.reload = function() { $scope.loading = true; delete $scope.error; $timeout($scope.enter, 200); }; /** * Catch click for quick fix * @param event */ $scope.doQuickFix = function(action) { if (action === 'settings') { $ionicHistory.nextViewOptions({ historyRoot: true }); $state.go('app.settings'); } }; $scope.changeLanguage = function(langKey) { $translate.use(langKey); $scope.hideLocalesPopover(); csSettings.data.locale = _.findWhere($scope.locales, {id: langKey}); }; /* -- show/hide locales popup -- */ $scope.showLocalesPopover = function(event) { UIUtils.popover.show(event, { templateUrl: 'templates/api/locales_popover.html', scope: $scope, autoremove: true, afterShow: function(popover) { $scope.localesPopover = popover; } }); }; $scope.hideLocalesPopover = function() { if ($scope.localesPopover) { $scope.localesPopover.hide(); $scope.localesPopover = null; } }; // For DEV ONLY /*$timeout(function() { $scope.loginAndGo(); }, 500);*/ } JoinController.$inject = ['$timeout', 'Modals']; JoinModalController.$inject = ['$scope', '$state', 'UIUtils', 'CryptoUtils', 'csSettings', 'Modals', 'csWallet', 'mkWallet', 'BMA']; angular.module('cesium.join.controllers', ['cesium.services']) .config(['$stateProvider', function($stateProvider) { 'ngInject'; $stateProvider .state('app.join', { url: "/join", views: { 'menuContent': { templateUrl: "templates/home/home.html", controller: 'JoinCtrl' } } }) ; }]) .controller('JoinCtrl', JoinController) .controller('JoinModalCtrl', JoinModalController) ; function JoinController($timeout, Modals) { 'ngInject'; // Open join modal $timeout(function() { Modals.showJoin(); }, 100); } function JoinModalController($scope, $state, UIUtils, CryptoUtils, csSettings, Modals, csWallet, mkWallet, BMA) { 'ngInject'; $scope.formData = { pseudo: '' }; $scope.slides = { slider: null, options: { loop: false, effect: 'slide', speed: 500 } }; $scope.isLastSlide = false; $scope.search = { looking: true }; $scope.showUsername = false; $scope.showPassword = false; $scope.smallscreen = UIUtils.screen.isSmall(); $scope.userIdPattern = BMA.constants.regex.USER_ID; $scope.slidePrev = function() { $scope.slides.slider.unlockSwipes(); $scope.slides.slider.slidePrev(); $scope.slides.slider.lockSwipes(); $scope.isLastSlide = false; }; $scope.slideNext = function() { $scope.slides.slider.unlockSwipes(); $scope.slides.slider.slideNext(); $scope.slides.slider.lockSwipes(); $scope.isLastSlide = $scope.slides.slider.activeIndex === 4; }; $scope.showAccountPubkey = function() { $scope.formData.computing=true; CryptoUtils.scryptKeypair($scope.formData.username, $scope.formData.password) .then(function(keypair) { $scope.formData.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); $scope.formData.computing=false; }) .catch(function(err) { $scope.formData.computing=false; console.error('>>>>>>>' , err); UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR'); }); }; $scope.formDataChanged = function() { $scope.formData.computing=false; $scope.formData.pubkey=null; }; $scope.doNext = function(formName) { console.debug("[join] form " + formName + " OK. index=" + $scope.slides.slider.activeIndex); if (!formName) { formName = ($scope.slides.slider.activeIndex === 1 ? 'passwordForm' : ($scope.slides.slider.activeIndex === 2 ? 'pseudoForm' : formName)); } if (formName) { $scope[formName].$submitted=true; if(!$scope[formName].$valid) { return; } if (formName === 'passwordForm') { $scope.slideNext(); $scope.showAccountPubkey(); } else { $scope.slideNext(); if (formName === 'pseudoForm') { $scope.showAccountPubkey(); } } } }; $scope.doNewAccount = function(confirm) { if (!confirm) { return UIUtils.alert.confirm('ACCOUNT.NEW.CONFIRMATION_WALLET_ACCOUNT') .then(function(confirm) { if (confirm) { $scope.doNewAccount(true); } }); } UIUtils.loading.show(); csWallet.login($scope.formData.username, $scope.formData.password) .then(function(data) { $scope.closeModal(); csSettings.data.wallet = csSettings.data.wallet || {}; csSettings.data.wallet.alertIfUnusedWallet = false; // do not alert if empty // Fill a default profile mkWallet.setDefaultProfile({ title: $scope.formData.pseudo }); // Redirect to wallet $state.go('app.view_wallet'); }) .catch(function(err) { UIUtils.loading.hide(); console.error('>>>>>>>' , err); UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR'); }); }; $scope.showHelpModal = function(helpAnchor) { if (!helpAnchor) { helpAnchor = $scope.slides.slider.activeIndex == 1 ? 'join-salt' : ( $scope.slides.slider.activeIndex == 2 ? 'join-password' : 'join-pseudo'); } Modals.showHelp({anchor: helpAnchor}); }; // TODO: remove auto add account when done /*$timeout(function() { $scope.formData.username="azertypoi"; $scope.formData.confirmUsername=$scope.formData.username; $scope.formData.password="azertypoi"; $scope.formData.confirmPassword=$scope.formData.password; $scope.formData.pseudo="azertypoi"; $scope.doNext(); $scope.doNext(); //$scope.form = {$valid:true}; }, 2000);*/ } LoginModalController.$inject = ['$scope', '$timeout', 'CryptoUtils', 'UIUtils', 'Modals', 'csSettings', 'Device']; angular.module('cesium.login.controllers', ['cesium.services']) .controller('LoginModalCtrl', LoginModalController) ; function LoginModalController($scope, $timeout, CryptoUtils, UIUtils, Modals, csSettings, Device) { 'ngInject'; $scope.computing = false; $scope.pubkey = null; $scope.formData = { rememberMe: csSettings.data.rememberMe }; $scope.showSalt = csSettings.data.showLoginSalt; $scope.showPubkeyButton = false; $scope.autoComputePubkey = false; Device.ready().then(function() { $scope.autoComputePubkey = ionic.Platform.grade.toLowerCase()==='a' && !UIUtils.screen.isSmall(); }); // Login form submit $scope.doLogin = function() { if(!$scope.form.$valid) { return; } UIUtils.loading.show(); $scope.closeModal($scope.formData); }; $scope.formDataChanged = function() { $scope.computing=false; $scope.pubkey = null; if ($scope.autoComputePubkey && $scope.formData.username && $scope.formData.password) { $scope.showPubkey(); } else { $scope.showPubkeyButton = $scope.formData.username && $scope.formData.password; } }; $scope.$watch('formData.username', $scope.formDataChanged, true); $scope.$watch('formData.password', $scope.formDataChanged, true); $scope.showPubkey = function() { $scope.computing=true; $scope.showPubkeyButton = false; $scope.pubkey = ''; $timeout(function() { var salt = $scope.formData.username; var pwd = $scope.formData.password; CryptoUtils.scryptKeypair(salt, pwd).then( function (keypair) { // form has changed: retry if (salt !== $scope.formData.username || pwd !== $scope.formData.password) { $scope.showPubkey(); } else { $scope.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); $scope.computing = false; } } ) .catch(function (err) { $scope.pubkey = ''; $scope.computing = false; UIUtils.loading.hide(); console.error('>>>>>>>', err); UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR'); }); }, 500); }; $scope.showJoinModal = function() { $scope.closeModal(); $timeout(function() { Modals.showJoin(); }, 300); }; $scope.showAccountSecurityModal = function() { $scope.closeModal(); $timeout(function() { Modals.showAccountSecurity(); }, 300); }; /* // TODO : for DEV only $timeout(function() { $scope.formData = { username: 'abc', password: 'def', }; //$scope.form = {$valid:true}; }, 900);*/ } HelpController.$inject = ['$scope', '$state', '$timeout', '$anchorScroll', 'csSettings']; HelpModalController.$inject = ['$scope', '$timeout', '$anchorScroll', 'csSettings', 'parameters']; HelpTipController.$inject = ['$scope', '$rootScope', '$state', '$window', '$ionicSideMenuDelegate', '$timeout', '$q', '$anchorScroll', 'UIUtils', 'csConfig', 'csSettings', 'csCurrency', 'Device', 'csWallet']; HelpTourController.$inject = ['$scope']; angular.module('cesium.help.controllers', ['cesium.services']) .config(['$stateProvider', function($stateProvider) { 'ngInject'; $stateProvider .state('app.help_tour', { url: "/tour", views: { 'menuContent': { templateUrl: "templates/home/home.html", controller: 'HelpTourCtrl' } } }) .state('app.help', { url: "/help?anchor", views: { 'menuContent': { templateUrl: "templates/help/view_help.html", controller: 'HelpCtrl' } } }) .state('app.help_anchor', { url: "/help/:anchor", views: { 'menuContent': { templateUrl: "templates/help/view_help.html", controller: 'HelpCtrl' } } }) ; }]) .controller('HelpCtrl', HelpController) .controller('HelpModalCtrl', HelpModalController) .controller('HelpTipCtrl', HelpTipController) .controller('HelpTourCtrl', HelpTourController) ; function HelpController($scope, $state, $timeout, $anchorScroll, csSettings) { 'ngInject'; $scope.$on('$ionicView.enter', function(e) { $scope.locale = csSettings.data.locale.id; if ($state.stateParams && $state.stateParams.anchor) { $timeout(function () { $anchorScroll($state.stateParams.anchor); }, 100); } }); } function HelpModalController($scope, $timeout, $anchorScroll, csSettings, parameters) { 'ngInject'; $scope.locale = csSettings.data.locale.id; if (parameters && parameters.anchor) { $timeout(function() { $anchorScroll(parameters.anchor); }, 100); } } /* ---------------------------- * Help Tip * ---------------------------- */ function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDelegate, $timeout, $q, $anchorScroll, UIUtils, csConfig, csSettings, csCurrency, Device, csWallet) { $scope.tour = false; // Is a tour or a helptip ? $scope.continue = true; $scope.executeStep = function(partName, steps, index) { index = angular.isDefined(index) ? index : 0; if (index >= steps.length) { return $q.when(true); // end } var step = steps[index]; if (typeof step !== 'function') { throw new Error('[helptip] Invalid step at index {0} of \'{1}\' tour: step must be a function'.format(index, partName)); } var promise = step(); if (typeof promise === 'boolean') { promise = $q.when(promise); } return promise .then(function(next) { if (angular.isUndefined(next)) { $scope.continue = false; return index; // keep same index (no button press: popover just closed) } if (!next || index === steps.length - 1) { return next ? -1 : index+1; // last step OK, so mark has finished } return $scope.executeStep(partName, steps, index+1); }) .catch(function(err) { if (err && err.message == 'transition prevented') { console.error('ERROR: in help tour [{0}], in step [{1}] -> use large if exists, to prevent [transition prevented] error'.format(partName, index)); } else { console.error('ERROR: in help tour [{0}], in step [{1}] : {2}'.format(partName, index, err)); } $scope.continue = false; return index; }); }; $scope.showHelpTip = function(id, options) { options = options || {}; options.bindings = options.bindings || {}; options.bindings.value =options.bindings.value || ''; options.bindings.hasNext = angular.isDefined(options.bindings.hasNext) ? options.bindings.hasNext : true; options.timeout = options.timeout || (Device.enable ? 900 : 500); options.autoremove = true; // avoid memory leak options.bindings.tour = $scope.tour; options.backdropClickToClose = !$scope.tour; return UIUtils.popover.helptip(id, options); }; $scope.showHelpModal = function(helpAnchor) { Modals.showHelp({anchor: helpAnchor}); }; $scope.startHelpTour = function() { $scope.tour = true; $scope.continue = true; // Currency tour return $scope.startCurrencyTour(0, true) .then(function(endIndex){ if (!endIndex || $scope.cancelled) return false; csSettings.data.helptip.currency=endIndex; csSettings.store(); return $scope.continue; }) // Network tour .then(function(next){ if (!next) return false; return $scope.startNetworkTour(0, true) .then(function(endIndex){ if (!endIndex || $scope.cancelled) return false; csSettings.data.helptip.network=endIndex; csSettings.store(); return $scope.continue; }); }) // Wot tour .then(function(next){ if (!next) return false; return $scope.startWotTour(0, true) .then(function(endIndex){ if (!endIndex || $scope.cancelled) return false; csSettings.data.helptip.wot=endIndex; csSettings.store(); return $scope.continue; }); }) // Identity certifications tour .then(function(next){ if (!next) return false; return $scope.startWotCertTour(0, true) .then(function(endIndex){ if (!endIndex) return false; csSettings.data.helptip.wotCerts=endIndex; csSettings.store(); return $scope.continue; }); }) // Wallet tour (if NOT login) .then(function(next){ if (!next) return false; return $scope.startWalletNoLoginTour(0, true); }) // Wallet tour (if login) .then(function(next){ if (!next) return false; if (!csWallet.isLogin()) return true; // not login: continue return $scope.startWalletTour(0, true) .then(function(endIndex){ if (!endIndex) return false; csSettings.data.helptip.wallet=endIndex; csSettings.store(); return $scope.continue; }); }) // Wallet certifications tour .then(function(next){ if (!next) return false; if (!csWallet.isLogin()) return true; // not login: continue return $scope.startWalletCertTour(0, true) .then(function(endIndex){ if (!endIndex) return false; csSettings.data.helptip.walletCerts=endIndex; csSettings.store(); return $scope.continue; }); }) // TX tour (if login) .then(function(next){ if (!next) return false; if (!csWallet.isLogin()) return true; // not login: continue return $scope.startTxTour(0, true) .then(function(endIndex){ if (!endIndex) return false; csSettings.data.helptip.tx=endIndex; csSettings.store(); return $scope.continue; }); }) // Header tour .then(function(next){ if (!next) return false; return $scope.startHeaderTour(0, true); }) // Settings tour .then(function(next){ if (!next) return false; return $scope.startSettingsTour(0, true); }) // Finish tour .then(function(next){ if (!next) return false; return $scope.finishTour(); }); }; /** * Features tour on currency * @returns {*} */ $scope.startCurrencyTour = function(startIndex, hasNext) { var showWotTabIfNeed = function() { if ($state.is('app.currency.tab_parameters')) { $state.go('app.currency.tab_wot'); } }; var contentParams; var steps = [ function(){ $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-currency', { bindings: { content: 'HELP.TIP.MENU_BTN_CURRENCY', icon: { position: 'left' } } }); }, function () { if ($ionicSideMenuDelegate.isOpen()) { $ionicSideMenuDelegate.toggleLeft(false); } return $state.go(UIUtils.screen.isSmall() ? 'app.currency' : 'app.currency_view_lg') .then(function () { return $scope.showHelpTip('helptip-currency-mass-member', { bindings: { content: 'HELP.TIP.CURRENCY_MASS', icon: { position: 'center' } } }); }); }, function () { if (!csSettings.data.useRelative) return true; //skip but continue return $scope.showHelpTip('helptip-currency-mass-member-unit', { bindings: { content: 'HELP.TIP.CURRENCY_UNIT_RELATIVE', contentParams: contentParams, icon: { position: UIUtils.screen.isSmall() ? 'right' : 'center' } } }); }, function () { if (!csSettings.data.useRelative) return true; //skip but continue return $scope.showHelpTip('helptip-currency-change-unit', { bindings: { content: 'HELP.TIP.CURRENCY_CHANGE_UNIT', contentParams: contentParams, icon: { position: UIUtils.screen.isSmall() ? 'right' : 'center' } } }); }, function () { if (csSettings.data.useRelative) return true; //skip but continue return $scope.showHelpTip('helptip-currency-change-unit', { bindings: { content: 'HELP.TIP.CURRENCY_CHANGE_UNIT_TO_RELATIVE', contentParams: contentParams, icon: { position: UIUtils.screen.isSmall() ? 'right' : 'center' } } }); }, function () { if (UIUtils.screen.isSmall()) { $anchorScroll('helptip-currency-rules-anchor'); } return $scope.showHelpTip('helptip-currency-rules', { bindings: { content: 'HELP.TIP.CURRENCY_RULES', icon: { position: 'center', glyph: 'ion-information-circled' } } }); }, function () { showWotTabIfNeed(); return $scope.showHelpTip('helptip-currency-newcomers', { bindings: { content: 'HELP.TIP.CURRENCY_WOT', icon: { position: 'center' } }, timeout: 1200 // need for Firefox }); } ]; // Get currency parameters, with currentUD return csCurrency.default().then(function(currency) { contentParams = currency.parameters; // Launch steps return $scope.executeStep('currency', steps, startIndex); }); }; /** * Features tour on network * @returns {*} */ $scope.startNetworkTour = function(startIndex, hasNext) { var showNetworkTabIfNeed = function() { if ($state.is('app.currency')) { // Select the second tabs $timeout(function () { var tabs = $window.document.querySelectorAll('ion-tabs .tabs a'); if (tabs && tabs.length == 3) { angular.element(tabs[2]).triggerHandler('click'); } }, 100); } }; var contentParams; var steps = [ function(){ if (UIUtils.screen.isSmall()) return true; // skip but continue $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-network', { bindings: { content: 'HELP.TIP.MENU_BTN_NETWORK', icon: { position: 'left' } } }); }, function () { if ($ionicSideMenuDelegate.isOpen()) { $ionicSideMenuDelegate.toggleLeft(false); } return $state.go(UIUtils.screen.isSmall() ? 'app.currency.tab_network' : 'app.network') .then(function () { showNetworkTabIfNeed(); return $scope.showHelpTip('helptip-network-peers', { bindings: { content: 'HELP.TIP.NETWORK_BLOCKCHAIN', icon: { position: 'center', glyph: 'ion-information-circled' } }, timeout: 1200 // need for Firefox }); }); }, function() { showNetworkTabIfNeed(); return $scope.showHelpTip('helptip-network-peer-0', { bindings: { content: 'HELP.TIP.NETWORK_PEERS', icon: { position: UIUtils.screen.isSmall() ? undefined : 'center' } }, timeout: 1000, retry: 20 }); }, function() { showNetworkTabIfNeed(); return $scope.showHelpTip('helptip-network-peer-0-block', { bindings: { content: 'HELP.TIP.NETWORK_PEERS_BLOCK_NUMBER', icon: { position: UIUtils.screen.isSmall() ? undefined : 'center' } } }); }, function() { showNetworkTabIfNeed(); var locale = csSettings.data.locale.id; return $scope.showHelpTip('helptip-network-peers', { bindings: { content: 'HELP.TIP.NETWORK_PEERS_PARTICIPATE', contentParams: { installDocUrl: (csConfig.helptip && csConfig.helptip.installDocUrl) ? (csConfig.helptip.installDocUrl[locale] ? csConfig.helptip.installDocUrl[locale] : csConfig.helptip.installDocUrl) : 'http://duniter.org' }, icon: { position: 'center', glyph: 'ion-information-circled' }, hasNext: hasNext } }); } ]; // Get currency parameters, with currentUD return csCurrency.default().then(function(currency) { contentParams = currency.parameters; // Launch steps return $scope.executeStep('network', steps, startIndex); }); }; /** * Features tour on WOT registry * @returns {*} */ $scope.startWotTour = function(startIndex, hasNext) { var contentParams; var steps = [ function() { $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-wot', { bindings: { content: 'HELP.TIP.MENU_BTN_WOT', icon: { position: 'left' } }, onError: 'continue' }); }, function() { if ($ionicSideMenuDelegate.isOpen()) { $ionicSideMenuDelegate.toggleLeft(false); } return $state.go('app.wot_lookup') .then(function(){ return $scope.showHelpTip('helptip-wot-search-text', { bindings: { content: UIUtils.screen.isSmall() ? 'HELP.TIP.WOT_SEARCH_TEXT_XS' : 'HELP.TIP.WOT_SEARCH_TEXT', icon: { position: 'center' } } }); }); }, function() { return $scope.showHelpTip('helptip-wot-search-result-0', { bindings: { content: 'HELP.TIP.WOT_SEARCH_RESULT', icon: { position: 'center' } }, timeout: 700, retry: 15 }); }, function() { var element = $window.document.getElementById('helptip-wot-search-result-0'); if (!element) return true; $timeout(function() { angular.element(element).triggerHandler('click'); }); return $scope.showHelpTip('helptip-wot-view-certifications', { bindings: { content: 'HELP.TIP.WOT_VIEW_CERTIFICATIONS' }, timeout: 2500 }); }, function() { return $scope.showHelpTip('helptip-wot-view-certifications', { bindings: { content: 'HELP.TIP.WOT_VIEW_CERTIFICATIONS_COUNT', contentParams: contentParams, icon: { position: 'center', glyph: 'ion-information-circled' } } }); }, function() { return $scope.showHelpTip('helptip-wot-view-certifications-count', { bindings: { content: 'HELP.TIP.WOT_VIEW_CERTIFICATIONS_CLICK', icon: { position: 'center' }, hasNext: hasNext } }); } ]; // Get currency parameters, with currentUD return csCurrency.default().then(function(currency) { contentParams = currency.parameters; contentParams.currentUD = $rootScope.walletData.currentUD; // Launch steps return $scope.executeStep('wot', steps, startIndex); }); }; /** * Features tour on wot certifications * @returns {*} */ $scope.startWotCertTour = function(startIndex, hasNext) { var steps = [ function() { // If on identity: click on certifications if ($state.is('app.wot_identity')) { var element = $window.document.getElementById('helptip-wot-view-certifications'); if (!element) return true; $timeout(function() { angular.element(element).triggerHandler('click'); }); } return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'fab-certify': 'helptip-certs-certify', { bindings: { content: 'HELP.TIP.WOT_VIEW_CERTIFY', icon: { position: UIUtils.screen.isSmall() ? 'bottom-right' : 'center' } }, timeout: UIUtils.screen.isSmall() ? 2000 : 1000, retry: 10 }); }, function() { return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'fab-certify': 'helptip-certs-certify', { bindings: { content: 'HELP.TIP.CERTIFY_RULES', icon: { position: 'center', glyph: 'ion-alert-circled' }, hasNext: hasNext } }); } ]; return $scope.executeStep('certs', steps, startIndex); }; /** * Features tour on wallet (if not login) * @returns {*} */ $scope.startWalletNoLoginTour = function(startIndex, hasNext) { if (csWallet.isLogin()) return $q.when(true); // skip if login var steps = [ function () { $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-account', { bindings: { content: $rootScope.walletData.isMember ? 'HELP.TIP.MENU_BTN_ACCOUNT_MEMBER' : 'HELP.TIP.MENU_BTN_ACCOUNT', icon: { position: 'left' }, hasNext: hasNext } }); } ]; return $scope.executeStep('wallet-no-login', steps, startIndex); }; /** * Features tour on wallet screens * @returns {*} */ $scope.startWalletTour = function(startIndex, hasNext) { if (!csWallet.isLogin()) return $q.when(true); // skip if not login var contentParams; var steps = [ function () { $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-account', { bindings: { content: $rootScope.walletData.isMember ? 'HELP.TIP.MENU_BTN_ACCOUNT_MEMBER' : 'HELP.TIP.MENU_BTN_ACCOUNT', icon: { position: 'left' } } }); }, function () { if ($ionicSideMenuDelegate.isOpen()) { $ionicSideMenuDelegate.toggleLeft(false); } // Go to wallet return $state.go('app.view_wallet') .then(function () { return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'helptip-wallet-options-xs' : 'helptip-wallet-options', { bindings: { content: 'HELP.TIP.WALLET_OPTIONS', icon: { position: UIUtils.screen.isSmall() ? 'right' : 'center' } } }); }); }, // Wallet pubkey function () { $anchorScroll('helptip-wallet-pubkey'); return $scope.showHelpTip('helptip-wallet-pubkey', { bindings: { content: 'HELP.TIP.WALLET_PUBKEY', icon: { position: 'bottom-center' } }, timeout: UIUtils.screen.isSmall() ? 2000 : 500, retry: 10 }); }, function () { $anchorScroll('helptip-wallet-certifications'); return $scope.showHelpTip('helptip-wallet-certifications', { bindings: { content: UIUtils.screen.isSmall() ? 'HELP.TIP.WALLET_RECEIVED_CERTIFICATIONS': 'HELP.TIP.WALLET_CERTIFICATIONS', icon: { position: 'center' } }, timeout: 500, onError: 'continue', hasNext: hasNext }); } ]; // Get currency parameters, with currentUD return csCurrency.default() .then(function(currency) { contentParams = currency.parameters; contentParams.currentUD = $rootScope.walletData.currentUD; // Launch steps return $scope.executeStep('wallet', steps, startIndex); }); }; /** * Features tour on wallet certifications * @returns {*} */ $scope.startWalletCertTour = function(startIndex, hasNext) { if (!csWallet.isLogin()) return $q.when(true); var contentParams; var skipAll = false; var steps = [ function() { // If on wallet : click on certifications if ($state.is('app.view_wallet')) { var element = $window.document.getElementById('helptip-wallet-certifications'); if (!element) { skipAll = true; return true; } $timeout(function() { angular.element(element).triggerHandler('click'); }); } if (!UIUtils.screen.isSmall()) return true; // skip this helptip if not in tabs mode return $scope.showHelpTip('helptip-received-certs', { bindings: { content: 'HELP.TIP.WALLET_RECEIVED_CERTS' } }); }, function() { if (skipAll || !UIUtils.screen.isSmall()) return true; return $state.go('app.view_wallet') // go back to wallet (small device only) .then(function() { return $scope.showHelpTip('helptip-wallet-given-certifications', { bindings: { content: 'HELP.TIP.WALLET_GIVEN_CERTIFICATIONS', icon: { position: 'center' } }, timeout: 500 }); }); }, function() { if (skipAll) return true; // Click on given cert link (small device only) if ($state.is('app.view_wallet')) { var element = $window.document.getElementById('helptip-wallet-given-certifications'); if (!element) { skipAll = true; return true; } $timeout(function() { angular.element(element).triggerHandler('click'); }, 500); } return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'fab-select-certify': 'helptip-certs-select-certify', { bindings: { content: 'HELP.TIP.WALLET_CERTIFY', icon: { position: UIUtils.screen.isSmall() ? 'bottom-right' : 'center' } }, timeout: UIUtils.screen.isSmall() ? 2000 : 500, retry: 10 }); }, function() { if ($scope.tour || skipAll) return hasNext; // skip Rules if features tour (already display) return $scope.showHelpTip('helptip-certs-stock', { bindings: { content: 'HELP.TIP.CERTIFY_RULES', icon: { position: 'center', glyph: 'ion-alert-circled' }, hasNext: hasNext } }); } /* FIXME : how to select the left tab ? ,function() { return $scope.showHelpTip('helptip-certs-stock', { bindings: { content: 'HELP.TIP.WALLET_CERT_STOCK', contentParams: contentParams, icon: { position: 'center' }, hasNext: hasNext } }); }*/ ]; return csCurrency.default().then(function(currency) { contentParams = currency.parameters; return $scope.executeStep('certs', steps, startIndex); }); }; /** * Features tour on TX screen * @returns {*} */ $scope.startTxTour = function(startIndex, hasNext) { if (!csWallet.isLogin()) return $q.when(true); // skip if not login var contentParams; var steps = [ function () { $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-tx', { bindings: { content: $rootScope.walletData.isMember ? 'HELP.TIP.MENU_BTN_TX_MEMBER' : 'HELP.TIP.MENU_BTN_TX', icon: { position: 'left' } } }); }, function () { if ($ionicSideMenuDelegate.isOpen()) { $ionicSideMenuDelegate.toggleLeft(false); } // Go to wallet return $state.go('app.view_wallet_tx') .then(function () { return $scope.showHelpTip('helptip-wallet-balance', { bindings: { content: csSettings.data.useRelative ? 'HELP.TIP.WALLET_BALANCE_RELATIVE' : 'HELP.TIP.WALLET_BALANCE', contentParams: contentParams, icon: { position: 'center' } }, retry: 20 // 10 * 500 = 5s max }); }); }, function () { return $scope.showHelpTip('helptip-wallet-balance', { bindings: { content: 'HELP.TIP.WALLET_BALANCE_CHANGE_UNIT', contentParams: contentParams, icon: { position: 'center', glyph: 'ion-information-circled' } } }); } ]; // Get currency parameters, with currentUD return csCurrency.default() .then(function(currency) { contentParams = currency.parameters; contentParams.currentUD = $rootScope.walletData.currentUD; // Launch steps return $scope.executeStep('tx', steps, startIndex); }); }; /** * header tour * @returns {*} */ $scope.startHeaderTour = function(startIndex, hasNext) { if (UIUtils.screen.isSmall()) return $q.when(true); function _getProfilBtnElement() { var elements = $window.document.querySelectorAll('#helptip-header-bar-btn-profile'); if (!elements || !elements.length) return null; return _.find(elements, function(el) {return el.offsetWidth > 0;}); } var steps = [ function () { if (UIUtils.screen.isSmall()) return true; // skip for small screen var element = _getProfilBtnElement(); if (!element) return true; return $scope.showHelpTip(element, { bindings: { content: 'HELP.TIP.HEADER_BAR_BTN_PROFILE', icon: { position: 'right' } } }); }, function () { // small screens if (UIUtils.screen.isSmall()) { $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-settings', { bindings: { content: 'HELP.TIP.MENU_BTN_SETTINGS', icon: { position: 'left' }, hasNext: hasNext }, timeout: 1000 }); } // wide screens else { var element = _getProfilBtnElement(); if (!element) return true; $timeout(function() { angular.element(element).triggerHandler('click'); }); return $scope.showHelpTip('helptip-popover-profile-btn-settings', { bindings: { content: 'HELP.TIP.MENU_BTN_SETTINGS', icon: { position: 'center' }, hasNext: hasNext }, timeout: 1000 }) .then(function(res) { // close profile popover $scope.closeProfilePopover(); return res; }); } } ]; return $scope.executeStep('header', steps, startIndex); }; /** * Settings tour * @returns {*} */ $scope.startSettingsTour = function(startIndex, hasNext) { var contentParams; var steps = [ function () { if (!UIUtils.screen.isSmall()) return true; $ionicSideMenuDelegate.toggleLeft(true); return $scope.showHelpTip('helptip-menu-btn-settings', { bindings: { content: 'HELP.TIP.MENU_BTN_SETTINGS', icon: { position: 'left' } }, timeout: 1000 }); }, function () { if ($ionicSideMenuDelegate.isOpen()) { $ionicSideMenuDelegate.toggleLeft(false); } // Go to settings return $state.go('app.settings') .then(function () { return $scope.showHelpTip('helptip-settings-btn-unit-relative', { bindings: { content: 'HELP.TIP.SETTINGS_CHANGE_UNIT', contentParams: contentParams, icon: { position: 'right', style: 'margin-right: 60px' }, hasNext: hasNext }, timeout: 1000 }); }); } ]; return csCurrency.default() .then(function(currency) { contentParams = currency.parameters; return $scope.executeStep('settings', steps, startIndex); }); }; /** * Finish the features tour (last step) * @returns {*} */ $scope.finishTour = function() { if ($ionicSideMenuDelegate.isOpen()) { $ionicSideMenuDelegate.toggleLeft(false); } // If login: redirect to wallet if (csWallet.isLogin()) { return $state.go('app.view_wallet') .then(function(){ return $scope.showHelpTip('helptip-wallet-certifications', { bindings: { content: 'HELP.TIP.END_LOGIN', hasNext: false } }); }); } // If not login: redirect to home else { var contentParams; return $q.all([ $scope.showHome(), csCurrency.default() .then(function(parameters) { contentParams = parameters; }) ]) .then(function(){ return $scope.showHelpTip('helptip-home-logo', { bindings: { content: 'HELP.TIP.END_NOT_LOGIN', contentParams: contentParams, hasNext: false } }); }); } }; } /* ---------------------------- * Help tour (auto start from home page) * ---------------------------- */ function HelpTourController($scope) { $scope.$on('$ionicView.enter', function(e, state) { $scope.startHelpTour(); }); } WalletController.$inject = ['$scope', '$q', '$ionicPopup', '$timeout', '$state', 'UIUtils', 'csWallet', '$translate', '$ionicPopover', 'Modals', 'csSettings', 'esHttp'];angular.module('cesium.wallet.controllers', ['cesium.services']) .config(['$stateProvider', function($stateProvider) { 'ngInject'; $stateProvider .state('app.view_wallet', { url: "/wallet", views: { 'menuContent': { templateUrl: "templates/wallet/view_wallet.html", controller: 'WalletCtrl' } } }) ; }]) .controller('WalletCtrl', WalletController) ; function WalletController($scope, $q, $ionicPopup, $timeout, $state, UIUtils, csWallet, $translate, $ionicPopover, Modals, csSettings, esHttp) { 'ngInject'; $scope.loading = true; $scope.settings = csSettings.data; $scope.likeData = { views: {}, likes: {}, follows: {}, abuses: {}, stars: {} }; $scope.$on('$ionicView.enter', function(e, state) { if ($scope.loading) { // load once $scope.loadWallet() .then(function(walletData) { $scope.formData = walletData; $scope.loading=false; // very important, to avoid TX to be display before wallet.currentUd is loaded $scope.updateView(); //$scope.showQRCode('qrcode', $scope.formData.pubkey, 1100); //$scope.showHelpTip(); UIUtils.loading.hide(); // loading could have be open (e.g. new account) }) .catch(function(err){ if (err === 'CANCELLED') { $scope.showHome(); } }); } else { // update view (to refresh profile and subscriptions) $scope.updateView(); } $scope.$broadcast('$recordView.enter', state); }); $scope.updateView = function() { $scope.motion.show({selector: '#wallet .item'}); $scope.$broadcast('$$rebind::' + 'rebind'); // force rebind }; // Listen new events (can appears from security wizard also) $scope.$watchCollection('formData.events', function(newEvents, oldEvents) { if (!oldEvents || $scope.loading || angular.equals(newEvents, oldEvents)) return; $scope.updateView(); }); $scope.setRegisterForm = function(registerForm) { $scope.registerForm = registerForm; }; // Clean controller data when logout $scope.onWalletLogout = function() { delete $scope.qrcode; // clean QRcode delete $scope.formData; $scope.loading = true; }; csWallet.api.data.on.logout($scope, $scope.onWalletLogout); // Ask uid $scope.showUidPopup = function() { return $q(function(resolve, reject) { $translate(['ACCOUNT.NEW.TITLE', 'ACCOUNT.POPUP_REGISTER.TITLE', 'ACCOUNT.POPUP_REGISTER.HELP', 'COMMON.BTN_OK', 'COMMON.BTN_CANCEL']) .then(function (translations) { $scope.formData.newUid = (!!$scope.formData.uid ? ''+$scope.formData.uid : ''); // Choose UID popup $ionicPopup.show({ templateUrl: 'templates/wallet/popup_register.html', title: translations['ACCOUNT.POPUP_REGISTER.TITLE'], subTitle: translations['ACCOUNT.POPUP_REGISTER.HELP'], scope: $scope, buttons: [ { text: translations['COMMON.BTN_CANCEL'] }, { text: translations['COMMON.BTN_OK'], type: 'button-positive', onTap: function(e) { $scope.registerForm.$submitted=true; if(!$scope.registerForm.$valid || !$scope.formData.newUid) { //don't allow the user to close unless he enters a uid e.preventDefault(); } else { return $scope.formData.newUid; } } } ] }) .then(function(uid) { if (!uid) { // user cancel delete $scope.formData.uid; UIUtils.loading.hide(); return; } resolve(uid); }); }); }); }; // Send self identity $scope.self = function() { $scope.hideActionsPopover(); return $scope.showUidPopup() .then(function(uid) { UIUtils.loading.show(); return csWallet.self(uid) .then(function() { $scope.updateView(); UIUtils.loading.hide(); }) .catch(function(err){ UIUtils.onError('ERROR.SEND_IDENTITY_FAILED')(err) .then(function() { $scope.self(); // loop }); }); }); }; $scope.doMembershipIn = function(retryCount) { return csWallet.membership.inside() .then(function() { $scope.updateView(); UIUtils.loading.hide(); }) .catch(function(err) { if (!retryCount || retryCount <= 2) { $timeout(function() { $scope.doMembershipIn(retryCount ? retryCount+1 : 1); }, 1000); } else { UIUtils.onError('ERROR.SEND_MEMBERSHIP_IN_FAILED')(err) .then(function() { $scope.membershipIn(); // loop }); } }); }; // Send membership IN $scope.membershipIn = function() { $scope.hideActionsPopover(); if ($scope.formData.isMember) { return UIUtils.alert.info("INFO.NOT_NEED_MEMBERSHIP"); } return $scope.showUidPopup() .then(function (uid) { UIUtils.loading.show(); // If uid changed, or self blockUid not retrieve : do self() first if (!$scope.formData.blockUid || uid != $scope.formData.uid) { $scope.formData.blockUid = null; $scope.formData.uid = uid; csWallet.self(uid, false/*do NOT load membership here*/) .then(function() { $scope.doMembershipIn(); }) .catch(function(err){ UIUtils.onError('ERROR.SEND_IDENTITY_FAILED')(err) .then(function() { $scope.membershipIn(); // loop }); }); } else { $scope.doMembershipIn(); } }) .catch(function(err){ UIUtils.loading.hide(); UIUtils.alert.info(err); $scope.membershipIn(); // loop }); }; // Send membership OUT $scope.membershipOut = function(confirm, confirmAgain) { $scope.hideActionsPopover(); // Ask user confirmation if (!confirm) { return UIUtils.alert.confirm('CONFIRM.MEMBERSHIP_OUT', 'CONFIRM.POPUP_WARNING_TITLE', { cssClass: 'warning', okText: 'COMMON.BTN_YES', okType: 'button-assertive' }) .then(function(confirm) { if (confirm) $scope.membershipOut(true); // loop with confirmation }); } if (!confirmAgain) { return UIUtils.alert.confirm("CONFIRM.MEMBERSHIP_OUT_2", 'CONFIRM.POPUP_TITLE', { cssClass: 'warning', okText: 'COMMON.BTN_YES', okType: 'button-assertive' }) .then(function (confirm) { if (confirm) $scope.membershipOut(true, true); // loop with all confirmations }); } UIUtils.loading.show(); return csWallet.membership.out() .then(function() { UIUtils.loading.hide(); UIUtils.toast.show('INFO.MEMBERSHIP_OUT_SENT'); }) .catch(UIUtils.onError('ERROR.SEND_MEMBERSHIP_OUT_FAILED')); }; // Updating wallet data $scope.doUpdate = function() { console.debug('[wallet] Updating wallet...'); return UIUtils.loading.show() .then(function() { return csWallet.refreshData(); }) .then(function() { return UIUtils.loading.hide(); }) .then(function() { $scope.updateView(); }) .catch(UIUtils.onError('ERROR.REFRESH_WALLET_DATA')); }; /** * Renew membership */ $scope.renewMembership = function(confirm) { if (!$scope.formData.isMember) { return UIUtils.alert.error("ERROR.ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION"); } if (!confirm && !$scope.formData.requirements.needRenew) { return $translate("CONFIRM.NOT_NEED_RENEW_MEMBERSHIP", {membershipExpiresIn: $scope.formData.requirements.membershipExpiresIn}) .then(function(message) { return UIUtils.alert.confirm(message); }) .then(function(confirm) { if (confirm) $scope.renewMembership(true); // loop with confirm }); } return UIUtils.alert.confirm("CONFIRM.RENEW_MEMBERSHIP") .then(function(confirm) { if (confirm) { UIUtils.loading.show(); return $scope.doMembershipIn(); } }) .catch(function(err){ UIUtils.loading.hide(); UIUtils.alert.error(err) // loop .then($scope.renewMembership); }); }; /** * Fix identity (e.g. when identity expired) */ $scope.fixIdentity = function() { if (!$scope.formData.uid) return; return $translate('CONFIRM.FIX_IDENTITY', {uid: $scope.formData.uid}) .then(function(message) { return UIUtils.alert.confirm(message); }) .then(function(confirm) { if (!confirm) return; UIUtils.loading.show(); // Reset membership data $scope.formData.blockUid = null; $scope.formData.sigDate = null; return csWallet.self($scope.formData.uid); }) .then(function() { return $scope.doMembershipIn(); }) .catch(function(err){ UIUtils.loading.hide(); UIUtils.alert.error(err) .then(function() { $scope.fixIdentity(); // loop }); }); }; /** * Fix membership, when existing MS reference an invalid block */ $scope.fixMembership = function() { if (!$scope.formData.uid) return; return UIUtils.alert.confirm("CONFIRM.FIX_MEMBERSHIP") .then(function(confirm) { if (!confirm) return; UIUtils.loading.show(); // Reset membership data $scope.formData.blockUid = null; $scope.formData.sigDate = null; return Wallet.self($scope.formData.uid, false/*do NOT load membership here*/); }) .then(function() { return $scope.doMembershipIn(); }) .catch(function(err){ UIUtils.loading.hide(); UIUtils.alert.info(err); $scope.fixMembership(); // loop }); }; /** * Catch click for quick fix * @param fix */ $scope.doQuickFix = function(event) { if (event === 'renew') { $scope.renewMembership(); } else if (event === 'fixMembership') { $scope.fixMembership(); } else if (event === 'fixIdentity') { $scope.fixIdentity(); } }; /* -- popup / UI -- */ // Transfer $scope.showTransferModal = function() { var hasCredit = (!!$scope.walletData.balance && $scope.walletData.balance > 0); if (!hasCredit) { UIUtils.alert.info('INFO.NOT_ENOUGH_CREDIT'); return; } Modals.showTransfer() .then(function(done){ if (done) { UIUtils.toast.show('INFO.TRANSFER_SENT'); $scope.$broadcast('$$rebind::' + 'balance'); // force rebind balance $scope.motion.show({selector: '.item-pending'}); } }); }; $scope.startWalletTour = function() { $scope.hideActionsPopover(); return $scope.showHelpTip(0, true); }; $scope.showHelpTip = function(index, isTour) { index = angular.isDefined(index) ? index : csSettings.data.helptip.wallet; isTour = angular.isDefined(isTour) ? isTour : false; if (index < 0) return; // Create a new scope for the tour controller var helptipScope = $scope.createHelptipScope(isTour); if (!helptipScope) return; // could be undefined, if a global tour already is already started helptipScope.tour = isTour; return helptipScope.startWalletTour(index, false) .then(function(endIndex) { helptipScope.$destroy(); if (!isTour) { csSettings.data.helptip.wallet = endIndex; csSettings.store(); } }); }; $scope.showQRCode = function(id, text, timeout) { if (!!$scope.qrcode) { return; } $scope.qrcode = new QRCode(id, { text: text, width: 200, height: 200, correctLevel: QRCode.CorrectLevel.L }); UIUtils.motion.toggleOn({selector: '#wallet #'+id+'.qrcode'}, timeout || 1100); }; $scope.showCertifications = function() { // Warn: do not use a simple link here (a ng-click is mandatory for help tour) $state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert' : 'app.wallet_cert_lg', { pubkey: $scope.formData.pubkey, uid: $scope.formData.name || $scope.formData.uid, type: 'received' }); }; $scope.showGivenCertifications = function() { // Warn: do not use a simple link here (a ng-click is mandatory for help tour) $state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert' : 'app.wallet_cert_lg', { pubkey: $scope.formData.pubkey, uid: $scope.formData.name || $scope.formData.uid, type: 'given' }); }; $scope.showActionsPopover = function(event) { if (!$scope.actionsPopover) { $ionicPopover.fromTemplateUrl('templates/wallet/popover_actions.html', { scope: $scope }).then(function(popover) { $scope.actionsPopover = popover; //Cleanup the popover when we're done with it! $scope.$on('$destroy', function() { $scope.actionsPopover.remove(); }); $scope.actionsPopover.show(event); }); } else { $scope.actionsPopover.show(event); } }; $scope.hideActionsPopover = function() { if ($scope.actionsPopover) { $scope.actionsPopover.hide(); } }; $scope.showSharePopover = function(event) { $scope.hideActionsPopover(); var title = $scope.formData.name || $scope.formData.uid || $scope.formData.pubkey; // Use pod share URL - see issue #69 var url = esHttp.getUrl('/user/profile/' + $scope.formData.pubkey + '/_share'); // Override default position, is small screen - fix #25 if (UIUtils.screen.isSmall()) { event = angular.element(document.querySelector('#wallet-share-anchor')) || event; } UIUtils.popover.share(event, { bindings: { url: url, titleKey: 'WOT.VIEW.POPOVER_SHARE_TITLE', titleValues: {title: title}, postMessage: title } }); }; $scope.showSecurityModal = function(){ $scope.hideActionsPopover(); Modals.showAccountSecurity(); }; } WotLookupController.$inject = ['$scope', '$state', '$timeout', '$focus', '$ionicPopover', '$ionicHistory', 'UIUtils', 'csConfig', 'csCurrency', 'csSettings', 'Device', 'BMA', 'csWallet', 'esDocument', 'esProfile']; WotLookupModalController.$inject = ['$scope', '$controller', '$focus', 'parameters']; WotIdentityAbstractController.$inject = ['$scope', '$rootScope', '$state', '$translate', '$ionicHistory', 'UIUtils', 'Modals', 'esHttp', 'csCurrency', 'csWot', 'csWallet']; WotIdentityViewController.$inject = ['$scope', '$rootScope', '$controller', '$timeout', 'UIUtils', 'csWallet'];angular.module('cesium.wot.controllers', ['cesium.services']) .config(['$stateProvider', function($stateProvider) { 'ngInject'; $stateProvider .state('app.wot_lookup', { url: "/wot?q&type&hash", views: { 'menuContent': { templateUrl: "templates/wot/lookup.html", controller: 'WotLookupCtrl' } } }) .state('app.wot_identity', { url: "/wot/:pubkey/:uid?action", views: { 'menuContent': { templateUrl: "templates/wot/view_identity.html", controller: 'WotIdentityViewCtrl' } } }) .state('app.wot_identity_uid', { url: "/lookup/:uid?action", views: { 'menuContent': { templateUrl: "templates/wot/view_identity.html", controller: 'WotIdentityViewCtrl' } } }); }]) .controller('WotLookupCtrl', WotLookupController) .controller('WotLookupModalCtrl', WotLookupModalController) .controller('WotIdentityAbstractCtrl', WotIdentityAbstractController) .controller('WotIdentityViewCtrl', WotIdentityViewController) ; function WotLookupController($scope, $state, $timeout, $focus, $ionicPopover, $ionicHistory, UIUtils, csConfig, csCurrency, csSettings, Device, BMA, csWallet, esDocument, esProfile) { 'ngInject'; var defaultSearchLimit = 10; $scope.search = { text: '', loading: true, type: null, results: [] }; $scope._source = ["issuer", "title", "city", "time", "avatar._content_type"]; $scope.entered = false; $scope.wotSearchTextId = 'wotSearchText'; $scope.enableFilter = true; $scope.allowMultiple = false; $scope.selection = []; $scope.showResultLabel = true; $scope.parameters = {}; // override in the modal controller $scope.enter = function(e, state) { if (!$scope.entered) { if (state.stateParams && state.stateParams.q) { // Query parameter $scope.search.text = state.stateParams.q; $timeout(function() { $scope.doSearch(); }, 100); } else if (state.stateParams && state.stateParams.hash) { // hash tag parameter $scope.search.text = '#' + state.stateParams.hash; $timeout(function() { $scope.doSearch(); }, 100); } else { $timeout(function() { // get new comers if (state.stateParams.type === 'newcomers' || (!csConfig.initPhase && !state.stateParams.type)) { $scope.doGetNewcomers(0, undefined, true/*skipLocationUpdate*/); } }, 100); } // removeIf(device) // Focus on search text (only if NOT device, to avoid keyboard opening) $focus($scope.wotSearchTextId); // endRemoveIf(device) $scope.entered = true; $timeout(UIUtils.ink, 100); $scope.showHelpTip(); } }; $scope.$on('$ionicView.enter', $scope.enter); $scope.resetWotSearch = function() { $scope.search = { text: null, loading: false, type: 'newcomers', results: [] }; }; $scope.updateLocationHref = function() { // removeIf(device) var stateParams = { q: undefined, hash: undefined, type: undefined }; if ($scope.search.type === 'text') { var text = $scope.search.text.trim(); if (text.match(/^#[\wḡĞǦğàáâãäåçèéêëìíîïðòóôõöùúûüýÿ]+$/)) { stateParams.hash = text.substr(1); } else { stateParams.q = text; } } else { stateParams.type = $scope.search.type; } // Update location href $ionicHistory.nextViewOptions({ disableAnimate: true, disableBack: true, historyRoot: true }); $state.go('app.wot_lookup', stateParams, { reload: false, inherit: true, notify: false }); // endRemoveIf(device) }; $scope.doSearchText = function() { $scope.doSearch(); $scope.updateLocationHref(); }; $scope.doSearch = function(offset, size) { var text = $scope.search.text.trim(); if ((UIUtils.screen.isSmall() && text.length < 3) || !text.length) { $scope.search.results = []; $scope.search.type = 'none'; return $q.when(); } $scope.search.loading = true; var options = { from: offset || 0, size: size || defaultSearchLimit, _source: $scope._source }; $scope.search.type = 'text'; return esProfile.searchText(text, options) .then(function(idties){ if ($scope.search.type !== 'text') return; // could have change if ($scope.search.text.trim() !== text) return; // search text has changed before received response if ((!idties || !idties.length) && BMA.regexp.PUBKEY.test(text)) { $scope.doDisplayResult([{pubkey: text}]); } else { $scope.doDisplayResult(idties, offset, size); } }) .catch(UIUtils.onError('ERROR.WOT_LOOKUP_FAILED')); }; $scope.doGetNewcomers = function(offset, size, skipLocationUpdate) { offset = offset || 0; size = size || defaultSearchLimit; if (size < defaultSearchLimit) size = defaultSearchLimit; $scope.hideActionsPopover(); $scope.search.loading = (offset === 0); $scope.search.type = 'newcomers'; // Update location href if (!offset && !skipLocationUpdate) { $scope.updateLocationHref(); } var options = { index: 'user', type: 'profile', from: offset, size: size, sort: {time: 'desc'} }; return esProfile.search(options) .then(function(idties){ if ($scope.search.type !== 'newcomers') return false; // could have change $scope.doDisplayResult(idties, offset, size); return true; }) .catch(function(err) { $scope.search.loading = false; $scope.search.results = (offset > 0) ? $scope.search.results : []; $scope.search.hasMore = false; UIUtils.onError('ERROR.LOAD_NEWCOMERS_FAILED')(err); }); }; $scope.showMore = function() { var offset = $scope.search.results ? $scope.search.results.length : 0; $scope.search.loadingMore = true; var searchFunction = ($scope.search.type === 'newcomers') ? $scope.doGetNewcomers : $scope.doGetPending; return searchFunction(offset) .then(function(ok) { if (ok) { $scope.search.loadingMore = false; $scope.$broadcast('scroll.infiniteScrollComplete'); } }) .catch(function(err) { console.error(err); $scope.search.loadingMore = false; $scope.search.hasMore = false; $scope.$broadcast('scroll.infiniteScrollComplete'); }); }; $scope.select = function(identity) { // identity = self -> open the user wallet if (csWallet.isUserPubkey(identity.pubkey)) { $state.go('app.view_wallet'); } // Open identity view else { $state.go('app.wot_identity', { pubkey: identity.pubkey, uid: identity.uid }); } }; $scope.next = function() { // This method should be override by sub controller (e.g. modal controller) console.log('Selected identities:', $scope.selection); }; $scope.toggleCheck = function(index, e) { var identity = $scope.search.results[index]; if (identity.checked) { $scope.addToSelection(identity); } else { $scope.removeSelection(identity, e); } }; $scope.toggleSelect = function(identity){ identity.selected = !identity.selected; }; $scope.addToSelection = function(identity) { var copyIdty = angular.copy(identity); if (copyIdty.name) { copyIdty.name = copyIdty.name.replace('', '').replace('', ''); // remove highlight } $scope.selection.push(copyIdty); }; $scope.removeSelection = function(identity, e) { // Remove from selection array var identityInSelection = _.findWhere($scope.selection, {id: identity.id}); if (identityInSelection) { $scope.selection.splice($scope.selection.indexOf(identityInSelection), 1); } // Uncheck in result array, if exists if (!$scope.search.loading) { var existIdtyInResult = _.findWhere($scope.search.results, {id: identity.id}); if (existIdtyInResult && existIdtyInResult.checked) { existIdtyInResult.checked = false; } } //e.preventDefault(); }; $scope.scanQrCode = function(){ if (!Device.barcode.enable) { return; } Device.barcode.scan() .then(function(result) { if (!result) { return; } BMA.uri.parse(result) .then(function(obj){ if (obj.pubkey) { $scope.search.text = obj.pubkey; } else if (result.uid) { $scope.search.text = obj.uid; } else { $scope.search.text = result; } $scope.doSearch(); }); }) .catch(UIUtils.onError('ERROR.SCAN_FAILED')); }; // Show help tip (show only not already shown) $scope.showHelpTip = function() { if (!$scope.isLogin()) return; var index = angular.isDefined(index) ? index : csSettings.data.helptip.wot; if (index < 0) return; if (index === 0) index = 1; // skip first step // Create a new scope for the tour controller var helptipScope = $scope.createHelptipScope(); if (!helptipScope) return; // could be undefined, if a global tour already is already started return helptipScope.startWotTour(index, false) .then(function(endIndex) { helptipScope.$destroy(); csSettings.data.helptip.wot = endIndex; csSettings.store(); }); }; $scope.doDisplayResult = function(res, offset, size) { res = res || {}; // pre-check result if already in selection if ($scope.allowMultiple && res.length && $scope.selection.length) { _.forEach($scope.selection, function(identity) { var identityInRes = _.findWhere(res, {id: identity.id}); if (identityInRes) { identityInRes.checked = true; } }); } if (!offset) { $scope.search.results = res || []; } else { $scope.search.results = $scope.search.results.concat(res); } $scope.search.loading = false; $scope.search.hasMore = res.length && $scope.search.results.length >= (offset + size); $scope.smallscreen = UIUtils.screen.isSmall(); if (!$scope.search.results.length) return; // Motion if (res.length > 0 && $scope.motion) { $scope.motion.show({selector: '.lookupForm .list .item', ink: true}); } }; /* -- show/hide popup -- */ $scope.showActionsPopover = function(event) { if (!$scope.actionsPopover) { $ionicPopover.fromTemplateUrl('templates/wot/lookup_popover_actions.html', { scope: $scope }).then(function(popover) { $scope.actionsPopover = popover; //Cleanup the popover when we're done with it! $scope.$on('$destroy', function() { $scope.actionsPopover.remove(); }); $scope.actionsPopover.show(event); }); } else { $scope.actionsPopover.show(event); } }; $scope.hideActionsPopover = function() { if ($scope.actionsPopover) { $scope.actionsPopover.hide(); } }; } function WotLookupModalController($scope, $controller, $focus, parameters){ 'ngInject'; // Initialize the super class and extend it. angular.extend(this, $controller('WotLookupCtrl', {$scope: $scope})); parameters = parameters || {}; $scope.search.loading = false; $scope.enableFilter = angular.isDefined(parameters.enableFilter) ? parameters.enableFilter : false; $scope.allowMultiple = angular.isDefined(parameters.allowMultiple) ? parameters.allowMultiple : false; $scope.parameters = parameters; $scope.showResultLabel = false; $scope.wotSearchTextId = 'wotSearchTextModal'; if ($scope.allowMultiple && parameters.selection) { $scope.selection = parameters.selection; } $scope.cancel = function(){ $scope.closeModal(); }; $scope.select = function(identity){ $scope.closeModal({ pubkey: identity.pubkey, uid: identity.uid, name: identity.name && identity.name.replace(/<\/?em>/ig, '') }); }; $scope.next = function() { $scope.closeModal($scope.selection); }; $scope.updateLocationHref = function() { // Do NOT change location href }; $scope.showHelpTip = function() { // silent }; // removeIf(device) // Focus on search text (only if NOT device, to avoid keyboard opening) $focus($scope.wotSearchTextId); // endRemoveIf(device) } /** * Abtract controller that load identity, that expose some useful methods in $scope, like 'certify()' * @param $scope * @param $state * @param $timeout * @param UIUtils * @param Modals * @param csConfig * @param csWot * @param csWallet * @constructor */ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $ionicHistory, UIUtils, Modals, esHttp, csCurrency, csWot, csWallet) { 'ngInject'; $scope.formData = { hasSelf: true }; $scope.disableCertifyButton = true; $scope.loading = true; $scope.load = function(pubkey, withCache, uid) { return csWot.load(pubkey, withCache, uid) .then(function(identity){ if (!identity) return UIUtils.onError('ERROR.IDENTITY_NOT_FOUND')().then($scope.showHome); $scope.formData = identity; $scope.revoked = identity.requirements && (identity.requirements.revoked || identity.requirements.pendingRevocation); $scope.canCertify = identity.hasSelf && (!csWallet.isLogin() || (!csWallet.isUserPubkey(pubkey))) && !$scope.revoked; $scope.canSelectAndCertify = identity.hasSelf && csWallet.isUserPubkey(pubkey); $scope.alreadyCertified = !$scope.canCertify || !csWallet.isLogin() ? false : (!!_.findWhere(identity.received_cert, { pubkey: csWallet.data.pubkey, valid: true }) || !!_.findWhere(identity.received_cert_pending, { pubkey: csWallet.data.pubkey, valid: true })); $scope.disableCertifyButton = $scope.alreadyCertified || $scope.revoked; $scope.loading = false; }) .catch(function(err) { $scope.loading = false; UIUtils.onError('ERROR.LOAD_IDENTITY_FAILED')(err); }); }; $scope.removeActionParamInLocationHref = function(state) { if (!state || !state.stateParams || !state.stateParams.action) return; var stateParams = angular.copy(state.stateParams); // Reset action param stateParams.action = null; // Update location href $ionicHistory.nextViewOptions({ disableAnimate: true, disableBack: false, historyRoot: false }); $state.go(state.stateName, stateParams, { reload: false, inherit: true, notify: false }); }; /* -- open screens -- */ $scope.showSharePopover = function(event) { var title = $scope.formData.name || $scope.formData.uid || $scope.formData.pubkey; // Use pod share URL - see issue #69 var url = esHttp.getUrl('/user/profile/' + $scope.formData.pubkey + '/_share'); // Override default position, is small screen - fix #25 if (UIUtils.screen.isSmall()) { event = angular.element(document.querySelector('#wot-share-anchor-'+$scope.formData.pubkey)) || event; } UIUtils.popover.share(event, { bindings: { url: url, titleKey: 'WOT.VIEW.POPOVER_SHARE_TITLE', titleValues: {title: title}, postMessage: title } }); }; } /** * Identity view controller - should extend WotIdentityAbstractCtrl */ function WotIdentityViewController($scope, $rootScope, $controller, $timeout, UIUtils, csWallet) { 'ngInject'; // Initialize the super class and extend it. angular.extend(this, $controller('WotIdentityAbstractCtrl', {$scope: $scope})); $scope.motion = UIUtils.motion.fadeSlideInRight; // Init likes here, to be able to use in extension $scope.options = $scope.options || {}; $scope.options.like = { kinds: ['VIEW', 'LIKE', 'ABUSE', 'FOLLOW', 'STAR'], index: 'user', type: 'profile' }; $scope.likeData = { views: {}, likes: {}, follows: {}, abuses: {}, stars: {} }; $scope.$on('$ionicView.enter', function(e, state) { var onLoadSuccess = function() { $scope.doMotion(); if (state.stateParams && state.stateParams.action) { $timeout(function() { $scope.doAction(state.stateParams.action.trim()); }, 100); $scope.removeActionParamInLocationHref(state); $scope.likeData.id = $scope.formData.pubkey; } }; if (state.stateParams && state.stateParams.pubkey && state.stateParams.pubkey.trim().length > 0) { if ($scope.loading) { // load once return $scope.load(state.stateParams.pubkey.trim(), true /*withCache*/, state.stateParams.uid) .then(onLoadSuccess); } } else if (state.stateParams && state.stateParams.uid && state.stateParams.uid.trim().length > 0) { if ($scope.loading) { // load once return $scope.load(null, true /*withCache*/, state.stateParams.uid) .then(onLoadSuccess); } } // Load from wallet pubkey else if (csWallet.isLogin()){ if ($scope.loading) { return $scope.load(csWallet.data.pubkey, true /*withCache*/, csWallet.data.uid) .then(onLoadSuccess); } } // Redirect to home else { $scope.showHome(); } }); $scope.doMotion = function() { $scope.motion.show({selector: '.view-identity .list .item'}); $scope.$broadcast('$csExtension.motion'); }; } SettingsController.$inject = ['$scope', '$q', '$window', '$ionicHistory', '$ionicPopup', '$timeout', '$translate', 'UIUtils', 'Modals', 'BMA', 'csHttp', 'csConfig', 'csSettings', 'csPlatform']; angular.module('cesium.settings.controllers', ['cesium.services']) .config(['$stateProvider', function($stateProvider) { 'ngInject'; $stateProvider .state('app.settings', { url: "/settings", views: { 'menuContent': { templateUrl: "templates/settings/settings.html", controller: 'SettingsCtrl' } } }) ; }]) .controller('SettingsCtrl', SettingsController) ; function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $timeout, $translate, UIUtils, Modals, BMA, csHttp, csConfig, csSettings, csPlatform) { 'ngInject'; $scope.formData = angular.copy(csSettings.data); $scope.popupData = {}; // need for the node popup $scope.loading = true; $scope.nodePopup = {}; $scope.bma = BMA; $scope.$on('$ionicView.enter', function() { csSettings.ready().then($scope.load); }); $scope.setPopupForm = function(popupForm) { $scope.popupForm = popupForm; }; $scope.load = function() { $scope.loading = true; // to avoid the call of csWallet.store() // Fill locales $scope.locales = angular.copy(csSettings.locales); // Apply settings angular.merge($scope.formData, csSettings.data); // Make sure to use full locale object (id+name) $scope.formData.locale = (csSettings.data.locale && csSettings.data.locale.id && _.findWhere($scope.locales, {id: csSettings.data.locale.id})) || _.findWhere($scope.locales, {id: csSettings.defaultSettings.locale.id}); return $timeout(function() { $scope.loading = false; // Set Ink UIUtils.ink({selector: '.item'}); $scope.showHelpTip(); }, 100); }; $scope.reset = function() { if ($scope.actionsPopover) { $scope.actionsPopover.hide(); } $scope.pendingSaving = true; csSettings.reset() .then(csPlatform.restart) .then(function() { // reload $scope.load(); $scope.pendingSaving = false; }); }; $scope.changeLanguage = function(langKey) { $translate.use(langKey); }; // Change node $scope.changeNode= function(node) { var port = !!$scope.formData.node.port && $scope.formData.node.port != 80 && $scope.formData.node.port != 443 ? $scope.formData.node.port : undefined; node = node || { host: $scope.formData.node.host, port: port, useSsl: angular.isDefined($scope.formData.node.useSsl) ? $scope.formData.node.useSsl : ($scope.formData.node.port == 443) }; $scope.showNodePopup(node) .then(function(newNode) { if (newNode.host === $scope.formData.node.host && newNode.port === $scope.formData.node.port && newNode.useSsl === $scope.formData.node.useSsl && !$scope.formData.node.temporary) { return; // same node = nothing to do } UIUtils.loading.show(); var nodeBMA = BMA.instance(newNode.host, newNode.port, newNode.useSsl, true /*cache*/); nodeBMA.isAlive() .then(function(alive) { if (!alive) { UIUtils.loading.hide(); return UIUtils.alert.error('ERROR.INVALID_NODE_SUMMARY') .then(function(){ $scope.changeNode(newNode); // loop }); } UIUtils.loading.hide(); angular.merge($scope.formData.node, newNode); delete $scope.formData.node.temporary; BMA.copy(nodeBMA); $scope.bma = BMA; // Restart platform (or start if not already started) csPlatform.restart(); // Reset history cache return $ionicHistory.clearCache(); }); }); }; $scope.showNodeList = function() { // Check if need a filter on SSL node var forceUseSsl = (csConfig.httpsMode === 'true' || csConfig.httpsMode === true || csConfig.httpsMode === 'force') || ($window.location && $window.location.protocol === 'https:') ? true : false; $ionicPopup._popupStack[0].responseDeferred.promise.close(); return Modals.showNetworkLookup({ enableFilter: true, // enable filter button bma: true, // only BMA node ssl: forceUseSsl ? true : undefined }) .then(function (peer) { if (peer) { var bma = peer.getBMA(); return { host: (bma.dns ? bma.dns : (peer.hasValid4(bma) ? bma.ipv4 : bma.ipv6)), port: bma.port || 80, useSsl: bma.useSsl || bma.port == 443 }; } }) .then(function(newNode) { $scope.changeNode(newNode); }); }; // Show node popup $scope.showNodePopup = function(node) { return $q(function(resolve, reject) { $scope.popupData.newNode = node.port ? [node.host, node.port].join(':') : node.host; $scope.popupData.useSsl = node.useSsl; if (!!$scope.popupForm) { $scope.popupForm.$setPristine(); } $translate(['SETTINGS.POPUP_PEER.TITLE', 'COMMON.BTN_OK', 'COMMON.BTN_CANCEL']) .then(function (translations) { // Choose UID popup $ionicPopup.show({ templateUrl: 'templates/settings/popup_node.html', title: translations['SETTINGS.POPUP_PEER.TITLE'], scope: $scope, buttons: [ { text: translations['COMMON.BTN_CANCEL'] }, { text: translations['COMMON.BTN_OK'], type: 'button-positive', onTap: function(e) { $scope.popupForm.$submitted=true; if(!$scope.popupForm.$valid || !$scope.popupForm.newNode) { //don't allow the user to close unless he enters a node e.preventDefault(); } else { return { server: $scope.popupData.newNode, useSsl: $scope.popupData.useSsl }; } } } ] }) .then(function(res) { if (!res) { // user cancel UIUtils.loading.hide(); return; } var parts = res.server.split(':'); parts[1] = parts[1] ? parts[1] : (res.useSsl ? 443 : 80); resolve({ host: parts[0], port: parts[1], useSsl: res.useSsl }); }); }); }); }; $scope.save = function() { if ($scope.loading || $scope.pendingSaving) return $q.when(); if ($scope.saving) { $scope.pendingSaving = true; // Retry later return $timeout(function() { $scope.pendingSaving = false; return $scope.save(); }, 500); } $scope.saving = true; // Async - to avoid UI lock return $timeout(function() { // Make sure to format helptip $scope.cleanupHelpTip(); // Applying csSettings.apply($scope.formData); // Store return csSettings.store(); }, 100) .then(function() { //return $timeout(function() { $scope.saving = false; //}, 100); }); }; $scope.onDataChanged = function(oldValue, newValue, scope) { if ($scope.loading || $scope.pendingSaving) return $q.when(); if ($scope.saving) { $scope.pendingSaving = true; // Retry later return $timeout(function() { $scope.pendingSaving = false; return $scope.onDataChanged(oldValue, newValue, scope); }, 500); } // Changes from the current scope: save changes if ((scope === $scope) && !angular.equals(oldValue, newValue)) { $scope.save(); } }; $scope.$watch('formData', $scope.onDataChanged, true); $scope.getServer = function() { if (!$scope.formData.node || !$scope.formData.node.host) return ''; return csHttp.getServer($scope.formData.node.host, $scope.formData.node.port); }; $scope.cleanupHelpTip = function() { var helptipChanged = $scope.formData.helptip.enable !== csSettings.data.helptip.enable; if (helptipChanged) { var enable = $scope.formData.helptip.enable; // Apply default values $scope.formData.helptip = angular.merge({}, csSettings.defaultSettings.helptip); // Then restore the enable flag $scope.formData.helptip.enable = enable; } }; /* -- modals & popover -- */ $scope.showActionsPopover = function(event) { UIUtils.popover.show(event, { templateUrl: 'templates/settings/popover_actions.html', scope: $scope, autoremove: true, afterShow: function(popover) { $scope.actionsPopover = popover; } }); }; $scope.hideActionsPopover = function() { if ($scope.actionsPopover) { $scope.actionsPopover.hide(); $scope.actionsPopover = null; } }; $scope.startSettingsTour = function() { $scope.hideActionsPopover(); return $scope.showHelpTip(0, true); }; // Show help tip (show only not already shown) $scope.showHelpTip = function(index, tour) { if (!$scope.isLogin() && !tour) return; index = angular.isDefined(index) ? index : csSettings.data.helptip.settings; if (index < 0) return; if (index === 0) index = 1; // skip first step // Create a new scope for the tour controller var helptipScope = $scope.createHelptipScope(tour); if (!helptipScope) return; // could be undefined, if a global tour already is already started return helptipScope.startSettingsTour(index, false) .then(function(endIndex) { helptipScope.$destroy(); csSettings.data.helptip.settings = endIndex; csSettings.store(); }); }; } angular.module('cesium.controllers', [ 'cesium.app.controllers', 'cesium.join.controllers', 'cesium.login.controllers', 'cesium.help.controllers', 'cesium.settings.controllers', 'cesium.wallet.controllers', 'cesium.wot.controllers' ]) ; angular.module('cesium.templates', []).run(['$templateCache', function($templateCache) {$templateCache.put('templates/menu.html','\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n

\n COMMON.APP_NAME\n

\n\n \n\n \n \n \n \n \n \n
\n\n \n \n\n \n \n \n {{:locale:\'MENU.HOME\'|translate}}\n \n\n \n \n\n \n
\n\n \n \n\n\n \n
\n\n\n \n \n {{:locale:\'MENU.ACCOUNT\'|translate}}\n \n \n\n \n \n\n \n \n {{:locale:\'MENU.SETTINGS\'|translate}}\n \n \n\n
\n\n
\n\n \n \n \n
\n\n\n
\n'); $templateCache.put('templates/modal_about.html','\n \n \n

ABOUT.TITLE

\n
\n\n \n\n
\n \n  {{\'COMMON.APP_VERSION\'|translate:$root.config}}\n \n

{{\'COMMON.APP_BUILD\'|translate:$root.config}}

\n ABOUT.LICENSE\n
\n\n \n \n \n\n \n \n\n \n

\n {{::$root.newRelease.url}}\n

\n
\n\n \n
\n \n {{\'ABOUT.CODE\' | translate}}\n

https://github.com/duniter-gchange/gchange-client

\n
\n\n \n
\n \n {{\'ABOUT.FORUM\' | translate}}\n

{{::$root.settings.userForumUrl}}

\n
\n\n
\n \n {{\'ABOUT.DEVELOPERS\' | translate}}\n

\n Benoit Lavenier\n

\n
\n \n \n

{{\'ABOUT.DEV_WARNING\'|translate}}

\n ABOUT.DEV_WARNING_MESSAGE\n
\n ABOUT.REPORT_ISSUE\n
\n\n \n\n
\n
\n
\n'); $templateCache.put('templates/api/locales_popover.html','\n \n \n \n\n'); $templateCache.put('templates/common/form_error_messages.html','
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n'); $templateCache.put('templates/common/popover_copy.html','\n \n
\n
\n \n
\n
\n
\n
\n'); $templateCache.put('templates/common/popover_helptip.html','\n \n

\n \n\n \n \n \n \n\n  \n

\n\n

\n \n \n

\n\n \n
\n \n \n \n
\n\n \n
\n \n \n
\n\n

\n \n

\n
\n
\n'); $templateCache.put('templates/common/popover_profile.html','\n'); $templateCache.put('templates/common/popover_share.html','\n \n
\n

\n \n
\n \n\n \n
\n
\n'); $templateCache.put('templates/join/modal_join.html','\n\n \n\n \n \n \n\n

ACCOUNT.NEW.TITLE

\n\n \n \n
\n\n\n \n\n \n \n \n
\n\n
\n\n \n\n \n
\n LOGIN.SALT\n \n \n
\n
\n
\n \n
\n
\n \n
\n
\n\n \n
\n ACCOUNT.NEW.SALT_CONFIRM\n \n \n
\n
\n
\n \n
\n
\n\n \n
\n COMMON.SHOW_VALUES\n \n
\n\n \n
\n
\n
\n
\n\n \n \n \n
\n\n \n\n
\n\n \n
\n LOGIN.PASSWORD\n \n \n
\n
\n
\n \n
\n
\n \n
\n
\n\n \n
\n ACCOUNT.NEW.PASSWORD_CONFIRM\n \n \n
\n
\n
\n \n
\n
\n\n \n
\n COMMON.SHOW_VALUES\n \n
\n
\n\n \n\n \n
\n
\n
\n\n \n \n \n
\n\n \n\n
\n\n \n
\n PROFILE.TITLE\n \n
\n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n\n \n
\n
\n
\n
\n\n \n\n \n \n \n\n
PROFILE.JOIN.LAST_SLIDE_CONGRATULATION
\n\n
\n\n \n
\n \n
\n COMMON.PUBKEY\n \n ACCOUNT.NEW.COMPUTING_PUBKEY\n \n \n {{formData.pubkey}}\n \n
\n
\n\n \n
\n
\n\n \n
\n'); $templateCache.put('templates/help/help.html','\n \n

HELP.JOIN.SECTION

\n\n \n
\n
LOGIN.SALT
\n
HELP.JOIN.SALT
\n
\n\n \n
\n
LOGIN.PASSWORD
\n
HELP.JOIN.PASSWORD
\n
\n\n \n

HELP.GLOSSARY.SECTION

\n\n \n
\n
COMMON.PUBKEY
\n
HELP.GLOSSARY.PUBKEY_DEF
\n
\n\n \n
\n
COMMON.UNIVERSAL_DIVIDEND
\n
HELP.GLOSSARY.UNIVERSAL_DIVIDEND_DEF
\n
\n\n'); $templateCache.put('templates/help/modal_help.html','\n\n \n \n\n

HELP.TITLE

\n
\n\n \n\n \n\n \n\n \n
\n'); $templateCache.put('templates/help/view_help.html','\n \n HELP.TITLE\n \n\n \n\n

HELP.TITLE

\n\n \n\n
\n
\n'); $templateCache.put('templates/home/home.html','\n \n\n \n\n \n \n \n \n\n \n\n \n \n\n
\n\n

\n \n\n \n

\n\n \n\n \n\n
\n
\n

\n \n

\n\n \n \n
\n
\n\n
\n\n\n \n\n \n\n \n\n \n\n
\n \n
\n
\n {{\'LOGIN.HAVE_ACCOUNT_QUESTION\'|translate}}\n \n \n \n
\n\n \n \n\n \n \n\n
\n\n
\n\n
\n
\n \n \n |\n \n HOME.BTN_ABOUT\n
\n
\n\n\n
\n\n
\n'); $templateCache.put('templates/login/modal_login.html','\n \n \n

\n

\n
\n \n \n
\n\n
\n\n \n
\n\n
\n\n \n \n
\n
\n \n
\n
\n\n \n
\n LOGIN.SHOW_SALT\n \n
\n\n \n \n
\n
\n \n
\n
\n\n\n \n \n\n\n \n
\n COMMON.PUBKEY\n \n {{\'COMMON.BTN_SHOW_PUBKEY\' | translate}}\n \n

\n {{pubkey}}\n

\n

\n \n

\n
\n\n
\n\n \n\n \n
\n {{\'LOGIN.NO_ACCOUNT_QUESTION\'|translate}}\n
\n \n LOGIN.CREATE_ACCOUNT\n \n
\n\n \n\n \n
\n
\n
\n'); $templateCache.put('templates/settings/popover_actions.html','\n \n

COMMON.POPOVER_ACTIONS_TITLE

\n
\n \n \n \n
\n'); $templateCache.put('templates/settings/popup_node.html','
\n\n
\n
\n \n \n
\n
\n
\n \n
\n
\n \n
\n
\n\n
\n \n {{\'SETTINGS.POPUP_PEER.USE_SSL\' | translate}}\n \n

\n \n \n

\n \n
\n\n\n \n \n {{\'SETTINGS.POPUP_PEER.BTN_SHOW_LIST\' | translate}}\n \n
\n\n \n
\n\n\n'); $templateCache.put('templates/settings/settings.html','\n SETTINGS.TITLE\n\n \n \n \n\n \n\n \n \n\n
\n\n \n
\n\n SETTINGS.DISPLAY_DIVIDER\n\n \n\n
\n
\n {{\'COMMON.BTN_RELATIVE_UNIT\' | translate}}\n
\n \n
\n\n \n\n \n\n SETTINGS.STORAGE_DIVIDER\n\n
\n
\n {{\'SETTINGS.USE_LOCAL_STORAGE\' | translate}}\n
\n

\n

\n \n
\n\n \n \n\n \n {{\'SETTINGS.AUTHENTICATION_SETTINGS\' | translate}}\n \n\n
\n
\n {{\'SETTINGS.REMEMBER_ME\' | translate}}\n
\n

\n\n \n
\n
\n\n \n
\n\n \n\n \n\n SETTINGS.NETWORK_SETTINGS\n\n \n \n \n \n
SETTINGS.PEER_SHORT
\n\n \n \n

\n \n \n

\n
{{bma.server}}
\n
\n
{{bma.server}}
\n \n
\n\n \n \n\n \n\n \n \n\n SETTINGS.PLUGINS_SETTINGS\n\n \n \n\n
\n
\n
\n
\n'); $templateCache.put('templates/wallet/popover_actions.html','\n \n

COMMON.POPOVER_ACTIONS_TITLE

\n
\n \n \n \n
\n'); $templateCache.put('templates/wallet/popover_unit.html','\n \n \n \n\n'); $templateCache.put('templates/wallet/view_wallet.html','\n \n \n \n\n \n\n \n\n \n\n \n \n\n \n
\n
\n \n \n \n

{{:rebind:formData.name}}

\n
\n \n \n
\n

\n \n

\n
\n\n \n\n \n \n \n\n
\n \n
\n\n
\n \n\n
\n\n
\n\n WOT.GENERAL_DIVIDER\n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n\n
\n
\n\n \n
\n
\n\n
\n'); $templateCache.put('templates/wot/identity.html','\n\n\n

\n \n

\n\n\n \n \n \n \n\n

\n \n {{::\'WOT.LOOKUP.MEMBER_FROM\' | translate:{time: identity.creationTime} }}\n

\n

\n \n \n {{::identity.city}} \n \n

\n

\n \n \n \n \n \n \n #\n \n \n

\n'); $templateCache.put('templates/wot/lookup.html','\n \n {{\'WOT.LOOKUP.TITLE\' | translate}}\n \n\n \n \n \n \n \n \n \n\n \n \n \n\n'); $templateCache.put('templates/wot/lookup_form.html','
\n\n
\n\n
\n\n
{{::parameters.help|translate}}
\n\n
\n \n   \n
\n\n
\n\n
\n \n\n \n \n
\n \n
\n
\n
\n\n
\n
\n

\n WOT.LOOKUP.NEWCOMERS\n

\n \n \n \n \n

\n COMMON.RESULTS_LIST\n

\n
\n\n \n
\n\n
\n

WOT.SEARCH_INIT_PHASE_WARNING

\n \n
\n\n \n
\n COMMON.SEARCH_NO_RESULT\n WOT.LOOKUP.NO_NEWCOMERS\n
\n\n \n \n\n \n \n
\n\n
\n\n \n\n \n
\n
\n \n\n \n
\n\n \n \n \n
\n\n \n \n\n
\n
\n'); $templateCache.put('templates/wot/lookup_popover_actions.html','\n \n

COMMON.POPOVER_FILTER_TITLE

\n
\n \n \n \n
\n'); $templateCache.put('templates/wot/modal_lookup.html','\n\n \n \n\n

\n {{::parameters.title?parameters.title:\'WOT.MODAL.TITLE\'|translate}}\n

\n\n \n
\n\n \n\n
\n \n \n
\n\n \n
\n
\n'); $templateCache.put('templates/wot/view_identity.html','\n \n \n\n \n\n
\n
\n \n \n \n

{{::formData.name|truncText: 30}}

\n
\n \n

{{::formData.uid}}

\n

{{::formData.pubkey | formatPubkey}}

\n
\n \n \n
\n

\n \n

\n\n\n
\n\n \n \n \n\n
\n\n
\n \n\n \n
\n\n \n\n
\n\n WOT.GENERAL_DIVIDER\n\n \n\n \n\n \n\n \n\n \n\n \n\n
\n \n \n
\n\n \n\n \n\n
\n\n \n
\n\n
\n\n \n
\n'); $templateCache.put('templates/wot/view_identity_tx.html','\n \n {{::formData.name||formData.uid}}\n \n \n\n \n\n \n\n
\n \n
\n\n
\n\n
\n\n \n\n
\n\n \n
\n {{\'ACCOUNT.BALANCE_ACCOUNT\'|translate}}\n
\n {{balance|formatAmount}} \n
\n
\n\n \n {{:locale:\'ACCOUNT.LAST_TX\'|translate}}\n  \n \n\n \n
\n
\n\n \n
\n\n \n\n
\n
\n
\n
\n');}]); angular.module("cesium.translations", []).config(["$translateProvider", function($translateProvider) { $translateProvider.translations("en-GB", { "COMMON": { "APP_NAME": "ğchange", "APP_VERSION": "v{{version}}", "APP_BUILD": "build {{build}}", "PUBKEY": "Public key", "MEMBER": "Member", "BLOCK" : "Block", "BTN_OK": "OK", "BTN_YES": "Yes", "BTN_NO": "No", "BTN_SEND": "Send", "BTN_SEND_MONEY": "Transfer money", "BTN_SEND_MONEY_SHORT": "Transfer", "BTN_SAVE": "Save", "BTN_YES_SAVE": "Yes, Save", "BTN_YES_CONTINUE": "Yes, Continue", "BTN_SHOW": "Show", "BTN_SHOW_PUBKEY": "Show key", "BTN_RELATIVE_UNIT": "Display amounts in UD?", "BTN_BACK": "Back", "BTN_NEXT": "Next", "BTN_IMPORT": "Import", "BTN_CANCEL": "Cancel", "BTN_CLOSE": "Close", "BTN_LATER": "Later", "BTN_LOGIN": "Sign In", "BTN_LOGOUT": "Logout", "BTN_ADD_ACCOUNT": "New Account", "BTN_SHARE": "Share", "BTN_EDIT": "Edit", "BTN_DELETE": "Delete", "BTN_ADD": "Add", "BTN_SEARCH": "Search", "BTN_REFRESH": "Refresh", "BTN_RETRY": "Retry", "BTN_START": "Start", "BTN_CONTINUE": "Continue", "BTN_CREATE": "Create", "BTN_UNDERSTOOD": "I understand", "BTN_OPTIONS": "Options", "BTN_HELP_TOUR": "Features tour", "BTN_HELP_TOUR_SCREEN": "Discover this screen", "BTN_DOWNLOAD": "Download", "BTN_DOWNLOAD_ACCOUNT_STATEMENT": "Download account statement", "BTN_MODIFY": "Modify", "CHOOSE_FILE": "Drag your file
or click to select", "DAYS": "days", "NO_ACCOUNT_QUESTION": "Not a member yet? Register now!", "SEARCH_NO_RESULT": "No result found", "LOADING": "Loading...", "LOADING_WAIT": "Loading...
(Waiting for node availability)", "SEARCHING": "Searching...", "FROM": "From", "TO": "To", "COPY": "Copy", "LANGUAGE": "Language", "UNIVERSAL_DIVIDEND": "Universal dividend", "UD": "UD", "DATE_PATTERN": "DD/MM/YYYY HH:mm", "DATE_FILE_PATTERN": "YYYY-MM-DD", "DATE_SHORT_PATTERN": "DD/MM/YY", "DATE_MONTH_YEAR_PATTERN": "MM/YYYY", "EMPTY_PARENTHESIS": "(empty)", "UID": "Pseudonym", "ENABLE": "Enabled", "DISABLE": "Disabled", "RESULTS_LIST": "Results:", "RESULTS_COUNT": "{{count}} results", "EXECUTION_TIME": "executed in {{duration|formatDurationMs}}", "SHOW_VALUES": "Display values openly?", "POPOVER_ACTIONS_TITLE": "Options", "POPOVER_FILTER_TITLE": "Filters", "SHOW_MORE": "Show more", "SHOW_MORE_COUNT": "(current limit at {{limit}})", "POPOVER_SHARE": { "TITLE": "Share", "SHARE_ON_TWITTER": "Share on Twitter", "SHARE_ON_FACEBOOK": "Share on Facebook", "SHARE_ON_DIASPORA": "Share on Diaspora*", "SHARE_ON_GOOGLEPLUS": "Share on Google+" }, "FILE": { "DATE" : "Date:", "TYPE" : "Type:", "SIZE": "Size:", "VALIDATING": "Validating..." } }, "SYSTEM": { "PICTURE_CHOOSE_TYPE": "Choose source:", "BTN_PICTURE_GALLERY": "Gallery", "BTN_PICTURE_CAMERA": "Camera" }, "MENU": { "HOME": "Home", "WOT": "Registry", "CURRENCY": "Currency", "ACCOUNT": "My Account", "WALLETS": "My wallets", "TRANSFER": "Transfer", "SCAN": "Scan", "SETTINGS": "Settings", "NETWORK": "Network", "TRANSACTIONS": "My transactions" }, "ABOUT": { "TITLE": "About", "LICENSE": "Free/libre software (License GNU AGPLv3).", "LATEST_RELEASE": "There is a newer version of {{'COMMON.APP_NAME' | translate}} (v{{version}})", "PLEASE_UPDATE": "Please update {{'COMMON.APP_NAME' | translate}} (latest version: v{{version}})", "CODE": "Source code:", "OFFICIAL_WEB_SITE": "Official web site:", "DEVELOPERS": "Developers:", "FORUM": "Forum:", "DEV_WARNING": "Warning", "DEV_WARNING_MESSAGE": "This application is still in active development.
Please report any issue to us!", "DEV_WARNING_MESSAGE_SHORT": "This App is still unstable (still under development).", "REPORT_ISSUE": "Report an issue" }, "HOME": { "TITLE": "Cesium", "MESSAGE": "Welcome to the {{'COMMON.APP_NAME'|translate}} Application!", "MESSAGE_CURRENCY": "Make exchanges in the libre currency {{currency|abbreviate}}!", "BTN_CURRENCY": "Explore currency", "BTN_ABOUT": "about", "BTN_HELP": "Help", "REPORT_ISSUE": "Report an issue", "NOT_YOUR_ACCOUNT_QUESTION" : "You do not own the account {{pubkey|formatPubkey}}?", "BTN_CHANGE_ACCOUNT": "Disconnect this account", "CONNECTION_ERROR": "Peer {{server}} unreachable or invalid address.

Check your Internet connection, or change node in the settings." }, "SETTINGS": { "TITLE": "Settings", "DISPLAY_DIVIDER": "Display", "STORAGE_DIVIDER": "Storage", "NETWORK_SETTINGS": "Network", "PEER": "Duniter peer address", "PEER_SHORT": "Peer address", "PEER_CHANGED_TEMPORARY": "Address used temporarily", "USE_LOCAL_STORAGE": "Enable local storage", "USE_LOCAL_STORAGE_HELP": "Allows you to save your settings", "WALLETS_SETTINGS": "My wallets", "USE_WALLETS_ENCRYPTION": "Secure the list", "USE_WALLETS_ENCRYPTION_HELP": "Enables you to encrypt the list of your wallets. Authentication required to access it.", "ENABLE_HELPTIP": "Enable contextual help tips", "ENABLE_UI_EFFECTS": "Enable visual effects", "HISTORY_SETTINGS": "Account operations", "DISPLAY_UD_HISTORY": "Display produced dividends?", "TX_HISTORY_AUTO_REFRESH": "Enable automatic refresh?", "TX_HISTORY_AUTO_REFRESH_HELP": "Updates the list of operations to each new block.", "AUTHENTICATION_SETTINGS": "Authentication", "KEEP_AUTH": "Expiration of authentication", "KEEP_AUTH_SHORT": "Expiration", "KEEP_AUTH_HELP": "Define when authentication is cleared from memory.", "KEEP_AUTH_OPTION": { "NEVER": "After each operation", "SECONDS": "After {{value}}s of inactivity", "MINUTE": "After {{value}}min of inactivity", "MINUTES": "After {{value}}min of inactivity", "HOUR": "After {{value}}h of inactivity", "ALWAYS": "At the end of the session" }, "KEYRING_FILE": "Keyring file", "KEYRING_FILE_HELP": "Allow auto-connect at startup, or to authenticate (only if \"Expiration of authentication\" is \"at the end of the session\"", "REMEMBER_ME": "Remember me ?", "REMEMBER_ME_HELP": "Allows to remain identified from one session to another, keeping the public key locally.", "PLUGINS_SETTINGS": "Extensions", "BTN_RESET": "Restore default values", "EXPERT_MODE": "Enable expert mode", "EXPERT_MODE_HELP": "Allow to see more details", "BLOCK_VALIDITY_WINDOW": "Block uncertainty time", "BLOCK_VALIDITY_WINDOW_SHORT": "Time of uncertainty", "BLOCK_VALIDITY_WINDOW_HELP": "Time to wait before considering an information is validated", "BLOCK_VALIDITY_OPTION": { "NONE": "No delay", "N": "{{time | formatDuration}} ({{count}} blocks)" }, "POPUP_PEER": { "TITLE" : "Duniter peer", "HOST" : "Address", "HOST_HELP": "Address: server:port", "USE_SSL" : "Secured?", "USE_SSL_HELP" : "(SSL Encryption)", "BTN_SHOW_LIST" : "Peer's list" } }, "BLOCKCHAIN": { "HASH": "Hash: {{hash}}", "VIEW": { "HEADER_TITLE": "Block #{{number}}-{{hash|formatHash}}", "TITLE_CURRENT": "Current block", "TITLE": "Block #{{number|formatInteger}}", "COMPUTED_BY": "Computed by", "SHOW_RAW": "Show raw data", "TECHNICAL_DIVIDER": "Technical informations", "VERSION": "Format version", "HASH": "Computed hash", "UNIVERSAL_DIVIDEND_HELP": "Money co-produced by each of the {{membersCount}} members", "EMPTY": "Aucune donnée dans ce bloc", "POW_MIN": "Minimal difficulty", "POW_MIN_HELP": "Difficulty imposed in calculating hash", "DATA_DIVIDER": "Data", "IDENTITIES_COUNT": "New identities", "JOINERS_COUNT": "Joiners", "ACTIVES_COUNT": "Renewals", "ACTIVES_COUNT_HELP": "Members having renewed their membership", "LEAVERS_COUNT": "Leavers", "LEAVERS_COUNT_HELP": "Members that now refused certification", "EXCLUDED_COUNT": "Excluded members", "EXCLUDED_COUNT_HELP": "Old members, excluded because missing membreship renewal or certifications", "REVOKED_COUNT": "Revoked identities", "REVOKED_COUNT_HELP": "These accounts may no longer be member", "TX_COUNT": "Transactions", "CERT_COUNT": "Certifications", "TX_TO_HIMSELF": "Change", "TX_OUTPUT_UNLOCK_CONDITIONS": "Unlock conditions", "TX_OUTPUT_OPERATOR": { "AND": "and", "OR": "or" }, "TX_OUTPUT_FUNCTION": { "SIG": "Sign of the public key", "XHX": "Password, including SHA256 =", "CSV": "Blocked during", "CLTV": "Bloqué until" } }, "LOOKUP": { "TITLE": "Blocks", "NO_BLOCK": "No bloc", "LAST_BLOCKS": "Last blocks:", "BTN_COMPACT": "Compact" } }, "CURRENCY": { "VIEW": { "TITLE": "Currency", "TAB_CURRENCY": "Currency", "TAB_WOT": "Web of trust", "TAB_NETWORK": "Network", "TAB_BLOCKS": "Blocks", "CURRENCY_SHORT_DESCRIPTION": "{{currency|capitalize}} is a libre money, started {{firstBlockTime | formatFromNow}}. It currently counts {{N}} members , who produce and collect a Universal Dividend (DU), each {{dt | formatPeriod}}.", "NETWORK_RULES_DIVIDER": "Network rules", "CURRENCY_NAME": "Currency name", "MEMBERS": "Members count", "MEMBERS_VARIATION": "Variation since {{duration|formatDuration}} (since last UD)", "MONEY_DIVIDER": "Money", "MASS": "Monetary mass", "SHARE": "Money share", "UD": "Universal Dividend", "C_ACTUAL": "Current growth", "MEDIAN_TIME": "Current blockchain time", "POW_MIN": "Common difficulty", "MONEY_RULES_DIVIDER": "Rules of currency", "C_RULE": "Theoretical growth target", "UD_RULE": "Universal dividend (formula)", "DT_REEVAL": "Period between two re-evaluation of the UD", "REEVAL_SYMBOL": "reeval", "DT_REEVAL_VALUE": "Every {{dtReeval|formatDuration}} ({{dtReeval/86400}} {{'COMMON.DAYS'|translate}})", "UD_REEVAL_TIME0": "Date of first reevaluation of the UD", "SIG_QTY_RULE": "Required number of certifications to become a member", "SIG_STOCK": "Maximum number of certifications sent by a member", "SIG_PERIOD": "Minimum delay between 2 certifications sent by one and the same issuer.", "SIG_WINDOW": "Maximum delay before a certification will be treated", "SIG_VALIDITY": "Lifetime of a certification that has been treated", "MS_WINDOW": "Maximum delay before a pending membership will be treated", "MS_VALIDITY": "Lifetime of a membership that has been treated", "STEP_MAX": "Maximum distance between a newcomer and each referring members.", "WOT_RULES_DIVIDER": "Rules for web of trust", "SENTRIES": "Required number of certifications (given and received) to become a referring member", "SENTRIES_FORMULA": "Required number of certifications to become a referring member (formula)", "XPERCENT":"Minimum percent of referring member to reach to match the distance rule", "AVG_GEN_TIME": "The average time between 2 blocks", "CURRENT": "current", "MATH_CEILING": "CEILING", "DISPLAY_ALL_RULES": "Display all rules?", "BTN_SHOW_LICENSE": "Show license", "WOT_DIVIDER": "Web of trust" }, "LICENSE": { "TITLE": "Currency license", "BTN_DOWNLOAD": "Download file", "NO_LICENSE_FILE": "License file not found." } }, "NETWORK": { "VIEW": { "MEDIAN_TIME": "Blockchain time", "LOADING_PEERS": "Loading peers...", "NODE_ADDRESS": "Address:", "SOFTWARE": "Software:", "WARN_PRE_RELEASE": "Pre-release (latest stable: {{version}})", "WARN_NEW_RELEASE": "Version {{version}} available", "WS2PID": "Identifier:", "PRIVATE_ACCESS": "Private access", "POW_PREFIX": "Proof of work prefix:", "ENDPOINTS": { "BMAS": "Secure endpoint (SSL)", "BMATOR": "TOR endpoint", "WS2P": "WS2P endpoint", "ES_USER_API": "Cesium+ data node" } }, "INFO": { "ONLY_SSL_PEERS": "Non-SSL nodes have a degraded display because Cesium works in HTTPS mode." } }, "PEER": { "PEERS": "Peers", "SIGNED_ON_BLOCK": "Signed on block", "MIRROR": "mirror", "MIRRORS": "Mirrors", "MIRROR_PEERS": "Mirror peers", "PEER_LIST" : "Peer's list", "MEMBERS" : "Members", "MEMBER_PEERS" : "Member peers", "ALL_PEERS" : "All peers", "DIFFICULTY" : "Difficulty", "API" : "API", "CURRENT_BLOCK" : "Block #", "POPOVER_FILTER_TITLE": "Filter", "OFFLINE": "Offline", "OFFLINE_PEERS": "Offline peers", "BTN_SHOW_PEER": "Show peer", "VIEW": { "TITLE": "Peer", "OWNER": "Owned by ", "SHOW_RAW_PEERING": "See peering document", "SHOW_RAW_CURRENT_BLOCK": "See current block (raw format)", "LAST_BLOCKS": "Last blocks", "KNOWN_PEERS": "Known peers :", "GENERAL_DIVIDER": "General information", "ERROR": { "LOADING_TOR_NODE_ERROR": "Could not get peer data, using the TOR network.", "LOADING_NODE_ERROR": "Could not get peer data" } } }, "WOT": { "SEARCH_HELP": "Search (member or public key)", "SEARCH_INIT_PHASE_WARNING": "During the pre-registration phase, the search for pending registrations may be long. Please wait ...", "REGISTERED_SINCE": "Registered on", "REGISTERED_SINCE_BLOCK": "Registered since block #", "NO_CERTIFICATION": "No validated certification", "NO_GIVEN_CERTIFICATION": "No given certification", "NOT_MEMBER_PARENTHESIS": "(non-member)", "IDENTITY_REVOKED_PARENTHESIS": "(identity revoked)", "MEMBER_PENDING_REVOCATION_PARENTHESIS": "(being revoked)", "EXPIRE_IN": "Expires", "NOT_WRITTEN_EXPIRE_IN": "Deadline
treatment", "EXPIRED": "Expired", "PSEUDO": "Pseudonym", "SIGNED_ON_BLOCK": "Emitted on block #{{block}}", "WRITTEN_ON_BLOCK": "Written on block #{{block}}", "GENERAL_DIVIDER": "General information", "NOT_MEMBER_ACCOUNT": "Non-member account", "NOT_MEMBER_ACCOUNT_HELP": "This is a simple wallet, with no pending membership application.", "TECHNICAL_DIVIDER": "Technical data", "BTN_CERTIFY": "Certify", "BTN_YES_CERTIFY": "Yes, certify", "BTN_SELECT_AND_CERTIFY": "New certification", "ACCOUNT_OPERATIONS": "Account operations", "VIEW": { "POPOVER_SHARE_TITLE": "Identity {{title}}" }, "LOOKUP": { "TITLE": "Registry", "NEWCOMERS": "New members:", "NEWCOMERS_COUNT": "{{count}} members", "PENDING": "Pending registrations:", "PENDING_COUNT": "{{count}} pending registrations", "REGISTERED": "Registered {{time | formatFromNow}}", "MEMBER_FROM": "Member since {{time|formatFromNowShort}}", "BTN_NEWCOMERS": "Latest members", "BTN_PENDING": "Pending registrations", "SHOW_MORE": "Show more", "SHOW_MORE_COUNT": "(current limit to {{limit}})", "NO_PENDING": "No pending registrations.", "NO_NEWCOMERS": "No members." }, "CONTACTS": { "TITLE": "Contacts" }, "MODAL": { "TITLE": "Search" }, "CERTIFICATIONS": { "TITLE": "{{uid}} - Certifications", "SUMMARY": "Received certifications", "LIST": "Details of received certifications", "PENDING_LIST": "Pending certifications", "RECEIVED": "Received certifications", "RECEIVED_BY": "Certifications received by {{uid}}", "ERROR": "Received certifications in error", "SENTRY_MEMBER": "Referring member" }, "OPERATIONS": { "TITLE": "{{uid}} - Operations" }, "GIVEN_CERTIFICATIONS": { "TITLE": "{{uid}} - Certifications sent", "SUMMARY": "Sent certifications", "LIST": "Details of sent certifications", "PENDING_LIST": "Pending certifications", "SENT": "Sent certifications", "SENT_BY": "Certifications sent by {{uid}}", "ERROR": "Sent certifications with error" } }, "LOGIN": { "TITLE": " Login", "SCRYPT_FORM_HELP": "Please enter your credentials.
Remember to check the public key for your account.", "PUBKEY_FORM_HELP": "Please enter a public account key:", "FILE_FORM_HELP": "Choose the keychain file to use:", "SCAN_FORM_HELP": "Scan the QR code of a wallet.", "SALT": "Identifier", "SALT_HELP": "Identifier", "SHOW_SALT": "Display identifier?", "PASSWORD": "Password", "PASSWORD_HELP": "Password", "PUBKEY_HELP": "Example: « AbsxSY4qoZRzyV2irfep1V9xw1EMNyKJw2TkuVD4N1mv »", "NO_ACCOUNT_QUESTION": "Don't have an account yet?", "HAVE_ACCOUNT_QUESTION": "Already have an account ?", "CREATE_ACCOUNT": "Create an account", "CREATE_FREE_ACCOUNT": "Create a free account", "FORGOTTEN_ID": "Forgot password?", "ASSOCIATED_PUBKEY": "Public key :", "BTN_METHODS": "Other methods", "BTN_METHODS_DOTS": "Change method...", "METHOD_POPOVER_TITLE": "Methods", "MEMORIZE_AUTH_FILE": "Memorize this keychain during the navigation session", "SCRYPT_PARAMETERS": "Paramètres (Scrypt) :", "AUTO_LOGOUT": { "TITLE": "Information", "MESSAGE": " You were logout automatically, due to prolonged inactivity.", "BTN_RELOGIN": "Sign In", "IDLE_WARNING": "You will be logout... {{countdown}}" }, "METHOD": { "SCRYPT_DEFAULT": "Secret and password", "SCRYPT_ADVANCED": "Advanced salt", "FILE": "Keychain file", "PUBKEY": "Public key only", "SCAN": "Scan a QR code" }, "SCRYPT": { "SIMPLE": "Light salt", "DEFAULT": "Standard salt", "SECURE": "Secure salt", "HARDEST": "Hardest salt", "EXTREME": "Extreme salt", "USER": "Personal value", "N": "N (Loop):", "r": "r (RAM):", "p": "p (CPU):" }, "FILE": { "DATE" : "Date:", "TYPE" : "Type:", "SIZE": "Size:", "VALIDATING": "Validating...", "HELP": "Expected file format: .dunikey (type PubSec). Other formats are under development (EWIF, WIF)." } }, "AUTH": { "TITLE": " Authentification", "METHOD_LABEL": "Méthode d'authentification", "BTN_AUTH": "S'authentifier", "SCRYPT_FORM_HELP": "Veuillez vous authentifier :", "ERROR": { "SCRYPT_DEFAULT": "Sallage simple (par défaut)", "SCRYPT_ADVANCED": "Sallage avancé", "FILE": "Fichier de trousseau" } }, "ACCOUNT": { "TITLE": "My Account", "BALANCE": "Balance", "LAST_TX": "Latest validated transactions", "BALANCE_ACCOUNT": "Account balance", "NO_TX": "No transaction", "SHOW_MORE_TX": "Show more", "SHOW_ALL_TX": "Show all", "TX_FROM_DATE": "(current limit to {{fromTime|medianFromNowShort}})", "PENDING_TX": "Pending transactions", "VALIDATING_TX": "Transactions being validated", "ERROR_TX": "Transaction not executed", "ERROR_TX_SENT": "Sent transactions", "PENDING_TX_RECEIVED": "Transactions awaiting receipt", "EVENTS": "Events", "OUT_DISTANCED": "Your current certifications come from a group too isolated from the Web of Trust (WoT): the maximum distance rule is violated.
You must obtain certifications from another area of the Web of Trust, or wait for it to tighten.", "WAITING_MEMBERSHIP": "Membership application sent. Waiting validation.", "WAITING_CERTIFICATIONS": "You need {{needCertificationCount}} certification(s) to become a member and produce the Universal Dividend. Your account is however already operational, to receive and send payments.", "WAITING_CERTIFICATIONS_HELP": "To get your certifications, only request members who know you enough, as required by the currency license that you have accepted.
If you do not know enough members, let them know on the user forum.", "WILL_MISSING_CERTIFICATIONS": "You will lack certifications soon (at least {{willNeedCertificationCount}} more are needed)", "WILL_NEED_RENEW_MEMBERSHIP": "Your membership will expire {{membershipExpiresIn|formatDurationTo}}. Remember to renew your membership before then.", "NEED_RENEW_MEMBERSHIP": "You are no longer a member because your membership has expired. Remember to renew your membership.", "NEED_RENEW_MEMBERSHIP_AFTER_CANCELLED": "You are no longer a member because your membership has been canceled for lack of certifications. Remember to renew your membership.", "NO_WAITING_MEMBERSHIP": "No membership application pending. If you'd like to become a member, please send the membership application.", "CERTIFICATION_COUNT": "Received certifications", "CERTIFICATION_COUNT_SHORT": "Certifications", "SIG_STOCK": "Stock of certifications to give", "BTN_RECEIVE_MONEY": "Receive", "BTN_SELECT_ALTERNATIVES_IDENTITIES": "Switch to another identity...", "BTN_FIX_MEMBERSHIP": "Resubmit membership request...", "BTN_MEMBERSHIP_RENEW": "Renew membership", "BTN_MEMBERSHIP_RENEW_DOTS": "Renew membership...", "BTN_MEMBERSHIP_OUT_DOTS": "Revoke membership...", "BTN_SECURITY_DOTS": "Sign-in and security...", "BTN_SHOW_DETAILS": "Display technical data", "LOCKED_OUTPUTS_POPOVER": { "TITLE": "Locked amount", "DESCRIPTION": "Here are the conditions for unlocking this amount:", "DESCRIPTION_MANY": "This transaction consists of several parts, of which the unlock conditions are:", "LOCKED_AMOUNT": "Conditions for the amount:" }, "NEW": { "TITLE": "Registration", "INTRO_WARNING_TIME": "Creating an account on {{name|capitalize}} is very simple. Please take sufficient time to do this correctly (not to forget the usernames, passwords, etc.).", "INTRO_WARNING_SECURITY": "Check that the hardware you are currently using (computer, tablet, phone) is secure and trustworthy .", "INTRO_WARNING_SECURITY_HELP": "Up-to-date anti-virus, firewall enabled, session protected by password or pin code...", "INTRO_HELP": "Click {{'COMMON.BTN_START'|translate}} to begin creating an account. You will be guided step by step.", "REGISTRATION_NODE": "Your registration will be registered via the Duniter peer {{server}} node, which will then be distributed to the rest of the currency network.", "REGISTRATION_NODE_HELP": "If you do not trust this peer, please change in the settings of Cesium.", "SELECT_ACCOUNT_TYPE": "Choose the type of account to create:", "MEMBER_ACCOUNT": "Member account", "MEMBER_ACCOUNT_TITLE": "Create a member account", "MEMBER_ACCOUNT_HELP": "If you are not yet registered as an individual (one account possible per individual).", "WALLET_ACCOUNT": "Simple wallet", "WALLET_ACCOUNT_TITLE": "Create a wallet", "WALLET_ACCOUNT_HELP": "If you represent a company, association, etc. or simply need an additional wallet. No universal dividend will be created by this account.", "SALT_WARNING": "Choose a identifier.
You need it for each connection to this account.

Make sure to remember this identifier.
If lost, there are no means to retrieve it!", "PASSWORD_WARNING": "Choose a password.
You need it for each connection to this account.

Make sure to remember this password.
If lost, there are no means to retrieve it!", "PSEUDO_WARNING": "Choose a pseudonym.
It may be used by other people to find you more easily.

.Use of commas, spaces and accents is not allowed.