From e84743fea3029fbbdcfddff5fca6731b6a83356c Mon Sep 17 00:00:00 2001 From: Anthony Law Date: Thu, 28 Jul 2022 14:27:45 +0800 Subject: [PATCH] Supernode program (#687) * feat: init node rewards modules (#678) * feat: init node rewards modules * fix: clean up comment * [node-rewards-program] task: add service to communicate with API #679 * [node-rewards-program] task: add service add methods: prepareEnrollTransaction, checkEnrollmentAddress, checkEnrollmentStatus and getEnrollmentHash * [node-rewards-program] fix: typo in error message * [node-rewards-program] fix: typo in class name * [node-rewards-program] task: add tests * [node-rewards-program] fix: fetch mock, improve prepareEnrollTransaction test, register service, cleanup * [node-rewards-program] feat: add getNodePayouts() and getNodeInfo() methods, add unit tests * [node-rewards-program] fix: typo in jsdoc * [node-rewards-program] fix: test comments * [node-rewards-program] fix: remove whitespaces * [node-rewards-program] task: refactor fetch requests * [node-rewards-program] task: refactor the enrollment and error tests, remove unused variable * [node-rewards-program] task: update codeword test, fix runPromiseErrorTest helper * [node-rewards-program] task: add enrollment tab component (#681) * feat: add account selection section * build: add object-rest-spread plugins * feat: add node reward program alert message * [node-rewards-program] feat: add UI Tab component * [node-rewards-program] task: refactor UI and typo * [node-rewards-program] task: node reward controller * [node-rewards-program] task: add unit test * [node-rewards-program] task: fix typo * [node-rewards-program] task: fix send enrollment logic * [node-rewards-program] task: refactor on assign selected account * [node-rewards-program] task: refactor on tab and rename * [node-rewards-program] task: fix typo and lint * [node-rewards-program] task: change from toFixed to ceil * [node-rewards-program] task: minor refactor on unit test * Enhancement and feedback (#682) * [node-rewards-program] task: fix send enrollment logic * [node-rewards-program] task: fix typo and lint * [node-rewards-program] fix: change payout table width * [node-rewards-program] fix: format endpoint url to display only hostname * [node-rewards-program] feat: add last round payout property into Status tab * [node-rewards-program] fix: replace money bag icon * [node-rewards-program] perf: add check required params before calling API services * [node-rewards-program] feat: add validation for codeword hash * [node-rewards-program] fix: assign public key for new created account * [node-rewards-program] fix: allow update domain name when account enrolled * [node-rewards-program] task: enable hardware wallet enrollment * [node-rewards-program] task: fix typo * [node-rewards-program] task: rename module to superNode program (#683) * [node-rewards-program] bug: fix minor bug and refactor. (#684) * [node-rewards-program] task: add count params in getNodePayouts * [node-rewards-program] bug: fix unable send enrollement tx to update domain name * [node-rewards-program] task: removed unused method * [node-rewards-program] task: rename enroll in program and form data node host * [node-rewards-program] task: refactor on the logic * [node-rewards-program] task: new validation alert for node host * [node-rewards-program] task: update invalid node host message * [node-rewards-program] task: duplicated lastPayoutRound in unit test * [node-rewards-program] task: patch fix handle lastPayoutRound in null * [node-rewards-program] task: add translation (#685) * [node-rewards-program] task: add chinese translation * [node-rewards-program] task: add spanish translation * [node-rewards-program] task: add italian translation * [node-rewards-program] task: add japanese translation * [node-rewards-program] task: add polish translation * [node-rewards-program] task: add russian translation * [node-rewards-program] task: patch spanish translation * [node-rewards-program] task: add ukrainian translation * [node-rewards-program] task: patch italian translation * [node-rewards-program] task: add default translation in German, Nederlands and Portuguese * [node-rewards-program] task: patch Polish translation * [node-rewards-program] task: patch latest supernode program url and api endpoint * [node-rewards-program] task: patch minor fix Co-authored-by: OlegMakarenko <33131259+OlegMakarenko@users.noreply.github.com> --- gulpfile.js | 6 +- package-lock.json | 16 + package.json | 1 + src/app/app.js | 2 + src/app/modules/home/home.html | 4 +- src/app/modules/languages/cn.js | 41 +- src/app/modules/languages/de.js | 41 +- src/app/modules/languages/en.js | 41 +- src/app/modules/languages/es.js | 41 +- src/app/modules/languages/it.js | 39 +- src/app/modules/languages/jp.js | 41 +- src/app/modules/languages/nl.js | 45 +- src/app/modules/languages/pl.js | 39 +- src/app/modules/languages/ptbr.js | 40 +- src/app/modules/languages/ru.js | 39 +- src/app/modules/languages/uk.js | 39 +- src/app/modules/portal/portal.html | 9 +- src/app/modules/superNodeProgram/index.js | 14 + .../superNodeProgram.config.js | 14 + .../superNodeProgram.controller.js | 405 +++++++++ .../superNodeProgram/superNodeProgram.html | 206 +++++ src/app/services/alert.service.js | 35 + src/app/services/index.js | 4 + src/app/services/superNodeProgram.service.js | 135 +++ src/images/moneyBag.png | Bin 0 -> 4846 bytes src/images/nem-logo.png | Bin 0 -> 13363 bytes src/sass/_new.scss | 9 + tests/data/accountData.js | 34 +- tests/helper/index.js | 22 + tests/specs/superNodeProgram.spec.js | 275 ++++++ tests/specs/superNodeProgramModule.spec.js | 809 ++++++++++++++++++ 31 files changed, 2416 insertions(+), 30 deletions(-) create mode 100644 src/app/modules/superNodeProgram/index.js create mode 100644 src/app/modules/superNodeProgram/superNodeProgram.config.js create mode 100644 src/app/modules/superNodeProgram/superNodeProgram.controller.js create mode 100644 src/app/modules/superNodeProgram/superNodeProgram.html create mode 100644 src/app/services/superNodeProgram.service.js create mode 100644 src/images/moneyBag.png create mode 100644 src/images/nem-logo.png create mode 100644 tests/helper/index.js create mode 100644 tests/specs/superNodeProgram.spec.js create mode 100644 tests/specs/superNodeProgramModule.spec.js diff --git a/gulpfile.js b/gulpfile.js index 2a880e3a..84a007d8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -59,7 +59,8 @@ gulp.task('browserify', ['views'], function() { plugins: [ "syntax-dynamic-import", "transform-runtime", - "transform-async-to-generator" + "transform-async-to-generator", + ["transform-object-rest-spread", { "useBuiltIns": true }] ], ignore: /(bower_components)|(node_modules)/ })) @@ -83,7 +84,8 @@ gulp.task('browserifyTests', function() { plugins: [ "syntax-dynamic-import", "transform-runtime", - "transform-async-to-generator" + "transform-async-to-generator", + ["transform-object-rest-spread", { "useBuiltIns": true }] ], ignore: /(bower_components)|(node_modules)/ })) diff --git a/package-lock.json b/package-lock.json index 0b0d26b9..eaf11edb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2234,6 +2234,12 @@ "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", "dev": true }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==", + "dev": true + }, "babel-plugin-transform-async-to-generator": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", @@ -2488,6 +2494,16 @@ "babel-runtime": "^6.0.0" } }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha512-ocgA9VJvyxwt+qJB0ncxV8kb/CjfTcECUY4tQ5VT7nP6Aohzobm8CDFaQ5FHdvZQzLmf0sgDxB8iRXZXxwZcyA==", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, "babel-plugin-transform-regenerator": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", diff --git a/package.json b/package.json index f675c6ad..baaf3ebb 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "angular-translate": "2.11.0", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.3.13", "babelify": "^7.2.0", diff --git a/src/app/app.js b/src/app/app.js index 134a27b7..c0a67ae6 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -57,6 +57,7 @@ import './modules/domainNameSystem'; import './modules/importWalletQrCode'; import './modules/NEMonster'; import './modules/catapultOptin'; +import './modules/superNodeProgram'; // Create and bootstrap application const requires = [ @@ -116,6 +117,7 @@ const requires = [ 'app.dnsSearch', 'app.NEMonster', 'app.importWalletQrCode', + 'app.superNodeProgram', 'app.catapultOptin' ]; diff --git a/src/app/modules/home/home.html b/src/app/modules/home/home.html index 5f50b504..10f3eb79 100644 --- a/src/app/modules/home/home.html +++ b/src/app/modules/home/home.html @@ -23,7 +23,7 @@

- +
@@ -56,7 +56,7 @@

{{ 'PORTAL_HARVESTING_TEXT' | translate }}

-

{{ 'PORTAL_CATAPULTOPTIN_TEXT' | translate }}

+

{{ 'PORTAL_SUPER_NODE_PROGRAM_TEXT' | translate }}

{{ 'PORTAL_APOSTILLE_TEXT' | translate }}

diff --git a/src/app/modules/languages/cn.js b/src/app/modules/languages/cn.js index e83b9636..90a21d13 100644 --- a/src/app/modules/languages/cn.js +++ b/src/app/modules/languages/cn.js @@ -402,6 +402,10 @@ function ChineseProvider($translateProvider) { PORTAL_ADDRESS_BOOK_TEXT: ' 将标签分配给地址以轻松跟踪联系人.', PORTAL_ADDRESS_BOOK_BTN: '地址簿管理', PORTAL_INVOICE_TEXT: '创建账单以通过QR码共享', + PORTAL_SUPER_NODE_PROGRAM_TITLE: '超级节点活动', + PORTAL_SUPER_NODE_PROGRAM_TEXT: '参与超级节点活动以获得$XEM和保护网络,最低参与资格只需 10,000 XEM 起。', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: '点击此处.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: '查看超级节点活动', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: '地址簿”', @@ -723,7 +727,7 @@ function ChineseProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: '官方网站', FAQ_ANSWER_6_BTT: '官方BTT帖子', FAQ_QUESTION_7: '仪表板上没有显示任何内容', - FAQ_ANSWER_7: '请您务必检查顶部导航栏中的节点圆.
红色圆圈表示与节点的连接失败.
点击“节点”并从下拉列表中选择另一个或使用自定义节点。.
Supernodes.nem.io 有很多可以使用的节点.', + FAQ_ANSWER_7: '请您务必检查顶部导航栏中的节点圆.
红色圆圈表示与节点的连接失败.
点击“节点”并从下拉列表中选择另一个或使用自定义节点。.
Supernode 有很多可以使用的节点.', FAQ_QUESTION_8: '签署人不会看到签署交易。', FAQ_ANSWER_8: '在这种情况下,请转到“服务”, 寻找“多重签名和多用户帐户”并点击“签署多重交易”.', FAQ_QUESTION_9: '什么是最好的安全措施 ?', @@ -992,7 +996,40 @@ function ChineseProvider($translateProvider) { POST_OPTIN_CONFIRM_MODAL_CHECKBOX: "我确认Symbol目标地址与我的Symbol帐户地址匹配", POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "请确认 Symbol 目标地址是您的 Symbol 多重签名帐户地址。如果不匹配,请重新开始该过程并提供多重签名帐户的有效Symbol公钥。", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "我确认 Symbol 目标地址与 Symbol 多重签名帐户地址匹配", - OPTIN_NIS1_PUBLIC_KEY: '您输入的公钥是 NIS1 密钥!您需要输入一个Symbol公钥。' + OPTIN_NIS1_PUBLIC_KEY: '您输入的公钥是 NIS1 密钥!您需要输入一个Symbol公钥。', + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: '超级节点活动', + ACCOUNT_NAME: '帐户', + BALANCE_NAME: '余额', + TAB_STATUS_NAME: '状态', + TAB_ENROLL_IN_PROGRAM_NAME: '参与活动', + TAB_PAYOUT_HISTORY_NAME: '支付历史', + + STATUS_NODE_NAME: '节点名字', + STATUS_NAME: '状态', + STATUS_ACTIVE_NAME: '活跃', + STATUS_INACTIVE_NAME: '待用', + STATUS_PUBLIC_KEY_NAME: '公钥', + STATUS_REMOTE_PUBLIC_KEY_NAME: '远程公钥', + STATUS_LAST_PAYOUT_ROUND_NAME: '最后一轮支付', + STATUS_TOTAL_REWARDS_NAME: '总奖励', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: '参与地址', + ENROLL_IN_PROGRAM_NODE_HOST: '节点主机', + ENROLL_IN_PROGRAM_BUTTON_NAME: '参与', + + PAYOUT_HISTORY_FROM_NAME: '从', + PAYOUT_HISTORY_TO_NAME: '至', + PAYOUT_HISTORY_AMOUNT_NAME: '量', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: '交易哈希', + PAYOUT_HISTORY_DATE_NAME: '日期', + + INVALID_ENROLLMENT_ADDRESS: '注册地址无效.', + ADDRESS_ENROLLED: '当前地址已注册.', + INVALID_CODEWORD_HASH: 'codeword哈希无效.', + ACCOUNT_MISSING_PUBLICKEY: '这账户必须进行一次交易来获取公钥.', + INVALID_FORMAT_NODE_HOST: '不允许使用协议 (http://) 和端口 (:7890).', }); } diff --git a/src/app/modules/languages/de.js b/src/app/modules/languages/de.js index eba37f8c..06811845 100644 --- a/src/app/modules/languages/de.js +++ b/src/app/modules/languages/de.js @@ -400,6 +400,10 @@ function GermanProvider($translateProvider) { PORTAL_ADDRESS_BOOK_TEXT: 'Adressen können mit Labeln versehen werden. Diese vereinfachen die Verwaltung Ihrer Kontakte', PORTAL_ADDRESS_BOOK_BTN: 'Adressbuch verwalten', PORTAL_INVOICE_TEXT: 'Create an invoice to share via QR code', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'SuperNode Program', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Enroll in the SuperNode Program to earn $XEM for securing the network. Minimum 10,000 XEM required.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Read more here.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Check & Enroll in Program', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Adressbuch', @@ -720,7 +724,7 @@ function GermanProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Offizielle Webseite', FAQ_ANSWER_6_BTT: 'Offizieller BitcoinTalk Thread', FAQ_QUESTION_7: 'Nothing is shown on the dashboard', - FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernodes.nem.io has a lot of nodes that you can use.', + FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernode has a lot of nodes that you can use.', FAQ_QUESTION_8: 'Cosignatories cannot see the transaction to sign', FAQ_ANSWER_8: 'In this case go to "Services", look for "Multisignature and Multi-User Accounts" and click on "Sign multisig transactions".', FAQ_QUESTION_9: 'What are the best security practices ?', @@ -998,7 +1002,40 @@ function GermanProvider($translateProvider) { POST_OPTIN_CONFIRM_MODAL_CHECKBOX: "I confirm that the Symbol destination address matches my Symbol account address", POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Please verify that the Symbol destination address is your Symbol multisig account address. If it doesn’t match, please start the process again and provide a valid Symbol public key of the multisig account.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "I confirm that the Symbol destination address matches the Symbol multisig account address", - OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.' + OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.', + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'NEM SUPERNODE PROGRAM', + ACCOUNT_NAME: 'Account', + BALANCE_NAME: 'Balance', + TAB_STATUS_NAME: 'Status', + TAB_ENROLL_IN_PROGRAM_NAME: 'Enroll in Program', + TAB_PAYOUT_HISTORY_NAME: 'Payout History', + + STATUS_NODE_NAME: 'Node Name', + STATUS_NAME: 'Status', + STATUS_ACTIVE_NAME: 'Active', + STATUS_INACTIVE_NAME: 'Inactive', + STATUS_PUBLIC_KEY_NAME: 'Public Key', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Remote Public Key', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Last Payout Round', + STATUS_TOTAL_REWARDS_NAME: 'Total Rewards', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Enroll Address', + ENROLL_IN_PROGRAM_NODE_HOST: 'Node Host', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Enroll in Program', + + PAYOUT_HISTORY_FROM_NAME: 'From Round', + PAYOUT_HISTORY_TO_NAME: 'To Round', + PAYOUT_HISTORY_AMOUNT_NAME: 'Amount', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Transaction Hash', + PAYOUT_HISTORY_DATE_NAME: 'Date', + + INVALID_ENROLLMENT_ADDRESS: 'Invalid enrollment address.', + ADDRESS_ENROLLED: 'Current address already enrolled to this period.', + INVALID_CODEWORD_HASH: 'Invalid codeword hash.', + ACCOUNT_MISSING_PUBLICKEY: 'You need to make a transaction to get a public key.', + INVALID_FORMAT_NODE_HOST: 'Protocol (http://) and port (:7890) are not allowed.', }); } diff --git a/src/app/modules/languages/en.js b/src/app/modules/languages/en.js index 682b8d34..86a0b0f7 100644 --- a/src/app/modules/languages/en.js +++ b/src/app/modules/languages/en.js @@ -403,6 +403,10 @@ function EnglishProvider($translateProvider) { PORTAL_ADDRESS_BOOK_BTN: 'Manage address book', PORTAL_INVOICE_TEXT: 'Create an invoice to share via QR code', PORTAL_SIGNED_MSG_TEXT: 'Create and verify signed messages to authenticate account ownership without transacting.', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'SuperNode Program', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Enroll in the SuperNode Program to earn $XEM for securing the network. Minimum 10,000 XEM required.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Read more here.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Check & Enroll in Program', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Address book', @@ -729,7 +733,7 @@ function EnglishProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Official website', FAQ_ANSWER_6_BTT: 'Official BitcoinTalk thread', FAQ_QUESTION_7: 'Nothing is shown on the dashboard', - FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernodes.nem.io has a lot of nodes that you can use.', + FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernode has a lot of nodes that you can use.', FAQ_QUESTION_8: 'Cosignatories cannot see the transaction to sign', FAQ_ANSWER_8: 'In this case go to "Services", look for "Multisignature and Multi-User Accounts" and click on "Sign multisig transactions".', FAQ_QUESTION_9: 'What are the best security practices ?', @@ -1048,7 +1052,40 @@ function EnglishProvider($translateProvider) { POST_OPTIN_CONFIRM_MODAL_CHECKBOX: "I confirm that the Symbol destination address matches my Symbol account address", POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Please verify that the Symbol destination address is your Symbol multisig account address. If it doesn’t match, please start the process again and provide a valid Symbol public key of the multisig account.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "I confirm that the Symbol destination address matches the Symbol multisig account address", - OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.' + OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.', + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'NEM SUPERNODE PROGRAM', + ACCOUNT_NAME: 'Account', + BALANCE_NAME: 'Balance', + TAB_STATUS_NAME: 'Status', + TAB_ENROLL_IN_PROGRAM_NAME: 'Enroll in Program', + TAB_PAYOUT_HISTORY_NAME: 'Payout History', + + STATUS_NODE_NAME: 'Node Name', + STATUS_NAME: 'Status', + STATUS_ACTIVE_NAME: 'Active', + STATUS_INACTIVE_NAME: 'Inactive', + STATUS_PUBLIC_KEY_NAME: 'Public Key', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Remote Public Key', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Last Payout Round', + STATUS_TOTAL_REWARDS_NAME: 'Total Rewards', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Enroll Address', + ENROLL_IN_PROGRAM_NODE_HOST: 'Node Host', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Enroll in Program', + + PAYOUT_HISTORY_FROM_NAME: 'From Round', + PAYOUT_HISTORY_TO_NAME: 'To Round', + PAYOUT_HISTORY_AMOUNT_NAME: 'Amount', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Transaction Hash', + PAYOUT_HISTORY_DATE_NAME: 'Date', + + INVALID_ENROLLMENT_ADDRESS: 'Invalid enrollment address.', + ADDRESS_ENROLLED: 'Current address already enrolled to this period.', + INVALID_CODEWORD_HASH: 'Invalid codeword hash.', + ACCOUNT_MISSING_PUBLICKEY: 'You need to make a transaction to get a public key.', + INVALID_FORMAT_NODE_HOST: 'Protocol (http://) and port (:7890) are not allowed.', }); } diff --git a/src/app/modules/languages/es.js b/src/app/modules/languages/es.js index a6a9d877..d48d3334 100644 --- a/src/app/modules/languages/es.js +++ b/src/app/modules/languages/es.js @@ -396,6 +396,10 @@ function SpanishProvider($translateProvider) { PORTAL_ADDRESS_BOOK_TEXT: 'Asignar etiquetas a las direcciones para realizar un seguimiento de sus contactos fácilmente.', PORTAL_ADDRESS_BOOK_BTN: 'Administrar libreta de direcciones', PORTAL_INVOICE_TEXT: 'Crear una factura para compartir a través del código QR', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'PROGRAMA SUPERNODOS NEM', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Únete al programa SuperNodo para ganar $XEM por segurizar la red. Se requiere un mínimo de 10,000 XEM.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Leer más aquí.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Verifica e inscríbete en el programa', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Libreta de direcciones', @@ -715,7 +719,7 @@ function SpanishProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Sitio web oficial', FAQ_ANSWER_6_BTT: 'Hilo ficial de BitcoinTalk', FAQ_QUESTION_7: 'No se muestra nada en el escritorio', - FAQ_ANSWER_7: 'Por favor asegúrate de verificar el círculo del nodo en la barra de navegación superior.
El círculo rojo significa que la conexión al nodo falló.
Haz clic en "Nodo" y selecciona otro de la lista desplegable o usa un nodo personalizado.
Supernodes.nem.io tiene una lista de nodos que puedes usar.', + FAQ_ANSWER_7: 'Por favor asegúrate de verificar el círculo del nodo en la barra de navegación superior.
El círculo rojo significa que la conexión al nodo falló.
Haz clic en "Nodo" y selecciona otro de la lista desplegable o usa un nodo personalizado.
Supernode tiene una lista de nodos que puedes usar.', FAQ_QUESTION_8: 'Los cofirmantes no pueden ver la transacción para firmar', FAQ_ANSWER_8: 'En este caso ve a "Servicios", busca "Cuentas multifirma y multiusuario " y haz clic en "Firmar transacciones multifirma ".', FAQ_QUESTION_9: '¿Cuáles son las mejores prácticas de seguridad?', @@ -976,9 +980,42 @@ function SpanishProvider($translateProvider) { POST_OPTIN_CONFIRM_MODAL_TEXT: 'Por favor verifica que la dirección de Symbol destino es la misma dirección que tu cuenta de Symbol. Puedes encontrar la dirección de tu cuenta de Symbol en la página de inicio de tu billetera de Symbol. Si no son la misma, por favor empieza el proceso de nuevo y entra una llave publica de Symbol valida para tu cuenta!', POST_OPTIN_CONFIRM_MODAL_CHECKBOX: 'Confirmo que la dirección de Symbol destino es la misma que la dirección de mi cuenta de Symbol', OPTIN_NIS1_PUBLIC_KEY: 'La llave publica que has entrado es una llave de NIS1! Debes poner una llave publica de Symbol', - POST_OPTIN_ERROR_INVALID_KEY: 'Llave inválida', + POST_OPTIN_ERROR_INVALID_KEY: 'Llave inválida', POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Por favor verifica que la dirección destino de Symbol coincide con la de tu dirección multisig. Si no coinciden, por favor inicia el proceso de nuevo e introduce la llave pública válida de la cuenta multisig.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "Confirmo que la dirección destino de Symbol coincide con la dirección de mi cuenta multisig.", + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'PROGRAMA SUPERNODOS NEM', + ACCOUNT_NAME: 'Cuenta', + BALANCE_NAME: 'Balance', + TAB_STATUS_NAME: 'Estado', + TAB_ENROLL_IN_PROGRAM_NAME: 'Regístrate en el programa', + TAB_PAYOUT_HISTORY_NAME: 'Historial de pagos acreditados', + + STATUS_NODE_NAME: 'Nombre del nodo', + STATUS_NAME: 'Estado', + STATUS_ACTIVE_NAME: 'Activo', + STATUS_INACTIVE_NAME: 'Inactivo', + STATUS_PUBLIC_KEY_NAME: 'Llave pública', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Clave pública Remota', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Última Ronda de Pago', + STATUS_TOTAL_REWARDS_NAME: 'Recompensas Totales', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Dirección de Registro', + ENROLL_IN_PROGRAM_NODE_HOST: 'Host de Nodo', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Regístrate en el programa', + + PAYOUT_HISTORY_FROM_NAME: 'Desde Ronda', + PAYOUT_HISTORY_TO_NAME: 'A Ronda', + PAYOUT_HISTORY_AMOUNT_NAME: 'Monto', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Hash de transacción', + PAYOUT_HISTORY_DATE_NAME: 'Fecha', + + INVALID_ENROLLMENT_ADDRESS: 'Dirección de registro no válida.', + ADDRESS_ENROLLED: 'Dirección actual ya registrada para este período.', + INVALID_CODEWORD_HASH: 'Código de hash no válido.', + ACCOUNT_MISSING_PUBLICKEY: 'Necesitas hacer una transacción para obtener una clave pública.', + INVALID_FORMAT_NODE_HOST: 'El protocolo (http: //) y el puerto (: 7890) no están permitidos.', }); } diff --git a/src/app/modules/languages/it.js b/src/app/modules/languages/it.js index 7ae63626..f6cd08ca 100644 --- a/src/app/modules/languages/it.js +++ b/src/app/modules/languages/it.js @@ -396,6 +396,10 @@ function ItalianProvider($translateProvider) { PORTAL_ADDRESS_BOOK_TEXT: 'Associe nomes de etiqueta aos endereços para gerenciar mais facilmente os seus contatos.', PORTAL_ADDRESS_BOOK_BTN: 'Gestisci rubrica', PORTAL_INVOICE_TEXT: 'Crea una fattura da condividere con il codice QR', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'PROGRAMMA SUPERNODO', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Iscriviti al programma SuperNode per guadagnare $XEM quale ricompensa per aver contribuito a rendere più sicura la rete. È richiesto un minimo di 10.000 XEM.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Leggi di più qui.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Verifica e registrati nel programma', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: "Rubrica", @@ -715,7 +719,7 @@ function ItalianProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Sito ufficiale', FAQ_ANSWER_6_BTT: 'Argomento ufficiale su BitcoinTalk', FAQ_QUESTION_7: 'Nulla viene visualizzato sul pannello.', - FAQ_ANSWER_7: 'Si prega di controllare il cerchio del nodo nella barra di navigazione in alto.
Cerchio rosso indica che la connessione al nodo non è riuscita.
Fai clic sul "Nodo" e seleziona un altro dall"elenco o utilizza un nodo personalizzato.
Supernodes.nem.io ha un elenco di nodi che puoi utilizzare.', + FAQ_ANSWER_7: 'Si prega di controllare il cerchio del nodo nella barra di navigazione in alto.
Cerchio rosso indica che la connessione al nodo non è riuscita.
Fai clic sul "Nodo" e seleziona un altro dall"elenco o utilizza un nodo personalizzato.
Supernode ha un elenco di nodi che puoi utilizzare.', FAQ_QUESTION_8: 'I pignoratori non possono vedere la transazione da firmare.', FAQ_ANSWER_8: 'In questo caso, andare su "Servizi", cercare "Account multi-firma o multi-utente" e fare clic su "Firma transazioni multi-firma".', FAQ_QUESTION_9: 'Quali sono le migliori pratiche di sicurezza?', @@ -941,6 +945,39 @@ function ItalianProvider($translateProvider) { POST_OPTIN_ERROR_INVALID_KEY: ' Chiave non valida', POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Please verify that the Symbol destination address is your Symbol multisig account address. If it doesn’t match, please start the process again and provide a valid Symbol public key of the multisig account.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "I confirm that the Symbol destination address matches the Symbol multisig account address", + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'PROGRAMMA SUPERNODO NEM', + ACCOUNT_NAME: 'Conto', + BALANCE_NAME: 'Saldo', + TAB_STATUS_NAME: 'Stato', + TAB_ENROLL_IN_PROGRAM_NAME: 'Iscriviti al programma', + TAB_PAYOUT_HISTORY_NAME: 'Storico dei premi accreditati', + + STATUS_NODE_NAME: 'Nome del nodo', + STATUS_NAME: 'Stato', + STATUS_ACTIVE_NAME: 'Attivo', + STATUS_INACTIVE_NAME: 'Non Attivo', + STATUS_PUBLIC_KEY_NAME: 'Chiave Pubblica', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Chiave Pubblica Remota', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Ultimo Round di Pagamento', + STATUS_TOTAL_REWARDS_NAME: 'Ricompense Totali', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Indirizzo di Registrazione', + ENROLL_IN_PROGRAM_NODE_HOST: 'Host del nodo', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Iscriviti al programma', + + PAYOUT_HISTORY_FROM_NAME: 'Dal Round', + PAYOUT_HISTORY_TO_NAME: 'Al Round', + PAYOUT_HISTORY_AMOUNT_NAME: 'Ammontare', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Hash della Transazione', + PAYOUT_HISTORY_DATE_NAME: 'Data', + + INVALID_ENROLLMENT_ADDRESS: 'Indirizzo di registrazione non valido.', + ADDRESS_ENROLLED: 'Indirizzo attuale già iscritto a questo periodo.', + INVALID_CODEWORD_HASH: 'Hash della parola di codice non valido.', + ACCOUNT_MISSING_PUBLICKEY: 'Devi effettuare una transazione per ottenere una chiave pubblica.', + INVALID_FORMAT_NODE_HOST: 'Il protocollo (http://) e la porta (:7890) non sono consentiti.', }); } diff --git a/src/app/modules/languages/jp.js b/src/app/modules/languages/jp.js index b544ab68..0d6ce69b 100644 --- a/src/app/modules/languages/jp.js +++ b/src/app/modules/languages/jp.js @@ -404,6 +404,10 @@ function JapaneseProvider($translateProvider) { PORTAL_ADDRESS_BOOK_BTN: 'アドレス帳の管理', PORTAL_INVOICE_TEXT: '共有するためのQRコード請求書を作成します', PORTAL_SIGNED_MSG_TEXT: 'トランザクションなしにアカウントの所有認証をするための署名済みメッセージの作成と検証を行います。', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'NEMスーパノードプログラム', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'スーパーノードプログラムに登録してネットワークの安全性を確保に貢献することで$XEMを獲得することができます。最低10,000XEMが必要です.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'より詳しくはこちらをお読みください.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: '確認してプログラムに参加する', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'アドレス帳', @@ -734,7 +738,7 @@ function JapaneseProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: "公式ウェブサイト", FAQ_ANSWER_6_BTT: "公式 BitcoinTalk スレッド", FAQ_QUESTION_7: 'ダッシュボードに何も表示されません', - FAQ_ANSWER_7: 'トップのナビゲーションバーにあるノードの円アイコンを確認してください。
赤い円はノードへの接続に失敗していることを表します。
"ノード"をクリックし、ほかのノードをドロップダウンリストから選択するかカスタムノードを指定してください。
Supernodes.nem.ioに利用可能なノードが掲載されています。', + FAQ_ANSWER_7: 'トップのナビゲーションバーにあるノードの円アイコンを確認してください。
赤い円はノードへの接続に失敗していることを表します。
"ノード"をクリックし、ほかのノードをドロップダウンリストから選択するかカスタムノードを指定してください。
Supernodeに利用可能なノードが掲載されています。', FAQ_QUESTION_8: '連署者に署名してほしいトランザクション表示されません。', FAQ_ANSWER_8: 'この場合、"サービス"へ移動し、"マルチシグおよびマルチユーザーアカウント"の項目から"マルチシグトランザクションの署名"を選択してください。', FAQ_QUESTION_9: '最適なセキュリティは?', @@ -1055,7 +1059,40 @@ function JapaneseProvider($translateProvider) { OPTIN_NIS1_PUBLIC_KEY: '一致しない場合は、再度手続きを開始し、あなたが所有するアカウントの有効なSymbol公開鍵を入力してください。"', POST_OPTIN_ERROR_INVALID_KEY: 'Symbolの送付先アドレスが、私のSymbolアカウントのアドレスと一致していることを確認します。', POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: 'Symbolの送信先アドレスが、Symbolのマルチシグアカウントのアドレスであることを確認してください。一致しない場合は、もう一度やり直してください。マルチシグアカウントの有効な公開鍵を入力してください。', - POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: 'Symbolの送付先アドレスが、Symbolのマルチシグアカウントのアドレスと一致することを確認しました。' + POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: 'Symbolの送付先アドレスが、Symbolのマルチシグアカウントのアドレスと一致することを確認しました。', + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'NEMスーパノードプログラム', + ACCOUNT_NAME: 'アカウント', + BALANCE_NAME: '残高', + TAB_STATUS_NAME: '状態', + TAB_ENROLL_IN_PROGRAM_NAME: 'プログラムへの参加', + TAB_PAYOUT_HISTORY_NAME: '支払履歴', + + STATUS_NODE_NAME: 'ノード名', + STATUS_NAME: '状態', + STATUS_ACTIVE_NAME: 'アクティブ', + STATUS_INACTIVE_NAME: '非アクティブ', + STATUS_PUBLIC_KEY_NAME: 'パブリックキー', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'リモートパブリックキー', + STATUS_LAST_PAYOUT_ROUND_NAME: '最新支払ラウンド', + STATUS_TOTAL_REWARDS_NAME: 'トータル履歴', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: '参加アドレス', + ENROLL_IN_PROGRAM_NODE_HOST: 'ノード主', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'プログラムへの参加', + + PAYOUT_HISTORY_FROM_NAME: '開始ラウンド', + PAYOUT_HISTORY_TO_NAME: '到達ラウンド', + PAYOUT_HISTORY_AMOUNT_NAME: '総量', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'トランザクションハッシユ', + PAYOUT_HISTORY_DATE_NAME: '日付', + + INVALID_ENROLLMENT_ADDRESS: '無効登録アドレス.', + ADDRESS_ENROLLED: 'このアドレスは既に登録されています.', + INVALID_CODEWORD_HASH: '無効なハッシユ符号.', + ACCOUNT_MISSING_PUBLICKEY: '公開鍵の取得にはトランザクションを発生させる必要があります.', + INVALID_FORMAT_NODE_HOST: 'プロトコル(http://)、ポート(:7890)は使用不可.', }); } diff --git a/src/app/modules/languages/nl.js b/src/app/modules/languages/nl.js index 4b755274..69014a83 100644 --- a/src/app/modules/languages/nl.js +++ b/src/app/modules/languages/nl.js @@ -395,11 +395,15 @@ function DutchProvider($translateProvider) { PORTAL_APOSTILLE_TITLE: 'Apostille', PORTAL_APOSTILLE_TEXT: 'Gebruik de NEM Apostille-dienst om blockchain gebaseerde notariële aktes te maken met een tijdstempel en het volgen en controleren van de bestandsechtheid.', PORTAL_APOSTILLE_BTN_1: 'Creëer', - PORTAL_APOSTILLE_BTN_2: 'Verifieer', + PORTAL_APOSTILLE_BTN_2: 'Verifieer', PORTAL_ADDRESS_BOOK_TEXT: 'Ken labels toe aan adressen om je contacten gemakkelijk bij te houden.', PORTAL_ADDRESS_BOOK_BTN: 'Beheer adressenboek', PORTAL_INVOICE_TEXT: 'Creëer een factuur om via QR code te delen', - PORTAL_SIGNED_MSG_TEXT: 'Creëer en verifieer een ondertekend bericht om het accounteigendom te verifiëren zonder transacties uit te voeren.', + PORTAL_SIGNED_MSG_TEXT: 'Creëer en verifieer een ondertekend bericht om het accounteigendom te verifiëren zonder transacties uit te voeren.', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'SuperNode Program', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Enroll in the SuperNode Program to earn $XEM for securing the network. Minimum 10,000 XEM required.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Read more here.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Check & Enroll in Program', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Adressenboek', @@ -725,7 +729,7 @@ function DutchProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Officiële website', FAQ_ANSWER_6_BTT: 'Officiële BitcoinTalk thread', FAQ_QUESTION_7: 'Er is niets weergegeven op het dashboard', - FAQ_ANSWER_7: 'Controleer de node-circel in de navigatiebar.
Rood betekent dat de connectie met de node is mislukt.
Klik op "Node" en selcteer een andere van de lijst of gebruik een aangepaste node.
Supernodes.nem.io bevat een lijst met nodes die je kunt gebruiken.', + FAQ_ANSWER_7: 'Controleer de node-circel in de navigatiebar.
Rood betekent dat de connectie met de node is mislukt.
Klik op "Node" en selcteer een andere van de lijst of gebruik een aangepaste node.
Supernode bevat een lijst met nodes die je kunt gebruiken.', FAQ_QUESTION_8: 'Mede-ondertekenaars kunnen de transactie die getekend moet worden niet zien', FAQ_ANSWER_8: 'Ga in dit geval naar "Diensten", ga naar "Multi-handtekening en multi-gebruiker accounts" en klik op "Teken een multi-handtekening transactie".', FAQ_QUESTION_9: 'Wat zijn de beste veiligheidsoverwegingen?', @@ -1004,7 +1008,40 @@ function DutchProvider($translateProvider) { POST_OPTIN_CONFIRM_MODAL_CHECKBOX: "I confirm that the Symbol destination address matches my Symbol account address", POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Please verify that the Symbol destination address is your Symbol multisig account address. If it doesn’t match, please start the process again and provide a valid Symbol public key of the multisig account.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "I confirm that the Symbol destination address matches the Symbol multisig account address", - OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.' + OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.', + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'NEM SUPERNODE PROGRAM', + ACCOUNT_NAME: 'Account', + BALANCE_NAME: 'Balance', + TAB_STATUS_NAME: 'Status', + TAB_ENROLL_IN_PROGRAM_NAME: 'Enroll in Program', + TAB_PAYOUT_HISTORY_NAME: 'Payout History', + + STATUS_NODE_NAME: 'Node Name', + STATUS_NAME: 'Status', + STATUS_ACTIVE_NAME: 'Active', + STATUS_INACTIVE_NAME: 'Inactive', + STATUS_PUBLIC_KEY_NAME: 'Public Key', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Remote Public Key', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Last Payout Round', + STATUS_TOTAL_REWARDS_NAME: 'Total Rewards', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Enroll Address', + ENROLL_IN_PROGRAM_NODE_HOST: 'Node Host', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Enroll in Program', + + PAYOUT_HISTORY_FROM_NAME: 'From Round', + PAYOUT_HISTORY_TO_NAME: 'To Round', + PAYOUT_HISTORY_AMOUNT_NAME: 'Amount', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Transaction Hash', + PAYOUT_HISTORY_DATE_NAME: 'Date', + + INVALID_ENROLLMENT_ADDRESS: 'Invalid enrollment address.', + ADDRESS_ENROLLED: 'Current address already enrolled to this period.', + INVALID_CODEWORD_HASH: 'Invalid codeword hash.', + ACCOUNT_MISSING_PUBLICKEY: 'You need to make a transaction to get a public key.', + INVALID_FORMAT_NODE_HOST: 'Protocol (http://) and port (:7890) are not allowed.', }); } diff --git a/src/app/modules/languages/pl.js b/src/app/modules/languages/pl.js index 28441080..5a716c5f 100644 --- a/src/app/modules/languages/pl.js +++ b/src/app/modules/languages/pl.js @@ -399,6 +399,10 @@ function PolishProvider($translateProvider) { PORTAL_ADDRESS_BOOK_TEXT: 'Przydziel etykiety do adresu\' by prosto zarządzać swoimi kontaktami.', PORTAL_ADDRESS_BOOK_BTN: 'Zarządzaj książką adresową', PORTAL_INVOICE_TEXT: 'Create an invoice to share via QR code', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'PROGRAM NEM SUPERNODE', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Przystąp do Programu dla SuperNode\'ów aby zyskać $XEM w zamian za zabezpieczenie sieci. Wymagane minimum 10,000 XEM.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Czytaj więcej tutaj.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Sprawdź i przystąp do Programu', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Książka adresowa', @@ -720,7 +724,7 @@ function PolishProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Oficjalna strona', FAQ_ANSWER_6_BTT: 'Oficjalny wątek BitcoinTalk', FAQ_QUESTION_7: 'Nothing is shown on the dashboard', - FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernodes.nem.io has a lot of nodes that you can use.', + FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernode has a lot of nodes that you can use.', FAQ_QUESTION_8: 'Cosignatories cannot see the transaction to sign', FAQ_ANSWER_8: 'In this case go to "Services", look for "Multisignature and Multi-User Accounts" and click on "Sign multisig transactions".', FAQ_QUESTION_9: 'What are the best security practices ?', @@ -1048,6 +1052,39 @@ function PolishProvider($translateProvider) { POST_OPTIN_ERROR_INVALID_KEY: 'Niepoprawny klucz', POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Zweryfikuj, że zaprezentowany docelowy adres Symbol jest Twoim adresem z multipodpisem. Jeśli adresy nie są takie same zacznij proces od początku i wprowadź poprawny klucz publiczny Twojego konta Symbol z multipodpisem.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "Potwierdzam, że prezentowany adres do wypłaty w sieci Symbol zgadza się z moim adresem konta Symbol z multipodpisem.", + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'PROGRAM NEM SUPERNODE', + ACCOUNT_NAME: 'Konto', + BALANCE_NAME: 'Saldo', + TAB_STATUS_NAME: 'Status', + TAB_ENROLL_IN_PROGRAM_NAME: 'Przystąp do Programu', + TAB_PAYOUT_HISTORY_NAME: 'Historia płatności', + + STATUS_NODE_NAME: 'Nazwa serwera', + STATUS_NAME: 'Status', + STATUS_ACTIVE_NAME: 'Aktywny', + STATUS_INACTIVE_NAME: 'Nieaktywny', + STATUS_PUBLIC_KEY_NAME: 'Klucz publiczny', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Zdalny klucz publiczny', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Ostatnia runda wypłat', + STATUS_TOTAL_REWARDS_NAME: 'Suma nagród', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Adres do zapisów', + ENROLL_IN_PROGRAM_NODE_HOST: 'Host serwera', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Przystąp do Programu', + + PAYOUT_HISTORY_FROM_NAME: 'Od rundy', + PAYOUT_HISTORY_TO_NAME: 'Do rundy', + PAYOUT_HISTORY_AMOUNT_NAME: 'Kwota', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Hasz transakcji', + PAYOUT_HISTORY_DATE_NAME: 'Data', + + INVALID_ENROLLMENT_ADDRESS: 'Niepoprawny adres do zapisów.', + ADDRESS_ENROLLED: 'Wybrany adres już zapisał się w obecnym okresie.', + INVALID_CODEWORD_HASH: 'Niepoprawny hash słowa kodowego.', + ACCOUNT_MISSING_PUBLICKEY: 'Musisz zrobić transakcje wychodzącą aby uzyskać klucz publiczny.', + INVALID_FORMAT_NODE_HOST: 'Protokół (http://) i port (:7890) nie są dozwolone.', }); } diff --git a/src/app/modules/languages/ptbr.js b/src/app/modules/languages/ptbr.js index 517a5f61..3f74c23d 100644 --- a/src/app/modules/languages/ptbr.js +++ b/src/app/modules/languages/ptbr.js @@ -396,6 +396,10 @@ function PortugueseBRProvider($translateProvider) { PORTAL_ADDRESS_BOOK_TEXT: 'Associe nomes de etiqueta aos endereços para gerenciar mais facilmente os seus contatos.', PORTAL_ADDRESS_BOOK_BTN: 'Gerenciar caderno de contatos', PORTAL_INVOICE_TEXT: 'Criar uma fatura para compartilhar através de QR code', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'SuperNode Program', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Enroll in the SuperNode Program to earn $XEM for securing the network. Minimum 10,000 XEM required.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Read more here.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Check & Enroll in Program', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Caderno de contatos', @@ -715,7 +719,7 @@ function PortugueseBRProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Website oficial', FAQ_ANSWER_6_BTT: 'Assunto oficial na BitcoinTalk', FAQ_QUESTION_7: 'Nada é exibido no painel.', - FAQ_ANSWER_7: 'Por favor, verifique o círculo do nodo na barra de navegação do topo.
Círculo vermelho significa que a conexão com o nodo falhou.
Clique no "Nodo" e selecione outro da lista ou use um nodo customizado.
Supernodes.nem.io possui uma lista de nodos que você pode utilizar.', + FAQ_ANSWER_7: 'Por favor, verifique o círculo do nodo na barra de navegação do topo.
Círculo vermelho significa que a conexão com o nodo falhou.
Clique no "Nodo" e selecione outro da lista ou use um nodo customizado.
Supernode possui uma lista de nodos que você pode utilizar.', FAQ_QUESTION_8: 'Cosignatários não podem ver a transação para assinar.', FAQ_ANSWER_8: 'Neste caso vá para "Serviços", procure por "Contas Multiassinatura ou Multiusuários" e clique em "Assinar transações multiassinatura".', FAQ_QUESTION_9: 'Quais são as melhores práticas de segurança?', @@ -993,8 +997,40 @@ function PortugueseBRProvider($translateProvider) { POST_OPTIN_CONFIRM_MODAL_CHECKBOX: "I confirm that the Symbol destination address matches my Symbol account address", POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Please verify that the Symbol destination address is your Symbol multisig account address. If it doesn’t match, please start the process again and provide a valid Symbol public key of the multisig account.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "I confirm that the Symbol destination address matches the Symbol multisig account address", - OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.' + OPTIN_NIS1_PUBLIC_KEY: 'The public key you entered is a NIS1 key! You need to enter a Symbol public key.', + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'NEM SUPERNODE PROGRAM', + ACCOUNT_NAME: 'Account', + BALANCE_NAME: 'Balance', + TAB_STATUS_NAME: 'Status', + TAB_ENROLL_IN_PROGRAM_NAME: 'Enroll in Program', + TAB_PAYOUT_HISTORY_NAME: 'Payout History', + + STATUS_NODE_NAME: 'Node Name', + STATUS_NAME: 'Status', + STATUS_ACTIVE_NAME: 'Active', + STATUS_INACTIVE_NAME: 'Inactive', + STATUS_PUBLIC_KEY_NAME: 'Public Key', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Remote Public Key', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Last Payout Round', + STATUS_TOTAL_REWARDS_NAME: 'Total Rewards', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Enroll Address', + ENROLL_IN_PROGRAM_NODE_HOST: 'Node Host', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Enroll in Program', + + PAYOUT_HISTORY_FROM_NAME: 'From Round', + PAYOUT_HISTORY_TO_NAME: 'To Round', + PAYOUT_HISTORY_AMOUNT_NAME: 'Amount', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Transaction Hash', + PAYOUT_HISTORY_DATE_NAME: 'Date', + + INVALID_ENROLLMENT_ADDRESS: 'Invalid enrollment address.', + ADDRESS_ENROLLED: 'Current address already enrolled to this period.', + INVALID_CODEWORD_HASH: 'Invalid codeword hash.', + ACCOUNT_MISSING_PUBLICKEY: 'You need to make a transaction to get a public key.', + INVALID_FORMAT_NODE_HOST: 'Protocol (http://) and port (:7890) are not allowed.', }); } diff --git a/src/app/modules/languages/ru.js b/src/app/modules/languages/ru.js index bd0c9f2d..1a63396c 100644 --- a/src/app/modules/languages/ru.js +++ b/src/app/modules/languages/ru.js @@ -397,6 +397,10 @@ function RussianProvider($translateProvider) { PORTAL_ADDRESS_BOOK_TEXT: 'Назначьте метки адресам, чтобы легко отслеживать контакты.', PORTAL_ADDRESS_BOOK_BTN: 'Управление адресной книгой', PORTAL_INVOICE_TEXT: 'Create an invoice to share via QR code', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'ПРОГРАММА СУПЕРНОД NEM', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Присоединяйтесь к программе SuperNode, чтобы получить $XEM в обмен на защиту вашей сети. Требуется минимум 10 000 XEM.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Подробнее здесь.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Проверить и зарегистрироваться в программе', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Адресная книга', @@ -718,7 +722,7 @@ function RussianProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Official website', FAQ_ANSWER_6_BTT: 'Official BitcoinTalk thread', FAQ_QUESTION_7: 'Nothing is shown on the dashboard', - FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernodes.nem.io has a lot of nodes that you can use.', + FAQ_ANSWER_7: 'Please be sure to check the node circle in the top navigation bar.
Red circle means that connection to the node failed.
Click on "Node" and select another one from the dropdown list or use a custom node.
Supernode has a lot of nodes that you can use.', FAQ_QUESTION_8: 'Cosignatories cannot see the transaction to sign', FAQ_ANSWER_8: 'In this case go to "Services", look for "Multisignature and Multi-User Accounts" and click on "Sign multisig transactions".', FAQ_QUESTION_9: 'What are the best security practices ?', @@ -947,6 +951,39 @@ function RussianProvider($translateProvider) { POST_OPTIN_ERROR_INVALID_KEY: 'Неправильный ключ', POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Убедитесь, что в качестве адреса назначения Symbol указан адрес вашего мультисиг аккаунта Symbol. Если он не совпадает, начните процесс снова и предоставьте корректный публичный ключ от мультисиг аккаунта Symbol.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "Я подтверждаю, что адрес назначения совпадает с адресом мультисиг аккаунта Symbol", + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'ПРОГРАММА СУПЕРНОД NEM', + ACCOUNT_NAME: 'Аккаунт', + BALANCE_NAME: 'Баланс', + TAB_STATUS_NAME: 'Статус', + TAB_ENROLL_IN_PROGRAM_NAME: 'Зарегистрироваться в программе', + TAB_PAYOUT_HISTORY_NAME: 'История выплат', + + STATUS_NODE_NAME: 'Имя ноды', + STATUS_NAME: 'Статус', + STATUS_ACTIVE_NAME: 'Активный', + STATUS_INACTIVE_NAME: 'Неактивный', + STATUS_PUBLIC_KEY_NAME: 'Публичный ключ', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Удаленный публичный ключ', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Последний раунд выплат', + STATUS_TOTAL_REWARDS_NAME: 'Сумма вознаграждений', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Адрес регистрации', + ENROLL_IN_PROGRAM_NODE_HOST: 'Хост ноды', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Зарегистрироваться в программе', + + PAYOUT_HISTORY_FROM_NAME: 'От раунда', + PAYOUT_HISTORY_TO_NAME: 'В раунд', + PAYOUT_HISTORY_AMOUNT_NAME: 'Сумма', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Хэш транзакции', + PAYOUT_HISTORY_DATE_NAME: 'Дата', + + INVALID_ENROLLMENT_ADDRESS: 'Недействительный адрес регистрации.', + ADDRESS_ENROLLED: 'Текущий адрес уже зарегистрирован на этот период.', + INVALID_CODEWORD_HASH: 'Недействительный хэш кодового слова.', + ACCOUNT_MISSING_PUBLICKEY: 'Вам нужно совершить хотя бы одну транзакцию, чтобы получить публичный ключ.', + INVALID_FORMAT_NODE_HOST: 'Протокол (http://) и порт (:7890) запрещены.', }); } diff --git a/src/app/modules/languages/uk.js b/src/app/modules/languages/uk.js index c6bf2e1b..f54c1a02 100644 --- a/src/app/modules/languages/uk.js +++ b/src/app/modules/languages/uk.js @@ -400,6 +400,10 @@ function UkrainianProvider($translateProvider) { PORTAL_ADDRESS_BOOK_BTN: 'Управління адресною книгою', PORTAL_INVOICE_TEXT: 'Створити рахунок-фактуру для передачі за допомогою QR коду', PORTAL_SIGNED_MSG_TEXT: 'Створити та підтвердити підписані повідомлення для автентифікації права власності на обліковий запис без здійснення транзакції.', + PORTAL_SUPER_NODE_PROGRAM_TITLE: 'ПРОГРАМА СУПЕРНОД NEM', + PORTAL_SUPER_NODE_PROGRAM_TEXT: 'Зареєструйтеся в програмі SuperNode і заробляйте $XEM, поліпшуючи безпеку мережі. Для реєстрації необхідно мінімум 10 000 XEM.', + PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK: 'Детальніше тут.', + PORTAL_SUPER_NODE_PROGRAM_BTN_1: 'Перевірити та зареєструватися в програмі.', // ADDRESS BOOK MODULE ADDRESS_BOOK_TITLE: 'Адресна книга', @@ -726,7 +730,7 @@ function UkrainianProvider($translateProvider) { FAQ_ANSWER_6_WEBSITE: 'Офіційному вебсайті', FAQ_ANSWER_6_BTT: 'Офіційній сторінці на BitcoinTalk', FAQ_QUESTION_7: 'На інформаційній панелі нічого не відображається', - FAQ_ANSWER_7: 'Будь-ласка, перевірте колір кола, яке знаходиться на верхній навігаційній панелі та відображає стан вузла.
Червоне коло означає, що немає підключення до вузла.
Натисніть на кнопку "Вузол" та виберіть інший з випадаючого списку, або вкажіть свій вузол.
Supernodes.nem.io містить перелік вузлів, які Ви можете використовувати.', + FAQ_ANSWER_7: 'Будь-ласка, перевірте колір кола, яке знаходиться на верхній навігаційній панелі та відображає стан вузла.
Червоне коло означає, що немає підключення до вузла.
Натисніть на кнопку "Вузол" та виберіть інший з випадаючого списку, або вкажіть свій вузол.
Supernode містить перелік вузлів, які Ви можете використовувати.', FAQ_QUESTION_8: 'У підписантів не відображається транзакція, яку їм треба підписати', FAQ_ANSWER_8: 'У цьому випадку треба перейти до меню "Сервіси", знайти розділ "Мультипідписи та Багатокористувацькі облікові записи" та натиснути на "Підписати мультипідписні транзакції".', FAQ_QUESTION_9: 'Які найкращі практики з безпеки ?', @@ -1047,6 +1051,39 @@ function UkrainianProvider($translateProvider) { POST_OPTIN_ERROR_INVALID_KEY: 'Неправильний ключ', POST_OPTIN_CONFIRM_MODAL_TEXT_MULTISIG: "Переконайтесь, що адреса призначення Symbol - це адреса вашого мультисиг акаунту Symbol. Якщо вони не збігаються, будь ласка, почніть процес знову та надайте дійсний відкритий ключ мультисиг акаунту Symbol.", POST_OPTIN_CONFIRM_MODAL_CHECKBOX_MULTISIG: "Я підтверджую, що адреса призначення співпадає з адресою мультисиг акаунту Symbol", + + // SUPERNODE PROGRAM MODULE + SUPER_NODE_PROGRAM_TITLE: 'ПРОГРАМА СУПЕРНОД NEM', + ACCOUNT_NAME: 'Акаунт', + BALANCE_NAME: 'Баланс', + TAB_STATUS_NAME: 'Статус', + TAB_ENROLL_IN_PROGRAM_NAME: 'Зареєструватися у програмі', + TAB_PAYOUT_HISTORY_NAME: 'Історія виплат', + + STATUS_NODE_NAME: 'Ім\'я ноди', + STATUS_NAME: 'Статус', + STATUS_ACTIVE_NAME: 'Активний', + STATUS_INACTIVE_NAME: 'Неактивний', + STATUS_PUBLIC_KEY_NAME: 'Відкритий (публічний) ключ', + STATUS_REMOTE_PUBLIC_KEY_NAME: 'Віддалений відкритий (публічний) ключ', + STATUS_LAST_PAYOUT_ROUND_NAME: 'Останній раунд виплат', + STATUS_TOTAL_REWARDS_NAME: 'Сума винагород', + + ENROLL_IN_PROGRAM_ADDRESS_NAME: 'Адреса реєстрації', + ENROLL_IN_PROGRAM_NODE_HOST: 'Хост ноди', + ENROLL_IN_PROGRAM_BUTTON_NAME: 'Зареєструватися у програмі', + + PAYOUT_HISTORY_FROM_NAME: 'Від раунда', + PAYOUT_HISTORY_TO_NAME: 'До раунда', + PAYOUT_HISTORY_AMOUNT_NAME: 'Сума', + PAYOUT_HISTORY_TRANSACTION_HASH_NAME: 'Хеш транзакції', + PAYOUT_HISTORY_DATE_NAME: 'Дата', + + INVALID_ENROLLMENT_ADDRESS: 'Недійсна адреса реєстрації.', + ADDRESS_ENROLLED: 'Поточна адреса вже зареєстрована на цей період.', + INVALID_CODEWORD_HASH: 'Недійсний хеш кодового слова.', + ACCOUNT_MISSING_PUBLICKEY: 'Вам потрібно здійснити хоча б одну транзакцію, щоб отримати відкритий (публічний) ключ.', + INVALID_FORMAT_NODE_HOST: 'Протокол (http://) і порт (:7890) заборонені.', }); } diff --git a/src/app/modules/portal/portal.html b/src/app/modules/portal/portal.html index e7543377..a9f2b7ac 100644 --- a/src/app/modules/portal/portal.html +++ b/src/app/modules/portal/portal.html @@ -117,18 +117,17 @@
{{ 'PORTAL_MOSAIC_TITLE' | translate }}
- +
-
{{ 'PORTAL_CATAPULTOPTIN_TITLE' | translate }}
+
{{ 'PORTAL_SUPER_NODE_PROGRAM_TITLE' | translate }}
-

