diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json index 63858b1e6bef924225adb938ec8d43059f517216..6f7b9ebcdae950af9479655ce4f4b68f738ea87c 100644 --- a/www/i18n/locale-en.json +++ b/www/i18n/locale-en.json @@ -265,7 +265,6 @@ "BTN_MEMBERSHIP_RENEW_DOTS": "Renew membership...", "BTN_MEMBERSHIP_OUT_DOTS": "Revoke membership...", "BTN_SEND_IDENTITY_DOTS": "Publish identity...", - "BTN_REVOKE": "Revoke<span class='hidden-xs hidden-sm'> definitely</span> this identity...", "BTN_SHOW_DETAILS": "Display technical data", "NEW": { "TITLE": "Registration", @@ -295,6 +294,40 @@ "POPUP_REGISTER": { "TITLE": "Enter a pseudonym", "HELP": "A pseudonym is needed to let other members find you." + }, + "SECURITY":{ + "TITLE": "Sign-in and security", + "SAVE_KEYS": "Save your login", + "DOWNLOAD_REVOKE": "Save a recocation file", + "REVOKE" : "Revoke this identity", + "DEFINITELY_REVOKE" : "Revoke definitely this identity.", + "REVOCATION": "Revocation ...", + "LEVEL": "Security level", + "HELP_LEVEL": "Choose <strong> at least {{nb}} questions </strong> :", + "LOW_LEVEL": "Low <span class=\"hidden-xs\">(2 questions minimum)</span>", + "MEDIUM_LEVEL": "Medium <span class=\"hidden-xs\">(4 questions minimum)</span>", + "STRONG_LEVEL": "Strong <span class=\"hidden-xs \">(6 questions minimum)</span>", + "ADD_QUESTION" : "Add custom question", + "BTN_RESET" : "Reset", + "QUESTION_1": "Comment s'appelait votre meilleur ami lorsque vous étiez adolescent ?", + "QUESTION_2": "What was the name of your first pet ?", + "QUESTION_3": "Quel est le premier plat que vous avez appris à cuisiner ?", + "QUESTION_4": "Quel est le premier film que vous avez vu au cinéma ?", + "QUESTION_5": "Où êtes-vous allé la première fois que vous avez pris l'avion ?", + "QUESTION_6": "Comment s'appelait votre instituteur préféré à l'école primaire ?", + "QUESTION_7": "Quel serait selon vous le métier idéal ?", + "QUESTION_8": "Quel est le livre pour enfants que vous préférez ?", + "QUESTION_9": "Quel était le modèle de votre premier véhicule ?", + "QUESTION_10": "What was your nickname when you were a child ?", + "QUESTION_11": "Quel était votre personnage ou acteur de cinéma préféré lorsque vous étiez étudiant ?", + "QUESTION_12": "Quel était votre chanteur ou groupe préféré lorsque vous étiez étudiant ?", + "QUESTION_13": "Dans quelle ville vos parents se sont-ils rencontrés ?", + "QUESTION_14": "Comment s'appelait votre premier patron ?", + "QUESTION_15": "Quel est le nom de la rue où vous avez grandi ?", + "QUESTION_16": "Quel est le nom de la première plage où vous vous êtes baigné ?", + "QUESTION_17": "Quel est le premier album que vous avez acheté ?", + "QUESTION_18": "What is the name of your favorite sport team ?", + "QUESTION_19": "What was your grand-father's job ?" } }, "TRANSFER": { diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json index 211ab141b1b8db62732af9c5905a9de9a96ee9fa..67e7a8af3580b6b6ade6bb5c3ac5af5427c51481 100644 --- a/www/i18n/locale-fr-FR.json +++ b/www/i18n/locale-fr-FR.json @@ -301,10 +301,36 @@ "DOWNLOAD_REVOKE": "Sauvegarder un fichier de revocation", "REVOKE" : "Revoquer cette identité", "DEFINITELY_REVOKE" : "Revoquer définitivement cette identité", - "REVOCATION": "Révocation" - - } - }, + "REVOCATION": "Révocation ...", + "LEVEL": "Niveau de sécurité", + "HELP_LEVEL": "Pour générer un fichier de sauvegarde de vos identifiants, choisissez <strong> au moins {{nb}} questions :</strong>", + "LOW_LEVEL": "Faible <span class=\"hidden-xs\">(2 questions minimum)</span>", + "MEDIUM_LEVEL": "Moyen <span class=\"hidden-xs\">(4 questions minimum)</span>", + "STRONG_LEVEL": "Fort <span class=\"hidden-xs \">(6 questions minimum)</span>", + "ADD_QUESTION" : "Ajouter une question personnalisée ", + "BTN_RESET" : "Réinitialiser", + "BTN_CLEAN" : "vider", + "QUESTION_1": "Comment s'appelait votre meilleur ami lorsque vous étiez adolescent ?", + "QUESTION_2": "Comment s'appelait votre premier animal de compagnie ?", + "QUESTION_3": "Quel est le premier plat que vous avez appris à cuisiner ?", + "QUESTION_4": "Quel est le premier film que vous avez vu au cinéma ?", + "QUESTION_5": "Où êtes-vous allé la première fois que vous avez pris l'avion ?", + "QUESTION_6": "Comment s'appelait votre instituteur préféré à l'école primaire ?", + "QUESTION_7": "Quel serait selon vous le métier idéal ?", + "QUESTION_8": "Quel est le livre pour enfants que vous préférez ?", + "QUESTION_9": "Quel était le modèle de votre premier véhicule ?", + "QUESTION_10": "Quel était votre surnom lorsque vous étiez enfant ?", + "QUESTION_11": "Quel était votre personnage ou acteur de cinéma préféré lorsque vous étiez étudiant ?", + "QUESTION_12": "Quel était votre chanteur ou groupe préféré lorsque vous étiez étudiant ?", + "QUESTION_13": "Dans quelle ville vos parents se sont-ils rencontrés ?", + "QUESTION_14": "Comment s'appelait votre premier patron ?", + "QUESTION_15": "Quel est le nom de la rue où vous avez grandi ?", + "QUESTION_16": "Quel est le nom de la première plage où vous vous êtes baigné ?", + "QUESTION_17": "Quel est le premier album que vous avez acheté ?", + "QUESTION_18": "Quel est le nom de votre équipe de sport préférée ?", + "QUESTION_19": "Quel était le métier de votre grand-père ?" + } + }, "TRANSFER": { "TITLE": "Virement", "SUB_TITLE": "Faire un virement", @@ -384,7 +410,8 @@ "LOAD_PENDING_FAILED": "Echec du chargement des inscriptions en attente.", "ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION": "Vous devez <b>être membre</b> pour pouvoir effectuer cette action.", "ONLY_SELF_CAN_EXECUTE_THIS_ACTION": "Vous devez avoir <b>publié votre identité</b> pour pouvoir effectuer cette action.", - "REVOCATION_FAILED": "Echec de la révocation." + "REVOCATION_FAILED": "Echec de la révocation.", + "SALT_OR_PASSWORD_NOT_CONFIRMED": "Phrase de protection ou mot de passe incorrects" }, "INFO": { "POPUP_TITLE": "Information", diff --git a/www/js/controllers/settings-controllers.js b/www/js/controllers/settings-controllers.js index 5391f095a7a83a11d0a0ac87922a204f5b4bc45b..dd8ea310d7dd8ffd18e08ea8ea642a9459d01844 100644 --- a/www/js/controllers/settings-controllers.js +++ b/www/js/controllers/settings-controllers.js @@ -29,6 +29,7 @@ function SettingsController($scope, $q, $ionicPopup, $timeout, $translate, csHtt $scope.loading = true; $scope.nodePopup = {}; + $scope.$on('$ionicView.enter', function() { $scope.load(); }); diff --git a/www/js/controllers/wallet-controllers.js b/www/js/controllers/wallet-controllers.js index 225a0e592a84930a7fb2076c6fe1184e43bb9568..88037fdeac0d093c6dab5b5f5ae17ea3d730898d 100644 --- a/www/js/controllers/wallet-controllers.js +++ b/www/js/controllers/wallet-controllers.js @@ -558,7 +558,7 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state, }; } -function WalletSecurityModalController($scope, $state, UIUtils, Modals, csWallet){ +function WalletSecurityModalController($scope, $rootScope, UIUtils, csWallet, $translate, CryptoUtils, $timeout){ $scope.slides = { slider: null, options: { @@ -569,19 +569,154 @@ function WalletSecurityModalController($scope, $state, UIUtils, Modals, csWalle }; $scope.isLastSlide = false; $scope.smallscreen = UIUtils.screen.isSmall(); + $scope.formData = { + addQuestion: '', + level: '4', + questions : [] + }; + + for (var i = 1; i<20; i++){ + $translate('ACCOUNT.SECURITY.QUESTION_' + i.toString()) + .then(function(translation){ + $scope.formData.questions.push({value: translation , checked: false}); + }) + }; + + $scope.restore = function(){ + if ($scope.slides.slider.activeIndex === 2) { + $scope.formData = { + addQuestion: '', + level: '4', + questions: [] + }; + for (var i = 1; i < 20; i++) { + $translate('ACCOUNT.SECURITY.QUESTION_' + i.toString()) + .then(function (translation) { + $scope.formData.questions.push({value: translation, checked: false}); + }) + } + } + if($scope.slides.slider.activeIndex === 3) { + _.each($scope.formData.questions, function(question){ + question.answer = undefined; + }) + } + }; + + $scope.addQuestion = function(){ + if ($scope.formData.addQuestion != '') { + $scope.formData.questions.push({value: $scope.formData.addQuestion, checked: true}); + $scope.formData.addQuestion = ''; + } + }; + + $scope.submit = function(){ + if (!$scope.isUserPubkey($scope.pubkey)){ + if ($scope.computing = false){ + UIUtils.alert.error('ERROR.SALT_OR_PASSWORD_NOT_CONFIRMED', 'ERROR.LOGIN_FAILED'); + return; + } + } + if(!$scope['loginForm'].$valid){ + return; + } + + var file = {file : _.filter($scope.formData.questions, function(question){ + return question.checked; + })}; + var record = { + salt: $scope.formData.username, + pwd: $scope.formData.password, + questions: '' + }; + _.each(file.file, function(question){ + record.questions += question.value + '\n'; + record.answer += question.answer; + }) + + return csWallet.getkeypairSaveId(record) + .then(function(record){ + csWallet.downloadSaveId(record); + }) + + }; + + $scope.isUserPubkey = function(pubkey) { + return csWallet.isUserPubkey(pubkey); + }; + + $scope.isRequired = function(){ + var questionChecked = _.filter($scope.formData.questions, function(question) { + return question.checked; + }) + return questionChecked.length < $scope.formData.level; + }; + + $scope.formDataChanged = function() { + $scope.computing = true; + $scope.pubkey = ''; + $timeout(function() { + var salt = $scope.formData.username; + var pwd = $scope.formData.password; + CryptoUtils.connect(salt, pwd).then( + function (keypair) { + $scope.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); + $scope.computing = false; + } + ) + .catch(function (err) { + $scope.pubkey = ''; + UIUtils.loading.hide(); + console.error('>>>>>>>', err); + UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR'); + $scope.computing = false; + }); + }, 500); + + + }; + $scope.$watch('formData.username', $scope.formDataChanged, true); + $scope.$watch('formData.password', $scope.formDataChanged, true); $scope.slidePrev = function() { + if ($scope.slides.slider.activeIndex === 2){ + $scope.slideTo(0); + $scope.isLastSlide = false; + return; + } $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 === 5; + $scope.isLastSlide = $scope.slides.slider.activeIndex === 4; + }; + + $scope.slideTo = function(index) { + $scope.slides.slider.unlockSwipes(); + $scope.slides.slider.slideTo(index); + $scope.slides.slider.lockSwipes(); + $scope.isLastSlide = $scope.slides.slider.activeIndex === 4; + }; + + $scope.doNext = function(formName) { + if (!formName) { + formName = $scope.slides.slider.activeIndex === 2 ? 'questionsForm' : + ($scope.slides.slider.activeIndex === 3 ? 'answersForm' : formName); + } + if (formName) { + $scope[formName].$submitted = true; + if (!$scope[formName].$valid) { + return; + } + $scope.slideNext(); + } }; diff --git a/www/js/services/wallet-services.js b/www/js/services/wallet-services.js index e448aaab56c1dbec4b69e628fbbfd8e8280bc654..4c9081ecba55e361c6d1d02207127bed27732574 100644 --- a/www/js/services/wallet-services.js +++ b/www/js/services/wallet-services.js @@ -10,85 +10,85 @@ angular.module('cesium.wallet.services', ['ngResource', 'ngApi', 'cesium.bma.ser factory = function(id) { var - constants = { - STORAGE_KEY: 'CESIUM_DATA', - /* Need for compat with old currencies (test_net and sou) */ - TX_VERSION: csConfig.compatProtocol_0_80 ? 3 : BMA.constants.PROTOCOL_VERSION, - IDTY_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION, - MS_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION, - CERT_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION, - REVOKE_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION - }, - data = {}, - - api = new Api(this, 'csWallet-' + id), - - resetData = function(init) { - data.pubkey= null; - data.keypair = { + constants = { + STORAGE_KEY: 'CESIUM_DATA', + /* Need for compat with old currencies (test_net and sou) */ + TX_VERSION: csConfig.compatProtocol_0_80 ? 3 : BMA.constants.PROTOCOL_VERSION, + IDTY_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION, + MS_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION, + CERT_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION, + REVOKE_VERSION: csConfig.compatProtocol_0_80 ? 2 : BMA.constants.PROTOCOL_VERSION + }, + data = {}, + + api = new Api(this, 'csWallet-' + id), + + resetData = function(init) { + data.pubkey= null; + data.keypair = { signSk: null, signPk: null }; - data.uid = null; - data.balance = 0; - data.sources = null; - data.sourcesIndexByKey = null; - data.currency= null; - data.parameters = null; - data.currentUD = null; - data.medianTime = null; - data.tx = data.tx || {}; - data.tx.history = []; - data.tx.pendings = []; - data.tx.errors = []; - data.requirements = {}; - data.blockUid = null; - data.sigDate = null; - data.isMember = false; - data.events = []; - data.loaded = false; - if (init) { - api.data.raise.init(data); - } - else { - if (!csSettings.data.useLocalStorage) { - csSettings.reset(); + data.uid = null; + data.balance = 0; + data.sources = null; + data.sourcesIndexByKey = null; + data.currency= null; + data.parameters = null; + data.currentUD = null; + data.medianTime = null; + data.tx = data.tx || {}; + data.tx.history = []; + data.tx.pendings = []; + data.tx.errors = []; + data.requirements = {}; + data.blockUid = null; + data.sigDate = null; + data.isMember = false; + data.events = []; + data.loaded = false; + if (init) { + api.data.raise.init(data); } - api.data.raise.reset(data); - } - }, + else { + if (!csSettings.data.useLocalStorage) { + csSettings.reset(); + } + api.data.raise.reset(data); + } + }, - reduceTxAndPush = function(txArray, result, processedTxMap, excludePending) { - if (!txArray || txArray.length === 0) { - return; - } - var txPendingsTimeByKey = excludePending ? [] : data.tx.pendings.reduce(function(res, tx) { - if (tx.time) { - res[tx.amount+':'+tx.hash] = tx.time; + reduceTxAndPush = function(txArray, result, processedTxMap, excludePending) { + if (!txArray || txArray.length === 0) { + return; } - return res; - }, []); + var txPendingsTimeByKey = excludePending ? [] : data.tx.pendings.reduce(function(res, tx) { + if (tx.time) { + res[tx.amount+':'+tx.hash] = tx.time; + } + return res; + }, []); - _.forEach(txArray, function(tx) { - if (!excludePending || tx.block_number !== null) { - var walletIsIssuer = false; - var otherIssuer = tx.issuers.reduce(function(issuer, res) { + _.forEach(txArray, function(tx) { + if (!excludePending || tx.block_number !== null) { + var walletIsIssuer = false; + var otherIssuer = tx.issuers.reduce(function(issuer, res) { walletIsIssuer = (res === data.pubkey) ? true : walletIsIssuer; return issuer + ((res !== data.pubkey) ? ', ' + res : ''); - }, ''); - if (otherIssuer.length > 0) { - otherIssuer = otherIssuer.substring(2); - } - var otherReceiver; - var outputBase; - var sources = []; - var amount = tx.outputs.reduce(function(sum, output, noffset) { + }, ''); + if (otherIssuer.length > 0) { + otherIssuer = otherIssuer.substring(2); + } + var otherReceiver; + var outputBase; + var sources = []; + 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 outputCondArray = outputArray[2].split('(', 3); var outputPubkey = (outputCondArray.length == 2 && outputCondArray[0] == 'SIG') ? - outputCondArray[1].substring(0,outputCondArray[1].length-1) : ''; + outputCondArray[1].substring(0,outputCondArray[1].length-1) : ''; if (outputPubkey == data.pubkey) { // output is for the wallet if (!walletIsIssuer) { return sum + outputAmount; @@ -116,613 +116,613 @@ angular.module('cesium.wallet.services', ['ngResource', 'ngApi', 'cesium.bma.ser return sum; }, 0); - var pubkey = amount > 0 ? otherIssuer : otherReceiver; - var time = tx.time; - if (tx.block_number === null) { - time = tx.blockstampTime || txPendingsTimeByKey[amount + ':' + tx.hash]; - } - - // 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: pubkey, - 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; + var pubkey = amount > 0 ? otherIssuer : otherReceiver; + var time = tx.time; + if (tx.block_number === null) { + time = tx.blockstampTime || txPendingsTimeByKey[amount + ':' + tx.hash]; } - result.push(newTx); - } - } - }); - }, - - resetSources = function(){ - data.sources = []; - data.sourcesIndexByKey = {}; - }, - - 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(sources) { - _(sources).forEach(function(src) { - addSource(src, data.sources, data.sourcesIndexByKey); - }); - }, - - login = function(salt, password) { - return CryptoUtils.connect(salt, password) - .then(function(keypair) { - // Copy result to properties - data.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); - data.keypair = keypair; - - // Call extend api - return api.data.raisePromise.login(data); - }) - // store if need - .then(function() { - if (csSettings.data.useLocalStorage) { - store(); + // 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: pubkey, + 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; + } + result.push(newTx); + } } - return data; }); - }, + }, - logout = function() { - return $q(function(resolve, reject) { + resetSources = function(){ + data.sources = []; + data.sourcesIndexByKey = {}; + }, - resetData(); // will reset keypair - store(); // store (if local storage enable) + 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; + } + }, - // Send logout event - api.data.raise.logout(); + addSources = function(sources) { + _(sources).forEach(function(src) { + addSource(src, data.sources, data.sourcesIndexByKey); + }); + }, - resolve(); - }); - }, - - isLogin = function() { - return !!data.pubkey; - }, - - isNeverUsed = function() { - if (!data.loaded) return undefined; // undefined if not full loaded - return !data.pubkey || - (!data.isMember && - (!data.requirements || !data.requirements.pendingMembership) && - !data.tx.history.length && - !data.tx.pendings.length); - }, - - // 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 - }; + login = function(salt, password) { + return CryptoUtils.connect(salt, password) + .then(function(keypair) { + // Copy result to properties + data.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); + data.keypair = keypair; - if (data.tx && data.tx.pendings && data.tx.pendings.length>0) { - var pendings = data.tx.pendings.reduce(function(res, tx){ - return tx.time ? res.concat({ - amount: tx.amount, - time: tx.time, - hash: tx.hash - }) : res; - }, []); - if (pendings.length) { - dataToStore.tx = { - pendings: pendings - }; + // Call extend api + return api.data.raisePromise.login(data); + }) + // store if need + .then(function() { + if (csSettings.data.useLocalStorage) { + store(); } - } + return data; + }); + }, - localStorage.setObject(constants.STORAGE_KEY, dataToStore); - } - else { - localStorage.setObject(constants.STORAGE_KEY, null); - } - } - else { - localStorage.setObject(constants.STORAGE_KEY, null); - } - }, + logout = function() { + return $q(function(resolve, reject) { + + resetData(); // will reset keypair + store(); // store (if local storage enable) + + // Send logout event + api.data.raise.logout(); - restore = function() { - return $q(function(resolve, reject){ - var dataStr = localStorage.get(constants.STORAGE_KEY); - if (!dataStr) { resolve(); - return; - } - fromJson(dataStr, false) - .then(function(storedData){ - if (storedData && storedData.keypair && storedData.pubkey) { - data.keypair = storedData.keypair; - data.pubkey = storedData.pubkey; - if (storedData.tx && storedData.tx.pendings) { - data.tx.pendings = storedData.tx.pendings; - } - data.loaded = false; + }); + }, - return $q.all([ - // Call extend api - api.data.raisePromise.login(data), + isLogin = function() { + return !!data.pubkey; + }, - // Load parameters - // This prevent timeout error, when loading a market record after a browser refresh (e.g. F5) - loadParameters(), + isNeverUsed = function() { + if (!data.loaded) return undefined; // undefined if not full loaded + return !data.pubkey || + (!data.isMember && + (!data.requirements || !data.requirements.pendingMembership) && + !data.tx.history.length && + !data.tx.pendings.length); + }, + + // 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 + }; + + if (data.tx && data.tx.pendings && data.tx.pendings.length>0) { + var pendings = data.tx.pendings.reduce(function(res, tx){ + return tx.time ? res.concat({ + amount: tx.amount, + time: tx.time, + hash: tx.hash + }) : res; + }, []); + if (pendings.length) { + dataToStore.tx = { + pendings: pendings + }; + } + } - // Load current UD is need by features tour - loadCurrentUD() - ]); + localStorage.setObject(constants.STORAGE_KEY, dataToStore); } else { - // Load parameters - // This prevent timeout error, when loading a market record after a browser refresh (e.g. F5) - return loadParameters(); + localStorage.setObject(constants.STORAGE_KEY, null); } - }) - .then(function(){ - resolve(data); - }) - .catch(function(err){reject(err);}); - }); - }, - - getData = function() { - return data; - }, - - resetRequirements = function() { - data.requirements = { - needSelf: true, - needMembership: true, - canMembershipOut: false, - needRenew: false, - pendingMembership: false, - certificationCount: 0, - needCertifications: false, - needCertificationCount: 0, - willNeedCertificationCount: 0 - }; - data.blockUid = null; - data.isMember = false; - data.sigDate = null; - cleanEventsByContext('requirements'); - }, - - loadRequirements = function() { - return $q(function(resolve, reject) { - - // Clean existing events - cleanEventsByContext('requirements'); + } + else { + localStorage.setObject(constants.STORAGE_KEY, null); + } + }, - // Get requirements - BMA.wot.requirements({pubkey: data.pubkey}) - .then(function(res){ - if (!res.identities || res.identities.length === 0) { - resetRequirements(); + restore = function() { + return $q(function(resolve, reject){ + var dataStr = localStorage.get(constants.STORAGE_KEY); + if (!dataStr) { resolve(); 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 - // - 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 * ((data.uid && idty.uid === data.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)); - } + fromJson(dataStr, false) + .then(function(storedData){ + if (storedData && storedData.keypair && storedData.pubkey) { + data.keypair = storedData.keypair; + data.pubkey = storedData.pubkey; + if (storedData.tx && storedData.tx.pendings) { + data.tx.pendings = storedData.tx.pendings; + } + data.loaded = false; - // Select the first identity - var idty = res.identities[0]; - - // Compute useful fields - idty.needSelf = false; - idty.needMembership = (idty.membershipExpiresIn <= 0 && idty.membershipPendingExpiresIn <= 0 ); - idty.needRenew = (!idty.needMembership && - idty.membershipExpiresIn <= csSettings.data.timeWarningExpireMembership && idty.membershipPendingExpiresIn <= 0); - idty.canMembershipOut = (idty.membershipExpiresIn > 0); - idty.pendingMembership = (idty.membershipExpiresIn <= 0 && idty.membershipPendingExpiresIn > 0); - idty.certificationCount = (idty.certifications) ? idty.certifications.length : 0; - idty.willExpireCertificationCount = idty.certifications ? idty.certifications.reduce(function(count, cert){ - return count + (cert.expiresIn <= csSettings.data.timeWarningExpire ? 1 : 0); - }, 0) : 0; - - data.requirements = idty; - data.uid = idty.uid; - data.blockUid = idty.meta.timestamp; - data.isMember = (idty.membershipExpiresIn > 0); - - var blockParts = idty.meta.timestamp.split('-', 2); - var blockNumber = parseInt(blockParts[0]); - var blockHash = blockParts[1]; - // Retrieve registration date - return BMA.blockchain.block({block: blockNumber}) - .then(function(block) { - data.sigDate = block.medianTime; + return $q.all([ + // Call extend api + api.data.raisePromise.login(data), + + // Load parameters + // This prevent timeout error, when loading a market record after a browser refresh (e.g. F5) + loadParameters(), - // Check if self has been done on a valid block - if (!data.isMember && blockNumber !== 0 && blockHash !== block.hash) { - addEvent({type: 'error', message: 'ERROR.WALLET_INVALID_BLOCK_HASH', context: 'requirements'}); - console.debug("Invalid membership for uid={0}: block hash changed".format(data.uid)); + // Load current UD is need by features tour + loadCurrentUD() + ]); } - // Check if self expired - else if (!data.isMember && data.requirements.expired) { - addEvent({type: 'error', message: 'ERROR.WALLET_IDENTITY_EXPIRED', context: 'requirements'}); - console.debug("Identity expired for uid={0}.".format(data.uid)); + else { + // Load parameters + // This prevent timeout error, when loading a market record after a browser refresh (e.g. F5) + return loadParameters(); } - resolve(); }) - .catch(function(err){ - // Special case for currency init (root block not exists): use now - if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND && blockNumber === 0) { - data.sigDate = Math.trunc(new Date().getTime() / 1000); + .then(function(){ + resolve(data); + }) + .catch(function(err){reject(err);}); + }); + }, + + getData = function() { + return data; + }, + + resetRequirements = function() { + data.requirements = { + needSelf: true, + needMembership: true, + canMembershipOut: false, + needRenew: false, + pendingMembership: false, + certificationCount: 0, + needCertifications: false, + needCertificationCount: 0, + willNeedCertificationCount: 0 + }; + data.blockUid = null; + data.isMember = false; + data.sigDate = null; + cleanEventsByContext('requirements'); + }, + + loadRequirements = function() { + return $q(function(resolve, reject) { + + // Clean existing events + cleanEventsByContext('requirements'); + + // Get requirements + BMA.wot.requirements({pubkey: data.pubkey}) + .then(function(res){ + if (!res.identities || res.identities.length === 0) { + resetRequirements(); + resolve(); + 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 + // - 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 * ((data.uid && idty.uid === data.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)); + } + + // Select the first identity + var idty = res.identities[0]; + + // Compute useful fields + idty.needSelf = false; + idty.needMembership = (idty.membershipExpiresIn <= 0 && idty.membershipPendingExpiresIn <= 0 ); + idty.needRenew = (!idty.needMembership && + idty.membershipExpiresIn <= csSettings.data.timeWarningExpireMembership && idty.membershipPendingExpiresIn <= 0); + idty.canMembershipOut = (idty.membershipExpiresIn > 0); + idty.pendingMembership = (idty.membershipExpiresIn <= 0 && idty.membershipPendingExpiresIn > 0); + idty.certificationCount = (idty.certifications) ? idty.certifications.length : 0; + idty.willExpireCertificationCount = idty.certifications ? idty.certifications.reduce(function(count, cert){ + return count + (cert.expiresIn <= csSettings.data.timeWarningExpire ? 1 : 0); + }, 0) : 0; + + data.requirements = idty; + data.uid = idty.uid; + data.blockUid = idty.meta.timestamp; + data.isMember = (idty.membershipExpiresIn > 0); + + var blockParts = idty.meta.timestamp.split('-', 2); + var blockNumber = parseInt(blockParts[0]); + var blockHash = blockParts[1]; + // Retrieve registration date + return BMA.blockchain.block({block: blockNumber}) + .then(function(block) { + data.sigDate = block.medianTime; + + // Check if self has been done on a valid block + if (!data.isMember && blockNumber !== 0 && blockHash !== block.hash) { + addEvent({type: 'error', message: 'ERROR.WALLET_INVALID_BLOCK_HASH', context: 'requirements'}); + console.debug("Invalid membership for uid={0}: block hash changed".format(data.uid)); + } + // Check if self expired + else if (!data.isMember && data.requirements.expired) { + addEvent({type: 'error', message: 'ERROR.WALLET_IDENTITY_EXPIRED', context: 'requirements'}); + console.debug("Identity expired for uid={0}.".format(data.uid)); + } + resolve(); + }) + .catch(function(err){ + // Special case for currency init (root block not exists): use now + if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND && blockNumber === 0) { + data.sigDate = Math.trunc(new Date().getTime() / 1000); + resolve(); + } + else { + reject(err); + } + }); + }) + .catch(function(err) { + resetRequirements(); + // If not a member: continue + if (!!err && + (err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER || + err.ucode == BMA.errorCodes.NO_IDTY_MATCHING_PUB_OR_UID)) { resolve(); } else { reject(err); } }); - }) - .catch(function(err) { - resetRequirements(); - // If not a member: continue - if (!!err && - (err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER || - err.ucode == BMA.errorCodes.NO_IDTY_MATCHING_PUB_OR_UID)) { - resolve(); - } - else { - reject(err); - } }); - }); - }, - - loadSources = function() { - return $q(function(resolve, reject) { - // Get transactions - BMA.tx.sources({pubkey: data.pubkey}) - .then(function(res){ - resetSources(); - var balance = 0; - if (res.sources) { - _.forEach(res.sources, function(src) { - src.consumed = false; - balance += powBase(src.amount, src.base); + }, + + loadSources = function() { + return $q(function(resolve, reject) { + // Get transactions + BMA.tx.sources({pubkey: data.pubkey}) + .then(function(res){ + resetSources(); + var balance = 0; + if (res.sources) { + _.forEach(res.sources, function(src) { + src.consumed = false; + balance += powBase(src.amount, src.base); + }); + addSources(res.sources); + } + data.balance = balance; + resolve(); + }) + .catch(function(err) { + resetSources(); + reject(err); }); - addSources(res.sources); - } - data.balance = balance; - resolve(); - }) - .catch(function(err) { - resetSources(); - reject(err); }); - }); - }, - - loadTransactions = function(fromTime) { - return $q(function(resolve, reject) { - var txHistory = []; - var udHistory = []; - var txPendings = []; - - var now = new Date().getTime(); - var nowInSec = Math.trunc(now / 1000); - fromTime = fromTime || (nowInSec - csSettings.data.walletHistoryTimeSecond); - var processedTxMap = {}; - - var reduceTx = function(res){ - reduceTxAndPush(res.history.sent, txHistory, processedTxMap, true/*exclude pending*/); - reduceTxAndPush(res.history.received, txHistory, processedTxMap, true/*exclude pending*/); - reduceTxAndPush(res.history.sending, txHistory, processedTxMap, true/*exclude pending*/); - reduceTxAndPush(res.history.pending, txPendings, processedTxMap, false/*include pending*/); - }; + }, + + loadTransactions = function(fromTime) { + return $q(function(resolve, reject) { + var txHistory = []; + var udHistory = []; + var txPendings = []; + + var now = new Date().getTime(); + var nowInSec = Math.trunc(now / 1000); + fromTime = fromTime || (nowInSec - csSettings.data.walletHistoryTimeSecond); + var processedTxMap = {}; + + var reduceTx = function(res){ + reduceTxAndPush(res.history.sent, txHistory, processedTxMap, true/*exclude pending*/); + reduceTxAndPush(res.history.received, txHistory, processedTxMap, true/*exclude pending*/); + reduceTxAndPush(res.history.sending, txHistory, processedTxMap, true/*exclude pending*/); + reduceTxAndPush(res.history.pending, txPendings, processedTxMap, false/*include pending*/); + }; - var jobs = [ - // get pendings history - BMA.tx.history.pending({pubkey: data.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: data.pubkey, from: i, to: i+sliceTime-1}) + var jobs = [ + // get pendings history + BMA.tx.history.pending({pubkey: data.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: data.pubkey, from: i, to: i+sliceTime-1}) + .then(reduceTx) + ); + } - jobs.push(BMA.tx.history.timesNoCache({pubkey: data.pubkey, from: nowInSec - (nowInSec % sliceTime), to: nowInSec+999999999}) - .then(reduceTx)); - } + jobs.push(BMA.tx.history.timesNoCache({pubkey: data.pubkey, from: nowInSec - (nowInSec % sliceTime), to: nowInSec+999999999}) + .then(reduceTx)); + } - // get all TX - else { - jobs.push(BMA.tx.history.all({pubkey: data.pubkey}) - .then(reduceTx) - ); - } + // get all TX + else { + jobs.push(BMA.tx.history.all({pubkey: data.pubkey}) + .then(reduceTx) + ); + } - // get UD history - if (csSettings.data.showUDHistory) { - jobs.push( - BMA.ud.history({pubkey: data.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 - }); - }, []); - })); - } + // get UD history + if (csSettings.data.showUDHistory) { + jobs.push( + BMA.ud.history({pubkey: data.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 - data.tx.history = txHistory.concat(udHistory).sort(function(tx1, tx2) { - return (tx2.time - tx1.time); - }); - data.tx.pendings = txPendings; - data.tx.fromTime = fromTime; - data.tx.toTime = data.tx.history.length ? data.tx.history[0].time /*=max(tx.time)*/: fromTime; + // Execute jobs + $q.all(jobs) + .then(function(){ + // sort by time desc + data.tx.history = txHistory.concat(udHistory).sort(function(tx1, tx2) { + return (tx2.time - tx1.time); + }); + data.tx.pendings = txPendings; + data.tx.fromTime = fromTime; + data.tx.toTime = data.tx.history.length ? data.tx.history[0].time /*=max(tx.time)*/: fromTime; - // Call extend api - return api.data.raisePromise.loadTx(data.tx) - .then(function() { - console.debug('[wallet] TX history loaded in '+ (new Date().getTime()-now) +'ms'); - resolve(); + // Call extend api + return api.data.raisePromise.loadTx(data.tx) + .then(function() { + console.debug('[wallet] TX history loaded in '+ (new Date().getTime()-now) +'ms'); + resolve(); + }); + }) + .catch(function(err) { + data.tx.history = []; + data.tx.pendings = []; + data.tx.errors = []; + delete data.tx.fromTime; + delete data.tx.toTime; + reject(err); }); - }) - .catch(function(err) { - data.tx.history = []; - data.tx.pendings = []; - data.tx.errors = []; - delete data.tx.fromTime; - delete data.tx.toTime; - reject(err); }); - }); - }, + }, - processTransactionsAndSources = function() { - return BMA.wot.member.uids() - .then(function(uids){ - var txPendings = []; - var txErrors = []; - var balance = data.balance; + processTransactionsAndSources = function() { + return BMA.wot.member.uids() + .then(function(uids){ + var txPendings = []; + var txErrors = []; + var balance = data.balance; - // process TX history - _.forEach(data.tx.history, function(tx) { - tx.uid = uids[tx.pubkey] || null; - }); + // process TX history + _.forEach(data.tx.history, function(tx) { + tx.uid = uids[tx.pubkey] || null; + }); - var processPendingTx = function(tx) { - tx.uid = uids[tx.pubkey] || null; + var processPendingTx = function(tx) { + tx.uid = uids[tx.pubkey] || null; - 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 + 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(tx.sources); } - }); - if (tx.sources) { // add source output - addSources(tx.sources); + delete tx.sources; + delete tx.inputs; } - 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); - } - }; + 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; + 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; - }); - }, + data.tx.pendings = txPendings; + data.tx.errors = txErrors; + data.balance = balance; + }); + }, - loadParameters = function() { - return $q(function(resolve, reject) { - if (data.parameters && data.currency) { - resolve(); - return; - } - BMA.blockchain.parameters() - .then(function(json){ - data.currency = json.currency; - data.parameters = json; - if (data.currentUD == -1) data.currentUD = data.parameters.ud0; - resolve(); - }) - .catch(function(err) { - data.currency = null; - data.parameters = null; - reject(err); - }); - }); - }, - - loadCurrentUD = function() { - 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 ; + loadParameters = function() { + return $q(function(resolve, reject) { + if (data.parameters && data.currency) { + resolve(); + return; } - else { - var lastBlockWithUD = res.result.blocks[res.result.blocks.length - 1]; - return BMA.blockchain.block({ block: lastBlockWithUD }) - .then(function(block){ - data.currentUD = powBase(block.dividend, block.unitbase); - return data.currentUD; - }) - .catch(function(err) { - data.currentUD = null; - throw err; - }); - } - }) - .catch(function(err) { - data.currentUD = null; - throw err; + BMA.blockchain.parameters() + .then(function(json){ + data.currency = json.currency; + data.parameters = json; + if (data.currentUD == -1) data.currentUD = data.parameters.ud0; + resolve(); + }) + .catch(function(err) { + data.currency = null; + data.parameters = null; + reject(err); + }); }); - }, - - // Must be call after loadParameters() and loadRequirements() - finishLoadRequirements = function() { - data.requirements.needCertificationCount = (!data.requirements.needMembership && (data.requirements.certificationCount < data.parameters.sigQty)) ? - (data.parameters.sigQty - data.requirements.certificationCount) : 0; - data.requirements.willNeedCertificationCount = (!data.requirements.needMembership && - data.requirements.needCertificationCount === 0 && (data.requirements.certificationCount - data.requirements.willExpireCertificationCount) < data.parameters.sigQty) ? - (data.parameters.sigQty - data.requirements.certificationCount + data.requirements.willExpireCertificationCount) : 0; - data.requirements.pendingCertificationCount = 0 ; // init to 0, because not loaded here (see wot-service.js) - - // Add user events - if (data.requirements.pendingMembership) { - addEvent({type:'pending', message: 'ACCOUNT.WAITING_MEMBERSHIP', context: 'requirements'}); - } - if (data.requirements.needCertificationCount > 0) { - addEvent({type:'warn', message: 'ACCOUNT.WAITING_CERTIFICATIONS', messageParams: data.requirements, context: 'requirements'}); - } - if (data.requirements.willNeedCertificationCount > 0) { - addEvent({type:'warn', message: 'ACCOUNT.WILL_MISSING_CERTIFICATIONS', messageParams: data.requirements, context: 'requirements'}); - } - if (data.requirements.needRenew) { - addEvent({type:'warn', message: 'ACCOUNT.WILL_NEED_RENEW_MEMBERSHIP', messageParams: data.requirements, context: 'requirements'}); - } - }, + }, - loadSigStock = function() { - return $q(function(resolve, reject) { - // Get certified by, then count written certification - BMA.wot.certifiedBy({pubkey: data.pubkey}) + loadCurrentUD = function() { + return BMA.blockchain.stats.ud() .then(function(res){ - data.sigStock = !res.certifications ? 0 : res.certifications.reduce(function(res, cert) { - return cert.written === null ? res : res+1; - }, 0); - resolve(); - }) - .catch(function(err) { - if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER) { - data.sigStock = 0; - resolve(); // not found + // Special case for currency init + if (!res.result.blocks.length) { + data.currentUD = data.parameters ? data.parameters.ud0 : -1; + return data.currentUD ; } else { - reject(err); + var lastBlockWithUD = res.result.blocks[res.result.blocks.length - 1]; + return BMA.blockchain.block({ block: lastBlockWithUD }) + .then(function(block){ + data.currentUD = powBase(block.dividend, block.unitbase); + return data.currentUD; + }) + .catch(function(err) { + data.currentUD = null; + throw err; + }); } + }) + .catch(function(err) { + data.currentUD = null; + throw err; }); - }); - }, + }, - loadData = function(options) { + // Must be call after loadParameters() and loadRequirements() + finishLoadRequirements = function() { + data.requirements.needCertificationCount = (!data.requirements.needMembership && (data.requirements.certificationCount < data.parameters.sigQty)) ? + (data.parameters.sigQty - data.requirements.certificationCount) : 0; + data.requirements.willNeedCertificationCount = (!data.requirements.needMembership && + data.requirements.needCertificationCount === 0 && (data.requirements.certificationCount - data.requirements.willExpireCertificationCount) < data.parameters.sigQty) ? + (data.parameters.sigQty - data.requirements.certificationCount + data.requirements.willExpireCertificationCount) : 0; + data.requirements.pendingCertificationCount = 0 ; // init to 0, because not loaded here (see wot-service.js) - if (options && options.minData) { - return loadMinData(options); - } + // Add user events + if (data.requirements.pendingMembership) { + addEvent({type:'pending', message: 'ACCOUNT.WAITING_MEMBERSHIP', context: 'requirements'}); + } + if (data.requirements.needCertificationCount > 0) { + addEvent({type:'warn', message: 'ACCOUNT.WAITING_CERTIFICATIONS', messageParams: data.requirements, context: 'requirements'}); + } + if (data.requirements.willNeedCertificationCount > 0) { + addEvent({type:'warn', message: 'ACCOUNT.WILL_MISSING_CERTIFICATIONS', messageParams: data.requirements, context: 'requirements'}); + } + if (data.requirements.needRenew) { + addEvent({type:'warn', message: 'ACCOUNT.WILL_NEED_RENEW_MEMBERSHIP', messageParams: data.requirements, context: 'requirements'}); + } + }, - if (options || data.loaded) { - return refreshData(options); - } + loadSigStock = function() { + return $q(function(resolve, reject) { + // Get certified by, then count written certification + BMA.wot.certifiedBy({pubkey: data.pubkey}) + .then(function(res){ + data.sigStock = !res.certifications ? 0 : res.certifications.reduce(function(res, cert) { + return cert.written === null ? res : res+1; + }, 0); + resolve(); + }) + .catch(function(err) { + if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER) { + data.sigStock = 0; + resolve(); // not found + } + else { + reject(err); + } + }); + }); + }, + + loadData = function(options) { + + if (options && options.minData) { + return loadMinData(options); + } + + if (options || data.loaded) { + return refreshData(options); + } - return loadFullData(); - }, + return loadFullData(); + }, - loadFullData = function() { - data.loaded = false; + loadFullData = function() { + data.loaded = false; - return $q.all([ + return $q.all([ // Get currency parameters loadParameters(), @@ -749,576 +749,576 @@ angular.module('cesium.wallet.services', ['ngResource', 'ngApi', 'cesium.bma.ser console.error(err); }) ]) - .then(function() { - // Process transactions and sources - return processTransactionsAndSources(); - }) - .then(function() { - finishLoadRequirements(); // must be call after loadParameters() and loadRequirements() - 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 || {}; - options.parameters = angular.isDefined(options.parameters) ? options.parameters : !data.parameters; // do not load if already done - options.requirements = angular.isDefined(options.requirements) ? options.requirements : - (!data.requirements || angular.isUndefined(data.requirements.needSelf)); - if (!options.parameters && !options.requirements) { - return $q.when(data); - } - return refreshData(options); - }, + .then(function() { + // Process transactions and sources + return processTransactionsAndSources(); + }) + .then(function() { + finishLoadRequirements(); // must be call after loadParameters() and loadRequirements() + 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 || {}; + options.parameters = angular.isDefined(options.parameters) ? options.parameters : !data.parameters; // do not load if already done + options.requirements = angular.isDefined(options.requirements) ? options.requirements : + (!data.requirements || angular.isUndefined(data.requirements.needSelf)); + if (!options.parameters && !options.requirements) { + return $q.when(data); + } + return refreshData(options); + }, - refreshData = function(options) { + refreshData = function(options) { options = options || { - parameters: !data.parameters, // do not load if already done - currentUd: true, - requirements: true, - sources: true, - tx: { - enable: true, - fromTime: data.tx ? data.tx.fromTime : undefined // keep previous time - }, - sigStock: true, - api: true - }; + parameters: !data.parameters, // do not load if already done + currentUd: true, + requirements: true, + sources: true, + tx: { + enable: true, + fromTime: data.tx ? data.tx.fromTime : undefined // keep previous time + }, + sigStock: true, + api: true + }; - // Force some load (parameters & requirements) if not already loaded - options.parameters = angular.isDefined(options.parameters) ? options.parameters : !data.parameters; - options.requirements = angular.isDefined(options.requirements) ? options.requirements : !data.requirements; + // Force some load (parameters & requirements) if not already loaded + options.parameters = angular.isDefined(options.parameters) ? options.parameters : !data.parameters; + options.requirements = angular.isDefined(options.requirements) ? options.requirements : !data.requirements; - var jobs = []; + var jobs = []; - // Reset events - cleanEventsByContext('requirements'); + // Reset events + cleanEventsByContext('requirements'); - // Get parameters - if (options.parameters) jobs.push(loadParameters()); + // Get parameters + if (options.parameters) jobs.push(loadParameters()); - // Get current UD - if (options.currentUd) jobs.push(loadCurrentUD()); + // Get current UD + if (options.currentUd) jobs.push(loadCurrentUD()); - // Get requirements - if (options.requirements) { - jobs.push(loadRequirements() + // Get requirements + if (options.requirements) { + jobs.push(loadRequirements() + .then(function() { + finishLoadRequirements(); + })); + } + + if (options.sources || (options.tx && options.tx.enable)) { + // Get sources + jobs.push(loadSources()); + + // Get transactions + jobs.push(loadTransactions(options.tx.fromTime)); + } + + // Load sigStock + if (options.sigStock) jobs.push(loadSigStock()); + + // 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() { - finishLoadRequirements(); - })); - } + if (options.sources || (options.tx && options.tx.enable)) { + // Process transactions and sources + return processTransactionsAndSources(); + } + }) + .then(function(){ + return api.data.raisePromise.finishLoad(data); + }) + .then(function(){ + return data; + }); + }, - if (options.sources || (options.tx && options.tx.enable)) { - // Get sources - jobs.push(loadSources()); + isBase = function(amount, base) { + if (!base) return true; // no base + if (amount < Math.pow(10, base)) return false; // too small + var rest = '00000000' + amount; + var lastDigits = parseInt(rest.substring(rest.length-base)); + return lastDigits === 0; // no rest + }, - // Get transactions - jobs.push(loadTransactions(options.tx.fromTime)); - } + truncBase = function(amount, base) { + var pow = Math.pow(10, base); // = min value in this base + if (amount < pow) return 0; + return Math.trunc(amount / pow ) * pow; + }, - // Load sigStock - if (options.sigStock) jobs.push(loadSigStock()); + truncBaseOrMinBase = function(amount, base) { + var pow = Math.pow(10, base); + if (amount < pow) return pow; // + return Math.trunc(amount / pow ) * pow; + }, - // API extension (force if no other jobs) - if (!jobs.length || options.api) jobs.push(api.data.raisePromise.load(data, options)); + powBase = function(amount, base) { + return base <= 0 ? amount : amount * Math.pow(10, base); + }, - return $q.all(jobs) - .then(function() { - if (options.sources || (options.tx && options.tx.enable)) { - // Process transactions and sources - return processTransactionsAndSources(); + getInputs = function(amount, outputBase, filterBase) { + if (angular.isUndefined(filterBase)) { + filterBase = outputBase; } - }) - .then(function(){ - return api.data.raisePromise.finishLoad(data); - }) - .then(function(){ - return data; - }); - }, - - isBase = function(amount, base) { - if (!base) return true; // no base - if (amount < Math.pow(10, base)) return false; // too small - var rest = '00000000' + amount; - var lastDigits = parseInt(rest.substring(rest.length-base)); - return lastDigits === 0; // no rest - }, - - truncBase = function(amount, base) { - var pow = Math.pow(10, base); // = min value in this base - if (amount < pow) return 0; - return Math.trunc(amount / pow ) * pow; - }, - - truncBaseOrMinBase = function(amount, base) { - var pow = Math.pow(10, base); - if (amount < pow) return pow; // - return Math.trunc(amount / pow ) * pow; - }, - - powBase = function(amount, base) { - return base <= 0 ? amount : amount * Math.pow(10, base); - }, - - getInputs = function(amount, outputBase, filterBase) { - if (angular.isUndefined(filterBase)) { - filterBase = outputBase; - } - var sourcesAmount = 0; - var sources = []; - var minBase = filterBase; - var maxBase = filterBase; - _.find(data.sources, function(source) { - if (!source.consumed && source.base == filterBase){ - sourcesAmount += powBase(source.amount, source.base); - sources.push(source); - } - // Stop if enough sources - return (sourcesAmount >= amount); - }); + var sourcesAmount = 0; + var sources = []; + var minBase = filterBase; + var maxBase = filterBase; + _.find(data.sources, function(source) { + if (!source.consumed && source.base == filterBase){ + sourcesAmount += powBase(source.amount, source.base); + sources.push(source); + } + // Stop if enough sources + return (sourcesAmount >= amount); + }); - // IF not enough sources, get add inputs from lower base (recursively) - if (sourcesAmount < amount && filterBase > 0) { - filterBase -= 1; - var missingAmount = amount - sourcesAmount; - var lowerInputs = getInputs(missingAmount, outputBase, filterBase); - - // Add lower base inputs to result - if (lowerInputs.amount > 0) { - minBase = lowerInputs.minBase; - sourcesAmount += lowerInputs.amount; - [].push.apply(sources, lowerInputs.sources); + // IF not enough sources, get add inputs from lower base (recursively) + if (sourcesAmount < amount && filterBase > 0) { + filterBase -= 1; + var missingAmount = amount - sourcesAmount; + var lowerInputs = getInputs(missingAmount, outputBase, filterBase); + + // Add lower base inputs to result + if (lowerInputs.amount > 0) { + minBase = lowerInputs.minBase; + sourcesAmount += lowerInputs.amount; + [].push.apply(sources, lowerInputs.sources); + } } - } - return { - minBase: minBase, - maxBase: maxBase, - amount: sourcesAmount, - sources: sources - }; - }, - - /** - * Send a new transaction - */ - transfer = function(destPub, amount, comments, useRelative) { - return $q(function(resolve, reject) { - BMA.blockchain.current(true/*cache*/) - .then(function(block) { - if (!BMA.regex.PUBKEY.test(destPub)){ - reject({message:'ERROR.INVALID_PUBKEY'}); return; - } - if (!BMA.regex.COMMENT.test(comments)){ - reject({message:'ERROR.INVALID_COMMENT'}); return; - } - if (!isLogin()){ - reject({message:'ERROR.NEED_LOGIN_FIRST'}); return; - } - if (!amount) { - reject({message:'ERROR.AMOUNT_REQUIRED'}); return; - } - if (amount <= 0) { - reject({message:'ERROR.AMOUNT_NEGATIVE'}); return; - } - amount = Math.floor(amount); // remove decimals + return { + minBase: minBase, + maxBase: maxBase, + amount: sourcesAmount, + sources: sources + }; + }, - var inputs = { - amount: 0, - minBase: block.unitbase, - maxBase: block.unitbase + 1, - sources : [] - }; + /** + * Send a new transaction + */ + transfer = function(destPub, amount, comments, useRelative) { + return $q(function(resolve, reject) { + BMA.blockchain.current(true/*cache*/) + .then(function(block) { + if (!BMA.regex.PUBKEY.test(destPub)){ + reject({message:'ERROR.INVALID_PUBKEY'}); return; + } + if (!BMA.regex.COMMENT.test(comments)){ + reject({message:'ERROR.INVALID_COMMENT'}); return; + } + if (!isLogin()){ + reject({message:'ERROR.NEED_LOGIN_FIRST'}); return; + } + if (!amount) { + reject({message:'ERROR.AMOUNT_REQUIRED'}); return; + } + if (amount <= 0) { + reject({message:'ERROR.AMOUNT_NEGATIVE'}); return; + } + amount = Math.floor(amount); // remove decimals - // Get inputs, starting to use current base sources - var amountBase = 0; - while (inputs.amount < amount && amountBase <= block.unitbase) { - inputs = getInputs(amount, block.unitbase); + var inputs = { + amount: 0, + minBase: block.unitbase, + maxBase: block.unitbase + 1, + sources : [] + }; - if (inputs.amount < amount) { - // try to reduce amount (replace last digits to zero) - amountBase++; - if (amountBase <= block.unitbase) { - amount = truncBase(amount, amountBase); + // Get inputs, starting to use current base sources + var amountBase = 0; + while (inputs.amount < amount && amountBase <= block.unitbase) { + inputs = getInputs(amount, block.unitbase); + + if (inputs.amount < amount) { + // try to reduce amount (replace last digits to zero) + amountBase++; + if (amountBase <= block.unitbase) { + amount = truncBase(amount, amountBase); + } + } } - } - } - if (inputs.amount < amount) { - if (data.balance < amount) { - reject({message:'ERROR.NOT_ENOUGH_CREDIT'}); return; - } - else if (inputs.amount === 0) { - reject({message:'ERROR.ALL_SOURCES_USED'}); return; - } - else { - $translate('COMMON.UD') - .then(function(UD) { - var params; - if(useRelative) { - params = { - amount: ($filter('formatDecimal')(inputs.amount / data.currentUD)), - unit: UD, - subUnit: $filter('abbreviate')(data.currency) - }; + if (inputs.amount < amount) { + if (data.balance < amount) { + reject({message:'ERROR.NOT_ENOUGH_CREDIT'}); return; + } + else if (inputs.amount === 0) { + reject({message:'ERROR.ALL_SOURCES_USED'}); return; } else { - params = { - amount: ($filter('formatInteger')(inputs.amount)), - unit: $filter('abbreviate')(data.currency), - subUnit: '' - }; + $translate('COMMON.UD') + .then(function(UD) { + var params; + if(useRelative) { + params = { + amount: ($filter('formatDecimal')(inputs.amount / data.currentUD)), + unit: UD, + subUnit: $filter('abbreviate')(data.currency) + }; + } + else { + params = { + amount: ($filter('formatInteger')(inputs.amount)), + unit: $filter('abbreviate')(data.currency), + subUnit: '' + }; + } + $translate('ERROR.NOT_ENOUGH_SOURCES', params) + .then(function(message) { + reject({message: message}); + }); + }); } - $translate('ERROR.NOT_ENOUGH_SOURCES', params) - .then(function(message) { - reject({message: message}); - }); - }); - } - return; - } - // Avoid to get outputs on lower base - if (amountBase < inputs.minBase && !isBase(amount, inputs.minBase)) { - amount = truncBaseOrMinBase(amount, inputs.minBase); - console.debug("[wallet] Amount has been truncate to " + amount); - } - else if (amountBase > 0) { - console.debug("[wallet] Amount has been truncate to " + amount); - } - - // Send tx - createAndSendTx(block, destPub, amount, inputs, comments) - .then(function(res) { - data.balance -= amount; - _.forEach(inputs.sources, function(source) { - source.consumed=true; - }); - - // Add new sources - if (res && res.sources.length) { - addSources(res.sources); + return; + } + // Avoid to get outputs on lower base + if (amountBase < inputs.minBase && !isBase(amount, inputs.minBase)) { + amount = truncBaseOrMinBase(amount, inputs.minBase); + console.debug("[wallet] Amount has been truncate to " + amount); + } + else if (amountBase > 0) { + console.debug("[wallet] Amount has been truncate to " + amount); } - // Add TX to pendings - BMA.wot.member.get(destPub) - .then(function(member) { - data.tx.pendings.unshift({ - time: (Math.floor(moment().utc().valueOf() / 1000)), - amount: -amount, - pubkey: destPub, - uid: member ? member.uid : null, - comment: comments, - isUD: false, - hash: res.hash, - locktime: 0, - block_number: null + // Send tx + createAndSendTx(block, destPub, amount, inputs, comments) + .then(function(res) { + data.balance -= amount; + _.forEach(inputs.sources, function(source) { + source.consumed=true; }); - store(); // save pendings in local storage - resolve(); + + // Add new sources + if (res && res.sources.length) { + addSources(res.sources); + } + + // Add TX to pendings + BMA.wot.member.get(destPub) + .then(function(member) { + data.tx.pendings.unshift({ + time: (Math.floor(moment().utc().valueOf() / 1000)), + amount: -amount, + pubkey: destPub, + uid: member ? member.uid : null, + comment: comments, + isUD: false, + hash: res.hash, + locktime: 0, + block_number: null + }); + store(); // save pendings in local storage + resolve(); + }).catch(function(err){reject(err);}); }).catch(function(err){reject(err);}); - }).catch(function(err){reject(err);}); - }); - }); - }, - - /** - * Create TX doc and send it - * @param block the current block - * @param destPub - * @param amount - * @param inputs - * @param comments - * @return the hash of the sent TX - */ - createAndSendTx = function(block, destPub, amount, inputs, comments) { - - // Make sure a TX in compact mode has no more than 100 lines (fix #118) - if (inputs.sources.length > 40) { - console.debug("[Wallet] TX has to many sources. Will chain TX..."); - - // Compute a slice of sources - var firstSlice = { - minBase: block.unitbase, - maxBase: 0, - amount: 0, - sources: inputs.sources.slice(0, 39) - }; - _.forEach(firstSlice.sources, function(source) { - if (source.base < firstSlice.minBase) firstSlice.minBase = source.base; - if (source.base > firstSlice.maxBase) firstSlice.maxBase = source.base; - firstSlice.amount += powBase(source.amount, source.base); + }); }); + }, - // Send inputs first slice - return createAndSendTx(block, data.pubkey/*to himself*/, firstSlice.amount, firstSlice) // comment ot need - .then(function(res) { - _.forEach(firstSlice.sources, function(source) { - source.consumed=true; - }); - data.sources.push(res.sources); + /** + * Create TX doc and send it + * @param block the current block + * @param destPub + * @param amount + * @param inputs + * @param comments + * @return the hash of the sent TX + */ + createAndSendTx = function(block, destPub, amount, inputs, comments) { + + // Make sure a TX in compact mode has no more than 100 lines (fix #118) + if (inputs.sources.length > 40) { + console.debug("[Wallet] TX has to many sources. Will chain TX..."); + + // Compute a slice of sources + var firstSlice = { + minBase: block.unitbase, + maxBase: 0, + amount: 0, + sources: inputs.sources.slice(0, 39) + }; + _.forEach(firstSlice.sources, function(source) { + if (source.base < firstSlice.minBase) firstSlice.minBase = source.base; + if (source.base > firstSlice.maxBase) firstSlice.maxBase = source.base; + firstSlice.amount += powBase(source.amount, source.base); + }); - var secondSlice = { - minBase: block.unitbase, - maxBase: 0, - amount: 0, - sources: inputs.sources.slice(40).concat(res.sources) - }; - _.forEach(secondSlice.sources, function(source) { - if (source.base < secondSlice.minBase) secondSlice.minBase = source.base; - if (source.base > secondSlice.maxBase) secondSlice.maxBase = source.base; - secondSlice.amount += source.amount; - }); + // Send inputs first slice + return createAndSendTx(block, data.pubkey/*to himself*/, firstSlice.amount, firstSlice) // comment ot need + .then(function(res) { + _.forEach(firstSlice.sources, function(source) { + source.consumed=true; + }); + data.sources.push(res.sources); - // Send inputs second slice (recursive call) - return createAndSendTx(block, destPub, amount, secondSlice, comments); - }); - } + var secondSlice = { + minBase: block.unitbase, + maxBase: 0, + amount: 0, + sources: inputs.sources.slice(40).concat(res.sources) + }; + _.forEach(secondSlice.sources, function(source) { + if (source.base < secondSlice.minBase) secondSlice.minBase = source.base; + if (source.base > secondSlice.maxBase) secondSlice.maxBase = source.base; + secondSlice.amount += source.amount; + }); - var tx = 'Version: '+ constants.TX_VERSION +'\n' + - 'Type: Transaction\n' + - 'Currency: ' + data.currency + '\n' + - 'Blockstamp: ' + block.number + '-' + block.hash + '\n' + - 'Locktime: 0\n' + // no lock - 'Issuers:\n' + - data.pubkey + '\n' + - 'Inputs:\n'; - - _.forEach(inputs.sources, function(source) { - // if D : AMOUNT:BASE:D:PUBLIC_KEY:BLOCK_ID - // if T : AMOUNT:BASE:T:T_HASH:T_INDEX - tx += [source.amount, source.base, source.type, source.identifier,source.noffset].join(':')+"\n"; - }); + // Send inputs second slice (recursive call) + return createAndSendTx(block, destPub, amount, secondSlice, comments); + }); + } - tx += 'Unlocks:\n'; - for (i=0; i<inputs.sources.length; i++) { - // INPUT_INDEX:UNLOCK_CONDITION - tx += i + ':SIG(0)\n'; - } + var tx = 'Version: '+ constants.TX_VERSION +'\n' + + 'Type: Transaction\n' + + 'Currency: ' + data.currency + '\n' + + 'Blockstamp: ' + block.number + '-' + block.hash + '\n' + + 'Locktime: 0\n' + // no lock + 'Issuers:\n' + + data.pubkey + '\n' + + 'Inputs:\n'; + + _.forEach(inputs.sources, function(source) { + // if D : AMOUNT:BASE:D:PUBLIC_KEY:BLOCK_ID + // if T : AMOUNT:BASE:T:T_HASH:T_INDEX + tx += [source.amount, source.base, source.type, source.identifier,source.noffset].join(':')+"\n"; + }); - tx += 'Outputs:\n'; - // AMOUNT:BASE:CONDITIONS - var rest = amount; - var outputBase = inputs.maxBase; - var outputAmount; - var outputOffset = 0; - var newSources = []; - // Outputs to receiver (if not himself) - if (destPub !== data.pubkey) { + tx += 'Unlocks:\n'; + for (i=0; i<inputs.sources.length; i++) { + // INPUT_INDEX:UNLOCK_CONDITION + tx += i + ':SIG(0)\n'; + } + + tx += 'Outputs:\n'; + // AMOUNT:BASE:CONDITIONS + var rest = amount; + var outputBase = inputs.maxBase; + var outputAmount; + var outputOffset = 0; + var newSources = []; + // Outputs to receiver (if not himself) + if (destPub !== data.pubkey) { + while(rest > 0) { + outputAmount = truncBase(rest, outputBase); + rest -= outputAmount; + if (outputAmount > 0) { + outputAmount = outputBase === 0 ? outputAmount : outputAmount / Math.pow(10, outputBase); + tx += outputAmount + ':' + outputBase + ':SIG(' + destPub + ')\n'; + outputOffset++; + } + outputBase--; + } + rest = inputs.amount - amount; + outputBase = inputs.maxBase; + } + // Outputs to himself while(rest > 0) { outputAmount = truncBase(rest, outputBase); rest -= outputAmount; if (outputAmount > 0) { outputAmount = outputBase === 0 ? outputAmount : outputAmount / Math.pow(10, outputBase); - tx += outputAmount + ':' + outputBase + ':SIG(' + destPub + ')\n'; + tx += outputAmount +':'+outputBase+':SIG('+data.pubkey+')\n'; + newSources.push({ + type: 'T', + noffset: outputOffset, + amount: outputAmount, + base: outputBase + }); outputOffset++; } outputBase--; } - rest = inputs.amount - amount; - outputBase = inputs.maxBase; - } - // Outputs to himself - while(rest > 0) { - outputAmount = truncBase(rest, outputBase); - rest -= outputAmount; - if (outputAmount > 0) { - outputAmount = outputBase === 0 ? outputAmount : outputAmount / Math.pow(10, outputBase); - tx += outputAmount +':'+outputBase+':SIG('+data.pubkey+')\n'; - newSources.push({ - type: 'T', - noffset: outputOffset, - amount: outputAmount, - base: outputBase - }); - outputOffset++; - } - outputBase--; - } - tx += "Comment: "+ (comments||"") + "\n"; + tx += "Comment: "+ (comments||"") + "\n"; - return CryptoUtils.sign(tx, data.keypair) - .then(function(signature) { - var signedTx = tx + signature + "\n"; - return BMA.tx.process({transaction: signedTx}) - .catch(function(err) { - if (err && err.ucode === BMA.errorCodes.TX_ALREADY_PROCESSED) { - // continue - return; + return CryptoUtils.sign(tx, data.keypair) + .then(function(signature) { + var signedTx = tx + signature + "\n"; + return BMA.tx.process({transaction: signedTx}) + .catch(function(err) { + if (err && err.ucode === BMA.errorCodes.TX_ALREADY_PROCESSED) { + // continue + return; + } + throw err; + }) + .then(function() { + return CryptoUtils.util.hash(signedTx); + }) + .then(function(txHash) { + _.forEach(newSources, function(output) { + output.identifier= txHash; + output.consumed = false; + output.pending = true; + }); + return { + hash: txHash, + sources: newSources + }; + }); + }); + }, + + checkUidNotExists = function(uid, pubkey) { + return $q(function(resolve, reject) { + BMA.wot.lookup({ search: uid }) // search on uid + .then(function(res) { + var found = res.results && + res.results.length > 0 && + res.results.some(function(pub){ + return pub.uids && pub.uids.length > 0 && + pub.uids.some(function(idty) { + return (idty.uid === uid) && // same Uid + (idty.revoked || pub.pubkey !== pubkey); // but not same pubkey + }); + }); + if (found) { // uid is already used : display a message and call failed callback + reject('ACCOUNT.NEW.MSG_UID_ALREADY_USED'); + } + else { + resolve(uid); } - throw err; }) - .then(function() { - return CryptoUtils.util.hash(signedTx); + .catch(function() { + resolve(uid); // not found, so OK + }); + }); + }, + + checkPubkeyNotExists = function(uid, pubkey) { + return $q(function(resolve, reject) { + BMA.wot.lookup({ search: pubkey }) // search on pubkey + .then(function(res) { + var found = res.results && + res.results.length > 0 && + res.results.some(function(pub){ + return pub.pubkey === pubkey && + pub.uids && pub.uids.length > 0 && + pub.uids.some(function(idty) { + return (!idty.revoked); // excluded revoked uid + }); + }); + if (found) { // uid is already used : display a message and reopen the popup + reject('ACCOUNT.NEW.MSG_PUBKEY_ALREADY_USED'); + } + else { + resolve(uid); + } }) - .then(function(txHash) { - _.forEach(newSources, function(output) { - output.identifier= txHash; - output.consumed = false; - output.pending = true; - }); - return { - hash: txHash, - sources: newSources - }; + .catch(function() { + resolve(uid); // not found, so OK }); }); - }, - - checkUidNotExists = function(uid, pubkey) { - return $q(function(resolve, reject) { - BMA.wot.lookup({ search: uid }) // search on uid - .then(function(res) { - var found = res.results && - res.results.length > 0 && - res.results.some(function(pub){ - return pub.uids && pub.uids.length > 0 && - pub.uids.some(function(idty) { - return (idty.uid === uid) && // same Uid - (idty.revoked || pub.pubkey !== pubkey); // but not same pubkey - }); - }); - if (found) { // uid is already used : display a message and call failed callback - reject('ACCOUNT.NEW.MSG_UID_ALREADY_USED'); - } - else { - resolve(uid); - } - }) - .catch(function() { - resolve(uid); // not found, so OK - }); - }); - }, - - checkPubkeyNotExists = function(uid, pubkey) { - return $q(function(resolve, reject) { - BMA.wot.lookup({ search: pubkey }) // search on pubkey - .then(function(res) { - var found = res.results && - res.results.length > 0 && - res.results.some(function(pub){ - return pub.pubkey === pubkey && - pub.uids && pub.uids.length > 0 && - pub.uids.some(function(idty) { - return (!idty.revoked); // excluded revoked uid - }); - }); - if (found) { // uid is already used : display a message and reopen the popup - reject('ACCOUNT.NEW.MSG_PUBKEY_ALREADY_USED'); - } - else { - resolve(uid); - } - }) - .catch(function() { - resolve(uid); // not found, so OK + }, + + getIdentityDocument = function(uid, blockUid) { + uid = uid || data.uid; + blockUid = blockUid || data.blockUid; + if (!uid || !blockUid) { + throw {message: 'ERROR.WALLET_HAS_NO_SELF'}; + } + if (data.requirements && data.requirements.expired) { + throw {message: 'ERROR.WALLET_IDENTITY_EXPIRED'}; + } + + var identity = 'Version: '+ constants.IDTY_VERSION +'\n' + + 'Type: Identity\n' + + 'Currency: ' + data.currency + '\n' + + 'Issuer: ' + data.pubkey + '\n' + + 'UniqueID: ' + uid + '\n' + + 'Timestamp: ' + blockUid + '\n'; + return CryptoUtils.sign(identity, data.keypair) + .then(function(signature) { + identity += signature + '\n'; + console.debug('Has generate an identity document:\n----\n' + identity + '----'); + return identity; }); - }); - }, + }, - getIdentityDocument = function(uid, blockUid) { - uid = uid || data.uid; - blockUid = blockUid || data.blockUid; - if (!uid || !blockUid) { - throw {message: 'ERROR.WALLET_HAS_NO_SELF'}; - } - if (data.requirements && data.requirements.expired) { - throw {message: 'ERROR.WALLET_IDENTITY_EXPIRED'}; - } + /** + * Send self identity + */ + self = function(uid, needToLoadRequirements) { + if (!BMA.regex.USER_ID.test(uid)){ + throw new Error('ERROR.INVALID_USER_ID'); + } + var block; + return $q.all([ + // check uid used by another pubkey + checkUidNotExists(uid, data.pubkey), - var identity = 'Version: '+ constants.IDTY_VERSION +'\n' + - 'Type: Identity\n' + - 'Currency: ' + data.currency + '\n' + - 'Issuer: ' + data.pubkey + '\n' + - 'UniqueID: ' + uid + '\n' + - 'Timestamp: ' + blockUid + '\n'; - return CryptoUtils.sign(identity, data.keypair) - .then(function(signature) { - identity += signature + '\n'; - console.debug('Has generate an identity document:\n----\n' + identity + '----'); - return identity; - }); - }, - - /** - * Send self identity - */ - self = function(uid, needToLoadRequirements) { - if (!BMA.regex.USER_ID.test(uid)){ - throw new Error('ERROR.INVALID_USER_ID'); - } - var block; - return $q.all([ - // check uid used by another pubkey - checkUidNotExists(uid, data.pubkey), - - // Load parameters (need to known the currency) - loadParameters(), - - // Get th current block - BMA.blockchain.current() - .then(function(current) { - block = current; + // Load parameters (need to known the currency) + loadParameters(), + + // Get th current block + BMA.blockchain.current() + .then(function(current) { + block = 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) { + block = {number: 0, hash: BMA.constants.ROOT_BLOCK_HASH}; + } + else { + throw err; + } + }) + ]) + + // Create identity document + .then(function() { + return getIdentityDocument(uid, block.number + '-' + block.hash); }) - .catch(function(err) { - // Special case for currency init (root block not exists): use fixed values - if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { - block = {number: 0, hash: BMA.constants.ROOT_BLOCK_HASH}; + + // Send to node + .then(function (identity) { + return BMA.wot.add({identity: identity}); + }) + + .then(function () { + if (!!needToLoadRequirements) { + // Refresh membership data (if need) + return loadRequirements(); } else { - throw err; + data.uid = uid; + data.blockUid = block.number + '-' + block.hash; } }) - ]) - - // Create identity document - .then(function() { - return getIdentityDocument(uid, block.number + '-' + block.hash); - }) - - // Send to node - .then(function (identity) { - return BMA.wot.add({identity: identity}); - }) - - .then(function () { - if (!!needToLoadRequirements) { - // Refresh membership data (if need) - return loadRequirements(); - } - else { - data.uid = uid; - data.blockUid = block.number + '-' + block.hash; - } - }) - .catch(function (err) { - if (err && err.ucode === BMA.errorCodes.IDENTITY_SANDBOX_FULL) { - throw {ucode: BMA.errorCodes.IDENTITY_SANDBOX_FULL, message: 'ERROR.IDENTITY_SANDBOX_FULL'}; - } - throw err; - }); - }, - - /** - * Send membership (in or out) - */ - membership = function(sideIn) { - return function() { - var membership; - return BMA.blockchain.current() + .catch(function (err) { + if (err && err.ucode === BMA.errorCodes.IDENTITY_SANDBOX_FULL) { + throw {ucode: BMA.errorCodes.IDENTITY_SANDBOX_FULL, message: 'ERROR.IDENTITY_SANDBOX_FULL'}; + } + throw err; + }); + }, + + /** + * Send membership (in or out) + */ + membership = function(sideIn) { + return function() { + var membership; + 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) { @@ -1326,249 +1326,303 @@ angular.module('cesium.wallet.services', ['ngResource', 'ngApi', 'cesium.bma.ser } throw err; }) + .then(function(block) { + // Create membership to sign + membership = 'Version: '+ constants.MS_VERSION +'\n' + + 'Type: Membership\n' + + 'Currency: ' + data.currency + '\n' + + 'Issuer: ' + data.pubkey + '\n' + + 'Block: ' + block.number + '-' + block.hash + '\n' + + 'Membership: ' + (!!sideIn ? "IN" : "OUT" ) + '\n' + + 'UserID: ' + data.uid + '\n' + + 'CertTS: ' + data.blockUid + '\n'; + + return CryptoUtils.sign(membership, data.keypair); + }) + .then(function(signature) { + var signedMembership = membership + signature + '\n'; + // Send signed membership + return BMA.blockchain.membership({membership: signedMembership}); + }) + .then(function() { + return $timeout(function() { + return loadRequirements(); + }, 1000); // waiting for node to process membership doc + }) + .then(function() { + finishLoadRequirements(); + }); + }; + }, + + /** + * Send identity certification + */ + certify = function(uid, pubkey, timestamp, signature, isMember, wasMember) { + var current; + var cert; + + 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: Math.trunc(new Date().getTime() / 1000)}; + } + throw err; + }) .then(function(block) { - // Create membership to sign - membership = 'Version: '+ constants.MS_VERSION +'\n' + - 'Type: Membership\n' + + current = block; + // Create the self part to sign + cert = 'Version: '+ constants.CERT_VERSION +'\n' + + 'Type: Certification\n' + 'Currency: ' + data.currency + '\n' + 'Issuer: ' + data.pubkey + '\n' + - 'Block: ' + block.number + '-' + block.hash + '\n' + - 'Membership: ' + (!!sideIn ? "IN" : "OUT" ) + '\n' + - 'UserID: ' + data.uid + '\n' + - 'CertTS: ' + data.blockUid + '\n'; + 'IdtyIssuer: ' + pubkey + '\n' + + 'IdtyUniqueID: ' + uid + '\n' + + 'IdtyTimestamp: ' + timestamp + '\n' + + 'IdtySignature: ' + signature + '\n' + + 'CertTimestamp: ' + block.number + '-' + block.hash + '\n'; - return CryptoUtils.sign(membership, data.keypair); + return CryptoUtils.sign(cert, data.keypair); }) .then(function(signature) { - var signedMembership = membership + signature + '\n'; - // Send signed membership - return BMA.blockchain.membership({membership: signedMembership}); - }) - .then(function() { - return $timeout(function() { - return loadRequirements(); - }, 1000); // waiting for node to process membership doc + var signedCert = cert + signature + '\n'; + return BMA.wot.certify({cert: signedCert}); }) .then(function() { - finishLoadRequirements(); + return { + pubkey: pubkey, + uid: uid, + time: current.medianTime, + isMember: isMember, + wasMember: wasMember, + expiresIn: data.parameters.sigWindow, + pending: true, + block: current.number, + valid: true + }; }); - }; - }, - - /** - * Send identity certification - */ - certify = function(uid, pubkey, timestamp, signature, isMember, wasMember) { - var current; - var cert; - - 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: Math.trunc(new Date().getTime() / 1000)}; + }, + + 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); } - throw err; - }) - .then(function(block) { - current = block; - // Create the self part to sign - cert = 'Version: '+ constants.CERT_VERSION +'\n' + - 'Type: Certification\n' + - 'Currency: ' + data.currency + '\n' + - 'Issuer: ' + data.pubkey + '\n' + - 'IdtyIssuer: ' + pubkey + '\n' + - 'IdtyUniqueID: ' + uid + '\n' + - 'IdtyTimestamp: ' + timestamp + '\n' + - 'IdtySignature: ' + signature + '\n' + - 'CertTimestamp: ' + block.number + '-' + block.hash + '\n'; - - return CryptoUtils.sign(cert, data.keypair); - }) - .then(function(signature) { - var signedCert = cert + signature + '\n'; - return BMA.wot.certify({cert: signedCert}); - }) - .then(function() { - return { - pubkey: pubkey, - uid: uid, - time: current.medianTime, - isMember: isMember, - wasMember: wasMember, - expiresIn: data.parameters.sigWindow, - pending: true, - block: current.number, - valid: true - }; - }); - }, - - 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); + console.debug('Event without message. Skipping this 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.connect(salt, pwd) + .then(function(keypair){ + console.debug(keypair); + var nonce = CryptoUtils.util.random_nonce(); + record.nonce = CryptoUtils.util.encode_base58(nonce); + record.pubkey = CryptoUtils.util.encode_base58(keypair.signPk); + + return CryptoUtils.box.pack(record.salt, nonce, keypair.boxPk, keypair.boxSk) + .then(function (cypherSalt) { + record.salt = cypherSalt; + + return CryptoUtils.box.pack(record.pwd, nonce, keypair.boxPk, keypair.boxSk) + .then(function (cypherPwd) { + record.pwd = cypherPwd; + + return record; + }); + }); + }); + }, + + getSaveIDDocument = function(record) { + var saveId = 'Version: 10 \n' + + 'Type: SaveID\n' + + 'Questions: ' + '\n' + record.questions + + 'Issuer: ' + data.pubkey + '\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'); + }); + + }, - getRevocationDocument = function() { + getRevocationDocument = function() { - // Get current identity document - return getIdentityDocument() + // Get current identity document + return getIdentityDocument() // Create membership document (unsigned) - .then(function(identity){ - var identityLines = identity.trim().split('\n'); - var idtySignature = identityLines[identityLines.length-1]; + .then(function(identity){ + var identityLines = identity.trim().split('\n'); + var idtySignature = identityLines[identityLines.length-1]; - var revocation = 'Version: '+ constants.REVOKE_VERSION +'\n' + - 'Type: Revocation\n' + - 'Currency: ' + data.currency + '\n' + - 'Issuer: ' + data.pubkey + '\n' + - 'IdtyUniqueID: ' + data.uid + '\n' + - 'IdtyTimestamp: ' + data.blockUid + '\n' + - 'IdtySignature: ' + idtySignature + '\n'; + var revocation = 'Version: '+ constants.REVOKE_VERSION +'\n' + + 'Type: Revocation\n' + + 'Currency: ' + data.currency + '\n' + + 'Issuer: ' + data.pubkey + '\n' + + 'IdtyUniqueID: ' + data.uid + '\n' + + 'IdtyTimestamp: ' + data.blockUid + '\n' + + 'IdtySignature: ' + idtySignature + '\n'; - // Sign revocation document - return CryptoUtils.sign(revocation, data.keypair) + // Sign revocation document + return CryptoUtils.sign(revocation, data.keypair) // Add revocation to document - .then(function(signature) { - revocation += signature + '\n'; - console.debug('Has generate an revocation document:\n----\n' + revocation + '----'); - return revocation; - }); - }); - }, + .then(function(signature) { + revocation += signature + '\n'; + console.debug('Has generate an revocation document:\n----\n' + revocation + '----'); + return revocation; + }); + }); + }, - /** - * Send a revocation - */ - revoke = function() { + /** + * Send a revocation + */ + revoke = function() { - // Clear old events - cleanEventsByContext('revocation'); + // Clear old events + cleanEventsByContext('revocation'); - // Get revocation document - return getRevocationDocument() + // Get revocation document + return getRevocationDocument() // Send revocation document - .then(function(revocation) { - return BMA.wot.revoke({revocation: revocation}); - }) - - // Reload requirements - .then(function() { - - return $timeout(function() { - return loadRequirements(); - }, 1000); // waiting for node to process membership doc - }) - - .then(function() { - finishLoadRequirements(); - - // Add user event - addEvent({type:'pending', message: 'INFO.REVOCATION_SENT_WAITING_PROCESS', context: 'revocation'}, true); - }) - .catch(function(err) { - if (err && err.ucode == BMA.errorCodes.REVOCATION_ALREADY_REGISTERED) { - // Already registered by node: just add an event + .then(function(revocation) { + return BMA.wot.revoke({revocation: revocation}); + }) + + // Reload requirements + .then(function() { + + return $timeout(function() { + return loadRequirements(); + }, 1000); // waiting for node to process membership doc + }) + + .then(function() { + finishLoadRequirements(); + + // Add user event addEvent({type:'pending', message: 'INFO.REVOCATION_SENT_WAITING_PROCESS', context: 'revocation'}, true); + }) + .catch(function(err) { + if (err && err.ucode == BMA.errorCodes.REVOCATION_ALREADY_REGISTERED) { + // Already registered by node: just add an event + addEvent({type:'pending', message: 'INFO.REVOCATION_SENT_WAITING_PROCESS', context: 'revocation'}, true); + } + else { + throw err; + } + }) + ; + }, + + downloadRevocation = function(){ + return getRevocationDocument() + .then(function(revocation) { + var revocationFile = new Blob([revocation], {type: 'text/plain; charset=utf-8'}); + FileSaver.saveAs(revocationFile, 'revocation.txt'); + }); + }, + + cleanEventsByContext = function(context){ + data.events = data.events.reduce(function(res, event) { + if (event.context && event.context == context) return res; + return res.concat(event); + },[]); + }, + + /** + * Serialize to JSON string + */ + toJson = function() { + return $q(function(resolve, reject) { + var json = JSON.stringify(data); + resolve(json); + }); + }, + + /** + * 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 || '{}'); + 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.keypair.boxPk) { + var boxPk = new Uint8Array(32); + for (i = 0; i < 32; i++) boxPk[i] = obj.keypair.boxPk[i]; + keypair.boxPk = boxPk; + } + + if (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 { - throw err; + resolve(); } - }) - ; - }, - - downloadRevocation = function(){ - return getRevocationDocument() - .then(function(revocation) { - var revocationFile = new Blob([revocation], {type: 'text/plain; charset=utf-8'}); - FileSaver.saveAs(revocationFile, 'revocation.txt'); }); - }, - - cleanEventsByContext = function(context){ - data.events = data.events.reduce(function(res, event) { - if (event.context && event.context == context) return res; - return res.concat(event); - },[]); - }, - - /** - * Serialize to JSON string - */ - toJson = function() { - return $q(function(resolve, reject) { - var json = JSON.stringify(data); - resolve(json); - }); - }, - - /** - * 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 || '{}'); - 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.keypair.boxPk) { - var boxPk = new Uint8Array(32); - for (i = 0; i < 32; i++) boxPk[i] = obj.keypair.boxPk[i]; - keypair.boxPk = boxPk; - } - - if (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(); - } - }); - } - ; + } + ; // Register extension points api.registerEvent('data', 'init'); @@ -1600,6 +1654,8 @@ angular.module('cesium.wallet.services', ['ngResource', 'ngApi', 'cesium.bma.ser transfer: transfer, self: self, revoke: revoke, + downloadSaveId: downloadSaveId, + getkeypairSaveId: getkeypairSaveId, downloadRevocation: downloadRevocation, membership: { inside: membership(true), diff --git a/www/plugins/es/js/controllers/settings-controllers.js b/www/plugins/es/js/controllers/settings-controllers.js index be768c7f44034cf39a3065d5e4ea606f597b5689..caa9a0adebfaa64035a2386200fbe3eb50b5a62c 100644 --- a/www/plugins/es/js/controllers/settings-controllers.js +++ b/www/plugins/es/js/controllers/settings-controllers.js @@ -49,7 +49,7 @@ function ESExtendSettingsController ($scope, PluginService, csSettings) { * Settings extend controller */ function ESPluginSettingsController ($scope, $q, $translate, $ionicPopup, UIUtils, csSettings, csHttp, esMarket, - esRegistry, esUser) { + esRegistry, esUser, Modals) { 'ngInject'; $scope.formData = {}; @@ -71,7 +71,7 @@ function ESPluginSettingsController ($scope, $q, $translate, $ionicPopup, UIUti $scope.popupForm = popupForm; }; - // Change ESnode + // Change ESnode $scope.changeEsNode= function(node) { $scope.showNodePopup(node || $scope.formData) .then(function(node) { @@ -97,6 +97,31 @@ function ESPluginSettingsController ($scope, $q, $translate, $ionicPopup, UIUti }); }; + $scope.showNodeList = function() { + $ionicPopup._popupStack[0].responseDeferred.promise.close(); + return Modals.showNetworkLookup({enableFilter: true, type: 'member', endpointFilter: 'ES_USER_API'}) + .then(function (result) { + if (result) { + var parts = result.server.split(':'); + if (result.dns) { + return { + host: result.dns, + port: parts[1] || 80 + }; + } + else { + return { + host: parts[0], + port: parts[1] || 80 + }; + } + } + }) + .then(function(newNode) { + $scope.changeEsNode(newNode); + }); + }; + // Show node popup $scope.showNodePopup = function(node) { return $q(function(resolve, reject) { diff --git a/www/templates/modal_login.html b/www/templates/modal_login.html index ff3d3a71bbba3489c679f4aa659ba93ff88c53fb..409d45dc1c2779953d40040f0ec1ad10e8e1c8f0 100644 --- a/www/templates/modal_login.html +++ b/www/templates/modal_login.html @@ -25,7 +25,7 @@ ng-model="formData.username" ng-model-options="{ debounce: 650 }" class="highlight-light" - required> + required > <input ng-if="showSalt" name="username" type="text" placeholder="{{'LOGIN.SALT_HELP' | translate}}" ng-model="formData.username" diff --git a/www/templates/wallet/modal_security.html b/www/templates/wallet/modal_security.html index e6fce021b19b5e3115b90810b76bc2cf6c289da7..81cd38da31697777d985eed2756ef83cc9123071 100644 --- a/www/templates/wallet/modal_security.html +++ b/www/templates/wallet/modal_security.html @@ -15,7 +15,7 @@ <button class="button button-clear icon-right visible-xs" ng-if="!isLastSlide && slides.slider.activeIndex > 1" - ng-click="slideNext()"> + ng-click="doNext()"> <span translate>COMMON.BTN_NEXT</span> <i class="icon ion-ios-arrow-right"></i> </button> @@ -28,21 +28,21 @@ <ion-slide-page> <ion-content class="has-header padding"> <div class="list"> - <a class="item card item-icon-right stable-bg padding ink" - ng-click=""> + <div class="item card item-icon-right stable-bg padding ink dark" + ng-click="slideTo(2)"> <i class="icon ion-ios-arrow-right"></i> <span ng-bind-html="'ACCOUNT.SECURITY.SAVE_KEYS' | translate"></span> - </a> - <a class="item card item-icon-right stable-bg padding ink" - ng-click=""> + </div> + <div class="item card item-icon-right stable-bg padding ink dark" + ng-click="slideNext()"> <span ng-bind-html="'ACCOUNT.SECURITY.REVOCATION' | translate"></span> - <h4 class="gray"></h4> - <i class="icon dark ion-ios-arrow-right"></i> - </a> + <i class="icon ion-ios-arrow-right"></i> + </div> </div> <div class="padding hidden-xs text-right"> - <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL + <button class="button button-clear button-dark ink" ng-click="closeModal()" + type="button" translate>COMMON.BTN_CANCEL </button> </div> </ion-content> @@ -53,16 +53,16 @@ <ion-content class="has-header padding"> <h3 translate>ACCOUNT.SECURITY.REVOCATION</h3> <div class="list"> - <a class="item card item-icon-right stable-bg padding ink" + <div class="item card item-icon-right stable-bg padding ink dark" ng-click="downloadRevokeFile()"> <i class="icon ion-android-archive"></i> <span ng-bind-html="'ACCOUNT.SECURITY.DOWNLOAD_REVOKE' | translate"></span> - </a> - <a class="item card item-icon-right stable-bg padding ink" + </div> + <div class="item card item-icon-right stable-bg padding ink dark" ng-click="revokeIdentity()"> <i class="icon ion-minus-circled assertive"></i> <span ng-bind-html="'ACCOUNT.SECURITY.DEFINITELY_REVOKE' | translate"></span> - </a> + </div> </div> <div class="padding hidden-xs text-right"> <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL @@ -71,288 +71,172 @@ </ion-content> </ion-slide-page> - <!-- STEP 3: save keys --> + <!-- STEP 3: choose questions --> <ion-slide-page> - <ion-content class="has-header" scroll="false"> - <form name="saltForm" novalidate="" ng-submit="doNext('saltForm')"> - + <ion-content class="has-header padding" > + <h3 translate>ACCOUNT.SECURITY.SAVE_KEYS</h3> + <label class="item item-input item-select"> + <div class="input-label" translate> + ACCOUNT.SECURITY.LEVEL + </div> + <select ng-model="formData.level"> + <option value="2" ng-bind-html="'ACCOUNT.SECURITY.LOW_LEVEL' | translate"></option> + <option value="4" translate>ACCOUNT.SECURITY.MEDIUM_LEVEL</option> + <option value="6" translate>ACCOUNT.SECURITY.STRONG_LEVEL</option> + </select> + </label> + <div class="padding-top" translate="ACCOUNT.SECURITY.HELP_LEVEL " + translate-values="{nb: {{formData.level}}}"> + </div> + <form name="questionsForm" novalidate ng-submit="doNext('questionsForm')"> <div class="list" - ng-init="setForm(saltForm, 'saltForm')"> - - <div class="item item-text-wrap text-center padding hidden-xs" > - <a class="pull-right icon-help" ng-click="showHelpModal('join-salt')"></a> - <span translate>ACCOUNT.SECURITY.SAVE_KEYS</span> - </div> + ng-init="setForm(questionsForm, 'questionsForm')"> - <!-- salt --> - <div class="item item-input" - ng-class="{ 'item-input-error': saltForm.$submitted && saltForm.username.$invalid}"> - <span class="input-label" translate>LOGIN.SALT</span> - <input ng-if="!showUsername" - name="username" type="password" placeholder="{{'LOGIN.SALT_HELP' | translate}}" - ng-change="formDataChanged()" - ng-model="formData.username" - ng-minlength="8" - required> - <input ng-if="showUsername" - name="username" type="text" placeholder="{{'LOGIN.SALT_HELP' | translate}}" - ng-change="formDataChanged()" - ng-model="formData.username" - ng-minlength="8" - required> - </div> - <div class="form-errors" - ng-show="saltForm.$submitted && saltForm.username.$error" - ng-messages="saltForm.username.$error"> - <div class="form-error" ng-message="minlength"> - <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 8}"></span> - </div> - <div class="form-error" ng-message="required"> - <span translate="ERROR.FIELD_REQUIRED"></span> + <ion-checkbox ng-repeat="question in formData.questions" ng-model="question.checked" + ng-required="isRequired()"> + <span style="white-space: normal;">{{question.value | translate}}</span> + </ion-checkbox> + <div class="item item-icon-right no-padding-top"> + <a class="dark"><i class="icon ion-android-add" ng-click="addQuestion()"></i></a> + <div class="list list-inset"> + <label class="item item-input"> + <input type="text" placeholder="{{'ACCOUNT.SECURITY.ADD_QUESTION' | translate}}" ng-model="formData.addQuestion"/> + </label> </div> </div> + </div> + <div class="padding hidden-xs text-right"> + <button class="button button-clear button-dark ink" ng-click="closeModal()" + type="button" translate>COMMON.BTN_CANCEL + </button> + <button class="button button-clear button-dark" + ng-click="restore()" type="button" translate>ACCOUNT.SECURITY.BTN_RESET + </button> + <button class="button button-calm icon-right ion-chevron-right ink" + ng-disabled="questionsForm.$invalid" type="submit" translate> + COMMON.BTN_NEXT + <i class="icon ion-arrow-right-a"></i> + </button> + </div> + </form> - <!-- confirm salt --> - <div class="item item-input" - ng-class="{ 'item-input-error': saltForm.$submitted && saltForm.confirmSalt.$invalid}"> - <span class="input-label pull-right" translate>ACCOUNT.NEW.SALT_CONFIRM</span> - <input ng-if="!showUsername" - name="confirmUsername" type="password" - placeholder="{{'ACCOUNT.NEW.SALT_CONFIRM_HELP' | translate}}" - ng-model="formData.confirmUsername" - compare-to="formData.username"> - <input ng-if="showUsername" - name="confirmUsername" type="text" - placeholder="{{'ACCOUNT.NEW.SALT_CONFIRM_HELP' | translate}}" - ng-model="formData.confirmUsername" - compare-to="formData.username"> - </div> - <div class="form-errors" - ng-show="saltForm.$submitted && saltForm.confirmUsername.$error" - ng-messages="saltForm.confirmUsername.$error"> - <div class="form-error" ng-message="minlength"> - <span translate="ERROR.FIELD_TOO_SHORT"></span> - </div> - <div class="form-error" ng-message="maxlength"> - <span translate="ERROR.FIELD_TOO_LONG"></span> - </div> - <div class="form-error" ng-message="required"> - <span translate="ERROR.FIELD_REQUIRED"></span> - </div> - <div class="form-error" ng-message="compareTo"> - <span translate="ERROR.SALT_NOT_CONFIRMED"></span> - </div> - </div> + </ion-content> + </ion-slide-page> - <!-- Show values --> - <div class="item item-toggle dark"> - <span translate>COMMON.SHOW_VALUES</span> - <label class="toggle toggle-royal"> - <input type="checkbox" ng-model="showUsername"> - <div class="track"> - <div class="handle"></div> - </div> + <!-- STEP 4: answers --> + <ion-slide-page> + <ion-content class="has-header padding" > + <h3 translate>ACCOUNT.SECURITY.SAVE_KEYS</h3> + + <form name="answersForm" novalidate ng-submit="doNext('answersForm')"> + <div class="list" ng-init="setForm(answersForm, 'answersForm')"> + <ng-repeat ng-repeat="question in formData.questions |filter:true:checked"> + <label class="item item-input {{smallscreen ? 'item-stacked-label' : 'item-floating-label'}}" + ng-class="{'item-input-error': answersForm.$submitted && answersForm['question{{$index}}'].$invalid}"> + <span class="input-label" style="{{smallscreen ? 'white-space: normal' : ''}}">{{question.value | translate}}</span> + <input type="text" name="question{{$index}}" placeholder="{{smallscreen ? '' : question.value | translate}}" ng-model="question.answer" required /> </label> - </div> - + <div class="form-errors" + ng-show="answersForm.$submitted && answersForm['question{{$index}}'].$error" + ng-messages="answersForm['question{{$index}}'].$error"> + <div class="form-error" ng-message="required"> + <span translate="ERROR.FIELD_REQUIRED"></span> + </div> + </div> + </ng-repeat> <div class="padding hidden-xs text-right"> - <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL + <button class="button button-clear button-dark ink" ng-click="closeModal()" + type="button" translate>COMMON.BTN_CANCEL + </button> + <button class="button button-clear button-dark" + ng-click="restore()" type="button" translate>ACCOUNT.SECURITY.BTN_CLEAN </button> - <button class="button button-calm icon-right ion-chevron-right ink" type="submit" translate> + <button class="button button-calm icon-right ion-chevron-right ink" + ng-disabled="questionsForm.$invalid" type="submit" translate> COMMON.BTN_NEXT <i class="icon ion-arrow-right-a"></i> </button> </div> </div> + </form> </ion-content> </ion-slide-page> - <!-- STEP 4: password--> + <!-- STEP 5: confirm identity --> <ion-slide-page> - <ion-content class="has-header" scroll="false"> - <form name="passwordForm" novalidate="" ng-submit="doNext('passwordForm')"> - - <div class="item item-text-wrap text-center padding hidden-xs" > - <a class="pull-right icon-help" ng-click="showHelpModal('join-password')"></a> - <span translate>ACCOUNT.NEW.PASSWORD_WARNING</span> - </div> + <ion-content> + <form name="loginForm" novalidate="" ng-submit="submit()"> - <div class="list" - ng-init="setForm(passwordForm, 'passwordForm')"> + <div class="list" ng-init="setForm(loginForm, 'loginForm')"> - <!-- password --> - <div class="item item-input" - ng-class="{ 'item-input-error': passwordForm.$submitted && passwordForm.password.$invalid}"> - <span class="input-label" translate>LOGIN.PASSWORD</span> - <input ng-if="!showPassword" - name="password" type="password" placeholder="{{'LOGIN.PASSWORD_HELP' | translate}}" - ng-model="formData.password" - ng-change="formDataChanged()" - ng-minlength="8" + <!-- salt (=username, to enable browser login cache) --> + <label class="item item-input" + ng-class="{ 'item-input-error': loginForm.$submitted && loginForm.username.$invalid}"> + <span class="input-label hidden-xs" translate>LOGIN.SALT</span> + <input ng-if="!showSalt" + name="username" type="{{showSalt ? 'text' : 'password'}}" placeholder="{{'LOGIN.SALT_HELP' | translate}}" + ng-model="formData.username" + ng-model-options="{ debounce: 650 }" + class="highlight-light" required> - <input ng-if="showPassword" - name="text" type="text" placeholder="{{'LOGIN.PASSWORD_HELP' | translate}}" - ng-model="formData.password" - ng-change="formDataChanged()" - ng-minlength="8" + <input ng-if="showSalt" + name="username" type="text" placeholder="{{'LOGIN.SALT_HELP' | translate}}" + ng-model="formData.username" + ng-model-options="{ debounce: 650 }" + class="highlight-light" required> - </div> + </label> <div class="form-errors" - ng-show="passwordForm.$submitted && passwordForm.password.$error" - ng-messages="passwordForm.password.$error"> - <div class="form-error" ng-message="minlength"> - <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 8}"></span> - </div> - <div class="form-error" ng-message="maxlength"> - <span translate="ERROR.FIELD_TOO_LONG"></span> - </div> + ng-show="loginForm.$submitted && loginForm.username.$error" + ng-messages="loginForm.username.$error"> <div class="form-error" ng-message="required"> <span translate="ERROR.FIELD_REQUIRED"></span> </div> </div> - <!-- confirm password --> - <div class="item item-input" - ng-class="{ 'item-input-error': passwordForm.$submitted && passwordForm.confirmPassword.$invalid}"> - <span class="input-label" translate>ACCOUNT.NEW.PASSWORD_CONFIRM</span> - <input ng-if="!showPassword" - name="confirmPassword" type="password" - placeholder="{{'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP' | translate}}" - ng-model="formData.confirmPassword" - compare-to="formData.password"> - <input ng-if="showPassword" - name="confirmPassword" type="text" - placeholder="{{'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP' | translate}}" - ng-model="formData.confirmPassword" - compare-to="formData.password"> - </div> - <div class="form-errors" - ng-show="passwordForm.$submitted && passwordForm.confirmPassword.$error" - ng-messages="passwordForm.confirmPassword.$error"> - <div class="form-error" ng-message="minlength"> - <span translate="ERROR.FIELD_TOO_SHORT"></span> - </div> - <div class="form-error" ng-message="maxlength"> - <span translate="ERROR.FIELD_TOO_LONG"></span> - </div> - <div class="form-error" ng-message="required"> - <span translate="ERROR.FIELD_REQUIRED"></span> - </div> - <div class="form-error" ng-message="compareTo"> - <span translate="ERROR.PASSWORD_NOT_CONFIRMED"></span> - </div> - </div> - - <!-- Show values --> + <!-- Show salt --> <div class="item item-toggle dark"> - <span translate>COMMON.SHOW_VALUES</span> - <label class="toggle toggle-royal"> - <input type="checkbox" ng-model="showPassword"> + <span translate>LOGIN.SHOW_SALT</span> + <label class="toggle toggle-stable"> + <input type="checkbox" ng-model="showSalt"> <div class="track"> <div class="handle"></div> </div> </label> </div> - </div> - - <div class="padding hidden-xs text-right"> - <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL - </button> - <button class="button button-calm icon-right ion-chevron-right ink" type="submit" translate> - COMMON.BTN_NEXT - </button> - </div> - - <div class="padding hidden-xs"> - </div> - </form> - </ion-content> - </ion-slide-page> - - <!-- STEP 5: pseudo--> - <ion-slide-page ng-if="formData.isMember"> - <ion-content class="has-header" scroll="false"> - <form name="pseudoForm" novalidate="" ng-submit="doNext('pseudoForm')"> - - <div class="item item-text-wrap text-center padding hidden-xs" > - <a class="pull-right icon-help" ng-click="showHelpModal('join-pseudo')"></a> - <span translate>ACCOUNT.NEW.PSEUDO_WARNING</span> - </div> - - <div class="list" - ng-init="setForm(pseudoForm, 'pseudoForm')"> - <!-- pseudo --> - <div class="item item-input" - ng-if="formData.isMember" - ng-class="{'item-input-error': pseudoForm.$submitted && pseudoForm.pseudo.$invalid}"> - <span class="input-label" translate>ACCOUNT.NEW.PSEUDO</span> - <input name="pseudo" type="text" placeholder="{{'ACCOUNT.NEW.PSEUDO_HELP' | translate}}" - ng-model="formData.pseudo" - ng-minlength="3" - ng-maxlength="100" + <!-- password--> + <label class="item item-input" + ng-class="{ 'item-input-error': loginForm.$submitted && loginForm.password.$invalid}"> + <span class="input-label hidden-xs" translate>LOGIN.PASSWORD</span> + <input name="password" type="password" placeholder="{{'LOGIN.PASSWORD_HELP' | translate}}" + ng-model="formData.password" + ng-model-options="{ debounce: 650 }" + select-on-click required> - </div> + </label> <div class="form-errors" - ng-if="formData.isMember" - ng-show="pseudoForm.$submitted && pseudoForm.pseudo.$error" - ng-messages="pseudoForm.pseudo.$error"> - <div class="form-error" ng-message="minlength"> - <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 3}"></span> - </div> - <div class="form-error" ng-message="maxlength"> - <span translate="ERROR.FIELD_TOO_LONG_WITH_LENGTH" translate-values="{maxLength: 100}"></span> - </div> + ng-show="loginForm.$submitted && loginForm.password.$error" + ng-messages="loginForm.password.$error"> <div class="form-error" ng-message="required"> <span translate="ERROR.FIELD_REQUIRED"></span> </div> </div> + </div> - <div class="padding hidden-xs text-right"> - <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL - </button> - <button class="button button-calm icon-right ion-chevron-right ink" type="submit" translate> - COMMON.BTN_NEXT - </button> - </div> + <div class="padding hidden-xs text-right"> + <button class="button button-clear button-dark ink" ng-click="closeModal()" + type="button" translate>COMMON.BTN_CANCEL + </button> + <button class="button button-positive ink" type="submit" translate> + COMMON.BTN_SEND + <i class="icon ion-android-send"></i> + </button> </div> </form> </ion-content> </ion-slide-page> - <!-- STEP 6: last slide --> - <ion-slide-page> - <ion-content class="has-header" scroll="false"> - - <div class="padding text-center" translate>ACCOUNT.NEW.LAST_SLIDE_CONGRATULATION</div> - - <div class="list"> - - <ion-item class="item item-text-wrap item-border"> - <div class="dark pull-right padding-right" ng-if="formData.computing"> - <ion-spinner icon="android"></ion-spinner> - </div> - <span class="input-label" translate>COMMON.PUBKEY</span> - <span class="gray text-no-wrap" ng-if="formData.computing" translate> - ACCOUNT.NEW.COMPUTING_PUBKEY - </span> - <span class="gray text-no-wrap" ng-if="formData.pubkey"> - {{formData.pubkey}} - </span> - </ion-item> - </div> - - <div class="padding hidden-xs text-right"> - <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL - </button> - <button class="button button-positive ink" ng-click="doNewAccount()" translate> - COMMON.BTN_SEND - <i class="icon ion-android-send"></i> - </button> - </div> - </ion-content> - </ion-slide-page> - </ion-slides> </ion-modal-view> diff --git a/www/templates/wallet/popover_actions.html b/www/templates/wallet/popover_actions.html index 366e276e8214593b3ff22ca2820453f010d4b736..1b3ae35f8650ae878670460f8eccd9866e955bfd 100644 --- a/www/templates/wallet/popover_actions.html +++ b/www/templates/wallet/popover_actions.html @@ -70,7 +70,6 @@ <!--</a>--> <a class="item item-icon-left ink" - ng-if="!walletData.requirements.needSelf" ng-click="showSecurityModal()"> <i class="icon ion-android-lock"></i> <span ng-bind-html="'ACCOUNT.SECURITY.TITLE' | translate"></span>