{{ 'PORTAL_CATAPULTOPTIN_TEXT' | translate }} {{ 'PORTAL_CATAPULTOPTIN_TEXT_LINK' | translate }}

+

{{ 'PORTAL_SUPER_NODE_PROGRAM_TEXT' | translate }} {{ 'PORTAL_SUPER_NODE_PROGRAM_TEXT_LINK' | translate }}

-

{{ 'PORTAL_CATAPULTOPTIN_MULTISIG_TEXT' | translate }}

- +
diff --git a/src/app/modules/superNodeProgram/index.js b/src/app/modules/superNodeProgram/index.js new file mode 100644 index 00000000..73731887 --- /dev/null +++ b/src/app/modules/superNodeProgram/index.js @@ -0,0 +1,14 @@ +import angular from 'angular'; + +// Create the module where our functionality can attach +const superNodeProgramModule = angular.module('app.superNodeProgram', []); + +// Include our UI-Router config settings +import superNodeProgramConfig from './superNodeProgram.config'; +superNodeProgramModule.config(superNodeProgramConfig); + +// Controllers +import superNodeProgramCtrl from './superNodeProgram.controller'; +superNodeProgramModule.controller('SuperNodeProgramCtrl', superNodeProgramCtrl); + +export default superNodeProgramModule; diff --git a/src/app/modules/superNodeProgram/superNodeProgram.config.js b/src/app/modules/superNodeProgram/superNodeProgram.config.js new file mode 100644 index 00000000..7ab35ade --- /dev/null +++ b/src/app/modules/superNodeProgram/superNodeProgram.config.js @@ -0,0 +1,14 @@ +function SuperNodeProgramConfig($stateProvider) { + 'ngInject'; + + $stateProvider + .state('app.superNodeProgram', { + url: '/superNodeProgram', + controller: 'SuperNodeProgramCtrl', + controllerAs: '$ctrl', + templateUrl: 'modules/superNodeProgram/superNodeProgram.html', + title: 'SuperNode Program' + }); +}; + +export default SuperNodeProgramConfig; diff --git a/src/app/modules/superNodeProgram/superNodeProgram.controller.js b/src/app/modules/superNodeProgram/superNodeProgram.controller.js new file mode 100644 index 00000000..95eca90d --- /dev/null +++ b/src/app/modules/superNodeProgram/superNodeProgram.controller.js @@ -0,0 +1,405 @@ +import nem from 'nem-sdk'; + +class SuperNodeProgramCtrl { + + /** + * Initialize dependencies and properties + * + * @params {services} - Angular services to inject + */ + constructor($filter, Wallet, DataStore, $timeout, Alert, SuperNodeProgram) { + 'ngInject'; + + //// Module dependencies region //// + + this._filter = $filter; + this._Wallet = Wallet; + this._DataStore = DataStore; + this._$timeout = $timeout; + this._Alert = Alert; + this._superNodeProgram = SuperNodeProgram; + + //// End dependencies region //// + + // Initialization + this.init(); + } + + //// Module methods region //// + + /** + * Initialize module properties + */ + init () { + // Form use for enroll program + this.formData = { + nodeHost: '', + enrollAddress: '', + isMultisig: false, + mainPublicKey: '', + }; + + this.tab = { + status: 'status', + enroll: 'enroll', + payout: 'payout' + }; + + this.tabs = [{ + key: this.tab.status, + name: 'TAB_STATUS_NAME', + }, { + key: this.tab.enroll, + name: 'TAB_ENROLL_IN_PROGRAM_NAME', + }, { + key: this.tab.payout, + name: 'TAB_PAYOUT_HISTORY_NAME', + }] + + this.selectedTab = this.tab.status; + this.okPressed = false; + this.common = nem.model.objects.get("common"); + this.preparedTransaction = {}; + + this.currentPage = 0; + this.lastPage = 0; + + this.getAccount(); + + this.resetTab(); + + this.prepareTransaction(); + } + + /** + * Initialize accounts and set the first one as selected + */ + getAccount() { + const { meta, account } = this._DataStore.account.metaData; + + const currentAccount = { + ...account, + display: account.address, + formatBalance: this.computeXEMBalance(account.balance) + } + + if (meta.cosignatoryOf.length > 0) { + const multisigAccounts = meta.cosignatoryOf.map(account => { + return { + ...account, + display: account.address + ' - Multisig', + formatBalance: this.computeXEMBalance(account.balance), + isMultisig: true + } + }); + + // Build for account dropdown list + this.accounts = [...multisigAccounts, currentAccount]; + + this.selectedAccount = this.accounts[0]; + } else { + this.selectedAccount = currentAccount; + } + + this.setSelectedAccountInfo(); + } + + /** + * Assign the selected account to the balance view and form data + */ + setSelectedAccountInfo() { + this.balance = this.selectedAccount.formatBalance; + this.formData.mainPublicKey = this.selectedAccount.publicKey; + this.formData.isMultisig = this.selectedAccount.isMultisig || false; + } + + /** + * Format absolute XEM balance to relative string + * @param {number} absoluteBalance + * @returns {string} Relative balance string + */ + computeXEMBalance(absoluteBalance) { + return this._filter("fmtNemValue")(absoluteBalance || 0)[0] + "." + this._filter("fmtNemValue")(absoluteBalance || 0)[1]; + } + + /** + * Verify current selected tab + * @param {'status'|'enroll'|'payout'} tab + * @returns {boolean} boolean + */ + isTabSelected(tab) { + return this.selectedTab === tab; + } + + /** + * Set tab + * @param {'status'|'enroll'|'payout'} tab + */ + setTab(tab) { + this.selectedTab = tab; + + switch (tab) { + case this.tab.status: + this.selectStatusTab(); + break; + case this.tab.enroll: + this.selectEnrollTab(); + break; + case this.tab.payout: + this.selectPayoutHistory(); + break; + } + } + + /** + * Reset current tab to status + */ + resetTab() { + this.setTab(this.tab.status); + } + + /** + * Switch account if account contain multisig + */ + onChangeAccount() { + this.setSelectedAccountInfo(); + this.resetTab(); + } + + /** + * Prepare the transaction + * @param {string} codewordHash + * @returns {object} transaction object + */ + prepareTransaction(codewordHash) { + let transferTransaction = nem.model.objects.get("transferTransaction"); + + // Set enroll address to recipient + transferTransaction.recipient = this.formData.enrollAddress.toUpperCase().replace(/-/g, ''); + + transferTransaction.amount = 0 + + transferTransaction.mosaics = null; + + // If codewordHash is not provided, use the default one to make sure we have the right message length + // to calculate transaction fees + transferTransaction.message = `enroll ${this.formData.nodeHost} ${codewordHash ? codewordHash :'0'.repeat(64)}`; + + // Set multisig, if selected + if (this.formData.isMultisig) { + transferTransaction.isMultisig = true; + transferTransaction.multisigAccount = this.selectedAccount; + } + + // Prepare transaction object according to transfer type + const entity = nem.model.transactions.prepare("transferTransaction")(this.common, transferTransaction, this._Wallet.network); + + // Set the entity for fees in view + this.preparedTransaction = entity; + + return entity; + } + + /** + * Select enroll status tab + */ + async selectStatusTab() { + this.enrollStatus = { + nodeName: '-', + status: false, + endpoint: '-', + publicKey: '-', + remotePublicKey: '-', + totalReward: '-', + lastPayoutRound: '-' + } + + if (!this.formData.mainPublicKey) { + return; + } + + try { + const result = await this._superNodeProgram.getNodeInfo(this.formData.mainPublicKey); + + this._$timeout(() => { + this.enrollStatus = { + nodeName: result.name, + status: result.status, + endpoint: new URL(result.endpoint).hostname || '' , + publicKey: result.mainPublicKey, + remotePublicKey: result.remotePublicKey, + totalReward: this.computeXEMBalance(result.totalRewardsEarned), + nodeId: result.id, + lastPayoutRound: result.lastPayoutRound === -1 ? "-" : result.lastPayoutRound, + } + + this.lastPage = Math.ceil(result.rewardCount/15); + }) + } catch (error) { + console.log(error); + } + } + + /** + * Announce enroll transaction + */ + async sendEnroll() { + this.okPressed = true; + + // Password validation + if (!this._Wallet.decrypt(this.common)) { + this.okPressed = false; + return; + } + + // When account does not have a public key in network + // generate public key from private key + if(!this.formData.mainPublicKey) { + if (this.common.isHW) { + this._Alert.accountHasNoPublicKey(); + this.okPressed = false; + return; + } else { + this.formData.mainPublicKey = new nem.crypto.keyPair.create(this.common.privateKey).publicKey.toString(); + } + } + + // Validation for domain: protocol and port not required + const regex = new RegExp(/((https?:\/\/))|((:\d{1,4}))/); + if (regex.test(this.formData.nodeHost)) { + this._Alert.invalidFormatNodeHost(); + this.okPressed = false; + return; + } + + // Enroll validation and request codeWord + const [isEnrolled, isEnrollmentAddressValid, codewordHash] = await Promise.all([ + this._superNodeProgram.checkEnrollmentStatus(this.formData.mainPublicKey), + this._superNodeProgram.checkEnrollmentAddress(this.formData.enrollAddress), + this._superNodeProgram.getCodewordHash(this.formData.mainPublicKey) + ]) + + const isEnrolledWithSameDomain = isEnrolled && this.formData.nodeHost === this.enrollStatus.endpoint; + + if (isEnrolledWithSameDomain) { + this._$timeout(() => { + this._Alert.addressEnrolled(); + this.okPressed = false; + return; + }) + } + + if (!isEnrollmentAddressValid) { + this._$timeout(() => { + this._Alert.invalidEnrollmentAddress(); + this.okPressed = false; + return; + }) + } + + if (codewordHash.length !== 64) { + this._$timeout(() => { + this._Alert.invalidCodewordHash(); + this.okPressed = false; + return; + }) + } + + if(!isEnrolledWithSameDomain && isEnrollmentAddressValid && 64 === codewordHash.length) { + // Prepare transaction for enrollment + const entity = this.prepareTransaction(codewordHash); + + try { + await this._Wallet.transact(this.common, entity); + + this._$timeout(() => { + // Enable send button + this.okPressed = false; + return; + }) + } catch (error) { + this._$timeout(() => { + // Delete private key in common + this.common.privateKey = ''; + // Enable send button + this.okPressed = false; + return; + }); + } + } + } + + /** + * Set enroll endpoint, when current account is enrolled + */ + selectEnrollTab() { + if (this.enrollStatus.endpoint !== '-') { + this.formData.nodeHost = this.enrollStatus.endpoint; + // Update Fee view + this.prepareTransaction(); + } + } + + /** + * Fetch first page of payout history + */ + fetchFirst() { + this.currentPage = 0; + this.selectPayoutHistory(); + } + + /** + * Fetch last page of payout history + */ + fetchLast() { + this.currentPage = this.lastPage; + this.selectPayoutHistory(); + } + + /** + * Fetch next page of payout history + */ + fetchNext() { + ++this.currentPage; + this.selectPayoutHistory(); + } + + /** + * Fetch previous page of payout history + */ + fetchPrevious() { + --this.currentPage; + this.selectPayoutHistory(); + } + + /** + * Fetch payout history records + */ + async selectPayoutHistory() { + this.payoutHistory = []; + + if (!this.enrollStatus.nodeId) { + return; + } + + try { + const payouts = await this._superNodeProgram.getNodePayouts(this.enrollStatus.nodeId, this.currentPage); + + this._$timeout(() => { + this.payoutHistory = payouts.map(item => { + return { + ...item, + amount: this.computeXEMBalance(item.amount), + timestamp: new Date(item.timestamp).toGMTString() + } + }) + }) + } catch (error) { + console.log(error); + } + } + + //// End methods region //// +} + +export default SuperNodeProgramCtrl; diff --git a/src/app/modules/superNodeProgram/superNodeProgram.html b/src/app/modules/superNodeProgram/superNodeProgram.html new file mode 100644 index 00000000..e171a48c --- /dev/null +++ b/src/app/modules/superNodeProgram/superNodeProgram.html @@ -0,0 +1,206 @@ +
+
+
+
+

{{ 'SUPER_NODE_PROGRAM_TITLE' | translate }}

+
+
+
+
+
+

{{ 'ACCOUNT_NAME' | translate }}

+
+ +
+ +
+
+ + + + + +
+
+ +
+
+ + + +
+ + +
+
+
+
+
+
+
+
+ + +
+ +
+ +
+
+ + + + +
+
+ +
+
+ + + + + {{ $ctrl.enrollStatus.status ? 'STATUS_ACTIVE_NAME' : 'STATUS_INACTIVE_NAME' | translate }} + +
+
+ +
+
+ + + + +
+
+ +
+
+ + + + +
+
+ +
+
+ + + + +
+
+ +
+
+ + + +
+ + +
+
+
+
+ + +
+ +
+
+ + + + +
+
+ +
+
+ + + + +
+
+ + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + +
{{ 'PAYOUT_HISTORY_FROM_NAME' | translate }}{{ 'PAYOUT_HISTORY_TO_NAME' | translate }}{{ 'PAYOUT_HISTORY_AMOUNT_NAME' | translate }}{{ 'PAYOUT_HISTORY_TRANSACTION_HASH_NAME' | translate }}{{ 'PAYOUT_HISTORY_DATE_NAME' | translate }}
{{ payout.fromRoundId }}{{ payout.toRoundId }}+ {{ payout.amount }} + {{payout.transactionHash}} + - + {{ payout.timestamp }}
+ +
+

{{ 'GENERAL_NO_RESULTS' | translate }}

+
+ +
+
+
+ + + {{$ctrl.currentPage + 1}}/{{$ctrl.lastPage + 1}} + + +
+
+
+
+
+
+
+
diff --git a/src/app/services/alert.service.js b/src/app/services/alert.service.js index 0c940459..b4b78514 100644 --- a/src/app/services/alert.service.js +++ b/src/app/services/alert.service.js @@ -651,6 +651,41 @@ export default class Alert { }); } + invalidEnrollmentAddress() { + this._ngToast.create({ + content: this._$filter('translate')('INVALID_ENROLLMENT_ADDRESS'), + className: 'danger' + }); + } + + addressEnrolled() { + this._ngToast.create({ + content: this._$filter('translate')('ADDRESS_ENROLLED'), + className: 'warning', + }); + } + + invalidCodewordHash() { + this._ngToast.create({ + content: this._$filter('translate')('INVALID_CODEWORD_HASH'), + className: 'danger', + }); + } + + accountHasNoPublicKey() { + this._ngToast.create({ + content: this._$filter("translate")("ACCOUNT_MISSING_PUBLICKEY"), + className: 'danger' + }); + } + + invalidFormatNodeHost() { + this._ngToast.create({ + content: this._$filter('translate')('INVALID_FORMAT_NODE_HOST'), + className: 'danger', + }); + } + /*** * Success alerts */ diff --git a/src/app/services/index.js b/src/app/services/index.js index 1700b9d8..4fd6ed27 100644 --- a/src/app/services/index.js +++ b/src/app/services/index.js @@ -55,4 +55,8 @@ servicesModule.service('Voting', VotingService); import CatapultOptinService from './catapultOptin.service' servicesModule.service('CatapultOptin', CatapultOptinService); +// Set SuperNode Program service +import SuperNodeProgramService from './superNodeProgram.service' +servicesModule.service('SuperNodeProgram', SuperNodeProgramService); + export default servicesModule; diff --git a/src/app/services/superNodeProgram.service.js b/src/app/services/superNodeProgram.service.js new file mode 100644 index 00000000..f15eb476 --- /dev/null +++ b/src/app/services/superNodeProgram.service.js @@ -0,0 +1,135 @@ +import nem from 'nem-sdk'; + +const SUPER_NODE_PROGRAM_API_BASE_URL = 'https://nem.io/supernode/api'; + +/** Service to enroll in the SuperNode Program and to get the status related information */ +class SuperNodeProgram { + + /** + * Initialize dependencies and properties. + * + * @params {services} - Angular services to inject + */ + constructor($localStorage, $filter, Wallet) { + 'ngInject'; + + // Service dependencies region // + + this._storage = $localStorage; + this._$filter = $filter; + this._Wallet = Wallet; + + // End dependencies region // + + // Service properties region // + + this.apiBaseUrl = SUPER_NODE_PROGRAM_API_BASE_URL; + this.common = nem.model.objects.get('common'); + + // End properties region // + } + + // Service methods region // + + /** + * Make get request. + * + * @param {string} url - URL of the resource to fetch. + * @param {string} errorMessage - Description of the error that will be thrown in case of an unsuccessful request. + * + * @return {Promise} - Response. + */ + async _get(url, errorMessage) { + const response = await fetch(url); + + if (!response.ok) { + throw Error(errorMessage); + } + + return response; + } + + /** + * Checks if an address is the current enrollment address. + * + * @param {string} address - Address to check. + * + * @return {Promise} - If the address to check matches the current enrollment address. + */ + async checkEnrollmentAddress(address) { + const response = await this._get(`${this.apiBaseUrl}/enrollment/check/address/${address}`, 'failed_to_validate_enroll_address'); + const status = await response.text(); + + return status === 'true'; + } + + /** + * Checks if a signer public key is enrolled in the current period. + * + * @param {string} publicKey - Delegated harvesting public key to check. + * + * @return {Promise} - If the public key is enrolled in the current period. + */ + async checkEnrollmentStatus(publicKey) { + const successEnrollmentResponse = await this._get(`${this.apiBaseUrl}/enrollment/successes/${publicKey}?count=1`, 'failed_to_get_success_enrollments'); + const successEnrollments = await successEnrollmentResponse.json(); + + if (successEnrollments.length === 0) { + return false; + } + + const latestSuccessEnrollment = successEnrollments[0]; + const enrollAddress = latestSuccessEnrollment.recipientAddress; + + return this.checkEnrollmentAddress(enrollAddress); + } + + /** + * Gets codeword dependent hash for the current period given a public key. + * + * @param {string} publicKey - Signer public key to use in hash calculation. + * + * @return {Promise} - The codeword hex string, which should be used in enrollment messages. + */ + async getCodewordHash(publicKey) { + const response = await this._get(`${this.apiBaseUrl}/codeword/${publicKey}`, 'failed_to_get_codeword_hash'); + + return response.text(); + } + + /** + * Gets payout information for a single node. + * + * @param {string} nodeId - Id of the node to search. + * @param {number} pageNumber - Page number to return. + * @param {number} count - record size. + * + * @return {Promise>} - List of payouts. + */ + async getNodePayouts(nodeId, pageNumber, count = 10) { + const offset = count * pageNumber; + const response = await this._get(`${this.apiBaseUrl}/node/${nodeId}/payouts?count=${count}&offset=${offset}`, 'failed_to_get_payouts_page'); + + return response.json(); + } + + /** + * Gets detailed information about a single node. + * + * @param {string} nodeId - Id of the node to search. Can be one of: + * - Database node id + * - Hex encoded node main public key + * - Node IP (or host) + * + * @return {Promise} - Node info. + */ + async getNodeInfo(nodeId) { + const response = await this._get(`${this.apiBaseUrl}/node/${nodeId}`, 'failed_to_get_node_info'); + + return response.json(); + } + + // End methods region // +} + +export default SuperNodeProgram; diff --git a/src/images/moneyBag.png b/src/images/moneyBag.png new file mode 100644 index 0000000000000000000000000000000000000000..3f54c732ffb945aaf731c2d32619537a8459e0d7 GIT binary patch literal 4846 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 NSRJ^bW+luH^*`-O3vK`a literal 0 HcmV?d00001 diff --git a/src/images/nem-logo.png b/src/images/nem-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..94399ad942941bf46bb678e668404db05354dd7a GIT binary patch literal 13363 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
  • SoN*zeSdz=wqm0$$jyNmd}roD8>T zQ2ZzGJ4uXhlP#=op>|KCF@9g6z(e2!B*SEhlb5?D@l*UfPODO^sxWsmbow@-UZ~>7q0E@iX_i=RN8P8c&F!IX-p7u|MB$ z`Sd#*jo4OeRV-2H5Uj>zg==@EoJu~cofgfVZ468$+^ES5_uvETwi<`ZZG#YrbaS%j z1mc|iBVIV-gpkGw%OEvm(4@#ue#gp{hzicDKk+ENBHJ>yxq;@<#>>BZ;b)6-{Vi?oEQN+qs+UwZK~)CnY&qh?OPs1s zv7XQo`%QpWxV7uGcWu{30-3yKZl8=9HM$9YDDmhlbk%9eNQvXdb$4Rxwj-}&0dpzreAnX3FR4J)` zxugcoj%#i#TnlUTImH}4m>WiAZIJplJy~Q>mu|RB51g2c zL;J~~j2ihPT=To#yzM_n5@Nu3Iw&aHb{nf{sPvMXAJg%p38KS`iFsNn> zdYO9w0Lx^<9 zQZ77d*Mr9laL^O1CIneve~yWjsllpX??RN z`29U9V`n6tRv&4Ks^tMh8!gd4(S_UeuAI@NU4fmVOvl4;bN^i^TN^Xik~Lx-^1j=} z#kPbJNW93Sk}uNiVC*9B-AA8P=zP5wa zZ>^ea5CDr)7G_yjgpZNl4PaEtre(j^j}K&FDjt>+VIyv*Gg9o(B`(%KokVmF%CJm4 zau+qa-aQKS>|qA1NX>~X&45^&^LioOwz_Z^uW5O6G+7y?-LHh_&7%y zm`ohzO2}qs5sbm7yInO|E2~{68Xx$N)6e{|+Kywp8Jgo$hadM(`&a6xna*r0g@+BA zDC`1H-4i*rG&V9_W@Z<9KHTlLg*gQNNmC;Hv*?RSD8L%}i}P;F8hJN8nY|yt=@Q_f zfpbjYdzOt&APztZz~5HH;dkG3^vge@w)5DohUWOk7aaHPgO)8nRt*;CDqtHWje;xm z>y#R_*EOg^xWbnf&*Xn}fpm@xg++tB(~;e~=lk$x z9KW(rUA>}Fef^PI=W08TCzakk(sy9*C$p|=owf*0ty6!(51@4Fv7sq z4>drqY?~2O0R50`X+RjB@o$4Q$u!qoyBW5@WEDea9Q$-KRYH+P^;Z(5?Cp z)I8DSDKj+3d#?W8F4xVjyV9u2e$~df>XiaD+v%!WwJfEqn5i5TK*3;Y8EoMN6i8W% zk5Mb!Gx!WN?!>yHEQPDFs02^O3ITmAppmbftpDBnCU*VM0gtnJ{_%{b)X*GjtWDl` z#l_$2sOab(dqlWN6iOc-GtY)b?k}M_*W=b=p?L$z1lcBLwc8HS1I{zMEaC6tNeU>+ z4%s6uWn|~;&$u|K`usW&0e_(73)3}zf!fX z2E=OFa>inBA{sM2m_68R(5ZksSK|_3q)-z{*nIvj&p7FC4}8uw>S-{xdoX;s@$IAk z=)KRLUVchg^fm^7jRaUrF{*&K)+=L@0n#rrse(i4L#3US8#cux<(jGliFj}bxLHw6 zZ&*=@4|uv5nd52W(W7zJwb$_9JdL>mzB=($nYkWbV+y z-@ASJuUwRo=&VRfA9>}<@sGUW_mAzUr_p%Y8JgpwwQI{4-2BrIw)5g$8MIJ(Ft=v# zKdQoQaBXmFQn-R!C`x6;C-zNO>I=Kp%WwJo5yyW`Jq^dx($J&v_MiOlCAW2&p9Oi? ziw*D^7~JpmYG#yWCLv#G&&8wH)Ff;qXwR5v? zvQZKY;Ki)-u`8;QCbPSZPp$g$;m3aVv9@Y$uko}u^k|&%lOO!y9rN>_%Y$J5kpDgK zPV4;rtK%Q|#L+Kbr~dzVV#lTH)>U46?d5-U=FhKrh5CPN{9m25w%P(Dq$vOZ002ov JPDHLkV1m&eI{yFw literal 0 HcmV?d00001 diff --git a/src/sass/_new.scss b/src/sass/_new.scss index ff282451..6364a05f 100644 --- a/src/sass/_new.scss +++ b/src/sass/_new.scss @@ -1916,5 +1916,14 @@ textarea { margin-top: 1.5rem; } +/*** SuperNode Program PAGE ***/ +.status-active { + color: $green; + text-transform: uppercase; +} +.status-inactive { + color: $red; + text-transform: uppercase; +} diff --git a/tests/data/accountData.js b/tests/data/accountData.js index c1010c36..6e1b7974 100644 --- a/tests/data/accountData.js +++ b/tests/data/accountData.js @@ -134,12 +134,43 @@ let mainnetAccountData = { "balance": 0, "importance": 0, "vestedBalance": 0, - "publicKey": null, + "publicKey": '343648ce70fcaf85a7cb32d1dd76dcacae81303eed1fb0cdfad2847f2482017f', "label": null, "multisigInfo": {} } } +let mainnetCosignerAccountData = { + meta: { + cosignatories: [], + cosignatoryOf: [{ + address: "NCC7KUVPQYBTPBABABR5D724CJAOMIA2RJERW3N7", + harvestedBlocks: 0, + balance: 16000000, + importance: 0, + vestedBalance: 0, + publicKey: "671ca866718ed174a21e593fc1e250837c03935bc79e2daad3bd018c444d78a7", + label: null, + multisigInfo: { + cosignatoriesCount: 1, + minCosignatories: 1 + } + }], + status: "LOCKED", + remoteStatus: "INACTIVE" + }, + account: { + address: "NC2YRCZB25RHND45HMX7YAZPHYMBKT5VQYMNAOCO", + harvestedBlocks: 0, + balance: 0, + importance: 0, + vestedBalance: 0, + publicKey: "405b4ca23f4851d32af03f6d7f8877b1184deed93e37ebd54b974e953629999e", + label: null, + multisigInfo: {} + } +} + let mainnetNamespaceOwned = { "NCTIKLMIWKRZC3TRKD5JYZUQHV76LGS3TTSUIXM6": { "nano": { @@ -228,6 +259,7 @@ module.exports = { testnetMosaicOwned, testnetMosaicDefinitionMetaDataPair, mainnetAccountData, + mainnetCosignerAccountData, mainnetNamespaceOwned, mainnetMosaicOwned, mainnetMosaicDefinitionMetaDataPair diff --git a/tests/helper/index.js b/tests/helper/index.js new file mode 100644 index 00000000..e1ee284a --- /dev/null +++ b/tests/helper/index.js @@ -0,0 +1,22 @@ +/** + * Tests promise to throw given error. + * + * @param {Promise} promiseToTest - Promise to be tested. + * @param {any} expectedError - Error to be thrown. + */ +export const runPromiseErrorTest = async (promiseToTest, expectedError) => { + // Arrange: + let thrownError; + + // Act: + try { + await promiseToTest; + } + catch (error) { + thrownError = error; + } + + // Assert: + expect(thrownError instanceof expectedError.constructor).toBe(true); + expect(thrownError.message).toBe(expectedError.message); +}; diff --git a/tests/specs/superNodeProgram.spec.js b/tests/specs/superNodeProgram.spec.js new file mode 100644 index 00000000..db21d63c --- /dev/null +++ b/tests/specs/superNodeProgram.spec.js @@ -0,0 +1,275 @@ +import WalletFixture from '../data/wallet'; +import { runPromiseErrorTest } from '../helper'; + +const SUPER_NODE_PROGRAM_API_BASE_URL = 'https://nem.io/supernode/api'; + +function mockFetch(returnValue, ok = true) { + return spyOn(window, 'fetch').and.returnValue(Promise.resolve({ + ok, + json: () => Promise.resolve(returnValue), + text: () => Promise.resolve(returnValue), + })); +} + +describe('SuperNodeProgram service tests', () => { + let SuperNodeProgram; + let Wallet; + + beforeEach(angular.mock.module('app')); + + beforeEach(angular.mock.inject((_SuperNodeProgram_, _Wallet_) => { + SuperNodeProgram = _SuperNodeProgram_; + Wallet = _Wallet_; + + Wallet.use(WalletFixture.mainnetWallet); + })); + + describe('checkEnrollmentAddress()', () => { + it('should return true when API returns "true" string', async (done) => { + // Arrange: + mockFetch('true'); + const expectedResult = true; + + // Act: + const result = await SuperNodeProgram.checkEnrollmentAddress(''); + + // Assert: + expect(result).toBe(expectedResult); + done(); + }); + + it('should return false when API returns "false" string', async (done) => { + // Arrange: + mockFetch('false'); + const expectedResult = false; + + // Act: + const result = await SuperNodeProgram.checkEnrollmentAddress(''); + + // Assert: + expect(result).toBe(expectedResult); + done(); + }); + + it('should throw an error when request failed', async (done) => { + // Arrange: + mockFetch(null, false); + const expectedError = Error('failed_to_validate_enroll_address'); + const promiseToTest = SuperNodeProgram.checkEnrollmentAddress(''); + + // Act + Assert: + await runPromiseErrorTest(promiseToTest, expectedError); + done(); + }); + }); + + describe('checkEnrollmentStatus()', () => { + const runSuccessfullEnrollmentRecipientAddressValidityTest = async isAddressValid => { + // Arrange: + spyOn(SuperNodeProgram, 'checkEnrollmentAddress').and.returnValue(Promise.resolve(isAddressValid)); + mockFetch([{ + recipientAddress: 'NCHESTYVD2P6P646AMY7WSNG73PCPZDUQNSD6JAK', + transactionHeight: 1, + transactionHash: 'bba40188dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891aua', + host: 'http://108.61.168.86:7890' + }]); + const publicKey = '00a30788dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891a4d'; + + // Act: + const enrollmentStatus = await SuperNodeProgram.checkEnrollmentStatus(publicKey); + + // Assert: + expect(enrollmentStatus).toBe(isAddressValid); + }; + + it('should return true when there is successfull enrollment for given public key and recipient address is a valid enrollment address', async (done) => { + await runSuccessfullEnrollmentRecipientAddressValidityTest(true); + done(); + }); + + it('should return false when there is successfull enrollment for given public key and recipient address is not a valid enrollment address', async (done) => { + await runSuccessfullEnrollmentRecipientAddressValidityTest(false); + done(); + }); + + it('should return false when there is no successfull enrollment for given public key', async (done) => { + // Arrange: + mockFetch([]); + const publicKey = '00a30788dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891a4d'; + const expectedEnrollmentStatus = false; + + // Act: + const enrollmentStatus = await SuperNodeProgram.checkEnrollmentStatus(publicKey); + + // Assert: + expect(enrollmentStatus).toBe(expectedEnrollmentStatus); + done(); + }); + + it('should throw an error when request failed', async (done) => { + // Arrange: + mockFetch(null, false); + const publicKey = '00a30788dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891a4d'; + const expectedError = Error('failed_to_get_success_enrollments'); + const promiseToTest = SuperNodeProgram.checkEnrollmentStatus(publicKey); + + // Act + Assert: + await runPromiseErrorTest(promiseToTest, expectedError); + done(); + }); + }); + + describe('getCodewordHash()', () => { + it('should fetch codeword hash string', async (done) => { + // Arrange: + const baseUrl = SUPER_NODE_PROGRAM_API_BASE_URL; + const publicKey = '00a30788dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891a4d'; + const expectedString = 'hash'; + const expectedEndpoint = `${baseUrl}/codeword/${publicKey}`; + const mockedFetch = mockFetch(expectedString); + + // Act: + const result = await SuperNodeProgram.getCodewordHash(publicKey); + + // Assert: + expect(mockedFetch).toHaveBeenCalledWith(expectedEndpoint); + expect(result).toEqual(expectedString); + done(); + }); + + it('should throw an error when request failed', async (done) => { + // Arrange: + mockFetch(null, false); + const expectedError = Error('failed_to_get_codeword_hash'); + const promiseToTest = SuperNodeProgram.getCodewordHash(''); + + // Act + Assert: + await runPromiseErrorTest(promiseToTest, expectedError); + done(); + }); + }); + + describe('getNodePayouts()', () => { + it('should fetch to be called with valid endpoint url when nodeId and pageNumber=0 passed', async (done) => { + // Arrange: + const mockedFetch = mockFetch([]); + const baseUrl = SUPER_NODE_PROGRAM_API_BASE_URL; + const nodeId = 1; + const pageNumber = 0; + const count = 10; + const offset = 0; + const expectedEndpoint = `${baseUrl}/node/${nodeId}/payouts?count=${count}&offset=${offset}`; + + // Act: + await SuperNodeProgram.getNodePayouts(nodeId, pageNumber); + + // Assert: + expect(mockedFetch).toHaveBeenCalledWith(expectedEndpoint); + done(); + }); + + it('should fetch to be called with valid endpoint url when nodeId and pageNumber=4 passed', async (done) => { + // Arrange: + const mockedFetch = mockFetch([]); + const baseUrl = SUPER_NODE_PROGRAM_API_BASE_URL; + const nodeId = 1; + const pageNumber = 4; + const count = 10; + const offset = 40; + const expectedEndpoint = `${baseUrl}/node/${nodeId}/payouts?count=${count}&offset=${offset}`; + + // Act: + await SuperNodeProgram.getNodePayouts(nodeId, pageNumber); + + // Assert: + expect(mockedFetch).toHaveBeenCalledWith(expectedEndpoint); + done(); + }); + + it('should return valid DTO', async (done) => { + // Arrange: + const expectedDTO = new Array(15).fill({ + amount: 0, + fromRoundId: 844, + isPaid: false, + timestamp: '2022-06-16T12:15:27.427749+00:00', + toRoundId: 844, + transactionHash: null + }); + const nodeId = 1; + const pageNumber = 0; + mockFetch(expectedDTO); + + // Act: + const result = await SuperNodeProgram.getNodePayouts(nodeId, pageNumber); + + // Assert: + expect(result).toEqual(expectedDTO); + done(); + }); + + it('should throw an error when request failed', async (done) => { + // Arrange: + mockFetch(null, false); + const nodeId = 1; + const pageNumber = 4; + const expectedError = Error('failed_to_get_payouts_page'); + const promiseToTest = SuperNodeProgram.getNodePayouts(nodeId, pageNumber); + + // Act + Assert: + await runPromiseErrorTest(promiseToTest, expectedError); + done(); + }); + }); + + describe('getNodeInfo()', () => { + it('should fetch to be called with valid endpoint url', async (done) => { + // Arrange: + const mockedFetch = mockFetch(null); + const baseUrl = SUPER_NODE_PROGRAM_API_BASE_URL; + const nodeId = 1; + const expectedEndpoint = `${baseUrl}/node/${nodeId}`; + + // Act: + await SuperNodeProgram.getNodeInfo(nodeId); + + // Assert: + expect(mockedFetch).toHaveBeenCalledWith(expectedEndpoint); + done(); + }); + + it('should return valid DTO', async (done) => { + // Arrange: + const expectedDTO = { + endpoint: 'http://108.61.168.86:7890', + lastPayoutRound: 9172, + name: 'Alice6', + remotePublicKey: '00a30788dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891a4d', + mainPublicKey: '00a30788dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891a4d', + status: 'active', + totalRewardsEarned: 863404000342 + }; + const nodeId = 1; + mockFetch(expectedDTO); + + // Act: + const result = await SuperNodeProgram.getNodeInfo(nodeId); + + // Assert: + expect(result).toEqual(expectedDTO); + done(); + }); + + it('should throw an error when request failed', async (done) => { + // Arrange: + mockFetch(null, false); + const nodeId = 1; + const expectedError = Error('failed_to_get_node_info'); + const promiseToTest = SuperNodeProgram.getNodeInfo(nodeId); + + // Act + Assert: + await runPromiseErrorTest(promiseToTest, expectedError); + done(); + }); + }); +}); diff --git a/tests/specs/superNodeProgramModule.spec.js b/tests/specs/superNodeProgramModule.spec.js new file mode 100644 index 00000000..75ee9337 --- /dev/null +++ b/tests/specs/superNodeProgramModule.spec.js @@ -0,0 +1,809 @@ +import AccountDataFixture from '../data/accountData'; +import nem from 'nem-sdk'; + +describe('SuperNode program module tests', () => { + let $controller, $rootScope, $timeout, DataStore, Nodes, SuperNodeProgram, Alert, Wallet; + + const mockEnrollStatus = { + id: 1, + endpoint: 'https://superlong.domain.name:7890', + lastPayoutRound: 9999, + name: 'Alice6', + remotePublicKey: 'ca37a2ee96b94fd5e7879105733c97a21ffc2c00d961b2995a59a82463708e6a', + mainPublicKey: '00a30788dc1f042da959309639a884d8f6a87086cda10300d2a7c3a0e0891a4d', + status: 'active', + totalRewardsEarned: 12000000 + }; + + beforeEach(() => { + angular.mock.module('app'); + + angular.mock.inject((_$controller_, _$rootScope_, _$timeout_, _DataStore_, _Nodes_, _SuperNodeProgram_, _Alert_, _Wallet_) => { + $controller = _$controller_; + $rootScope = _$rootScope_; + $timeout = _$timeout_; + DataStore = _DataStore_; + Nodes = _Nodes_; + SuperNodeProgram = _SuperNodeProgram_; + Alert = _Alert_; + Wallet = _Wallet_; + }) + + Nodes.setDefault(); + + DataStore.account.metaData = AccountDataFixture.mainnetAccountData; + }) + + describe('getAccount', () => { + it('returns normal account and balance', () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + ctrl.getAccount(); + + // Assert: + expect(ctrl.selectedAccount).toEqual({ + ...AccountDataFixture.mainnetAccountData.account, + display: AccountDataFixture.mainnetAccountData.account.address, + formatBalance: ctrl.computeXEMBalance(AccountDataFixture.mainnetAccountData.account.balance) + }); + expect(ctrl.balance).toEqual('0.000000'); + expect(ctrl.formData.isMultisig).toEqual(false); + expect(ctrl.formData.mainPublicKey).toEqual('343648ce70fcaf85a7cb32d1dd76dcacae81303eed1fb0cdfad2847f2482017f'); + }) + + it('returns multisig account and balance', () => { + // Arrange: + DataStore.account.metaData = AccountDataFixture.mainnetCosignerAccountData; + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + ctrl.getAccount(); + + // Assert: + const expectedAccounts = [{ + ...AccountDataFixture.mainnetCosignerAccountData.meta.cosignatoryOf[0], + display: 'NCC7KUVPQYBTPBABABR5D724CJAOMIA2RJERW3N7 - Multisig', + formatBalance: '16.000000', + isMultisig: true + }, { + ...AccountDataFixture.mainnetCosignerAccountData.account, + display: 'NC2YRCZB25RHND45HMX7YAZPHYMBKT5VQYMNAOCO', + formatBalance: '0.000000' + }] + + expect(ctrl.accounts).toEqual(expectedAccounts); + expect(ctrl.selectedAccount).toEqual(expectedAccounts[0]); + expect(ctrl.balance).toEqual(expectedAccounts[0].formatBalance); + expect(ctrl.formData.isMultisig).toEqual(expectedAccounts[0].isMultisig); + expect(ctrl.formData.mainPublicKey).toEqual(expectedAccounts[0].publicKey); + }) + }) + + describe('setSelectedAccountInfo', () => { + const runBasicSetSelectedAccountInfoTests = selectedAccount => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + ctrl.selectedAccount = selectedAccount; + + // Act: + ctrl.setSelectedAccountInfo(); + + // Assert: + expect(ctrl.balance).toEqual(selectedAccount.formatBalance); + expect(ctrl.formData.isMultisig).toEqual(selectedAccount.isMultisig); + expect(ctrl.formData.mainPublicKey).toEqual(selectedAccount.publicKey); + } + + it('set normal account info to form data and balance', () => { + // Arrange: + runBasicSetSelectedAccountInfoTests({ + publicKey: 'ca37a2ee96b94fd5e7879105733c97a21ffc2c00d961b2995a59a82463708e6a', + formatBalance: '0.000010', + isMultisig: false + }); + }) + + it('set multisig account info to form data and balance', () => { + // Arrange: + runBasicSetSelectedAccountInfoTests({ + publicKey: 'ca37a2ee96b94fd5e7879105733c97a21ffc2c00d961b2995a59a82463708e6a', + formatBalance: '0.000010', + isMultisig: true + }); + }) + }) + + describe('computeXEMBalance', () => { + it('returns formatted XEM balance in string (6 divisibility)', () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + const balance = ctrl.computeXEMBalance(10); + + // Assert: + expect(balance).toEqual('0.000010'); + }) + }) + + describe('isTabSelected', () => { + const runBasicTabSelectedTests = ({selectedTab, isTabToTestSelected, expectedResult}) => { + it(`returns ${expectedResult} when selected tab ${expectedResult ? 'matched' : 'unmatched'} with current tab`, () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + ctrl.selectedTab = selectedTab; + + // Act: + const resultTab = ctrl.isTabSelected(isTabToTestSelected); + + // Assert: + expect(resultTab).toEqual(expectedResult); + }); + }; + + // Arrange: + const tabs = [ + { + selectedTab: 'status', + isTabToTestSelected: 'status', + expectedResult: true + }, + { + selectedTab: 'enroll', + isTabToTestSelected: 'status', + expectedResult: false + }, + ] + + tabs.forEach(tab => { + runBasicTabSelectedTests(tab); + }) + }) + + describe('setTab', () => { + const runBasicSetTabTests = (selectedTab, fn) => { + it(`${fn} called when ${selectedTab} tab selected`, () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, fn); + + // Act: + ctrl.setTab(selectedTab); + + // Assert: + expect(ctrl.selectedTab).toEqual(selectedTab); + expect(ctrl[fn]).toHaveBeenCalled(); + }) + + } + + // Arrange: + const tabs = [ + { + selectedTab: 'status', + fn: 'selectStatusTab' + }, + { + selectedTab: 'enroll', + fn: 'selectEnrollTab' + }, + { + selectedTab: 'payout', + fn: 'selectPayoutHistory' + } + ] + + tabs.forEach(tab => { + runBasicSetTabTests(tab.selectedTab, tab.fn); + }) + }) + + describe('resetTab', () => { + it('returns to status tab as default', () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'setTab'); + + // Act: + ctrl.resetTab(); + + // Assert: + expect(ctrl.selectedTab).toEqual('status'); + expect(ctrl.setTab).toHaveBeenCalled(); + }) + }) + + describe('onChangeAccount', () => { + it('returns selected account info', () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'setSelectedAccountInfo'); + + spyOn(ctrl, 'resetTab'); + + // Act: + ctrl.onChangeAccount(); + + // Assert: + expect(ctrl.setSelectedAccountInfo).toHaveBeenCalled(); + expect(ctrl.resetTab).toHaveBeenCalled(); + }) + }) + + describe('prepareTransaction', () => { + it('returns normal account transaction object', () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + ctrl.formData.enrollAddress = 'NC2YRCZB25RHND45HMX7YAZPHYMBKT5VQYMNAOCO'; + + // Act: + ctrl.prepareTransaction(); + + // Assert: + expect(ctrl.preparedTransaction.type).toEqual(nem.model.transactionTypes.transfer); + expect(ctrl.preparedTransaction.recipient).toEqual(ctrl.formData.enrollAddress); + expect(ctrl.preparedTransaction.fee).toEqual(200000); + expect(ctrl.preparedTransaction.amount).toEqual(0); + expect(ctrl.preparedTransaction.mosaics).toEqual(null); + expect(ctrl.preparedTransaction.message.payload).toEqual(nem.utils.convert.utf8ToHex(`enroll ${ctrl.formData.nodeHost} ${'0'.repeat(64)}`)); + }) + + it('returns multisig account transaction object', () => { + // Arrange: + DataStore.account.metaData = AccountDataFixture.mainnetCosignerAccountData; + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + ctrl.formData.enrollAddress = 'NC2YRCZB25RHND45HMX7YAZPHYMBKT5VQYMNAOCO'; + + // Act: + ctrl.prepareTransaction(); + + // Assert: + expect(ctrl.preparedTransaction.type).toEqual(nem.model.transactionTypes.multisigTransaction); + expect(ctrl.preparedTransaction.fee).toEqual(150000); + + expect(ctrl.preparedTransaction.otherTrans.recipient).toEqual(ctrl.formData.enrollAddress); + expect(ctrl.preparedTransaction.otherTrans.fee).toEqual(200000); + expect(ctrl.preparedTransaction.otherTrans.amount).toEqual(0); + expect(ctrl.preparedTransaction.otherTrans.mosaics).toEqual(null); + expect(ctrl.preparedTransaction.otherTrans.message.payload).toEqual(nem.utils.convert.utf8ToHex(`enroll ${ctrl.formData.nodeHost} ${'0'.repeat(64)}`)); + }) + + it('returns transaction object provided codewordHash for message payload', () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + ctrl.prepareTransaction('hash'); + + // Assert: + expect(ctrl.preparedTransaction.message.payload).toEqual(nem.utils.convert.utf8ToHex(`enroll ${ctrl.formData.nodeHost} hash`)); + }) + }) + + describe('selectStatusTab', () => { + const expectedEnrollStatusWithoutResponse = { + nodeName: '-', + status: false, + endpoint: '-', + publicKey: '-', + remotePublicKey: '-', + totalReward: '-', + lastPayoutRound: '-', + }; + + const expectedEnrollStatusWithResponse = { + nodeName: mockEnrollStatus.name, + status: mockEnrollStatus.status, + endpoint: 'superlong.domain.name', + publicKey: mockEnrollStatus.mainPublicKey, + remotePublicKey: mockEnrollStatus.remotePublicKey, + totalReward: '12.000000', + nodeId: mockEnrollStatus.id, + lastPayoutRound: mockEnrollStatus.lastPayoutRound, + }; + + it('returns default enroll status when public key does not exist', async (done) => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + ctrl.formData.mainPublicKey = ''; + + // Act: + await ctrl.selectStatusTab(); + $timeout.flush(); + + // Assert: + expect(ctrl.enrollStatus).toEqual(expectedEnrollStatusWithoutResponse); + done(); + }) + + it('returns default enroll status when super node service throws error', async (done) => { + // Arrange: + spyOn(SuperNodeProgram, 'getNodeInfo').and.returnValue(Promise.reject('error')); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + await ctrl.selectStatusTab(); + $timeout.flush(); + + // Assert: + expect(ctrl.enrollStatus).toEqual(expectedEnrollStatusWithoutResponse); + done(); + }) + + it('returns enroll status when accounts enrolled', async (done) => { + // Arrange: + spyOn(SuperNodeProgram, 'getNodeInfo').and.returnValue(Promise.resolve(mockEnrollStatus)); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + await ctrl.selectStatusTab(); + $timeout.flush(); + + // Assert: + expect(ctrl.enrollStatus).toEqual(expectedEnrollStatusWithResponse); + done(); + }) + + it('returns enroll status when no payouts have been completed', async (done) => { + // Arrange: + spyOn(SuperNodeProgram, 'getNodeInfo').and.returnValue(Promise.resolve({ + ...mockEnrollStatus, + lastPayoutRound: -1 + })); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + await ctrl.selectStatusTab(); + $timeout.flush(); + + // Assert: + expect(ctrl.enrollStatus).toEqual({ + ...expectedEnrollStatusWithResponse, + lastPayoutRound: '-' + }); + done(); + }) + }) + + describe('sendEnroll', () => { + it('returns alert when account enrolled and new domain name same as previously registered', async (done) => { + // Arrange: + spyOn(Wallet, 'decrypt').and.returnValue(true); + + spyOn(SuperNodeProgram, 'checkEnrollmentStatus').and.returnValue(Promise.resolve(true)); + spyOn(SuperNodeProgram, 'checkEnrollmentAddress').and.returnValue(Promise.resolve(true)); + spyOn(SuperNodeProgram, 'getCodewordHash').and.returnValue(Promise.resolve('40DA0977BE27B03B61FBE461225007E1528FD87F20A92BF8E031CDA59C4592E1')); + + spyOn(Alert, 'addressEnrolled'); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'prepareTransaction'); + + ctrl.enrollStatus.endpoint = 'old.domain.name'; + + ctrl.formData.nodeHost = 'old.domain.name'; + + // Act: + await ctrl.sendEnroll(); + + $timeout.flush(); + + // Assert: + expect(Alert.addressEnrolled).toHaveBeenCalled(); + expect(ctrl.prepareTransaction).not.toHaveBeenCalled(); + done(); + }) + + it('returns alert when enrollment address incorrect', async (done) => { + // Arrange: + spyOn(Wallet, 'decrypt').and.returnValue(true); + + spyOn(SuperNodeProgram, 'checkEnrollmentStatus').and.returnValue(Promise.resolve(true)); + spyOn(SuperNodeProgram, 'checkEnrollmentAddress').and.returnValue(Promise.resolve(false)); + spyOn(SuperNodeProgram, 'getCodewordHash').and.returnValue(Promise.resolve('40DA0977BE27B03B61FBE461225007E1528FD87F20A92BF8E031CDA59C4592E1')); + + spyOn(Alert, 'invalidEnrollmentAddress'); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'prepareTransaction'); + + // Act: + await ctrl.sendEnroll(); + + $timeout.flush(); + + // Assert: + expect(Alert.invalidEnrollmentAddress).toHaveBeenCalled(); + expect(ctrl.okPressed).toEqual(false); + expect(ctrl.prepareTransaction).not.toHaveBeenCalled(); + done(); + }); + + it('returns alert when codeword hash is empty', async (done) => { + // Arrange: + spyOn(Wallet, 'decrypt').and.returnValue(true); + + spyOn(SuperNodeProgram, 'checkEnrollmentStatus').and.returnValue(Promise.resolve(false)); + spyOn(SuperNodeProgram, 'checkEnrollmentAddress').and.returnValue(Promise.resolve(true)); + spyOn(SuperNodeProgram, 'getCodewordHash').and.returnValue(Promise.resolve('0')); + + spyOn(Alert, 'invalidCodewordHash'); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'prepareTransaction'); + + // Act: + await ctrl.sendEnroll(); + + $timeout.flush(); + + // Assert: + expect(Alert.invalidCodewordHash).toHaveBeenCalled(); + expect(ctrl.okPressed).toEqual(false); + expect(ctrl.prepareTransaction).not.toHaveBeenCalled(); + done(); + }); + + it('returns alert when main public key is not exist (hardware wallet)', async (done) => { + // Arrange: + spyOn(Wallet, 'decrypt').and.returnValue(true); + + spyOn(Alert, 'accountHasNoPublicKey'); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'prepareTransaction'); + + ctrl.formData.mainPublicKey = ''; + + ctrl.common = { + password: '', + privateKey: '', + isHW: true + } + + // Act: + await ctrl.sendEnroll(); + + // Assert: + expect(Alert.accountHasNoPublicKey).toHaveBeenCalled(); + expect(ctrl.okPressed).toEqual(false); + expect(ctrl.prepareTransaction).not.toHaveBeenCalled(); + done(); + }) + + it('returns alert when form data node host content protocol or port', async (done) => { + // Arrange: + spyOn(Wallet, 'decrypt').and.returnValue(true); + + spyOn(Alert, 'invalidFormatNodeHost'); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'prepareTransaction'); + + ctrl.formData.nodeHost = 'http://old.domain.name:7890'; + + // Act: + await ctrl.sendEnroll(); + + // Assert: + expect(Alert.invalidFormatNodeHost).toHaveBeenCalled(); + expect(ctrl.okPressed).toEqual(false); + expect(ctrl.prepareTransaction).not.toHaveBeenCalled(); + done(); + }) + + it('returns transaction object and announce transaction', async (done) => { + // Arrange: + spyOn(Wallet, 'decrypt').and.returnValue(true); + spyOn(Wallet, 'transact'); + + spyOn(SuperNodeProgram, 'checkEnrollmentStatus').and.returnValue(Promise.resolve(false)); + spyOn(SuperNodeProgram, 'checkEnrollmentAddress').and.returnValue(Promise.resolve(true)); + spyOn(SuperNodeProgram, 'getCodewordHash').and.returnValue(Promise.resolve('40DA0977BE27B03B61FBE461225007E1528FD87F20A92BF8E031CDA59C4592E1')); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'prepareTransaction'); + + ctrl.formData.mainPublicKey = ''; + + ctrl.common = { + password: '', + privateKey: '3c11dc7bc6d8cda5c75b041975362d87eb7de5aef1316f432a1bbfe3e9f19ffb', + isHW: false + } + + // Act: + await ctrl.sendEnroll(); + + $timeout.flush(); + + // Assert: + expect(Wallet.transact).toHaveBeenCalled(); + expect(ctrl.formData.mainPublicKey).toEqual('b6405c6d3e96228e8a57c7c84013600486dfd64a7bf14ed7c1daccfbd391f386'); + expect(ctrl.prepareTransaction).toHaveBeenCalled(); + expect(ctrl.okPressed).toEqual(false); + done(); + }); + + it('successful announce transaction when enrolled account update domain', async (done) => { + // Arrange: + spyOn(Wallet, 'decrypt').and.returnValue(true); + spyOn(Wallet, 'transact'); + + spyOn(SuperNodeProgram, 'checkEnrollmentStatus').and.returnValue(Promise.resolve(true)); + spyOn(SuperNodeProgram, 'checkEnrollmentAddress').and.returnValue(Promise.resolve(true)); + spyOn(SuperNodeProgram, 'getCodewordHash').and.returnValue(Promise.resolve('40DA0977BE27B03B61FBE461225007E1528FD87F20A92BF8E031CDA59C4592E1')); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'prepareTransaction'); + + ctrl.formData.mainPublicKey = ''; + + ctrl.common = { + password: '', + privateKey: '3c11dc7bc6d8cda5c75b041975362d87eb7de5aef1316f432a1bbfe3e9f19ffb', + isHW: false + } + + ctrl.enrollStatus.endpoint = 'old.domain.name'; + + ctrl.formData.nodeHost = 'new.domain.name'; + + // Act: + await ctrl.sendEnroll(); + + $timeout.flush(); + + // Assert: + expect(Wallet.transact).toHaveBeenCalled(); + expect(ctrl.formData.mainPublicKey).toEqual('b6405c6d3e96228e8a57c7c84013600486dfd64a7bf14ed7c1daccfbd391f386'); + expect(ctrl.prepareTransaction).toHaveBeenCalled(); + expect(ctrl.okPressed).toEqual(false); + done(); + }); + }) + + describe('selectEnrollTab', () => { + it('set IP or domain to empty when no accounts have enrolled', async (done) => { + // Arrange: + spyOn(SuperNodeProgram, 'getNodeInfo').and.returnValue(Promise.reject('error')); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + await ctrl.selectStatusTab(); + $timeout.flush(); + + // Act: + ctrl.selectEnrollTab(); + + // Assert: + expect(ctrl.formData.nodeHost).toEqual(''); + expect(ctrl.preparedTransaction.type).toEqual(nem.model.transactionTypes.transfer); + expect(ctrl.preparedTransaction.fee).toEqual(200000); + expect(ctrl.preparedTransaction.message.payload).toEqual(nem.utils.convert.utf8ToHex(`enroll ${ctrl.formData.nodeHost} ${'0'.repeat(64)}`)); + done(); + }) + + it('set IP or domain to exist endpoint when accounts enrolled', async (done) => { + // Arrange: + spyOn(SuperNodeProgram, 'getNodeInfo').and.returnValue(Promise.resolve(mockEnrollStatus)); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + await ctrl.selectStatusTab(); + $timeout.flush(); + + // Act: + ctrl.selectEnrollTab(); + + // Assert: + expect(ctrl.formData.nodeHost).toEqual(new URL(mockEnrollStatus.endpoint).hostname); + expect(ctrl.preparedTransaction.type).toEqual(nem.model.transactionTypes.transfer); + expect(ctrl.preparedTransaction.fee).toEqual(200000); + expect(ctrl.preparedTransaction.message.payload).toEqual(nem.utils.convert.utf8ToHex(`enroll ${ctrl.formData.nodeHost} ${'0'.repeat(64)}`)); + done() + }) + }) + + describe('selectPayoutHistory', () => { + const runBasicPaginationTests = ({page, fn, expectedResult}) => { + it(`returns ${page} page of payout history`, () => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + spyOn(ctrl, 'selectPayoutHistory'); + + ctrl.currentPage = 5; + ctrl.lastPage = 10; + + // Act: + ctrl[fn](); + + // Assert: + expect(ctrl.currentPage).toEqual(expectedResult); + expect(ctrl.selectPayoutHistory).toHaveBeenCalled(); + }) + } + + describe('pagination', () => { + // Arrange: + const page = [{ + page: 'first', + fn: 'fetchFirst', + expectedResult: 0 + },{ + page: 'last', + fn: 'fetchLast', + expectedResult: 10 + },{ + page: 'next', + fn: 'fetchNext', + expectedResult: 6 + },{ + page: 'previous', + fn: 'fetchPrevious', + expectedResult: 4 + }] + + page.forEach(pageInfo => { + runBasicPaginationTests(pageInfo); + }) + }) + + it('returns empty array when node id is not exist', async (done) => { + // Arrange: + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + await ctrl.selectPayoutHistory(); + $timeout.flush(); + + // Assert: + expect(ctrl.payoutHistory).toEqual([]); + done(); + }) + + it('returns empty array when super node services throws error', async (done) => { + // Arrange: + spyOn(SuperNodeProgram, 'getNodePayouts').and.returnValue(Promise.reject('error')); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + // Act: + await ctrl.selectPayoutHistory(); + $timeout.flush(); + + // Assert: + expect(ctrl.payoutHistory).toEqual([]); + done(); + }) + + it('returns payout history records', async (done) => { + // Arrange: + const mockPayouts = [{ + amount: 24000000, + fromRoundId: 1267, + isPaid: true, + timestamp: "2022-06-30T23:22:48.135315+00:00", + toRoundId: 1267, + transactionHash: "442D3EC7E12202B3DD8C4A54660BE088235BFD4522A165A522BDC0D368434D94" + }, + { + amount: 24000000, + fromRoundId: 1266, + isPaid: true, + timestamp: "2022-06-30T23:17:48.114989+00:00", + toRoundId: 1266, + transactionHash: "B5D55D9858479441C81F6A638F17C1E30692BE8771FC44BFF62A989E3048A77E" + }, + { + amount: 24000000, + fromRoundId: 1265, + isPaid: true, + timestamp: "2022-06-30T23:07:48.093682+00:00", + toRoundId: 1265, + transactionHash: "4C5A91F8FE70A49506176634E30AF7436D42E8BB951CB2DE6E485175CC99781F" + }]; + + spyOn(SuperNodeProgram, 'getNodeInfo').and.returnValue(Promise.resolve(mockEnrollStatus)); + spyOn(SuperNodeProgram, 'getNodePayouts').and.returnValue(Promise.resolve(mockPayouts)); + + const ctrl = $controller('SuperNodeProgramCtrl', { + $scope: $rootScope.$new() + }); + + await ctrl.selectStatusTab(); + $timeout.flush(); + + // Act: + await ctrl.selectPayoutHistory(); + $timeout.flush(); + + // Assert: + ctrl.payoutHistory.forEach((payout, index) => { + expect(payout).toEqual({ + ...mockPayouts[index], + amount: ctrl.computeXEMBalance(mockPayouts[index].amount), + timestamp: new Date(mockPayouts[index].timestamp).toGMTString(), + }); + done(); + }) + }) + }) +}) \ No newline at end of file