├── docs └── nop-output.png ├── .gitignore ├── test ├── .eslintrc.json ├── actions │ ├── prepare_action.ts │ ├── payout_action.ts │ ├── change_url_action.ts │ └── retire_action.ts ├── services │ ├── system.ts │ ├── crypto.ts │ ├── store.ts │ ├── validations.ts │ └── setup_creator.ts ├── models │ ├── system_model.ts │ └── atlas_mode_model.ts └── phases │ ├── manual_submission_phase.ts │ ├── check_docker_available.ts │ ├── prepare_docker_phase.ts │ ├── get_node_url_phase.ts │ ├── get_node_ip_phase.ts │ ├── get_user_email_phase.ts │ ├── select_node_type_phase.ts │ ├── get_private_key_phase.ts │ ├── check_address_whitelisting_status_phase.ts │ ├── select_action_phase.ts │ ├── perform_onboarding_phase.ts │ └── select_network_phase.ts ├── tests.sh ├── setup_templates ├── atlas │ ├── ambnet │ │ ├── parity_config.toml │ │ └── docker-compose.yml │ ├── ambnet-dev │ │ ├── parity_config.toml │ │ └── docker-compose.yml │ └── ambnet-test │ │ ├── parity_config.toml │ │ └── docker-compose.yml ├── hermes │ ├── ambnet │ │ ├── parity_config.toml │ │ └── docker-compose.yml │ ├── ambnet-dev │ │ ├── parity_config.toml │ │ └── docker-compose.yml │ └── ambnet-test │ │ ├── parity_config.toml │ │ └── docker-compose.yml └── apollo │ ├── ambnet │ ├── parity_config.toml │ └── docker-compose.yml │ ├── ambnet-dev │ ├── parity_config.toml │ └── docker-compose.yml │ └── ambnet-test │ ├── parity_config.toml │ └── docker-compose.yml ├── tsconfig.json ├── src ├── utils │ ├── web3_utils.ts │ ├── file.ts │ ├── file_download.ts │ ├── role_converters.ts │ └── http_utils.ts ├── menu_actions │ ├── quit_action.ts │ ├── prepare_action.ts │ ├── payout_action.ts │ ├── change_url_action.ts │ └── retire_action.ts ├── interfaces │ └── network.ts ├── phases │ ├── manual_submission_phase.ts │ ├── check_docker_available_phase.ts │ ├── prepare_docker_phase.ts │ ├── get_node_url_phase.ts │ ├── get_node_ip_phase.ts │ ├── get_user_email_phase.ts │ ├── select_node_type_phase.ts │ ├── accept_tos_phase.ts │ ├── check_address_whitelisting_status_phase.ts │ ├── get_private_key_phase.ts │ ├── select_action_phase.ts │ ├── perform_onboarding_phase.ts │ └── select_network_phase.ts ├── consts.ts ├── services │ ├── system.ts │ ├── crypto.ts │ ├── store.ts │ ├── validations.ts │ └── setup_creator.ts ├── getUrl.ts ├── start.ts ├── models │ ├── atlas_mode_model.ts │ ├── smart_contracts_model.ts │ └── state_model.ts └── messages.ts ├── update.sh ├── config ├── config.ts └── networks.json ├── .travis.yml ├── package.json ├── run-update.sh ├── force-update.sh ├── CONTRIBUTION.md ├── .eslintrc.json ├── README.md └── USING.md /docs/nop-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascendia-network/ambrosus-nop/HEAD/docs/nop-output.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dist 3 | node_modules 4 | yarn-error.log 5 | state.json 6 | output*/ 7 | .DS_Store 8 | 9 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-underscore-dangle": ["error", { "allow": ["_getStatusCode", "_isJSON", "_getData"] }] 4 | }, 5 | "env": { 6 | "mocha": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | execute="mocha --require ts-node/register" 6 | 7 | for testfile; do 8 | $execute "$testfile" 9 | done 10 | 11 | if [ "$#" = "0" ]; then 12 | find ./test -name '*.ts' -type f -exec $execute {} \; 13 | fi 14 | -------------------------------------------------------------------------------- /setup_templates/atlas/ambnet/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | warp = false 7 | 8 | [rpc] 9 | apis = ["web3", "eth", "net"] 10 | interface = "all" 11 | cors = ["*"] 12 | 13 | [websockets] 14 | apis = ["web3", "eth", "net"] 15 | interface = "all" 16 | 17 | [ipc] 18 | disable = true 19 | 20 | [misc] 21 | logging = "own_tx=trace" 22 | -------------------------------------------------------------------------------- /setup_templates/hermes/ambnet/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | warp = false 7 | 8 | [rpc] 9 | apis = ["web3", "eth", "net"] 10 | interface = "all" 11 | cors = ["*"] 12 | 13 | [websockets] 14 | apis = ["web3", "eth", "net"] 15 | interface = "all" 16 | 17 | [ipc] 18 | disable = true 19 | 20 | [misc] 21 | logging = "own_tx=trace" 22 | -------------------------------------------------------------------------------- /setup_templates/atlas/ambnet-dev/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | warp = false 7 | 8 | [rpc] 9 | apis = ["web3", "eth", "net"] 10 | interface = "all" 11 | cors = ["*"] 12 | 13 | [websockets] 14 | apis = ["web3", "eth", "net"] 15 | interface = "all" 16 | 17 | [ipc] 18 | disable = true 19 | 20 | [misc] 21 | logging = "own_tx=trace" 22 | -------------------------------------------------------------------------------- /setup_templates/atlas/ambnet-test/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | warp = false 7 | 8 | [rpc] 9 | apis = ["web3", "eth", "net"] 10 | interface = "all" 11 | cors = ["*"] 12 | 13 | [websockets] 14 | apis = ["web3", "eth", "net"] 15 | interface = "all" 16 | 17 | [ipc] 18 | disable = true 19 | 20 | [misc] 21 | logging = "own_tx=trace" 22 | -------------------------------------------------------------------------------- /setup_templates/hermes/ambnet-dev/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | warp = false 7 | 8 | [rpc] 9 | apis = ["web3", "eth", "net"] 10 | interface = "all" 11 | cors = ["*"] 12 | 13 | [websockets] 14 | apis = ["web3", "eth", "net"] 15 | interface = "all" 16 | 17 | [ipc] 18 | disable = true 19 | 20 | [misc] 21 | logging = "own_tx=trace" 22 | -------------------------------------------------------------------------------- /setup_templates/hermes/ambnet-test/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | warp = false 7 | 8 | [rpc] 9 | apis = ["web3", "eth", "net"] 10 | interface = "all" 11 | cors = ["*"] 12 | 13 | [websockets] 14 | apis = ["web3", "eth", "net"] 15 | interface = "all" 16 | 17 | [ipc] 18 | disable = true 19 | 20 | [misc] 21 | logging = "own_tx=trace" 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "sourceMap": true, 7 | "resolveJsonModule": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ], 14 | "include": [ 15 | "./src/**/*", 16 | "./config/*" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/web3_utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import * as utils from 'web3-utils'; 11 | export default utils; 12 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | cd "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )" 4 | if [[ -d /etc/cron.daily ]]; then 5 | rm -f /etc/cron.daily/amb*-nop 6 | ln -fs $PWD/update.sh /etc/cron.daily/ambrosus-nop 7 | fi 8 | 9 | cat > /etc/sysctl.d/10-ambrosus.conf <<-END 10 | net.ipv6.conf.all.disable_ipv6=1 11 | END 12 | sysctl -p /etc/sysctl.d/10-ambrosus.conf 13 | 14 | git checkout yarn.lock 15 | git checkout run-update.sh 16 | git pull origin master 17 | 18 | chmod +x run-update.sh 19 | source run-update.sh 20 | -------------------------------------------------------------------------------- /src/menu_actions/quit_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | /** 11 | * @returns {function(): boolean} returns true when we want to quit the NOP 12 | */ 13 | const quitAction = () => () => true; 14 | 15 | export default quitAction; 16 | -------------------------------------------------------------------------------- /src/interfaces/network.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | export interface Network { 10 | domain: string; 11 | rpc: string; 12 | chainspec: string; 13 | headContractAddress: string; 14 | dockerTag: string; 15 | name: string; 16 | } 17 | -------------------------------------------------------------------------------- /setup_templates/apollo/ambnet/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | nat = "extip:" 7 | warp = false 8 | 9 | [rpc] 10 | apis = ["web3", "eth", "net", "parity"] 11 | interface = "all" 12 | 13 | [account] 14 | password = ["/app/password.pwds"] 15 | 16 | [mining] 17 | extra_data = "" 18 | engine_signer = "" 19 | reseal_on_txs = "none" 20 | force_sealing = true 21 | gas_floor_target = "40000000" 22 | gas_cap = "40000000" 23 | tx_gas_limit = "40000000" 24 | 25 | [misc] 26 | logging = "own_tx=trace" 27 | -------------------------------------------------------------------------------- /setup_templates/apollo/ambnet-dev/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | nat = "extip:" 7 | warp = false 8 | 9 | [rpc] 10 | apis = ["web3", "eth", "net", "parity"] 11 | interface = "all" 12 | 13 | [account] 14 | password = ["/app/password.pwds"] 15 | 16 | [mining] 17 | extra_data = "" 18 | engine_signer = "" 19 | reseal_on_txs = "none" 20 | force_sealing = true 21 | gas_floor_target = "40000000" 22 | gas_cap = "40000000" 23 | tx_gas_limit = "40000000" 24 | 25 | [misc] 26 | logging = "own_tx=trace" 27 | -------------------------------------------------------------------------------- /setup_templates/apollo/ambnet-test/parity_config.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | base_path = "/app/" 3 | chain = "/app/chain.json" 4 | 5 | [network] 6 | nat = "extip:" 7 | warp = false 8 | 9 | [rpc] 10 | apis = ["web3", "eth", "net", "parity"] 11 | interface = "all" 12 | 13 | [account] 14 | password = ["/app/password.pwds"] 15 | 16 | [mining] 17 | extra_data = "" 18 | engine_signer = "" 19 | reseal_on_txs = "none" 20 | force_sealing = true 21 | gas_floor_target = "40000000" 22 | gas_cap = "40000000" 23 | tx_gas_limit = "40000000" 24 | 25 | [misc] 26 | logging = "own_tx=trace" 27 | -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | export const config = Object.freeze({ 11 | storePath: process.env.STORE_PATH || 'state.json', 12 | templateDirectory: process.env.TEMPLATE_DIRECTORY || './setup_templates/', 13 | outputDirectory: process.env.OUTPUT_DIRECTORY || './output' 14 | }); 15 | -------------------------------------------------------------------------------- /src/menu_actions/prepare_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {APOLLO_CODE, ATLAS_CODE, HERMES_CODE} from '../consts'; 11 | 12 | const prepareAction = (action, nodeTypes = [ATLAS_CODE, HERMES_CODE, APOLLO_CODE]) => ({ 13 | performAction: action, 14 | nodeTypes 15 | }); 16 | 17 | export default prepareAction; 18 | -------------------------------------------------------------------------------- /src/phases/manual_submission_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import StateModel from '../models/state_model'; 11 | 12 | const manualSubmissionPhase = async () => { 13 | const submission = await StateModel.assembleSubmission(); 14 | Dialog.displaySubmissionDialog(submission); 15 | return submission; 16 | }; 17 | 18 | export default manualSubmissionPhase; 19 | -------------------------------------------------------------------------------- /src/phases/check_docker_available_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import System from '../services/system'; 11 | 12 | const checkDockerAvailablePhase = async () => { 13 | if (!await System.isDockerAvailable()) { 14 | Dialog.dockerMissingDialog(); 15 | return false; 16 | } 17 | Dialog.dockerDetectedDialog(); 18 | return true; 19 | }; 20 | 21 | export default checkDockerAvailablePhase; 22 | -------------------------------------------------------------------------------- /config/networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "domain": "ambrosus-test.io", 4 | "rpc": "https://network.ambrosus-test.io", 5 | "chainspec": "https://chainspec.ambrosus-test.io", 6 | "headContractAddress": "0x0000000000000000000000000000000000000F10", 7 | "dockerTag": "v1.1.19" 8 | }, 9 | "dev": { 10 | "domain": "ambrosus-dev.io", 11 | "rpc": "https://network.ambrosus-dev.io", 12 | "chainspec": "https://chainspec.ambrosus-dev.io", 13 | "headContractAddress": "0x0000000000000000000000000000000000000F10", 14 | "dockerTag": "v1.1.19" 15 | }, 16 | "main": { 17 | "domain": "ambrosus.io", 18 | "rpc": "https://network.ambrosus.io", 19 | "chainspec": "https://chainspec.ambrosus.io", 20 | "headContractAddress": "0x0000000000000000000000000000000000000F10", 21 | "dockerTag": "v1.1.19" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: node_js 4 | node_js: 5 | - '14' 6 | 7 | cache: 8 | yarn: true 9 | directories: 10 | - node_modules 11 | 12 | install: 13 | - npx yarn install 14 | 15 | script: 16 | - npx yarn dev:lint 17 | - npx yarn test 18 | 19 | notifications: 20 | slack: 21 | secure: Bvo3LqZdclBl8b58M/pKjPBDWE/G9VOh9Mr59a4dbHipRm38JHaX2ZDVWSG7c6Caxhkyl8vBh+CLbDl215OWWlZ0PFvEFH+luViwHBZfvB5u8Jimm0CGq6fEtZHh2WWiiZC78ZXFiu7AldpHnMuD9DIbLFgbuV991PKr269KR5zImI3pEBIVng+8VMF0mQeXa7AmSS3Z4zp90VYcNLxsScNu9aBdc4QsCGMMPIMBDV9ePj9MbgbtkeuUAN25wS+Nhv707K8j4IzGDLz14wJINcBVpcxN5weSdfwgpDKcNo7S+Inn1JVwlXVU8K8M4y9BP6LnaIfviOTW4+LI+aEc0cAmqAnnTPMi5+pJHSW9EpTzjbtLD5zmFkGxNPjvgVdtYMXMSW9QANHnxIChd9GoaDHChVQXoeazxfvWXR+8xIH9lF1324HTDJ1sQAQjIorTAhK1wR026KKCmREWqn5Sw6EeIjZ1D2nAUh+77M3oPHySKwXdEdTUd7AQ1PTyxQQ74j32kiygp1eL+h2vU0zBByIKr4dNmYZCZfrQYiHfm03HsEOoWc/T+L4KhxOF+9BZiwuDzMG/gly83nuFopcnZdoOH0bPL9/3tCwJvu4TxrsztDFA4yxEc2TKf0FY3S13dxRasiFvY2GidYFXUiTAzgFctMXApRL+gWJrDVo7zf8= 22 | -------------------------------------------------------------------------------- /src/phases/prepare_docker_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | 11 | import {ATLAS_1, ATLAS_2, ATLAS_3, HERMES} from '../consts'; 12 | import Dialog from '../models/dialog_model'; 13 | import StateModel from '../models/state_model'; 14 | 15 | const prepareDockerPhase = async () => { 16 | await StateModel.prepareSetupFiles(); 17 | 18 | const role = await StateModel.getRole(); 19 | if (role === HERMES || role === ATLAS_1 || role === ATLAS_2 || role === ATLAS_3) { 20 | const url = await StateModel.getNodeUrl(); 21 | if (url) { 22 | Dialog.healthCheckUrlDialog(); 23 | } 24 | } 25 | }; 26 | 27 | export default prepareDockerPhase; 28 | -------------------------------------------------------------------------------- /test/actions/prepare_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import prepareAction from '../../src/menu_actions/prepare_action'; 12 | 13 | const {expect} = chai; 14 | 15 | describe('Prepare action', () => { 16 | it('returns action object', async () => { 17 | expect(prepareAction('action', [])).to.deep.equal({ 18 | performAction: 'action', 19 | nodeTypes: [] 20 | }); 21 | }); 22 | 23 | it('show action for all node types by default', async () => { 24 | expect(prepareAction('action')).to.deep.equal({ 25 | performAction: 'action', 26 | nodeTypes: ['1', '2', '3'] 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {constants} from 'ambrosus-node-contracts'; 11 | 12 | export const APOLLO_CODE = constants.APOLLO; 13 | export const HERMES_CODE = constants.HERMES; 14 | export const ATLAS_CODE = constants.ATLAS; 15 | export const NO_ROLE_CODE = constants.NONE; 16 | 17 | export const ATLAS_1_STAKE = constants.ATLAS1_STAKE; 18 | export const ATLAS_2_STAKE = constants.ATLAS2_STAKE; 19 | export const ATLAS_3_STAKE = constants.ATLAS3_STAKE; 20 | 21 | export const APOLLO = 'Apollo'; 22 | export const HERMES = 'Hermes'; 23 | export const ATLAS_1 = 'Atlas Zeta'; 24 | export const ATLAS_2 = 'Atlas Sigma'; 25 | export const ATLAS_3 = 'Atlas Omega'; 26 | -------------------------------------------------------------------------------- /src/phases/get_node_url_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import StateModel from '../models/state_model'; 11 | 12 | const getNodeUrl = async () => { 13 | const storedNodeUrl = await StateModel.getNodeUrl(); 14 | if (storedNodeUrl !== null) { 15 | return storedNodeUrl; 16 | } 17 | 18 | const answers = await Dialog.askForNodeUrlDialog(); 19 | const {nodeUrl} = answers; 20 | await StateModel.storeNodeUrl(nodeUrl); 21 | return nodeUrl; 22 | }; 23 | 24 | const getNodeUrlPhase = async () => { 25 | const nodeUrl = await getNodeUrl(); 26 | await Dialog.nodeUrlDetectedDialog(nodeUrl); 27 | return nodeUrl; 28 | }; 29 | 30 | export default getNodeUrlPhase; 31 | -------------------------------------------------------------------------------- /src/phases/get_node_ip_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import StateModel from '../models/state_model'; 11 | 12 | const getNodeIP = async () => { 13 | const storedNodeIP = await StateModel.getNodeIP(); 14 | if (storedNodeIP !== null) { 15 | return storedNodeIP; 16 | } 17 | 18 | const answers = await Dialog.askForNodeIPDialog(); 19 | const {nodeIP : providedIP} = answers; 20 | await StateModel.storeNodeIP(providedIP); 21 | return providedIP; 22 | }; 23 | 24 | const getNodeIPPhase = async () => { 25 | const nodeIP = await getNodeIP(); 26 | await Dialog.nodeIPDetectedDialog(nodeIP); 27 | return nodeIP; 28 | }; 29 | 30 | export default getNodeIPPhase; 31 | -------------------------------------------------------------------------------- /setup_templates/apollo/ambnet/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | parity: 5 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.2.2.6 6 | container_name: parity 7 | command: --config /app/parity_config.toml 8 | working_dir: /app 9 | user: root 10 | restart: unless-stopped 11 | ports: 12 | - '127.0.0.1:8545:8545/tcp' 13 | - '30303:30303/tcp' 14 | - '30303:30303/udp' 15 | volumes: 16 | - ./chain.json:/app/chain.json 17 | - ./password.pwds:/app/password.pwds 18 | - ./parity_config.toml:/app/parity_config.toml 19 | - ./keyfile:/app/keys//keyfile 20 | - ./chains:/app/chains 21 | 22 | ethstats-client: 23 | image: ghcr.io/ambrosus/eth-net-intelligence-api 24 | container_name: ethstats_client 25 | restart: unless-stopped 26 | depends_on: 27 | - parity 28 | environment: 29 | RPC_HOST: parity 30 | WS_SERVER: wss://stats-api. 31 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 32 | INSTANCE_NAME: 'apollo ' 33 | -------------------------------------------------------------------------------- /setup_templates/apollo/ambnet-dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | parity: 5 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.2.2.5 6 | container_name: parity 7 | command: --config /app/parity_config.toml 8 | working_dir: /app 9 | user: root 10 | restart: unless-stopped 11 | ports: 12 | - '127.0.0.1:8545:8545/tcp' 13 | - '30303:30303/tcp' 14 | - '30303:30303/udp' 15 | volumes: 16 | - ./chain.json:/app/chain.json 17 | - ./password.pwds:/app/password.pwds 18 | - ./parity_config.toml:/app/parity_config.toml 19 | - ./keyfile:/app/keys//keyfile 20 | - ./chains:/app/chains 21 | 22 | ethstats-client: 23 | image: ghcr.io/ambrosus/eth-net-intelligence-api 24 | container_name: ethstats_client 25 | restart: unless-stopped 26 | depends_on: 27 | - parity 28 | environment: 29 | RPC_HOST: parity 30 | WS_SERVER: wss://stats-api. 31 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 32 | INSTANCE_NAME: 'apollo ' 33 | -------------------------------------------------------------------------------- /setup_templates/apollo/ambnet-test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | parity: 5 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.2.2.5 6 | container_name: parity 7 | command: --config /app/parity_config.toml 8 | working_dir: /app 9 | user: root 10 | restart: unless-stopped 11 | ports: 12 | - '127.0.0.1:8545:8545/tcp' 13 | - '30303:30303/tcp' 14 | - '30303:30303/udp' 15 | volumes: 16 | - ./chain.json:/app/chain.json 17 | - ./password.pwds:/app/password.pwds 18 | - ./parity_config.toml:/app/parity_config.toml 19 | - ./keyfile:/app/keys//keyfile 20 | - ./chains:/app/chains 21 | 22 | ethstats-client: 23 | image: ghcr.io/ambrosus/eth-net-intelligence-api 24 | container_name: ethstats_client 25 | restart: unless-stopped 26 | depends_on: 27 | - parity 28 | environment: 29 | RPC_HOST: parity 30 | WS_SERVER: wss://stats-api. 31 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 32 | INSTANCE_NAME: 'apollo ' 33 | -------------------------------------------------------------------------------- /src/menu_actions/payout_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import SmartContractsModel from '../models/smart_contracts_model'; 11 | import web3Utils from '../utils/web3_utils'; 12 | 13 | const payoutAction = () => async () => { 14 | const availablePayout = web3Utils.fromWei(await SmartContractsModel.payoutsActions.getTotalAvailablePayout(), 'ether'); 15 | Dialog.availablePayoutDialog(availablePayout); 16 | if (availablePayout !== '0' && (await Dialog.confirmPayoutWithdrawalDialog()).payoutConfirmation) { 17 | await SmartContractsModel.payoutsActions.withdraw(); 18 | Dialog.withdrawalSuccessfulDialog(availablePayout); 19 | } 20 | return false; 21 | }; 22 | 23 | export default payoutAction; 24 | -------------------------------------------------------------------------------- /src/phases/get_user_email_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import StateModel from '../models/state_model'; 11 | 12 | const getUserEmail = async () => { 13 | const existingUserEmail = await StateModel.getUserEmail(); 14 | if (existingUserEmail !== null) { 15 | return existingUserEmail; 16 | } 17 | 18 | const answers = await Dialog.askForUserEmailDialog(); 19 | const {userEmail} = answers; 20 | await StateModel.storeUserEmail(userEmail); 21 | return userEmail; 22 | }; 23 | 24 | const getUserEmailPhase = async () => { 25 | const userEmail = await getUserEmail(); 26 | await Dialog.userEmailDetectedDialog(userEmail); 27 | return userEmail; 28 | }; 29 | 30 | export default getUserEmailPhase; 31 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import * as fs from 'fs'; 11 | 12 | const writeFile = (path, data, opts = {}) => fs.promises.writeFile(path, data, opts); 13 | const readFile = (path): Promise => fs.promises.readFile(path).then((buffer) => buffer.toString()); 14 | const removeFile = (path) => fs.promises.unlink(path); 15 | const removeDirectory = (path) => fs.promises.rmdir(path); 16 | const makeDirectory = (path) => fs.promises.mkdir(path); 17 | const checkFileExists = (path): Promise => fs.promises.access(path).then(() => true) 18 | .catch(() => false); 19 | const getPath = (path): Promise => fs.promises.lstat(path); 20 | 21 | export {writeFile, readFile, removeFile, checkFileExists, removeDirectory, makeDirectory, getPath}; 22 | -------------------------------------------------------------------------------- /src/menu_actions/change_url_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import StateModel from '../models/state_model'; 11 | import SmartContractsModel from '../models/smart_contracts_model'; 12 | 13 | const changeUrlAction = () => async () => { 14 | Dialog.nectarWarningDialog(); 15 | const oldUrl = await StateModel.getNodeUrl(); 16 | const {nodeUrl: newUrl} = await Dialog.askForNodeUrlDialog(); 17 | if (await Dialog.changeUrlConfirmationDialog(oldUrl, newUrl)) { 18 | await SmartContractsModel.rolesWrapper.setNodeUrl(await StateModel.getAddress(), newUrl); 19 | await StateModel.storeNodeUrl(newUrl); 20 | Dialog.changeUrlSuccessfulDialog(newUrl); 21 | } 22 | return false; 23 | }; 24 | 25 | export default changeUrlAction; 26 | -------------------------------------------------------------------------------- /src/services/system.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {exec} from 'child_process'; 11 | 12 | class System { 13 | async isDockerAvailable() { 14 | try { 15 | const {stdout} = await this.execCmd('docker -v'); 16 | const regex = /^Docker version ([0-9.\-a-z]+), build ([0-9a-f]+)/; 17 | return regex.exec(stdout) !== null; 18 | } catch ({err}) { 19 | return false; 20 | } 21 | } 22 | 23 | async execCmd(cmd): Promise { 24 | return new Promise((resolve, reject) => { 25 | exec(cmd, (err, stdout, stderr) => { 26 | if (err === null) { 27 | resolve({stdout, stderr}); 28 | } else { 29 | reject(err); 30 | } 31 | }); 32 | }); 33 | } 34 | } 35 | 36 | export default new System(); 37 | -------------------------------------------------------------------------------- /src/phases/select_node_type_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {APOLLO} from '../consts'; 11 | import Dialog from '../models/dialog_model'; 12 | import StateModel from '../models/state_model'; 13 | 14 | const selectRole = async () => { 15 | const existingRole = await StateModel.getRole(); 16 | if (existingRole !== null) { 17 | return existingRole; 18 | } 19 | 20 | const {nodeType} = await Dialog.askForNodeTypeDialog(); 21 | if (nodeType === APOLLO) { 22 | const deposit = await Dialog.askForApolloMinimalDepositDialog(); 23 | await StateModel.storeApolloMinimalDeposit(deposit); 24 | } 25 | await StateModel.storeRole(nodeType); 26 | return nodeType; 27 | }; 28 | 29 | const selectNodeTypePhase = async () => { 30 | const selectedRole = await selectRole(); 31 | Dialog.roleSelectedDialog(selectedRole); 32 | return selectedRole; 33 | }; 34 | 35 | export default selectNodeTypePhase; 36 | -------------------------------------------------------------------------------- /src/utils/file_download.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import * as https from 'https'; 11 | import * as fs from 'fs'; 12 | import * as http from 'http'; 13 | 14 | const makeRequest = async (url): Promise => new Promise((resolve, reject) => { 15 | https.get(url, (response) => { 16 | if (response.statusCode !== 200) { 17 | reject(`Request to ${url} failed: ${response.statusCode}`); 18 | } else { 19 | resolve(response); 20 | } 21 | }); 22 | }); 23 | 24 | export default async (url, outputFilePath) => { 25 | const writeStream = fs.createWriteStream(outputFilePath); 26 | const response = await makeRequest(url); 27 | response.pipe(writeStream); 28 | return new Promise((resolve, reject) => { 29 | writeStream.on('finish', () => { 30 | writeStream.close(); 31 | resolve(void(0)); 32 | }); 33 | writeStream.on('error', reject); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/phases/accept_tos_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import StateModel from '../models/state_model'; 11 | import SmartContractsModel from '../models/smart_contracts_model'; 12 | 13 | const acceptTosPhase = async () => { 14 | const existingSignedTos = await StateModel.getSignedTos(); 15 | if (existingSignedTos !== null) { 16 | return existingSignedTos; 17 | } 18 | const tosText = await StateModel.readTosText(); 19 | const {acceptanceSentence} = await Dialog.acceptTosDialog(tosText); 20 | const acceptedTosText = `${tosText}\n${acceptanceSentence}`; 21 | await StateModel.createTosFile(acceptedTosText); 22 | const tosTextHash = SmartContractsModel.hashData(tosText); 23 | await StateModel.storeTosHash(tosTextHash); 24 | const privateKey = await StateModel.getPrivateKey(); 25 | const tosSignature = SmartContractsModel.signMessage(acceptedTosText, privateKey); 26 | await StateModel.storeSignedTos(tosSignature); 27 | }; 28 | 29 | export default acceptTosPhase; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ambrosus-nop", 3 | "version": "0.1.6", 4 | "private": true, 5 | "description": "", 6 | "repository": "git@github.com:ambrosus/ambrosus-nop.git", 7 | "license": "MPL-2.0-no-copyleft-exception", 8 | "author": "Ambrosus", 9 | "scripts": { 10 | "build": "tsc", 11 | "dev:lint": "eslint src test config", 12 | "dev:lint:fix": "eslint src test config --fix", 13 | "dev:start": "ts-node src/start.ts", 14 | "start": "node dist/src/start.js", 15 | "test": "./tests.sh" 16 | }, 17 | "dependencies": { 18 | "ambrosus-node-contracts": "0.0.83", 19 | "base64url": "^3.0.1", 20 | "chalk": "^4.1.1", 21 | "inquirer": "^6.5.2", 22 | "ip-regex": "^4.3.0", 23 | "ts-node": "^10.4.0", 24 | "typescript": "^4.5.2", 25 | "web3": "^1.3.6" 26 | }, 27 | "devDependencies": { 28 | "@types/mocha": "^9.0.0", 29 | "@types/node": "^14.17.34", 30 | "@typescript-eslint/eslint-plugin": "^5.4.0", 31 | "@typescript-eslint/parser": "^5.4.0", 32 | "chai": "^4.1.2", 33 | "chai-as-promised": "^7.1.1", 34 | "eslint": "^7.31.0", 35 | "eslint-plugin-header": "^3.1.1", 36 | "eslint-plugin-import": "^2.22.1", 37 | "mocha": "^9.1.3", 38 | "nock": "^13.1.1", 39 | "sinon": "^11.1.1", 40 | "sinon-chai": "^3.2.0" 41 | }, 42 | "engines": { 43 | "node": ">=14", 44 | "yarn": ">=1.3.2 <2.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/services/system.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | import System from '../../src/services/system'; 15 | 16 | chai.use(chaiAsPromised); 17 | chai.use(sinonChai); 18 | const {expect} = chai; 19 | 20 | describe('System', () => { 21 | let execCmdStub; 22 | 23 | beforeEach(async () => { 24 | execCmdStub = sinon.stub(); 25 | System.execCmd = execCmdStub; 26 | }); 27 | 28 | describe('isDockerAvailable', () => { 29 | it('expects the cmd to return with status code 0 and the docker version string', async () => { 30 | execCmdStub.resolves({stdout: 'Docker version 18.06.0-ce, build 0ffa825\n', stderr: ''}); 31 | await expect(System.isDockerAvailable()).to.be.eventually.fulfilled.with.true; 32 | }); 33 | 34 | it('detects status code other then 0', async () => { 35 | execCmdStub.rejects({err: 'Command not found', stdout: 'Docker version 18.06.0-ce, build 0ffa825\n', stderr: ''}); 36 | await expect(System.isDockerAvailable()).to.be.eventually.fulfilled.with.false; 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/phases/check_address_whitelisting_status_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import Dialog from '../models/dialog_model'; 11 | import StateModel from '../models/state_model'; 12 | import SmartContractsModel from '../models/smart_contracts_model'; 13 | 14 | const checkAddressWhitelistingStatusPhase = async (): Promise<{requiredDeposit: string, roleAssigned: string}> => { 15 | const userAddress = await StateModel.getAddress(); 16 | if (await SmartContractsModel.isAddressWhitelisted(userAddress) === false) { 17 | Dialog.addressIsNotWhitelistedDialog(); 18 | return null; 19 | } 20 | 21 | const {requiredDeposit, roleAssigned} = await SmartContractsModel.getAddressWhitelistingData(userAddress); 22 | Dialog.addressIsWhitelistedDialog(requiredDeposit, roleAssigned); 23 | 24 | const existingRole = await StateModel.getRole(); 25 | if (existingRole === null) { 26 | await StateModel.storeRole(roleAssigned); 27 | } 28 | if (existingRole !== null && existingRole !== roleAssigned) { 29 | throw new Error('Role selected differs from role assigned in whitelist. Please contact Ambrosus Tech Support'); 30 | } 31 | 32 | return {requiredDeposit, roleAssigned}; 33 | }; 34 | 35 | export default checkAddressWhitelistingStatusPhase; 36 | -------------------------------------------------------------------------------- /src/utils/role_converters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import { 11 | APOLLO, 12 | APOLLO_CODE, 13 | ATLAS_1, 14 | ATLAS_1_STAKE, 15 | ATLAS_2, 16 | ATLAS_2_STAKE, ATLAS_3, ATLAS_3_STAKE, 17 | ATLAS_CODE, 18 | HERMES, 19 | HERMES_CODE, NO_ROLE_CODE 20 | } from '../consts'; 21 | 22 | const roleToRoleCode = (role) => { 23 | switch (role) { 24 | case HERMES: 25 | return HERMES_CODE; 26 | case APOLLO: 27 | return APOLLO_CODE; 28 | case ATLAS_1: 29 | case ATLAS_2: 30 | case ATLAS_3: 31 | return ATLAS_CODE; 32 | default: 33 | return NO_ROLE_CODE; 34 | } 35 | }; 36 | 37 | const roleCodeToRole = (roleCode, deposit) => { 38 | switch (roleCode) { 39 | case ATLAS_CODE: 40 | return atlasStakeAmountToRole(deposit); 41 | case HERMES_CODE: 42 | return HERMES; 43 | case APOLLO_CODE: 44 | return APOLLO; 45 | default: 46 | return null; 47 | } 48 | }; 49 | 50 | const atlasStakeAmountToRole = (deposit) => { 51 | switch (deposit) { 52 | case ATLAS_1_STAKE: 53 | return ATLAS_1; 54 | case ATLAS_2_STAKE: 55 | return ATLAS_2; 56 | case ATLAS_3_STAKE: 57 | return ATLAS_3; 58 | default: 59 | return null; 60 | } 61 | }; 62 | 63 | export {roleToRoleCode, roleCodeToRole}; 64 | -------------------------------------------------------------------------------- /test/models/system_model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import System from '../../src/services/system'; 16 | 17 | chai.use(chaiAsPromised); 18 | chai.use(sinonChai); 19 | const {expect} = chai; 20 | 21 | describe('System Model', () => { 22 | let systemStub; 23 | let systemModel; 24 | 25 | beforeEach(async () => { 26 | systemStub = { 27 | isDockerAvailable: sinon.stub() 28 | }; 29 | System.isDockerAvailable = systemStub.isDockerAvailable; 30 | systemModel = System; 31 | }); 32 | 33 | describe('isDockerAvailable', () => { 34 | it('returns true if docker is available', async () => { 35 | systemStub.isDockerAvailable.resolves(true); 36 | await expect(System.isDockerAvailable()).to.be.eventually.fulfilled.with.true; 37 | await expect(systemStub.isDockerAvailable).to.have.been.calledOnce; 38 | }); 39 | 40 | it('returns false if docker is not available', async () => { 41 | systemStub.isDockerAvailable.resolves(false); 42 | await expect(System.isDockerAvailable()).to.be.eventually.fulfilled.with.false; 43 | await expect(systemStub.isDockerAvailable).to.have.been.calledOnce; 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /run-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash 5 | source $HOME/.nvm/nvm.sh 6 | echo "The $(command -v nvm) has been installed." 7 | 8 | DEFAULT_NODE_VERSION="14" 9 | REQUIRED_NODE_VERSION=$(cat $PWD/package.json | grep -Eo '"node": "[^0-9][^0-9]?[0-9]+?' | grep -Eo '[0-9]+') 10 | if [ "$REQUIRED_NODE_VERSION" = "" ]; then 11 | REQUIRED_NODE_VERSION=$DEFAULT_NODE_VERSION 12 | fi 13 | 14 | SYSTEM_NODE_VERSION=$(nvm current | cut -d '.' -f 1 | cut -b 2-) 15 | if [ "$SYSTEM_NODE_VERSION" = "" ] || [ "$SYSTEM_NODE_VERSION" != "$REQUIRED_NODE_VERSION" ]; then 16 | nvm install "$REQUIRED_NODE_VERSION" 17 | fi 18 | 19 | nvm use "$REQUIRED_NODE_VERSION" 20 | nvm alias default "$REQUIRED_NODE_VERSION" 21 | 22 | BLOCKCHAIN_DB_FILE_PATH=$(find ./output/chains -name "db_version") 23 | BLOCKHAIN_DB_VERSION=$(cat $BLOCKCHAIN_DB_FILE_PATH) 24 | BLOCKHAIN_DB_UPDATE_PATH=$(find ./output/chains -name "overlayrecent") 25 | NETWORK_ENV=$(echo $BLOCKCHAIN_DB_FILE_PATH | cut -d/ -f4) 26 | 27 | if [ ! -d "3.1-db-upgrade-tool" ]; then 28 | git clone https://github.com/openethereum/3.1-db-upgrade-tool 29 | fi 30 | 31 | if [ 16 -gt $BLOCKHAIN_DB_VERSION ]; then 32 | apt-get -yq install cargo clang llvm 33 | cd 3.1-db-upgrade-tool 34 | docker container stop parity 35 | echo "I AGREE" | cargo run ../$BLOCKHAIN_DB_UPDATE_PATH 36 | cd .. 37 | rm -rf 3.1-db-upgrade-tool 38 | fi 39 | 40 | yarn 41 | yarn build 42 | if [ -f output/docker-compose.yml ]; then 43 | yarn start update 44 | docker-compose -f output/docker-compose.yml pull 45 | docker-compose -f output/docker-compose.yml down 46 | docker-compose -f output/docker-compose.yml up -d 47 | fi 48 | -------------------------------------------------------------------------------- /force-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash 5 | source $HOME/.nvm/nvm.sh 6 | echo "The $(command -v nvm) has been installed." 7 | 8 | DEFAULT_NODE_VERSION="14" 9 | REQUIRED_NODE_VERSION=$(cat $PWD/package.json | grep -Eo '"node": "[^0-9][^0-9]?[0-9]+?' | grep -Eo '[0-9]+') 10 | if [ "$REQUIRED_NODE_VERSION" = "" ]; then 11 | REQUIRED_NODE_VERSION=$DEFAULT_NODE_VERSION 12 | fi 13 | 14 | SYSTEM_NODE_VERSION=$(nvm current | cut -d '.' -f 1 | cut -b 2-) 15 | if [ "$SYSTEM_NODE_VERSION" = "" ] || [ "$SYSTEM_NODE_VERSION" != "$REQUIRED_NODE_VERSION" ]; then 16 | nvm install "$REQUIRED_NODE_VERSION" 17 | fi 18 | 19 | nvm use "$REQUIRED_NODE_VERSION" 20 | nvm alias default "$REQUIRED_NODE_VERSION" 21 | 22 | BLOCKCHAIN_DB_FILE_PATH=$(find ./output/chains -name "db_version") 23 | BLOCKHAIN_DB_VERSION=$(cat $BLOCKCHAIN_DB_FILE_PATH) 24 | BLOCKHAIN_DB_UPDATE_PATH=$(find ./output/chains -name "overlayrecent") 25 | NETWORK_ENV=$(echo $BLOCKCHAIN_DB_FILE_PATH | cut -d/ -f4) 26 | 27 | if [ ! -d "3.1-db-upgrade-tool" ]; then 28 | git clone https://github.com/openethereum/3.1-db-upgrade-tool 29 | fi 30 | 31 | if [ 16 -gt $BLOCKHAIN_DB_VERSION ]; then 32 | apt-get -yq install cargo clang llvm 33 | cd 3.1-db-upgrade-tool 34 | docker container stop parity 35 | echo "I AGREE" | cargo run ../$BLOCKHAIN_DB_UPDATE_PATH 36 | cd .. 37 | rm -rf 3.1-db-upgrade-tool 38 | fi 39 | 40 | yarn 41 | yarn build 42 | if [ -f output/docker-compose.yml ]; then 43 | yarn start update 44 | docker-compose -f output/docker-compose.yml pull 45 | docker-compose -f output/docker-compose.yml down 46 | docker-compose -f output/docker-compose.yml up -d 47 | fi 48 | -------------------------------------------------------------------------------- /test/phases/manual_submission_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import manualSubmissionPhase from '../../src/phases/manual_submission_phase'; 16 | import StateModel from '../../src/models/state_model'; 17 | import Dialog from '../../src/models/dialog_model'; 18 | 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | const {expect} = chai; 22 | 23 | describe('Manual Submission Phase', () => { 24 | let stateModelStub; 25 | 26 | const exampleSubmission = { 27 | foo: 'bar' 28 | }; 29 | 30 | const call = manualSubmissionPhase; 31 | 32 | beforeEach(async () => { 33 | stateModelStub = { 34 | assembleSubmission: sinon.stub() 35 | }; 36 | StateModel.assembleSubmission = stateModelStub.assembleSubmission; 37 | Dialog.displaySubmissionDialog = sinon.stub().resolves(); 38 | }); 39 | 40 | it('displays form to send', async () => { 41 | stateModelStub.assembleSubmission.resolves(exampleSubmission); 42 | 43 | const ret = await call(); 44 | 45 | expect(stateModelStub.assembleSubmission).to.have.been.calledOnce; 46 | expect(Dialog.displaySubmissionDialog).to.have.been.calledWith(exampleSubmission); 47 | expect(ret).to.deep.equal(exampleSubmission); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/phases/get_private_key_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import Dialog from '../models/dialog_model'; 11 | import Crypto from '../services/crypto'; 12 | import StateModel from '../models/state_model'; 13 | 14 | const getPrivateKey = async () => { 15 | const storedPrivateKey = await StateModel.getPrivateKey(); 16 | if (storedPrivateKey !== null) { 17 | return storedPrivateKey; 18 | } 19 | 20 | const answers = await Dialog.askForPrivateKeyDialog(); 21 | const {source} = answers; 22 | switch (source) { 23 | case 'manual': { 24 | const {privateKey} = answers; 25 | await StateModel.storePrivateKey(privateKey); 26 | const address = await StateModel.getAddress(); 27 | await StateModel.storeAddress(address); 28 | return privateKey; 29 | } 30 | case 'generate': { 31 | const privateKey = await StateModel.generateAndStoreNewPrivateKey(); 32 | const address = await StateModel.getAddress(); 33 | await StateModel.storeAddress(address); 34 | return privateKey; 35 | } 36 | default: 37 | throw new Error('Unexpected source'); 38 | } 39 | }; 40 | 41 | const getPrivateKeyPhase = async () => { 42 | const privateKey = await getPrivateKey(); 43 | const address = await Crypto.addressForPrivateKey(privateKey); 44 | Dialog.privateKeyDetectedDialog(address); 45 | return privateKey; 46 | }; 47 | 48 | export default getPrivateKeyPhase; 49 | -------------------------------------------------------------------------------- /src/phases/select_action_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | import Dialog from '../models/dialog_model'; 10 | import {constants} from 'ambrosus-node-contracts'; 11 | import {roleToRoleCode} from '../utils/role_converters'; 12 | import messages from '../messages'; 13 | import prepareAction from '../menu_actions/prepare_action'; 14 | import changeUrlAction from '../menu_actions/change_url_action'; 15 | import payoutAction from '../menu_actions/payout_action'; 16 | import retireAction from '../menu_actions/retire_action'; 17 | import quitAction from '../menu_actions/quit_action'; 18 | 19 | export const defaultActions = { 20 | [messages.actions.changeUrl]: prepareAction(changeUrlAction(), [constants.ATLAS, constants.HERMES]), 21 | [messages.actions.payouts]: prepareAction(payoutAction(), [constants.ATLAS]), 22 | [messages.actions.retire]: prepareAction(retireAction()), 23 | [messages.actions.quit]: prepareAction(quitAction()) 24 | }; 25 | 26 | export const selectActionPhase = (role, actions) => async () => { 27 | const actionSelectionList = Object.keys(actions) 28 | .filter((key) => actions[key].nodeTypes.includes(roleToRoleCode(role))); 29 | let shouldQuit = false; 30 | while (!shouldQuit) { 31 | const {action: selectedAction} = await Dialog.selectActionDialog(actionSelectionList); 32 | try { 33 | shouldQuit = await actions[selectedAction].performAction(); 34 | } catch (err) { 35 | if (err.message.includes('Insufficient funds')) { 36 | Dialog.insufficientFundsDialog(); 37 | } else { 38 | Dialog.genericErrorDialog(err.message); 39 | } 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/getUrl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import Web3 from 'web3'; 11 | import fs from 'fs'; 12 | import {HeadWrapper, RolesWrapper} from 'ambrosus-node-contracts'; 13 | 14 | const headContractAddress = '0x0000000000000000000000000000000000000F10'; 15 | 16 | const readFile = (path) => 17 | new Promise((resolve, reject) => { 18 | fs.readFile(path, 'utf8', (err, data) => { 19 | if (err) { 20 | reject(err); 21 | } else { 22 | resolve(data); 23 | } 24 | }); 25 | }); 26 | 27 | async function readConfig() { 28 | try { 29 | return JSON.parse(String(await readFile('state.json'))); 30 | } catch (err) { 31 | console.log('Read config file error:', err); 32 | } 33 | return undefined; 34 | } 35 | 36 | async function getUrl() { 37 | try { 38 | const config = await readConfig(); 39 | if (config === undefined) { 40 | return; 41 | } 42 | const web3 = new Web3(); 43 | web3.setProvider(new Web3.providers.HttpProvider(config.network.rpc)); 44 | const account = web3.eth.accounts.privateKeyToAccount(config.privateKey); 45 | web3.eth.accounts.wallet.add(account); 46 | web3.eth.defaultAccount = account.address; 47 | // console.log('Default address:', web3.eth.defaultAccount); 48 | const headWrapper = new HeadWrapper(headContractAddress, web3, web3.eth.defaultAccount); 49 | const rolesWrapper = new RolesWrapper(headWrapper, web3, web3.eth.defaultAccount); 50 | const url = await rolesWrapper.nodeUrl(account.address); 51 | console.log(url); 52 | } catch (err) { 53 | console.log('Error:', err); 54 | } 55 | } 56 | 57 | getUrl(); 58 | -------------------------------------------------------------------------------- /src/services/crypto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import Web3 from 'web3'; 11 | import {Account} from 'web3-core'; 12 | 13 | class Crypto { 14 | public web3: Web3; 15 | public account: Account; 16 | public address: string; 17 | 18 | constructor() { 19 | this.web3 = new Web3(); 20 | } 21 | 22 | setWeb3(web3: any) { 23 | this.web3 = web3; 24 | } 25 | 26 | setWeb3UsingRpc(rpc: string) { 27 | this.web3 = new Web3(rpc); 28 | } 29 | 30 | setAccount(privateKey) { 31 | this.account = this.web3.eth.accounts.privateKeyToAccount(privateKey); 32 | this.web3.eth.accounts.wallet.add(this.account); 33 | const {address} = this.account; 34 | this.address = address; 35 | this.web3.eth.defaultAccount = address; 36 | } 37 | 38 | async generatePrivateKey() { 39 | const account = this.web3.eth.accounts.create(); 40 | return account.privateKey; 41 | } 42 | 43 | async addressForPrivateKey(privateKey) { 44 | const account = this.web3.eth.accounts.privateKeyToAccount(privateKey); 45 | return account.address; 46 | } 47 | 48 | async getBalance(address) { 49 | return this.web3.utils.toBN(await this.web3.eth.getBalance(address)); 50 | } 51 | 52 | getEncryptedWallet(privateKey, password) { 53 | return this.web3.eth.accounts.encrypt(privateKey, password); 54 | } 55 | 56 | getRandomPassword() { 57 | return this.web3.utils.randomHex(32); 58 | } 59 | 60 | sign(data, privateKey) { 61 | return this.web3.eth.accounts.sign(data, privateKey); 62 | } 63 | 64 | hashData(data) { 65 | return this.web3.utils.sha3(data); 66 | } 67 | } 68 | 69 | export default new Crypto(); 70 | -------------------------------------------------------------------------------- /src/services/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {readFile, writeFile, checkFileExists} from '../utils/file'; 11 | import {config} from '../../config/config'; 12 | 13 | class Store { 14 | public storeFilePath: string; 15 | constructor() { 16 | this.storeFilePath = config.storePath; 17 | if (!this.storeFilePath) { 18 | throw new Error(`Unable to initiate Store`); 19 | } 20 | } 21 | 22 | async write(key: string, value: any) { 23 | const contents = await this.readFile(); 24 | contents[key] = value; 25 | await this.writeFile(contents); 26 | } 27 | 28 | async clear(key: string) { 29 | const contents = await this.readFile(); 30 | if (key in contents) { 31 | delete contents[key]; 32 | } 33 | await this.writeFile(contents); 34 | } 35 | 36 | async read(key) { 37 | const contents = await this.readFile(); 38 | if (contents[key] === undefined) { 39 | throw new Error(`The value for ${key} is missing in the store at ${this.storeFilePath}`); 40 | } 41 | return contents[key]; 42 | } 43 | 44 | async safeRead(key) { 45 | if (await this.has(key)) { 46 | return this.read(key); 47 | } 48 | return null; 49 | } 50 | 51 | async has(key) { 52 | const contents = await this.readFile(); 53 | return contents[key] !== undefined; 54 | } 55 | 56 | async readFile() { 57 | if (await checkFileExists(this.storeFilePath)) { 58 | return JSON.parse(String(await readFile(this.storeFilePath))); 59 | } 60 | return {}; 61 | } 62 | 63 | async writeFile(contents) { 64 | await writeFile(this.storeFilePath, JSON.stringify(contents, null, 2), {mode: 0o660}); 65 | } 66 | } 67 | 68 | export default new Store(); 69 | -------------------------------------------------------------------------------- /src/services/validations.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import ipRegex from 'ip-regex'; 11 | import psl from 'psl'; 12 | 13 | class Validations { 14 | isValidPrivateKey(candidate) { 15 | const addressRegex = /^0x[0-9a-f]{64}$/i; 16 | return addressRegex.exec(candidate) !== null; 17 | } 18 | 19 | isValidUrl = (inputUrl) => { 20 | try { 21 | const stringUrl = inputUrl.toString(); 22 | const url = new URL(stringUrl); 23 | const {protocol, hostname} = url; 24 | if (!(['http:', 'https:'].includes(protocol))) { 25 | return false; 26 | } 27 | if (!psl.isValid(hostname)) { 28 | const ipv4 = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 29 | if (!ipv4.test(hostname)) { 30 | return false; 31 | } 32 | } 33 | return inputUrl === `${protocol}//${hostname}`; 34 | // eslint-disable-next-line no-empty 35 | } catch {} 36 | return false; 37 | }; 38 | 39 | isValidEmail(candidate) { 40 | const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 41 | return emailRegex.exec(candidate) !== null; 42 | } 43 | 44 | isValidIP(candidate) { 45 | return ipRegex({exact: true}).test(candidate); 46 | } 47 | 48 | isValidNumber(candidate) { 49 | return candidate.length > 0 && !isNaN(candidate); 50 | } 51 | 52 | isValidTosConfirmation(confirmation) { 53 | const tosConfirmationRegex = /^I, .+, read and agreed with the terms and conditions above\.$/; 54 | return tosConfirmationRegex.exec(confirmation) !== null; 55 | } 56 | } 57 | 58 | export default new Validations(); 59 | -------------------------------------------------------------------------------- /src/phases/perform_onboarding_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {APOLLO} from '../consts'; 11 | import Dialog from '../models/dialog_model'; 12 | import StateModel from '../models/state_model'; 13 | import SmartContractsModel from '../models/smart_contracts_model'; 14 | 15 | const performOnboardingPhase = async (whitelistingStatus: {requiredDeposit: string, roleAssigned: string}) => { 16 | const userAddress = await StateModel.getAddress(); 17 | const onboardedRole = await SmartContractsModel.getOnboardedRole(userAddress); 18 | if (onboardedRole) { 19 | Dialog.alreadyOnboardedDialog(onboardedRole); 20 | return true; 21 | } 22 | const onboardDeposit = whitelistingStatus.roleAssigned === APOLLO ? 23 | await Dialog.askForApolloDepositDialog(whitelistingStatus.requiredDeposit) : 24 | whitelistingStatus.requiredDeposit; 25 | if (!await SmartContractsModel.hasEnoughBalance(userAddress, onboardDeposit)) { 26 | Dialog.notEnoughBalanceDialog(onboardDeposit); 27 | return false; 28 | } 29 | const dialogResult = await Dialog.onboardingConfirmationDialog(userAddress, whitelistingStatus.roleAssigned, onboardDeposit); 30 | if (!dialogResult.onboardingConfirmation) { 31 | return false; 32 | } 33 | 34 | try { 35 | await SmartContractsModel.performOnboarding(userAddress, whitelistingStatus.roleAssigned, 36 | onboardDeposit, await StateModel.getNodeUrl()); 37 | } catch (error) { 38 | if (error.message.includes('Insufficient funds')) { 39 | Dialog.insufficientFundsDialog(); 40 | } else { 41 | Dialog.genericErrorDialog(error.message); 42 | } 43 | return false; 44 | } 45 | 46 | Dialog.onboardingSuccessfulDialog(); 47 | return true; 48 | }; 49 | 50 | export default performOnboardingPhase; 51 | -------------------------------------------------------------------------------- /src/utils/http_utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import * as http from 'http'; 11 | import * as https from 'https'; 12 | 13 | class HttpUtils { 14 | async httpPost(url: string, data): Promise<{data: string, statusCode: number}> { 15 | const httpModule = url.startsWith('https') ? https : http; 16 | return new Promise((resolve, reject) => { 17 | const req = httpModule.request(url, 18 | {method: 'POST', headers: {'Content-Type': 'application/json'}}, (res) => { 19 | const result: any = {data: '', statusCode: 0}; 20 | res.setEncoding('utf8'); 21 | res.on('data', (chunk) => { 22 | result.data += chunk; 23 | }); 24 | res.on('end', () => { 25 | if (res.statusCode === 200 || res.statusCode === 201) { 26 | result.statusCode = res.statusCode; 27 | resolve(result); 28 | } else { 29 | reject(res.statusCode); 30 | } 31 | }); 32 | }); 33 | req.on('error', reject); 34 | req.write(data); 35 | req.end(); 36 | }); 37 | } 38 | 39 | async httpGet(url: string): Promise { 40 | const httpModule = url.startsWith('https') ? https : http; 41 | return new Promise((resolve, reject) => { 42 | const req = httpModule.get(url, (res) => { 43 | let data = ''; 44 | res.on('data', (chunk) => { 45 | data += chunk; 46 | }); 47 | res.on('end', () => { 48 | try { 49 | resolve(JSON.parse(data)); 50 | } catch (err) { 51 | reject(err); 52 | } 53 | }); 54 | }); 55 | req.on('error', reject); 56 | req.end(); 57 | }); 58 | } 59 | } 60 | 61 | export default new HttpUtils(); 62 | -------------------------------------------------------------------------------- /test/phases/check_docker_available.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import Dialog from '../../src/models/dialog_model'; 16 | import System from '../../src/services/system'; 17 | import checkDockerAvailablePhase from '../../src/phases/check_docker_available_phase'; 18 | 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | const {expect} = chai; 22 | 23 | describe('Check docker available', () => { 24 | let systemModelStub; 25 | 26 | const call = checkDockerAvailablePhase; 27 | 28 | beforeEach(async () => { 29 | systemModelStub = { 30 | isDockerAvailable: sinon.stub() 31 | }; 32 | System.isDockerAvailable = systemModelStub.isDockerAvailable; 33 | Dialog.dockerDetectedDialog = sinon.stub().resolves(); 34 | Dialog.dockerMissingDialog = sinon.stub().resolves(); 35 | }); 36 | 37 | it('displays confirmation dialog if docker available', async () => { 38 | systemModelStub.isDockerAvailable.resolves(true); 39 | 40 | const ret = await call(); 41 | 42 | expect(systemModelStub.isDockerAvailable).to.have.been.calledOnce; 43 | expect(Dialog.dockerDetectedDialog).to.have.been.called; 44 | expect(Dialog.dockerMissingDialog).to.not.have.been.called; 45 | expect(ret).to.equal(true); 46 | }); 47 | 48 | it('displays error dialog if not docker available', async () => { 49 | systemModelStub.isDockerAvailable.resolves(false); 50 | 51 | const ret = await call(); 52 | 53 | expect(systemModelStub.isDockerAvailable).to.have.been.calledOnce; 54 | expect(Dialog.dockerDetectedDialog).to.not.have.been.called; 55 | expect(Dialog.dockerMissingDialog).to.have.been.called; 56 | expect(ret).to.equal(false); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /setup_templates/atlas/ambnet-dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | mongodb: 5 | image: mongo:4.2.9 6 | container_name: mongod 7 | restart: unless-stopped 8 | volumes: 9 | - ./data/db:/data/db 10 | - ./data/configdb:/data/configdb 11 | 12 | parity: 13 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.0.7 14 | container_name: parity 15 | command: --config /app/parity_config.toml 16 | working_dir: /app 17 | user: root 18 | restart: unless-stopped 19 | ports: 20 | - '127.0.0.1:8545:8545/tcp' 21 | - '30303:30303/tcp' 22 | - '30303:30303/udp' 23 | volumes: 24 | - ./chain.json:/app/chain.json 25 | - ./parity_config.toml:/app/parity_config.toml 26 | - ./chains:/app/chains 27 | 28 | ethstats-client: 29 | image: ghcr.io/ambrosus/eth-net-intelligence-api 30 | container_name: ethstats_client 31 | restart: unless-stopped 32 | depends_on: 33 | - parity 34 | environment: 35 | RPC_HOST: parity 36 | WS_SERVER: wss://stats-api. 37 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 38 | INSTANCE_NAME: 'atlas ' 39 | 40 | worker: &atlas-props 41 | image: ambrosus/ambrosus-node: 42 | container_name: atlas_worker 43 | command: sh -c 'yarn migrate && yarn start:atlas' 44 | restart: unless-stopped 45 | logging: 46 | options: 47 | max-size: '100m' 48 | max-file: '3' 49 | depends_on: 50 | - mongodb 51 | - parity 52 | environment: 53 | - WEB3_NODEPRIVATEKEY= 54 | - HEAD_CONTRACT_ADDRESS= 55 | - WEB3_RPC=http://parity:8545 56 | - MONGO_HOSTS=mongodb:27017 57 | - MONGO_DB_NAME=ambrosus 58 | - AUTHORIZATION_WITH_SECRET_KEY_ENABLED=false 59 | - NODE_ENV=production 60 | 61 | server: 62 | container_name: atlas_server 63 | <<: *atlas-props 64 | command: sh -c 'yarn start:server' 65 | logging: 66 | options: 67 | max-size: '100m' 68 | max-file: '3' 69 | depends_on: 70 | - worker 71 | ports: 72 | - 80:9876 73 | -------------------------------------------------------------------------------- /setup_templates/atlas/ambnet/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | mongodb: 5 | image: mongo:4.2.9 6 | container_name: mongod 7 | restart: unless-stopped 8 | volumes: 9 | - ./data/db:/data/db 10 | - ./data/configdb:/data/configdb 11 | 12 | parity: 13 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.2.2.6 14 | container_name: parity 15 | command: --config /app/parity_config.toml 16 | working_dir: /app 17 | user: root 18 | restart: unless-stopped 19 | ports: 20 | - '127.0.0.1:8545:8545/tcp' 21 | - '30303:30303/tcp' 22 | - '30303:30303/udp' 23 | volumes: 24 | - ./chain.json:/app/chain.json 25 | - ./parity_config.toml:/app/parity_config.toml 26 | - ./chains:/app/chains 27 | 28 | ethstats-client: 29 | image: ghcr.io/ambrosus/eth-net-intelligence-api 30 | container_name: ethstats_client 31 | restart: unless-stopped 32 | depends_on: 33 | - parity 34 | environment: 35 | RPC_HOST: parity 36 | WS_SERVER: wss://stats-api. 37 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 38 | INSTANCE_NAME: 'atlas ' 39 | 40 | worker: &atlas-props 41 | image: ambrosus/ambrosus-node: 42 | container_name: atlas_worker 43 | command: sh -c 'yarn migrate && yarn start:atlas' 44 | restart: unless-stopped 45 | logging: 46 | options: 47 | max-size: '100m' 48 | max-file: '3' 49 | depends_on: 50 | - mongodb 51 | - parity 52 | environment: 53 | - WEB3_NODEPRIVATEKEY= 54 | - HEAD_CONTRACT_ADDRESS= 55 | - WEB3_RPC=http://parity:8545 56 | - MONGO_HOSTS=mongodb:27017 57 | - MONGO_DB_NAME=ambrosus 58 | - AUTHORIZATION_WITH_SECRET_KEY_ENABLED=false 59 | - NODE_ENV=production 60 | 61 | server: 62 | container_name: atlas_server 63 | <<: *atlas-props 64 | command: sh -c 'yarn start:server' 65 | logging: 66 | options: 67 | max-size: '100m' 68 | max-file: '3' 69 | depends_on: 70 | - worker 71 | ports: 72 | - 80:9876 73 | -------------------------------------------------------------------------------- /setup_templates/atlas/ambnet-test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | mongodb: 5 | image: mongo:4.2.9 6 | container_name: mongod 7 | restart: unless-stopped 8 | volumes: 9 | - ./data/db:/data/db 10 | - ./data/configdb:/data/configdb 11 | 12 | parity: 13 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.0.7 14 | container_name: parity 15 | command: --config /app/parity_config.toml 16 | working_dir: /app 17 | user: root 18 | restart: unless-stopped 19 | ports: 20 | - '127.0.0.1:8545:8545/tcp' 21 | - '30303:30303/tcp' 22 | - '30303:30303/udp' 23 | volumes: 24 | - ./chain.json:/app/chain.json 25 | - ./parity_config.toml:/app/parity_config.toml 26 | - ./chains:/app/chains 27 | 28 | ethstats-client: 29 | image: ghcr.io/ambrosus/eth-net-intelligence-api 30 | container_name: ethstats_client 31 | restart: unless-stopped 32 | depends_on: 33 | - parity 34 | environment: 35 | RPC_HOST: parity 36 | WS_SERVER: wss://stats-api. 37 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 38 | INSTANCE_NAME: 'atlas ' 39 | 40 | worker: &atlas-props 41 | image: ambrosus/ambrosus-node: 42 | container_name: atlas_worker 43 | command: sh -c 'yarn migrate && yarn start:atlas' 44 | restart: unless-stopped 45 | logging: 46 | options: 47 | max-size: '100m' 48 | max-file: '3' 49 | depends_on: 50 | - mongodb 51 | - parity 52 | environment: 53 | - WEB3_NODEPRIVATEKEY= 54 | - HEAD_CONTRACT_ADDRESS= 55 | - WEB3_RPC=http://parity:8545 56 | - MONGO_HOSTS=mongodb:27017 57 | - MONGO_DB_NAME=ambrosus 58 | - AUTHORIZATION_WITH_SECRET_KEY_ENABLED=false 59 | - NODE_ENV=production 60 | 61 | server: 62 | container_name: atlas_server 63 | <<: *atlas-props 64 | command: sh -c 'yarn start:server' 65 | logging: 66 | options: 67 | max-size: '100m' 68 | max-file: '3' 69 | depends_on: 70 | - worker 71 | ports: 72 | - 80:9876 73 | -------------------------------------------------------------------------------- /src/phases/select_network_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import Dialog from '../models/dialog_model'; 11 | import StateModel from '../models/state_model'; 12 | import {Network} from '../interfaces/network'; 13 | 14 | const hasNetworkChanged = (existingNetwork, availableNetwork) => !availableNetwork || Object.entries(availableNetwork) 15 | .some(([key, value]) => existingNetwork[key] !== value); 16 | 17 | async function askForNetwork(availableNetworks) { 18 | const availableNetworksNames = Object.keys(availableNetworks); 19 | 20 | if (availableNetworksNames.length === 0) { 21 | throw new Error('No networks are defined'); 22 | } else if (availableNetworksNames.length === 1) { 23 | return availableNetworksNames[0]; 24 | } else { 25 | const answers = await Dialog.askForNetworkDialog(availableNetworksNames); 26 | return answers.network; 27 | } 28 | } 29 | 30 | const selectNetwork = async (availableNetworks) => { 31 | const existingNetwork = await StateModel.getNetwork(); 32 | if (existingNetwork && !hasNetworkChanged(existingNetwork, availableNetworks[existingNetwork.name])) { 33 | return existingNetwork; 34 | } 35 | let selectedNetwork; 36 | if (existingNetwork && availableNetworks[existingNetwork.name]) { 37 | selectedNetwork = existingNetwork.name; 38 | } else { 39 | selectedNetwork = await askForNetwork(availableNetworks); 40 | } 41 | const networkToStore = {...availableNetworks[selectedNetwork], name: selectedNetwork}; 42 | await StateModel.storeNetwork(networkToStore); 43 | if (existingNetwork) { 44 | Dialog.dockerRestartRequiredDialog(); 45 | } 46 | return networkToStore; 47 | }; 48 | 49 | const selectNetworkPhase = async (availableNetworks) => { 50 | const selectedNetwork = await selectNetwork(availableNetworks); 51 | Dialog.networkSelectedDialog(selectedNetwork.name); 52 | return selectedNetwork as Network; 53 | }; 54 | 55 | export default selectNetworkPhase; 56 | -------------------------------------------------------------------------------- /test/phases/prepare_docker_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import prepareDockerPhase from '../../src/phases/prepare_docker_phase'; 16 | import {ATLAS_1, APOLLO} from '../../src/consts'; 17 | import StateModel from '../../src/models/state_model'; 18 | import Dialog from '../../src/models/dialog_model'; 19 | 20 | chai.use(chaiAsPromised); 21 | chai.use(sinonChai); 22 | const {expect} = chai; 23 | 24 | describe('Prepare Docker Phase', () => { 25 | let stateModelStub; 26 | 27 | const exampleUrl = 'https://hermes-node.com'; 28 | 29 | const call = prepareDockerPhase; 30 | 31 | beforeEach(async () => { 32 | stateModelStub = { 33 | getNodeUrl: sinon.stub().resolves(exampleUrl), 34 | getRole: sinon.stub().resolves(ATLAS_1), 35 | prepareSetupFiles: sinon.stub() 36 | }; 37 | StateModel.getNodeUrl = stateModelStub.getNodeUrl; 38 | StateModel.getRole = stateModelStub.getRole; 39 | StateModel.prepareSetupFiles = stateModelStub.prepareSetupFiles; 40 | 41 | Dialog.healthCheckUrlDialog = sinon.stub(); 42 | }); 43 | 44 | it('prepares setup files', async () => { 45 | await call(); 46 | expect(stateModelStub.prepareSetupFiles).to.be.calledOnce; 47 | }); 48 | 49 | it('shows node health url after successful installation', async () => { 50 | await call(); 51 | expect(Dialog.healthCheckUrlDialog).to.be.calledOnce; 52 | }); 53 | 54 | it('does not show health url if url has not been set', async () => { 55 | stateModelStub.getNodeUrl.resolves(null); 56 | 57 | await call(); 58 | expect(Dialog.healthCheckUrlDialog).to.be.not.called; 59 | }); 60 | 61 | it('does not show health url if role is Apollo', async () => { 62 | stateModelStub.getRole.resolves(APOLLO); 63 | 64 | await call(); 65 | expect(Dialog.healthCheckUrlDialog).to.be.not.called; 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Open Source Contribution Policy 2 | 3 | ## Policy 4 | We will accept contributions of good code that we can use from anyone. 5 | 6 | ## What this means 7 | * “We will accept”: This means that we will incorporate your contribution into the project’s codebase, adapt it as needed, and give you full credit for your work. 8 | * “contributions”: This means just about anything you wish to contribute to the project, as long as it is good code we can use. The easier you make it for us to accept your contribution, the happier we are, but if it’s good enough, we will do a reasonable amount of work to use it. 9 | * “of good code”: This means that we will accept contributions that work well and efficiently, that fit in with the goals of the project, that match the project’s coding style, and that do not impose an undue maintenance workload on us going forward. This does not mean just program code, either, but documentation and artistic works as appropriate to the project. 10 | * “that we can use”: This means that your contribution must be given freely and irrevocably, that you must have the right to contribute it for our unrestricted use, and that your contribution is made under a license that is compatible with the license the project has chosen and that permits us to include, distribute, and modify your work without restriction. 11 | * “from anyone”: This means exactly that. We don’t care about anything but your code. We don’t care about your race, religion, national origin, biological gender, perceived gender, sexual orientation, lifestyle, political viewpoint, or anything extraneous like that. We will neither reject your contribution nor grant it preferential treatment on any basis except the code itself. We do, however, reserve the right to tell you to go away if you behave too obnoxiously toward us. 12 | ## If Your Contribution Is Rejected 13 | If we reject your contribution, it means only that we do not consider it suitable for our project in the form it was submitted. It is not personal. If you ask civilly, we will be happy to discuss it with you and help you understand why it was rejected, and if possible improve it so we can accept it. If we are not able to reach an agreement, then you are free to fork our project, as with any Open Source project, and add your contribution to your fork. 14 | 15 | Taken from [Jay Maynard](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83) 16 | 17 | -------------------------------------------------------------------------------- /src/menu_actions/retire_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {constants} from 'ambrosus-node-contracts'; 11 | import Dialog from '../models/dialog_model'; 12 | import AtlasModeModel from '../models/atlas_mode_model'; 13 | import StateModel from '../models/state_model'; 14 | import SmartContractsModel from '../models/smart_contracts_model'; 15 | 16 | const isAtlas = async (): Promise => await SmartContractsModel.onboardActions.rolesWrapper.onboardedRole(SmartContractsModel.onboardActions.rolesWrapper.defaultAddress) === constants.ATLAS; 17 | const isAtlasWithBundles = async (): Promise => await SmartContractsModel.onboardActions.atlasStakeWrapper.isShelteringAny(SmartContractsModel.onboardActions.atlasStakeWrapper.defaultAddress); 18 | 19 | const retireAction = () => async () => { 20 | if (await isAtlas() && await isAtlasWithBundles()) { 21 | const retireMode = (await AtlasModeModel.getMode()).mode === 'retire'; 22 | if (retireMode) { 23 | if ((await Dialog.continueAtlasRetirementDialog()).continueConfirmation) { 24 | Dialog.retirementContinueDialog(); 25 | } else { 26 | if (await AtlasModeModel.setMode('normal')) { 27 | Dialog.retirementStopDialog(); 28 | return true; 29 | } 30 | Dialog.genericErrorDialog('Can not set normal mode: I/O error'); 31 | return false; 32 | } 33 | return true; 34 | } 35 | if (!(await Dialog.confirmRetirementDialog()).retirementConfirmation) { 36 | return false; 37 | } 38 | if (await AtlasModeModel.setMode('retire')) { 39 | Dialog.retirementStartSuccessfulDialog(); 40 | return true; 41 | } 42 | Dialog.genericErrorDialog('Can not set retire mode: I/O error'); 43 | return false; 44 | } 45 | 46 | if (!(await Dialog.confirmRetirementDialog()).retirementConfirmation) { 47 | return false; 48 | } 49 | await SmartContractsModel.onboardActions.retire(); 50 | Dialog.retirementSuccessfulDialog(); 51 | await StateModel.removeRole(); 52 | return true; 53 | }; 54 | 55 | export default retireAction; 56 | -------------------------------------------------------------------------------- /test/phases/get_node_url_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import Dialog from '../../src/models/dialog_model'; 16 | import StateModel from '../../src/models/state_model'; 17 | import getNodeTypePhase from '../../src/phases/get_node_url_phase'; 18 | 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | const {expect} = chai; 22 | 23 | describe('Get Node Url Phase', () => { 24 | let stateModelStub; 25 | let nodeUrlDetectedDialogStub; 26 | let askForNodeUrlDialogStub; 27 | 28 | const exampleUrl = 'https://amb-node.com'; 29 | 30 | const call = getNodeTypePhase; 31 | 32 | beforeEach(async () => { 33 | stateModelStub = { 34 | getNodeUrl: sinon.stub(), 35 | storeNodeUrl: sinon.stub() 36 | }; 37 | StateModel.getNodeUrl = stateModelStub.getNodeUrl; 38 | StateModel.storeNodeUrl = stateModelStub.storeNodeUrl; 39 | 40 | nodeUrlDetectedDialogStub = sinon.stub(); 41 | askForNodeUrlDialogStub = sinon.stub(); 42 | 43 | Dialog.nodeUrlDetectedDialog = nodeUrlDetectedDialogStub; 44 | Dialog.askForNodeUrlDialog = askForNodeUrlDialogStub; 45 | }); 46 | 47 | it('ends if a URL is already in the store', async () => { 48 | stateModelStub.getNodeUrl.resolves(exampleUrl); 49 | 50 | const ret = await call(); 51 | 52 | expect(stateModelStub.getNodeUrl).to.have.been.calledOnce; 53 | expect(askForNodeUrlDialogStub).to.have.not.been.called; 54 | expect(stateModelStub.storeNodeUrl).to.have.not.been.called; 55 | expect(nodeUrlDetectedDialogStub).to.have.been.calledOnceWith(exampleUrl); 56 | expect(ret).to.equal(exampleUrl); 57 | }); 58 | 59 | it('ask the user for a url and stores it', async () => { 60 | stateModelStub.getNodeUrl.resolves(null); 61 | askForNodeUrlDialogStub.resolves({ 62 | nodeUrl: exampleUrl 63 | }); 64 | 65 | const ret = await call(); 66 | 67 | expect(stateModelStub.getNodeUrl).to.have.been.calledOnce; 68 | expect(askForNodeUrlDialogStub).to.have.been.calledOnce; 69 | expect(stateModelStub.storeNodeUrl).to.have.been.calledOnceWith(exampleUrl); 70 | expect(nodeUrlDetectedDialogStub).to.have.been.calledOnceWith(exampleUrl); 71 | expect(ret).to.equal(exampleUrl); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/phases/get_node_ip_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import Dialog from '../../src/models/dialog_model'; 16 | import StateModel from '../../src/models/state_model'; 17 | import getNodeIPPhase from '../../src/phases/get_node_ip_phase'; 18 | 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | const {expect} = chai; 22 | 23 | describe('Get Node IP Phase', () => { 24 | let stateModelStub; 25 | let askForNodeIPDialogStub; 26 | let nodeIPDetectedDialogStub; 27 | 28 | const exampleStoredIP = '127.0.0.1'; 29 | const exampleUserProvidedIP = '192.168.0.2'; 30 | 31 | const call = getNodeIPPhase; 32 | 33 | beforeEach(async () => { 34 | stateModelStub = { 35 | getNodeIP: sinon.stub(), 36 | storeNodeIP: sinon.stub() 37 | }; 38 | StateModel.getNodeIP = stateModelStub.getNodeIP; 39 | StateModel.storeNodeIP = stateModelStub.storeNodeIP; 40 | 41 | askForNodeIPDialogStub = sinon.stub(); 42 | nodeIPDetectedDialogStub = sinon.stub(); 43 | 44 | Dialog.askForNodeIPDialog = askForNodeIPDialogStub; 45 | Dialog.nodeIPDetectedDialog = nodeIPDetectedDialogStub; 46 | }); 47 | 48 | it('ends if a IP is already in the store', async () => { 49 | stateModelStub.getNodeIP.resolves(exampleStoredIP); 50 | 51 | await expect(call()).to.eventually.be.equal(exampleStoredIP); 52 | 53 | expect(stateModelStub.getNodeIP).to.have.been.calledOnce; 54 | expect(askForNodeIPDialogStub).to.not.have.been.called; 55 | expect(stateModelStub.storeNodeIP).to.not.have.been.called; 56 | expect(nodeIPDetectedDialogStub).to.have.been.calledOnceWith(exampleStoredIP); 57 | }); 58 | 59 | it('ask the user for a IP address and stores it', async () => { 60 | stateModelStub.getNodeIP.resolves(null); 61 | askForNodeIPDialogStub.resolves({ 62 | nodeIP: exampleUserProvidedIP 63 | }); 64 | 65 | await expect(call()).to.eventually.be.equal(exampleUserProvidedIP); 66 | 67 | expect(stateModelStub.getNodeIP).to.have.been.calledOnce; 68 | expect(askForNodeIPDialogStub).to.have.been.calledOnce; 69 | expect(stateModelStub.storeNodeIP).to.have.been.calledOnceWith(exampleUserProvidedIP); 70 | expect(nodeIPDetectedDialogStub).to.have.been.calledOnceWith(exampleUserProvidedIP); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/phases/get_user_email_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import getUserEmail from '../../src/phases/get_user_email_phase'; 16 | import StateModel from '../../src/models/state_model'; 17 | import Dialog from '../../src/models/dialog_model'; 18 | 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | const {expect} = chai; 22 | 23 | describe('Get User Email Phase', () => { 24 | let stateModelStub; 25 | let askForUserEmailDialogStub; 26 | let userEmailDetectedDialogStub; 27 | 28 | const exampleEmail = 'amb_node_operator@mail.com'; 29 | 30 | const call = getUserEmail; 31 | 32 | beforeEach(async () => { 33 | stateModelStub = { 34 | getUserEmail: sinon.stub(), 35 | storeUserEmail: sinon.stub() 36 | }; 37 | StateModel.getUserEmail = stateModelStub.getUserEmail; 38 | StateModel.storeUserEmail = stateModelStub.storeUserEmail; 39 | 40 | askForUserEmailDialogStub = sinon.stub(); 41 | userEmailDetectedDialogStub = sinon.stub(); 42 | Dialog.askForUserEmailDialog = askForUserEmailDialogStub; 43 | Dialog.userEmailDetectedDialog = userEmailDetectedDialogStub; 44 | }); 45 | 46 | it('ends if a email is already in the store', async () => { 47 | stateModelStub.getUserEmail.resolves(exampleEmail); 48 | 49 | const ret = await call(); 50 | 51 | expect(stateModelStub.getUserEmail).to.have.been.calledOnce; 52 | expect(askForUserEmailDialogStub).to.have.not.been.called; 53 | expect(stateModelStub.storeUserEmail).to.have.not.been.called; 54 | expect(userEmailDetectedDialogStub).to.have.been.calledOnceWith(exampleEmail); 55 | expect(ret).to.equal(exampleEmail); 56 | }); 57 | 58 | it('stores correctly provided email', async () => { 59 | stateModelStub.getUserEmail.resolves(null); 60 | askForUserEmailDialogStub.resolves({ 61 | userEmail: exampleEmail 62 | }); 63 | 64 | const ret = await call(); 65 | 66 | expect(stateModelStub.getUserEmail).to.have.been.calledOnce; 67 | expect(askForUserEmailDialogStub).to.have.been.calledOnce; 68 | expect(stateModelStub.storeUserEmail).to.have.been.calledOnceWith(exampleEmail); 69 | expect(userEmailDetectedDialogStub).to.have.been.calledOnceWith(exampleEmail); 70 | expect(ret).to.equal(exampleEmail); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import StateModel from './models/state_model'; 11 | import SmartContractsModel from './models/smart_contracts_model'; 12 | import Dialog from './models/dialog_model'; 13 | import selectNetworkPhase from './phases/select_network_phase'; 14 | import {selectActionPhase, defaultActions} from './phases/select_action_phase'; 15 | import * as networks from '../config/networks.json'; 16 | import checkDockerAvailablePhase from './phases/check_docker_available_phase'; 17 | import getPrivateKeyPhase from './phases/get_private_key_phase'; 18 | import Crypto from './services/crypto'; 19 | import checkAddressWhitelistingStatusPhase from './phases/check_address_whitelisting_status_phase'; 20 | import selectNodeTypePhase from './phases/select_node_type_phase'; 21 | import {APOLLO} from './consts'; 22 | import getNodeIPPhase from './phases/get_node_ip_phase'; 23 | import getNodeUrlPhase from './phases/get_node_url_phase'; 24 | import getUserEmailPhase from './phases/get_user_email_phase'; 25 | import acceptTosPhase from './phases/accept_tos_phase'; 26 | import manualSubmissionPhase from './phases/manual_submission_phase'; 27 | import performOnboardingPhase from './phases/perform_onboarding_phase'; 28 | import prepareDockerPhase from './phases/prepare_docker_phase'; 29 | 30 | const start = async () => { 31 | Dialog.logoDialog(); 32 | 33 | if (!await checkDockerAvailablePhase()) { 34 | return; 35 | } 36 | const network = await selectNetworkPhase(networks); 37 | const privateKey = await getPrivateKeyPhase(); 38 | 39 | await StateModel.checkStateVariables(); 40 | 41 | Crypto.setWeb3UsingRpc(network.rpc); 42 | Crypto.setAccount(privateKey); 43 | 44 | SmartContractsModel.init(network); 45 | 46 | const whitelistingStatus = await checkAddressWhitelistingStatusPhase(); 47 | 48 | const role = await selectNodeTypePhase(); 49 | if (role === APOLLO) { 50 | await getNodeIPPhase(); 51 | } else { 52 | await getNodeUrlPhase(); 53 | } 54 | await getUserEmailPhase(); 55 | 56 | await acceptTosPhase(); 57 | if (whitelistingStatus === null) { 58 | await manualSubmissionPhase(); 59 | return; 60 | } 61 | 62 | const isOnboarded = await performOnboardingPhase(whitelistingStatus); 63 | if (!isOnboarded) { 64 | return; 65 | } 66 | 67 | await prepareDockerPhase(); 68 | 69 | const isInteractive = process.argv[2] !== 'update'; 70 | if (isInteractive) { 71 | await (selectActionPhase(role, defaultActions))(); 72 | } 73 | }; 74 | 75 | start() 76 | .catch((err) => { 77 | console.error(err); 78 | process.exit(1); 79 | }); 80 | -------------------------------------------------------------------------------- /src/models/atlas_mode_model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import base64url from 'base64url'; 11 | import {ATLAS_1, ATLAS_2, ATLAS_3} from '../consts'; 12 | import StateModel from './state_model'; 13 | import Crypto from '../services/crypto'; 14 | import HttpUtils from '../utils/http_utils'; 15 | 16 | class AtlasModeModel { 17 | async getMode() { 18 | try { 19 | const role = await StateModel.getRole(); 20 | const url = await StateModel.getNodeUrl(); 21 | if (url && (role === ATLAS_1 || role === ATLAS_2 || role === ATLAS_3)) { 22 | const nodeInfo = await HttpUtils.httpGet(`${url}/nodeinfo`); 23 | const {mode} = nodeInfo; 24 | return mode; 25 | } 26 | return {}; 27 | } catch (err) { 28 | return {}; 29 | } 30 | } 31 | 32 | async setMode(mode) { 33 | try { 34 | const role = await StateModel.getRole(); 35 | const url = await StateModel.getNodeUrl(); 36 | if (url && (role === ATLAS_1 || role === ATLAS_2 || role === ATLAS_3)) { 37 | const token = await this.createSetModeToken(mode, 10); 38 | const request = `{"mode":"${token}"}`; 39 | return 200 === (await HttpUtils.httpPost(`${url}/nodeinfo`, request)).statusCode; 40 | } 41 | return false; 42 | } catch (err) { 43 | console.log('I/O error:', err); 44 | return false; 45 | } 46 | } 47 | 48 | async createSetModeToken(mode, accessPeriod, from = Date.now()) { 49 | const idData = { 50 | mode, 51 | createdBy: Crypto.address, 52 | validUntil: Math.floor(from / 1000) + accessPeriod 53 | }; 54 | return base64url(this.serializeForHashing({ 55 | signature: Crypto.account.sign(this.serializeForHashing(idData)).signature, 56 | idData 57 | })); 58 | } 59 | 60 | serializeForHashing(object) { 61 | const isDict = (subject) => typeof subject === 'object' && !Array.isArray(subject); 62 | const isString = (subject) => typeof subject === 'string'; 63 | const isArray = (subject) => Array.isArray(subject); 64 | 65 | if (isDict(object)) { 66 | const content = Object.keys(object).sort() 67 | .map((key) => `"${key}":${this.serializeForHashing(object[key])}`) 68 | .join(','); 69 | return `{${content}}`; 70 | } else if (isArray(object)) { 71 | const content = object.map((item) => this.serializeForHashing(item)).join(','); 72 | return `[${content}]`; 73 | } else if (isString(object)) { 74 | return `"${object}"`; 75 | } 76 | return object.toString(); 77 | } 78 | } 79 | 80 | export default new AtlasModeModel(); 81 | -------------------------------------------------------------------------------- /setup_templates/hermes/ambnet/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | mongodb: 5 | image: mongo:4.2.9 6 | container_name: mongod 7 | restart: unless-stopped 8 | volumes: 9 | - ./data/db:/data/db 10 | - ./data/configdb:/data/configdb 11 | 12 | parity: 13 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.0.7 14 | container_name: parity 15 | command: --config /app/parity_config.toml 16 | working_dir: /app 17 | user: root 18 | restart: unless-stopped 19 | ports: 20 | - '127.0.0.1:8545:8545/tcp' 21 | - '30303:30303/tcp' 22 | - '30303:30303/udp' 23 | volumes: 24 | - ./chain.json:/app/chain.json 25 | - ./parity_config.toml:/app/parity_config.toml 26 | - ./chains:/app/chains 27 | 28 | ethstats-client: 29 | image: ghcr.io/ambrosus/eth-net-intelligence-api 30 | container_name: ethstats_client 31 | restart: unless-stopped 32 | depends_on: 33 | - parity 34 | environment: 35 | RPC_HOST: parity 36 | WS_SERVER: wss://stats-api. 37 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 38 | INSTANCE_NAME: 'hermes ' 39 | 40 | worker: &hermes-props 41 | image: ambrosus/ambrosus-node: 42 | container_name: hermes_worker 43 | command: sh -c 'yarn migrate && yarn start:hermes' 44 | restart: unless-stopped 45 | logging: 46 | options: 47 | max-size: '100m' 48 | max-file: '3' 49 | depends_on: 50 | - mongodb 51 | - parity 52 | environment: 53 | - WEB3_NODEPRIVATEKEY= 54 | - HEAD_CONTRACT_ADDRESS= 55 | - WEB3_RPC=http://parity:8545 56 | - MONGO_HOSTS=mongodb:27017 57 | - MONGO_DB_NAME=ambrosus 58 | - AUTHORIZATION_WITH_SECRET_KEY_ENABLED=false 59 | - NODE_ENV=production 60 | - USE_STATIC=1 61 | - WORKER_INTERVAL= 62 | - DASHBOARD_URL= 63 | - EMAIL_DEFAULT_FROM= 64 | - EMAIL_ORGREQ_TO= 65 | - EMAIL_API_KEY= 66 | - EMAIL_TMPL_ID_INVITE= 67 | - EMAIL_TMPL_ID_ORGREQ= 68 | - EMAIL_TMPL_ID_ORGREQ_APPROVE= 69 | - EMAIL_TMPL_ID_ORGREQ_REFUSE= 70 | 71 | server: 72 | image: ghcr.io/ambrosus/ambrosus-node-extended:v0.1.17 73 | container_name: hermes_server 74 | <<: *hermes-props 75 | command: yarn start:server 76 | logging: 77 | options: 78 | max-size: '100m' 79 | max-file: '3' 80 | volumes: 81 | - ~/ambrosus-nop:/opt/hermes 82 | volumes_from: 83 | - dashboard 84 | depends_on: 85 | - worker 86 | - dashboard 87 | ports: 88 | - 80:3000 89 | 90 | dashboard: 91 | image: ghcr.io/ambrosus/hermes-dashboard:v0.1.1 92 | container_name: dashboard 93 | -------------------------------------------------------------------------------- /setup_templates/hermes/ambnet-dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | mongodb: 5 | image: mongo:4.2.9 6 | container_name: mongod 7 | restart: unless-stopped 8 | volumes: 9 | - ./data/db:/data/db 10 | - ./data/configdb:/data/configdb 11 | 12 | parity: 13 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.0.7 14 | container_name: parity 15 | command: --config /app/parity_config.toml 16 | working_dir: /app 17 | user: root 18 | restart: unless-stopped 19 | ports: 20 | - '127.0.0.1:8545:8545/tcp' 21 | - '30303:30303/tcp' 22 | - '30303:30303/udp' 23 | volumes: 24 | - ./chain.json:/app/chain.json 25 | - ./parity_config.toml:/app/parity_config.toml 26 | - ./chains:/app/chains 27 | 28 | ethstats-client: 29 | image: ghcr.io/ambrosus/eth-net-intelligence-api 30 | container_name: ethstats_client 31 | restart: unless-stopped 32 | depends_on: 33 | - parity 34 | environment: 35 | RPC_HOST: parity 36 | WS_SERVER: wss://stats-api. 37 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 38 | INSTANCE_NAME: 'hermes ' 39 | 40 | worker: &hermes-props 41 | image: ambrosus/ambrosus-node: 42 | container_name: hermes_worker 43 | command: sh -c 'yarn migrate && yarn start:hermes' 44 | restart: unless-stopped 45 | logging: 46 | options: 47 | max-size: '100m' 48 | max-file: '3' 49 | depends_on: 50 | - mongodb 51 | - parity 52 | environment: 53 | - WEB3_NODEPRIVATEKEY= 54 | - HEAD_CONTRACT_ADDRESS= 55 | - WEB3_RPC=http://parity:8545 56 | - MONGO_HOSTS=mongodb:27017 57 | - MONGO_DB_NAME=ambrosus 58 | - AUTHORIZATION_WITH_SECRET_KEY_ENABLED=false 59 | - NODE_ENV=production 60 | - USE_STATIC=1 61 | - WORKER_INTERVAL= 62 | - DASHBOARD_URL= 63 | - EMAIL_DEFAULT_FROM= 64 | - EMAIL_ORGREQ_TO= 65 | - EMAIL_API_KEY= 66 | - EMAIL_TMPL_ID_INVITE= 67 | - EMAIL_TMPL_ID_ORGREQ= 68 | - EMAIL_TMPL_ID_ORGREQ_APPROVE= 69 | - EMAIL_TMPL_ID_ORGREQ_REFUSE= 70 | 71 | server: 72 | image: ghcr.io/ambrosus/ambrosus-node-extended:v0.1.17 73 | container_name: hermes_server 74 | <<: *hermes-props 75 | command: yarn start:server 76 | logging: 77 | options: 78 | max-size: '100m' 79 | max-file: '3' 80 | volumes: 81 | - ~/ambrosus-nop:/opt/hermes 82 | volumes_from: 83 | - dashboard 84 | depends_on: 85 | - worker 86 | - dashboard 87 | ports: 88 | - 80:3000 89 | 90 | dashboard: 91 | image: ghcr.io/ambrosus/hermes-dashboard:v0.1.1 92 | container_name: dashboard 93 | -------------------------------------------------------------------------------- /setup_templates/hermes/ambnet-test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.4' 3 | services: 4 | mongodb: 5 | image: mongo:4.2.9 6 | container_name: mongod 7 | restart: unless-stopped 8 | volumes: 9 | - ./data/db:/data/db 10 | - ./data/configdb:/data/configdb 11 | 12 | parity: 13 | image: ghcr.io/ambrosus/openethereum:v3.3.3-amb1.0.7 14 | container_name: parity 15 | command: --config /app/parity_config.toml 16 | working_dir: /app 17 | user: root 18 | restart: unless-stopped 19 | ports: 20 | - '127.0.0.1:8545:8545/tcp' 21 | - '30303:30303/tcp' 22 | - '30303:30303/udp' 23 | volumes: 24 | - ./chain.json:/app/chain.json 25 | - ./parity_config.toml:/app/parity_config.toml 26 | - ./chains:/app/chains 27 | 28 | ethstats-client: 29 | image: ghcr.io/ambrosus/eth-net-intelligence-api 30 | container_name: ethstats_client 31 | restart: unless-stopped 32 | depends_on: 33 | - parity 34 | environment: 35 | RPC_HOST: parity 36 | WS_SERVER: wss://stats-api. 37 | WS_SECRET: Z2hTiWBUfTNc5o9BAm 38 | INSTANCE_NAME: 'hermes ' 39 | 40 | worker: &hermes-props 41 | image: ambrosus/ambrosus-node: 42 | container_name: hermes_worker 43 | command: sh -c 'yarn migrate && yarn start:hermes' 44 | restart: unless-stopped 45 | logging: 46 | options: 47 | max-size: '100m' 48 | max-file: '3' 49 | depends_on: 50 | - mongodb 51 | - parity 52 | environment: 53 | - WEB3_NODEPRIVATEKEY= 54 | - HEAD_CONTRACT_ADDRESS= 55 | - WEB3_RPC=http://parity:8545 56 | - MONGO_HOSTS=mongodb:27017 57 | - MONGO_DB_NAME=ambrosus 58 | - AUTHORIZATION_WITH_SECRET_KEY_ENABLED=false 59 | - NODE_ENV=production 60 | - USE_STATIC=1 61 | - WORKER_INTERVAL= 62 | - DASHBOARD_URL= 63 | - EMAIL_DEFAULT_FROM= 64 | - EMAIL_ORGREQ_TO= 65 | - EMAIL_API_KEY= 66 | - EMAIL_TMPL_ID_INVITE= 67 | - EMAIL_TMPL_ID_ORGREQ= 68 | - EMAIL_TMPL_ID_ORGREQ_APPROVE= 69 | - EMAIL_TMPL_ID_ORGREQ_REFUSE= 70 | 71 | server: 72 | image: ghcr.io/ambrosus/ambrosus-node-extended:v0.1.17 73 | container_name: hermes_server 74 | <<: *hermes-props 75 | command: yarn start:server 76 | logging: 77 | options: 78 | max-size: '100m' 79 | max-file: '3' 80 | volumes: 81 | - ~/ambrosus-nop:/opt/hermes 82 | volumes_from: 83 | - dashboard 84 | depends_on: 85 | - worker 86 | - dashboard 87 | ports: 88 | - 80:3000 89 | 90 | dashboard: 91 | image: ghcr.io/ambrosus/hermes-dashboard:v0.1.1 92 | container_name: dashboard 93 | -------------------------------------------------------------------------------- /test/actions/payout_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import sinon from 'sinon'; 12 | import sinonChai from 'sinon-chai'; 13 | import chaiAsPromised from 'chai-as-promised'; 14 | import payoutAction from '../../src/menu_actions/payout_action'; 15 | import Dialog from '../../src/models/dialog_model'; 16 | import SmartContractsModel from '../../src/models/smart_contracts_model'; 17 | 18 | chai.use(sinonChai); 19 | chai.use(chaiAsPromised); 20 | const {expect} = chai; 21 | 22 | describe('Payout actions', () => { 23 | let confirmPayoutWithdrawalDialogStub; 24 | let payoutActionCall; 25 | const availablePayoutInWei = '42000000000000000000'; 26 | const availablePayoutInAmb = '42'; 27 | 28 | beforeEach(() => { 29 | confirmPayoutWithdrawalDialogStub = sinon.stub().resolves({payoutConfirmation: true}); 30 | Dialog.confirmPayoutWithdrawalDialog = confirmPayoutWithdrawalDialogStub; 31 | Dialog.availablePayoutDialog = sinon.stub().resolves(); 32 | Dialog.withdrawalSuccessfulDialog = sinon.stub().resolves(); 33 | SmartContractsModel.payoutsActions = { 34 | getTotalAvailablePayout: sinon.stub().resolves(availablePayoutInWei), 35 | withdraw: sinon.stub().resolves() 36 | }; 37 | payoutActionCall = payoutAction(); 38 | }); 39 | 40 | it('always returns false', async () => { 41 | expect(await payoutActionCall()).to.be.false; 42 | }); 43 | 44 | it('shows available payout dialog with amount converted to ambers', async () => { 45 | await payoutActionCall(); 46 | expect(Dialog.availablePayoutDialog).to.be.calledOnceWith(availablePayoutInAmb); 47 | }); 48 | 49 | it('withdraws payout when confirmation was positive', async () => { 50 | await payoutActionCall(); 51 | expect(SmartContractsModel.payoutsActions.withdraw).to.be.calledOnce; 52 | expect(Dialog.withdrawalSuccessfulDialog).to.be.calledOnceWith(availablePayoutInAmb); 53 | }); 54 | 55 | it('does not show successful dialog in case withdrawal fails', async () => { 56 | SmartContractsModel.payoutsActions.withdraw.rejects(); 57 | await expect(payoutActionCall()).to.be.rejected; 58 | expect(Dialog.withdrawalSuccessfulDialog).to.be.not.called; 59 | }); 60 | 61 | it('does not try to withdraw when available payout is 0', async () => { 62 | SmartContractsModel.payoutsActions.getTotalAvailablePayout.resolves('0'); 63 | await payoutActionCall(); 64 | expect(confirmPayoutWithdrawalDialogStub).to.be.not.called; 65 | expect(SmartContractsModel.payoutsActions.withdraw).to.be.not.called; 66 | }); 67 | 68 | it('does not perform withdraw when confirmation was negative', async () => { 69 | confirmPayoutWithdrawalDialogStub.resolves({payoutConfirmation: false}); 70 | await payoutActionCall(); 71 | expect(SmartContractsModel.payoutsActions.withdraw).to.be.not.called; 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/phases/select_node_type_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import selectNodeTypePhase from '../../src/phases/select_node_type_phase'; 16 | import {APOLLO, ATLAS_1} from '../../src/consts'; 17 | import StateModel from '../../src/models/state_model'; 18 | import Dialog from '../../src/models/dialog_model'; 19 | 20 | chai.use(chaiAsPromised); 21 | chai.use(sinonChai); 22 | const {expect} = chai; 23 | 24 | describe('Select Node Type Phase', () => { 25 | let stateModelStub; 26 | let askForNodeTypeDialogStub; 27 | let roleSelectedDialogStub; 28 | let askForApolloDepositDialogStub; 29 | const exampleRole = ATLAS_1; 30 | const exampleDeposit = '123'; 31 | 32 | const call = selectNodeTypePhase; 33 | 34 | beforeEach(async () => { 35 | stateModelStub = { 36 | storeRole: sinon.stub(), 37 | getRole: sinon.stub(), 38 | storeApolloMinimalDeposit: sinon.stub() 39 | }; 40 | StateModel.storeRole = stateModelStub.storeRole; 41 | StateModel.getRole = stateModelStub.getRole; 42 | StateModel.storeApolloMinimalDeposit = stateModelStub.storeApolloMinimalDeposit; 43 | 44 | askForNodeTypeDialogStub = sinon.stub(); 45 | askForApolloDepositDialogStub = sinon.stub().resolves(exampleDeposit); 46 | roleSelectedDialogStub = sinon.stub(); 47 | Dialog.askForNodeTypeDialog = askForNodeTypeDialogStub; 48 | Dialog.askForApolloMinimalDepositDialog = askForApolloDepositDialogStub; 49 | Dialog.roleSelectedDialog = roleSelectedDialogStub; 50 | }); 51 | 52 | it('ends if a role is already in the store', async () => { 53 | stateModelStub.getRole.resolves(exampleRole); 54 | 55 | const ret = await call(); 56 | 57 | expect(stateModelStub.getRole).to.have.been.calledOnce; 58 | expect(askForNodeTypeDialogStub).to.not.have.been.called; 59 | expect(roleSelectedDialogStub).to.have.been.calledOnceWith(exampleRole); 60 | expect(ret).to.equal(exampleRole); 61 | }); 62 | 63 | it('asks and saves deposit value when selected APOLLO', async () => { 64 | stateModelStub.getRole.resolves(null); 65 | askForNodeTypeDialogStub.resolves({ 66 | nodeType: APOLLO 67 | }); 68 | 69 | await call(); 70 | expect(askForApolloDepositDialogStub).to.be.calledOnce; 71 | expect(stateModelStub.storeApolloMinimalDeposit).to.be.calledOnceWith(exampleDeposit); 72 | }); 73 | 74 | it('stores correctly selected role', async () => { 75 | stateModelStub.getRole.resolves(null); 76 | askForNodeTypeDialogStub.resolves({ 77 | nodeType: exampleRole 78 | }); 79 | 80 | const ret = await call(); 81 | 82 | expect(stateModelStub.getRole).to.have.been.calledOnce; 83 | expect(askForNodeTypeDialogStub).to.have.been.calledOnce; 84 | expect(stateModelStub.storeRole).to.have.been.calledOnceWith(exampleRole); 85 | expect(roleSelectedDialogStub).to.have.been.calledOnceWith(exampleRole); 86 | expect(ret).to.equal(exampleRole); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/actions/change_url_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import sinon from 'sinon'; 12 | import sinonChai from 'sinon-chai'; 13 | import chaiAsPromised from 'chai-as-promised'; 14 | import Dialog from '../../src/models/dialog_model'; 15 | import StateModel from '../../src/models/state_model'; 16 | import SmartContractsModel from '../../src/models/smart_contracts_model'; 17 | import changeUrlAction from '../../src/menu_actions/change_url_action'; 18 | 19 | chai.use(sinonChai); 20 | chai.use(chaiAsPromised); 21 | const {expect} = chai; 22 | 23 | describe('Change url action', () => { 24 | let confirmationDialogStub; 25 | let changeUrlActionCall; 26 | const exampleNewUrl = 'http://new-url.com'; 27 | const exampleOldUrl = 'http://old-url.com'; 28 | const exampleAddress = '0xc0ffee'; 29 | 30 | beforeEach(() => { 31 | confirmationDialogStub = sinon.stub().resolves(true); 32 | Dialog.changeUrlConfirmationDialog = confirmationDialogStub; 33 | Dialog.askForNodeUrlDialog = sinon.stub().resolves({nodeUrl: exampleNewUrl}); 34 | Dialog.nectarWarningDialog = sinon.stub().resolves(); 35 | Dialog.changeUrlSuccessfulDialog = sinon.stub().resolves(); 36 | SmartContractsModel.rolesWrapper = { 37 | setNodeUrl: sinon.stub().resolves() 38 | }; 39 | StateModel.getNodeUrl = sinon.stub().resolves(exampleOldUrl); 40 | StateModel.getAddress = sinon.stub().resolves(exampleAddress); 41 | StateModel.storeNodeUrl = sinon.stub().resolves(); 42 | changeUrlActionCall = changeUrlAction(); 43 | }); 44 | 45 | it(`always returns false, as it never ends NOP`, async () => { 46 | expect(await changeUrlActionCall()).to.be.false; 47 | }); 48 | 49 | it('shows nectar warning dialog before asking for url input', async () => { 50 | await changeUrlActionCall(); 51 | expect(Dialog.nectarWarningDialog).to.be.calledBefore(Dialog.askForNodeUrlDialog); 52 | }); 53 | 54 | it('takes new url from dialog and passes it to the roles wrapper', async () => { 55 | await changeUrlActionCall(); 56 | expect(Dialog.askForNodeUrlDialog).to.be.calledOnce; 57 | expect(SmartContractsModel.rolesWrapper.setNodeUrl).to.be.calledOnceWith(exampleAddress, exampleNewUrl); 58 | }); 59 | 60 | it('updates url in state model', async () => { 61 | await changeUrlActionCall(); 62 | expect(StateModel.storeNodeUrl).to.be.calledOnceWith(exampleNewUrl); 63 | }); 64 | 65 | it('does not perform url change if operation was not confirmed', async () => { 66 | confirmationDialogStub.resolves(false); 67 | await changeUrlActionCall(); 68 | expect(SmartContractsModel.rolesWrapper.setNodeUrl).to.be.not.called; 69 | expect(StateModel.storeNodeUrl).to.be.not.called; 70 | expect(Dialog.changeUrlSuccessfulDialog).to.be.not.called; 71 | }); 72 | 73 | it('shows success notification after all actions were performed', async () => { 74 | await changeUrlActionCall(); 75 | expect(Dialog.changeUrlSuccessfulDialog).to.be.calledAfter(SmartContractsModel.rolesWrapper.setNodeUrl); 76 | expect(Dialog.changeUrlSuccessfulDialog).to.be.calledAfter(StateModel.storeNodeUrl); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": {}, 4 | "plugins": [ 5 | "import", 6 | "header", 7 | "@typescript-eslint" 8 | ], 9 | "env": { 10 | "node": true, 11 | "es6": true 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:import/errors", 17 | "plugin:import/warnings" 18 | ], 19 | "settings": { 20 | "import/resolver": { 21 | "node": { 22 | "extensions": [".js", ".ts", ".json", ".d.ts"] 23 | } 24 | } 25 | }, 26 | "rules": { 27 | "prefer-const": 2, 28 | "no-var": 2, 29 | "object-shorthand": 2, 30 | "quote-props": [ 31 | 2, 32 | "as-needed" 33 | ], 34 | "array-callback-return": 2, 35 | "prefer-destructuring": 2, 36 | "prefer-template": 2, 37 | "template-curly-spacing": 2, 38 | "no-eval": 2, 39 | "no-useless-escape": 2, 40 | "wrap-iife": 2, 41 | "no-loop-func": 2, 42 | "prefer-rest-params": 2, 43 | "no-new-func": 2, 44 | "space-before-function-paren": 0, 45 | "space-before-blocks": 2, 46 | "no-param-reassign": 2, 47 | "prefer-spread": 2, 48 | "prefer-arrow-callback": 2, 49 | "arrow-spacing": 2, 50 | "arrow-parens": 2, 51 | "arrow-body-style": 2, 52 | "no-confusing-arrow": 2, 53 | "no-useless-constructor": 2, 54 | "no-dupe-class-members": 2, 55 | "no-duplicate-imports": 2, 56 | "import/no-mutable-exports": 2, 57 | "import/prefer-default-export": 1, 58 | "import/first": 2, 59 | "no-iterator": 2, 60 | "generator-star-spacing": 2, 61 | "dot-notation": 2, 62 | "no-undef": 2, 63 | "one-var": [ 64 | 2, 65 | "never" 66 | ], 67 | "no-multi-assign": 2, 68 | "eqeqeq": 2, 69 | "no-case-declarations": 2, 70 | "no-nested-ternary": 2, 71 | "no-unneeded-ternary": 2, 72 | "no-mixed-operators": 2, 73 | "nonblock-statement-body-position": 2, 74 | "brace-style": 2, 75 | "no-else-return": 2, 76 | "spaced-comment": 2, 77 | "indent": [ 78 | "error", 79 | 2, 80 | { 81 | "SwitchCase": 1 82 | } 83 | ], 84 | "keyword-spacing": 2, 85 | "space-infix-ops": 2, 86 | "eol-last": 2, 87 | "newline-per-chained-call": 2, 88 | "padded-blocks": [ 89 | 2, 90 | "never" 91 | ], 92 | "space-in-parens": 2, 93 | "array-bracket-spacing": 2, 94 | "object-curly-spacing": 2, 95 | "comma-style": 2, 96 | "comma-dangle": 2, 97 | "radix": 2, 98 | "no-new-wrappers": 2, 99 | "id-length": 2, 100 | "camelcase": 2, 101 | "new-cap": 2, 102 | "no-underscore-dangle": 2, 103 | "no-restricted-globals": 2, 104 | "semi": [ 105 | "error", 106 | "always" 107 | ], 108 | "semi-spacing": 2, 109 | "no-unexpected-multiline": 2, 110 | "quotes": [ 111 | "error", 112 | "single", 113 | { 114 | "allowTemplateLiterals": true 115 | } 116 | ], 117 | "no-trailing-spaces": 2, 118 | "no-console": "off", 119 | "import/no-unresolved": "error", 120 | "curly": [ 121 | "error", 122 | "all" 123 | ], 124 | "header/header": [ 125 | 2, 126 | "block", 127 | "\nCopyright: Ambrosus Inc.\nEmail: tech@ambrosus.io\n\nThis Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.\n\nThis Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.\n" 128 | ] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/models/smart_contracts_model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import {APOLLO, ATLAS_1, ATLAS_2, ATLAS_3, HERMES, NO_ROLE_CODE} from '../consts'; 11 | import utils from '../utils/web3_utils'; 12 | import {roleCodeToRole} from '../utils/role_converters'; 13 | import Crypto from '../services/crypto'; 14 | import {HeadWrapper, KycWhitelistWrapper, RolesWrapper, PayoutsWrapper, TimeWrapper, PayoutsActions, OnboardActions, AtlasStakeStoreWrapper} from 'ambrosus-node-contracts'; 15 | import {Network} from '../interfaces/network'; 16 | 17 | class SmartContractsModel { 18 | headWrapper: any = null; 19 | kycWhitelistWrapper: any = null; 20 | rolesWrapper: any = null; 21 | timeWrapper: any = null; 22 | payoutsWrapper: any = null; 23 | atlasStakeWrapper: any = null; 24 | payoutsActions: any = null; 25 | onboardActions: any = null; 26 | 27 | init(network: Network) { 28 | this.headWrapper = new HeadWrapper(network.headContractAddress, Crypto.web3, Crypto.address); 29 | this.kycWhitelistWrapper = new KycWhitelistWrapper(this.headWrapper, Crypto.web3, Crypto.address); 30 | this.rolesWrapper = new RolesWrapper(this.headWrapper, Crypto.web3, Crypto.address); 31 | this.timeWrapper = new TimeWrapper(this.headWrapper, Crypto.web3, Crypto.address); 32 | this.payoutsWrapper = new PayoutsWrapper(this.headWrapper, Crypto.web3, Crypto.address); 33 | this.atlasStakeWrapper = new AtlasStakeStoreWrapper(this.headWrapper, Crypto.web3, Crypto.address); 34 | this.payoutsActions = new PayoutsActions(this.timeWrapper, this.payoutsWrapper); 35 | this.onboardActions = new OnboardActions(this.kycWhitelistWrapper, this.rolesWrapper, this.atlasStakeWrapper); 36 | } 37 | 38 | async isAddressWhitelisted(address) { 39 | return await this.kycWhitelistWrapper.isWhitelisted(address); 40 | } 41 | 42 | async getAddressWhitelistingData(address) { 43 | const requiredDeposit = await this.kycWhitelistWrapper.getRequiredDeposit(address); 44 | const roleAssigned = roleCodeToRole(await this.kycWhitelistWrapper.getRoleAssigned(address), requiredDeposit); 45 | 46 | return {requiredDeposit, roleAssigned}; 47 | } 48 | 49 | async hasEnoughBalance(address, requiredBalance) { 50 | const balance = await Crypto.getBalance(address); 51 | return utils.toBN(requiredBalance) 52 | .lte(balance); 53 | } 54 | 55 | async getOnboardedRole(address) { 56 | const roleCode = await this.rolesWrapper.onboardedRole(address); 57 | if (roleCode === NO_ROLE_CODE) { 58 | return null; 59 | } 60 | const deposit = await this.kycWhitelistWrapper.getRequiredDeposit(address); 61 | return roleCodeToRole(roleCode, deposit); 62 | } 63 | 64 | hashData(data) { 65 | return Crypto.hashData(data); 66 | } 67 | 68 | signMessage(data, privateKey) { 69 | const {signature} = Crypto.sign(data, privateKey); 70 | return signature; 71 | } 72 | 73 | async performOnboarding(address, role, deposit, url) { 74 | switch (role) { 75 | case HERMES: 76 | return this.rolesWrapper.onboardAsHermes(address, url); 77 | case APOLLO: 78 | return this.rolesWrapper.onboardAsApollo(address, deposit); 79 | case ATLAS_1: 80 | case ATLAS_2: 81 | case ATLAS_3: 82 | return this.rolesWrapper.onboardAsAtlas(address, deposit, url); 83 | default: 84 | throw new Error(`Unknown role: ${role}`); 85 | } 86 | } 87 | } 88 | 89 | export default new SmartContractsModel(); 90 | -------------------------------------------------------------------------------- /test/services/crypto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | import Crypto from '../../src/services/crypto'; 15 | 16 | chai.use(chaiAsPromised); 17 | chai.use(sinonChai); 18 | const {expect} = chai; 19 | 20 | describe('Crypto', () => { 21 | const exampleAddress = '0xb8CE9ab6943e0eCED004cDe8e3bBed6568B2Fa01'; 22 | const examplePrivateKey = '0x348ce564d427a3311b6536bbcff9390d69395b06ed6c486954e971d960fe8709'; 23 | let web3Stub; 24 | 25 | beforeEach(async () => { 26 | web3Stub = { 27 | eth: { 28 | accounts: { 29 | create: sinon.stub(), 30 | privateKeyToAccount: sinon.stub(), 31 | encrypt: sinon.stub() 32 | }, 33 | getBalance: sinon.stub() 34 | }, 35 | utils: { 36 | toBN: sinon.stub(), 37 | randomHex: sinon.stub() 38 | } 39 | }; 40 | Crypto.setWeb3(web3Stub); 41 | }); 42 | 43 | describe('generatePrivateKey', () => { 44 | const call = async () => Crypto.generatePrivateKey(); 45 | 46 | it('delegates the generation to web3 internally', async () => { 47 | web3Stub.eth.accounts.create.returns({ 48 | address: exampleAddress, 49 | privateKey: examplePrivateKey 50 | }); 51 | 52 | const ret = await call(); 53 | 54 | expect(web3Stub.eth.accounts.create).to.have.been.calledOnce; 55 | expect(ret).to.equal(examplePrivateKey); 56 | }); 57 | }); 58 | 59 | describe('addressForPrivateKey', () => { 60 | it('delegates the job to web3', async () => { 61 | web3Stub.eth.accounts.privateKeyToAccount.returns({ 62 | address: '0x9876' 63 | }); 64 | await expect(Crypto.addressForPrivateKey('0x1234')).to.eventually.be.equal('0x9876'); 65 | expect(web3Stub.eth.accounts.privateKeyToAccount).to.have.been.calledOnceWith('0x1234'); 66 | }); 67 | }); 68 | 69 | describe('getBalance', () => { 70 | it('calls getBalance on web3', async () => { 71 | const balance = '123'; 72 | const balanceBN = {value: 123}; 73 | web3Stub.eth.getBalance.resolves(balance); 74 | web3Stub.utils.toBN.returns(balanceBN); 75 | 76 | expect(await Crypto.getBalance(exampleAddress)).to.deep.equal(balanceBN); 77 | expect(web3Stub.eth.getBalance).to.be.calledOnceWith(exampleAddress); 78 | expect(web3Stub.utils.toBN).to.be.calledOnceWith(balance); 79 | }); 80 | }); 81 | 82 | describe('getEncryptedWallet', () => { 83 | const password = 'theFunniestPasswordICouldEverImagine'; 84 | const privateKey = '0xdeadface'; 85 | const encryptedWallet = {value: 123}; 86 | 87 | it('calls encrypt on web3', async () => { 88 | web3Stub.eth.accounts.encrypt.returns(encryptedWallet); 89 | 90 | expect(await Crypto.getEncryptedWallet(privateKey, password)).to.deep.equal(encryptedWallet); 91 | expect(web3Stub.eth.accounts.encrypt).to.be.calledOnceWith(privateKey, password); 92 | }); 93 | }); 94 | 95 | describe('getRandomPassword', () => { 96 | it('calls randomHex on web3', async () => { 97 | const randomHex = '0xdeadbeef'; 98 | web3Stub.utils.randomHex.resolves(randomHex); 99 | expect(await Crypto.getRandomPassword()).to.deep.equal(randomHex); 100 | expect(web3Stub.utils.randomHex).to.be.calledOnceWith(32); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/phases/get_private_key_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import Dialog from '../../src/models/dialog_model'; 16 | import StateModel from '../../src/models/state_model'; 17 | import Crypto from '../../src/services/crypto'; 18 | import getPrivateKeyPhase from '../../src/phases/get_private_key_phase'; 19 | 20 | chai.use(chaiAsPromised); 21 | chai.use(sinonChai); 22 | const {expect} = chai; 23 | 24 | describe('Get Private Key Phase', () => { 25 | const examplePrivateKey = '0x0123456701234567012345670123456701234567012345670123456701234567'; 26 | const exampleAddress = '0x012345670123456701234567012345670123456701234567'; 27 | let stateModelStub; 28 | let cryptoStub; 29 | let privateKeyDetectedDialogStub; 30 | let askForPrivateKeyDialogStub; 31 | 32 | const call = getPrivateKeyPhase; 33 | 34 | beforeEach(async () => { 35 | cryptoStub = { 36 | addressForPrivateKey: sinon.stub().resolves(exampleAddress) 37 | }; 38 | Crypto.addressForPrivateKey = cryptoStub.addressForPrivateKey; 39 | 40 | stateModelStub = { 41 | getAddress: sinon.stub(), 42 | storeAddress: sinon.stub(), 43 | getPrivateKey: sinon.stub(), 44 | storePrivateKey: sinon.stub(), 45 | generateAndStoreNewPrivateKey: sinon.stub() 46 | }; 47 | StateModel.getAddress = stateModelStub.getAddress; 48 | StateModel.storeAddress = stateModelStub.storeAddress; 49 | StateModel.getPrivateKey = stateModelStub.getPrivateKey; 50 | StateModel.storePrivateKey = stateModelStub.storePrivateKey; 51 | StateModel.generateAndStoreNewPrivateKey = stateModelStub.generateAndStoreNewPrivateKey; 52 | 53 | privateKeyDetectedDialogStub = sinon.stub().resolves(); 54 | askForPrivateKeyDialogStub = sinon.stub(); 55 | Dialog.privateKeyDetectedDialog = privateKeyDetectedDialogStub; 56 | Dialog.askForPrivateKeyDialog = askForPrivateKeyDialogStub; 57 | }); 58 | 59 | it('ends if a private key is already in the store', async () => { 60 | stateModelStub.getPrivateKey.resolves(examplePrivateKey); 61 | 62 | const ret = await call(); 63 | 64 | expect(stateModelStub.getPrivateKey).to.have.been.calledOnce; 65 | expect(askForPrivateKeyDialogStub).to.not.have.been.called; 66 | expect(cryptoStub.addressForPrivateKey).to.have.been.calledOnceWith(examplePrivateKey); 67 | expect(privateKeyDetectedDialogStub).to.have.been.calledOnceWith(exampleAddress); 68 | expect(ret).to.equal(examplePrivateKey); 69 | }); 70 | 71 | it('generates and stores a new private key (generate option)', async () => { 72 | stateModelStub.getPrivateKey.resolves(null); 73 | askForPrivateKeyDialogStub.resolves({ 74 | source: 'generate' 75 | }); 76 | stateModelStub.generateAndStoreNewPrivateKey.resolves(examplePrivateKey); 77 | 78 | const ret = await call(); 79 | 80 | expect(stateModelStub.getPrivateKey).to.have.been.calledOnce; 81 | expect(askForPrivateKeyDialogStub).to.have.been.calledOnce; 82 | expect(stateModelStub.generateAndStoreNewPrivateKey).to.have.been.calledOnce; 83 | expect(cryptoStub.addressForPrivateKey).to.have.been.calledOnceWith(examplePrivateKey); 84 | expect(privateKeyDetectedDialogStub).to.have.been.calledOnceWith(exampleAddress); 85 | expect(ret).to.equal(examplePrivateKey); 86 | }); 87 | 88 | it('stores the provided private key (manual option)', async () => { 89 | stateModelStub.getPrivateKey.resolves(null); 90 | askForPrivateKeyDialogStub.resolves({ 91 | source: 'manual', 92 | privateKey: examplePrivateKey 93 | }); 94 | stateModelStub.storePrivateKey.resolves(); 95 | 96 | const ret = await call(); 97 | 98 | expect(stateModelStub.getPrivateKey).to.have.been.calledOnce; 99 | expect(askForPrivateKeyDialogStub).to.have.been.calledOnce; 100 | expect(stateModelStub.storePrivateKey).to.have.been.calledOnceWith(examplePrivateKey); 101 | expect(cryptoStub.addressForPrivateKey).to.have.been.calledOnceWith(examplePrivateKey); 102 | expect(privateKeyDetectedDialogStub).to.have.been.calledOnceWith(exampleAddress); 103 | expect(ret).to.equal(examplePrivateKey); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/phases/check_address_whitelisting_status_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import checkAddressWhitelistingStatus from '../../src/phases/check_address_whitelisting_status_phase'; 16 | import Dialog from '../../src/models/dialog_model'; 17 | import StateModel from '../../src/models/state_model'; 18 | import SmartContractsModel from '../../src/models/smart_contracts_model'; 19 | import {ATLAS_1_STAKE, ATLAS_1, HERMES} from '../../src/consts'; 20 | 21 | chai.use(chaiAsPromised); 22 | chai.use(sinonChai); 23 | const {expect} = chai; 24 | 25 | describe('Check Address Whitelisting Status Phase', () => { 26 | const exampleAddress = '0x123'; 27 | let stateModelStub; 28 | let smartContractsModelStub; 29 | 30 | const exampleStatus = { 31 | requiredDeposit: ATLAS_1_STAKE, 32 | roleAssigned: ATLAS_1 33 | }; 34 | 35 | const call = checkAddressWhitelistingStatus; 36 | 37 | beforeEach(async () => { 38 | stateModelStub = { 39 | getRole: sinon.stub(), 40 | storeRole: sinon.stub(), 41 | getAddress: sinon.stub().resolves(exampleAddress) 42 | }; 43 | StateModel.getRole = stateModelStub.getRole; 44 | StateModel.storeRole = stateModelStub.storeRole; 45 | StateModel.getAddress = stateModelStub.getAddress; 46 | 47 | smartContractsModelStub = { 48 | isAddressWhitelisted: sinon.stub(), 49 | getAddressWhitelistingData: sinon.stub() 50 | }; 51 | SmartContractsModel.isAddressWhitelisted = smartContractsModelStub.isAddressWhitelisted; 52 | SmartContractsModel.getAddressWhitelistingData = smartContractsModelStub.getAddressWhitelistingData; 53 | 54 | Dialog.addressIsNotWhitelistedDialog = sinon.stub(); 55 | Dialog.addressIsWhitelistedDialog = sinon.stub(); 56 | }); 57 | 58 | it('ends if user is not whitelisted yet', async () => { 59 | smartContractsModelStub.isAddressWhitelisted.resolves(false); 60 | 61 | const ret = await call(); 62 | 63 | expect(smartContractsModelStub.isAddressWhitelisted).to.have.been.calledOnceWith(exampleAddress); 64 | expect(Dialog.addressIsNotWhitelistedDialog).to.have.been.called; 65 | expect(smartContractsModelStub.getAddressWhitelistingData).to.have.not.been.called; 66 | expect(Dialog.addressIsWhitelistedDialog).to.have.not.been.called; 67 | expect(stateModelStub.getRole).to.have.not.been.called; 68 | expect(ret).to.equal(null); 69 | }); 70 | 71 | it('returns address whitelisting status if address whitelisted already', async () => { 72 | smartContractsModelStub.isAddressWhitelisted.resolves(true); 73 | smartContractsModelStub.getAddressWhitelistingData.resolves(exampleStatus); 74 | stateModelStub.getRole.resolves(ATLAS_1); 75 | 76 | const ret = await call(); 77 | 78 | expect(smartContractsModelStub.isAddressWhitelisted).to.have.been.calledOnce; 79 | expect(Dialog.addressIsNotWhitelistedDialog).to.have.not.been.called; 80 | expect(smartContractsModelStub.getAddressWhitelistingData).to.have.been.calledOnceWith(exampleAddress); 81 | expect(Dialog.addressIsWhitelistedDialog).to.have.been.calledOnceWith(exampleStatus.requiredDeposit, exampleStatus.roleAssigned); 82 | expect(stateModelStub.getRole).to.have.been.calledOnceWith(); 83 | expect(ret).to.deep.equal(exampleStatus); 84 | }); 85 | 86 | it('stores fetched role if no role already in the store', async () => { 87 | smartContractsModelStub.isAddressWhitelisted.resolves(true); 88 | smartContractsModelStub.getAddressWhitelistingData.resolves(exampleStatus); 89 | stateModelStub.getRole.resolves(null); 90 | 91 | await call(); 92 | 93 | expect(stateModelStub.storeRole).to.have.been.calledOnceWith(exampleStatus.roleAssigned); 94 | }); 95 | 96 | it('throws if fetched role does not match already kept in the store', async () => { 97 | smartContractsModelStub.isAddressWhitelisted.resolves(true); 98 | smartContractsModelStub.getAddressWhitelistingData.resolves(exampleStatus); 99 | stateModelStub.getRole.resolves(HERMES); 100 | 101 | expect(call()).to.be.eventually.rejectedWith('Role selected differs from role assigned in whitelist. Please contact Ambrosus Tech Support'); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/phases/select_action_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | import {selectActionPhase} from '../../src/phases/select_action_phase'; 15 | import prepareAction from '../../src/menu_actions/prepare_action'; 16 | import {ATLAS_CODE, HERMES_CODE, HERMES, ATLAS_1} from '../../src/consts'; 17 | import Dialog from '../../src/models/dialog_model'; 18 | 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | const {expect} = chai; 22 | 23 | describe('Select Action Phase', () => { 24 | let selectActionDialogStub; 25 | let insufficientFundsDialogStub; 26 | let errorDialogStub; 27 | let actions; 28 | 29 | const startPhase = async (role) => selectActionPhase(role, actions); 30 | 31 | beforeEach(async () => { 32 | selectActionDialogStub = sinon.stub(); 33 | errorDialogStub = sinon.stub(); 34 | insufficientFundsDialogStub = sinon.stub(); 35 | Dialog.selectActionDialog = selectActionDialogStub; 36 | Dialog.genericErrorDialog = errorDialogStub; 37 | Dialog.insufficientFundsDialog = insufficientFundsDialogStub; 38 | 39 | actions = { 40 | 'First action': prepareAction(sinon.stub().resolves(false), [HERMES_CODE, ATLAS_CODE]), 41 | 'Second action': prepareAction(sinon.stub().resolves(false), [ATLAS_CODE]), 42 | Quit: prepareAction(sinon.stub().resolves(true)) 43 | }; 44 | }); 45 | 46 | it(`assembles hermes' action list and shows select action dialog`, async () => { 47 | selectActionDialogStub.resolves({action: 'First action'}); 48 | selectActionDialogStub.resolves({action: 'Quit'}); 49 | 50 | await (await startPhase(HERMES))(); 51 | 52 | expect(selectActionDialogStub).to.have.been.calledWithExactly(['First action', 'Quit']); 53 | }); 54 | 55 | it(`assembles atlas' action list and shows select action dialog`, async () => { 56 | selectActionDialogStub.onFirstCall().resolves({action: 'First action'}); 57 | selectActionDialogStub.onSecondCall().resolves({action: 'Quit'}); 58 | 59 | await (await startPhase(ATLAS_1))(); 60 | 61 | expect(selectActionDialogStub).to.have.been.calledWithExactly(['First action', 'Second action', 'Quit']); 62 | }); 63 | 64 | it('exits only after Quit action was selected', async () => { 65 | selectActionDialogStub.resolves({action: 'Second action'}); 66 | selectActionDialogStub.onCall(5).resolves({action: 'Quit'}); 67 | 68 | await (await startPhase(ATLAS_1))(); 69 | 70 | expect(selectActionDialogStub).to.have.callCount(6); 71 | }); 72 | 73 | it('calls action associated with selected key on every iteration', async () => { 74 | selectActionDialogStub.onFirstCall().resolves({action: 'First action'}); 75 | selectActionDialogStub.onSecondCall().resolves({action: 'Second action'}); 76 | selectActionDialogStub.onThirdCall().resolves({action: 'Quit'}); 77 | 78 | await (await startPhase(ATLAS_1))(); 79 | 80 | expect(actions['First action'].performAction).to.be.calledOnceWith(); 81 | expect(actions['First action'].performAction).to.be.calledBefore(actions['Second action']); 82 | expect(actions['Second action'].performAction).to.be.calledOnceWith(); 83 | expect(actions['Second action'].performAction).to.be.calledBefore(actions.Quit); 84 | expect(actions.Quit.performAction).to.be.calledOnceWith(); 85 | }); 86 | 87 | describe('In case of errors', () => { 88 | beforeEach(() => { 89 | selectActionDialogStub.onFirstCall().resolves({action: 'First action'}); 90 | selectActionDialogStub.onSecondCall().resolves({action: 'Quit'}); 91 | }); 92 | 93 | it('displays correct dialog when account has insufficient funds', async () => { 94 | actions['First action'].performAction.rejects(new Error('Error: Insufficient funds')); 95 | 96 | await (await startPhase(ATLAS_1))(); 97 | 98 | expect(insufficientFundsDialogStub).to.be.calledOnce; 99 | }); 100 | 101 | it('displays generic error dialog otherwise', async () => { 102 | actions['First action'].performAction.rejects(new Error('TestError')); 103 | 104 | await (await startPhase(ATLAS_1))(); 105 | 106 | expect(errorDialogStub).to.be.calledOnceWith('TestError'); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/services/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | 13 | import Store from '../../src/services/store'; 14 | import {readFile, writeFile, removeFile} from '../../src/utils/file'; 15 | 16 | chai.use(chaiAsPromised); 17 | const {expect} = chai; 18 | 19 | describe('Store', () => { 20 | const testFile = './testStore.json'; 21 | 22 | before(async () => { 23 | Store.storeFilePath = testFile; 24 | }); 25 | 26 | afterEach(async () => { 27 | await removeFile(testFile); 28 | }); 29 | 30 | describe('writing', () => { 31 | it('writes the value under the key in the file', async () => { 32 | const testKey = 'abc'; 33 | const testValue = '12345'; 34 | 35 | await expect(Store.write(testKey, testValue)).to.be.eventually.fulfilled; 36 | const fileContents = JSON.parse(await readFile(testFile)); 37 | expect(fileContents[testKey]).to.be.equal(testValue); 38 | }); 39 | 40 | it('override the value with the key in the file with the new value', async () => { 41 | const testKey = 'abc'; 42 | const testValue1 = '12345'; 43 | const testValue2 = '6421'; 44 | 45 | await expect(Store.write(testKey, testValue1)).to.be.eventually.fulfilled; 46 | await expect(Store.write(testKey, testValue2)).to.be.eventually.fulfilled; 47 | const fileContents = JSON.parse(await readFile(testFile)); 48 | expect(fileContents[testKey]).to.be.equal(testValue2); 49 | }); 50 | 51 | it('providing undefined as value for a key removes it from the store', async () => { 52 | const testKey = 'abc'; 53 | const testValue = '12345'; 54 | 55 | await expect(Store.write(testKey, testValue)).to.be.eventually.fulfilled; 56 | await expect(Store.write(testKey, undefined)).to.be.eventually.fulfilled; 57 | const fileContents = JSON.parse(await readFile(testFile)); 58 | expect(fileContents[testKey]).to.be.undefined; 59 | }); 60 | 61 | it(`doesn't change other values`, async () => { 62 | const testKey1 = 'abc'; 63 | const testKey2 = 'xyz'; 64 | const testValue1 = '12345'; 65 | const testValue2 = '6421'; 66 | 67 | await Store.write(testKey1, testValue1); 68 | await Store.write(testKey2, testValue2); 69 | const fileContents = JSON.parse(await readFile(testFile)); 70 | expect(fileContents[testKey1]).to.be.equal(testValue1); 71 | expect(fileContents[testKey2]).to.be.equal(testValue2); 72 | }); 73 | }); 74 | 75 | describe('reading', () => { 76 | beforeEach(async () => { 77 | const example = { 78 | oneTwoThree: 'test' 79 | }; 80 | await writeFile( 81 | testFile, 82 | JSON.stringify(example), null 83 | ); 84 | }); 85 | 86 | it('returns the value stored under key in the file', async () => { 87 | await expect(Store.read(`oneTwoThree`)).to.eventually.be.fulfilled.and.equal('test'); 88 | }); 89 | 90 | it('throws if requested key is not found in the file', async () => { 91 | await expect(Store.read(`otherKey`)).to.eventually.be.rejected; 92 | }); 93 | }); 94 | 95 | describe('safe reading', () => { 96 | beforeEach(async () => { 97 | const example = { 98 | oneTwoThree: 123 99 | }; 100 | await writeFile( 101 | testFile, 102 | JSON.stringify(example), null 103 | ); 104 | }); 105 | 106 | it('returns the value stored under key in the file', async () => { 107 | expect(await Store.safeRead(`oneTwoThree`)).to.equal(123); 108 | }); 109 | 110 | it('returns null if requested key is not found in the file', async () => { 111 | expect(await Store.safeRead(`otherKey`)).to.be.null; 112 | }); 113 | }); 114 | 115 | describe('checking for key', () => { 116 | const existingKey = 'abc'; 117 | const nonExistingKey = 'xyz'; 118 | 119 | beforeEach(async () => { 120 | await Store.write(existingKey, 'abc'); 121 | }); 122 | 123 | it('returns true if there is a value for the key', async () => { 124 | await expect(Store.has(existingKey)).to.eventually.be.true; 125 | }); 126 | 127 | it('returns false if there is no value for the key', async () => { 128 | await expect(Store.has(nonExistingKey)).to.eventually.be.false; 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/services/setup_creator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import * as path from 'path'; 11 | import {readFile, writeFile, getPath, makeDirectory} from '../utils/file'; 12 | import fileDownload from '../utils/file_download'; 13 | import {config} from '../../config/config'; 14 | 15 | const passwordFileName = 'password.pwds'; 16 | const keyFileName = 'keyfile'; 17 | const dockerFileName = 'docker-compose.yml'; 18 | const parityConfigFileName = 'parity_config.toml'; 19 | const chainDescriptionFileName = 'chain.json'; 20 | const tosFileName = 'TOS.txt'; 21 | const tosTextFileName = 'tos.txt'; 22 | 23 | class SetupCreator { 24 | public templateDirectory: string; 25 | public outputDirectory: string; 26 | 27 | constructor() { 28 | this.templateDirectory = config.templateDirectory; 29 | this.outputDirectory = config.outputDirectory; 30 | 31 | if (!(this.templateDirectory && this.outputDirectory)) { 32 | throw new Error(`Unable to initiate SetupCreator`); 33 | } 34 | } 35 | 36 | async createPasswordFile(password) { 37 | await this.ensureOutputDirectoryExists(); 38 | await writeFile(path.join(this.outputDirectory, passwordFileName), password); 39 | } 40 | 41 | async createKeyFile(encryptedWallet) { 42 | await this.ensureOutputDirectoryExists(); 43 | await writeFile(path.join(this.outputDirectory, keyFileName), JSON.stringify(encryptedWallet, null, 2)); 44 | } 45 | 46 | async readTosText() { 47 | return readFile(tosTextFileName); 48 | } 49 | 50 | async createTosFile(termsOfServiceText) { 51 | await this.ensureOutputDirectoryExists(); 52 | await writeFile(path.join(this.outputDirectory, tosFileName), termsOfServiceText); 53 | } 54 | 55 | async prepareDockerComposeFile( 56 | tag, 57 | nodeTypeName, 58 | address, 59 | privateKey, 60 | headContractAddress, 61 | networkName, 62 | domain, 63 | url?, 64 | mailInfo?, 65 | workerInterval? 66 | ) { 67 | await this.ensureOutputDirectoryExists(); 68 | let dockerFile = await readFile(path.join(this.templateDirectory, nodeTypeName, networkName, dockerFileName)); 69 | dockerFile = dockerFile.replace(//gi, tag); 70 | dockerFile = dockerFile.replace(//gi, address); 71 | dockerFile = dockerFile.replace(//gi, privateKey); 72 | dockerFile = dockerFile.replace(//gi, headContractAddress); 73 | dockerFile = dockerFile.replace(//gi, networkName); 74 | dockerFile = dockerFile.replace(//gi, domain); 75 | 76 | dockerFile = dockerFile.replace(//gi, workerInterval); 77 | 78 | const dashboardUrl = `${url}/dashboard`; 79 | 80 | dockerFile = dockerFile.replace(//gi, dashboardUrl); 81 | 82 | if (mailInfo) { 83 | dockerFile = dockerFile.replace(//gi, mailInfo.from); 84 | dockerFile = dockerFile.replace(//gi, mailInfo.orgRegTo); 85 | dockerFile = dockerFile.replace(//gi, mailInfo.apiKey); 86 | 87 | dockerFile = dockerFile.replace(//gi, mailInfo.templateIds.invite); 88 | dockerFile = dockerFile.replace(//gi, mailInfo.templateIds.orgReq); 89 | dockerFile = dockerFile.replace(//gi, mailInfo.templateIds.orgReqApprove); 90 | dockerFile = dockerFile.replace(//gi, mailInfo.templateIds.orgReqRefuse); 91 | } 92 | 93 | await writeFile(path.join(this.outputDirectory, dockerFileName), dockerFile); 94 | } 95 | 96 | async copyParityConfiguration(nodeTypeName, networkName, values) { 97 | await this.ensureOutputDirectoryExists(); 98 | let parityConfigFile = await readFile(path.join(this.templateDirectory, nodeTypeName, networkName, parityConfigFileName)); 99 | 100 | if (values.address !== undefined) { 101 | parityConfigFile = parityConfigFile.replace(//gi, values.address); 102 | } 103 | 104 | if (values.ip !== undefined) { 105 | parityConfigFile = parityConfigFile.replace(//gi, values.ip); 106 | } 107 | 108 | if (values.extraData !== undefined) { 109 | parityConfigFile = parityConfigFile.replace(//gi, values.extraData); 110 | } 111 | 112 | await writeFile(path.join(this.outputDirectory, parityConfigFileName), parityConfigFile); 113 | } 114 | 115 | async fetchChainJson(chainSpecUrl) { 116 | await this.ensureOutputDirectoryExists(); 117 | const outputFilePath = path.join(this.outputDirectory, chainDescriptionFileName); 118 | await fileDownload(chainSpecUrl, outputFilePath); 119 | const parsedChainJson = JSON.parse(await readFile(outputFilePath)); 120 | return parsedChainJson.name; 121 | } 122 | 123 | async ensureOutputDirectoryExists() { 124 | try { 125 | await getPath(this.outputDirectory); 126 | } catch (error) { 127 | await makeDirectory(this.outputDirectory); 128 | } 129 | } 130 | } 131 | 132 | export default new SetupCreator(); 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/ambrosus/ambrosus-nop.svg?token=3AeQ6aqcxJ7ZUnsz6KJt&branch=master)](https://travis-ci.com/ambrosus/ambrosus-nop) 2 | 3 | # The Ambrosus Node Onboarding Package 4 | The Ambrosus Node Onboarding Package (NOP) is a command-line tool which makes it easier for Ambrosus Masternodes operators to register, onboard and operate new nodes on AMB-NET (Apollo, Atlas and Hermes nodes). 5 | 6 | **ATTENTION:** Although anyone will be able to setup a virtual environment and run NOP, the last step (running the masternode application) will require your node to have been whitelisted. Request will be send automatically at the end of setup script. 7 | 8 | ## Table of Contents 9 | - **[Overview](#overview)** 10 | - **[Installation](#installation)** 11 | - **[Setting up the NOP](#setting-up-the-nop)** 12 | - **[Troubleshooting](#troubleshooting)** 13 | - **[Contribution](#contribution)** 14 | - **[Alternative community guide](#alternative-community-guide)** 15 | 16 | ## Overview 17 | 18 | Running an AMB-NET masternode is a three-step process: 19 | 1. Prepare a virtual machine environment 20 | 2. Run NOP to configure your Masternode 21 | 3. Run the masternode application 22 | 23 | ## Installation 24 | 25 | There are two ways to seup your virtual machine environment to run an Ambrosus masternode: 26 | 27 | 1. Use a ready-made machine image build and maintained by the Ambrosus team 28 | 2. Install the software and dependencies yourself on a machine 29 | 30 | The first option is simpler as it requires less technical experience. However, for obvious security reasons setting a machine from scratch is a more secure approach. 31 | 32 | ##### Creating a Droplet 33 | 34 | Typical DigitalOcean node (2 GB / 2 CPUs, 60 GB SSD disk, 3 TB transfer) should be good for any type of node 35 | 36 | There is detailed step by step information how to setup droplet on digitalocean https://www.digitalocean.com/docs/droplets/how-to/create/ 37 | 38 | Our brief instructions: 39 | 40 | Create an account and log in. Press 'Droplets' and then 'Create Droplet'. Use the OS Ubuntu and then choose what machine preferences and which data center suits you. Then either create a SSH key which you will use to access the instance or if you do not choose one you will get a password to your email. Write a hostname that suits you and launch the instance. 41 | 42 | Now lets setup a firewall for the instance to make sure the instance is accessible only through specific ports. Your instance should be launched and you should see it by pressing 'Droplets'. Click on the instance you launched and then press 'Networking' -> 'Manage Firewalls'. 43 | Add rules for the following ports according to the node you are running: 44 | - Hermes & Atlas: 45 | - Port range: 80 46 | Protocol: **TCP** 47 | - Port range: 443 48 | Protocol: **TCP** 49 | - Port range: 30303 50 | Protocol: **TCP** 51 | - Port range: 30303 52 | Protocol: **UDP** 53 | - Apollo: 54 | - Port range: 30303 55 | Protocol: **TCP** 56 | - Port range: 30303 57 | Protocol: **UDP** 58 | 59 | The source can have the default values. When you have added the ports apply them to your droplet and then create the firewall. 60 | 61 | Every cloud should have a similar setup. 62 | 63 | ## Setting up the NOP 64 | 65 | ##### Important notice 66 | 67 | To avoid issues with installation process, we strongly encourage you to use PC (not android or similar terminals) to install NOP. 68 | 69 | ##### Accessing the Virtual Machine 70 | To access the instance type the following command: 71 | ###### Without SSH key 72 | ``` ssh root@``` 73 | Now enter the password that you received by Email from digital ocean. 74 | ###### With SSH key 75 | ```ssh -i root@``` 76 | 77 | Once you're logged in on your virtual machine, run the following commands (1 line per command): 78 | ``` 79 | 1. source <(curl -s https://nop.ambrosus.io/setup.sh) 80 | ``` 81 | 82 | > **NOTE:** You should execute this script as a root user 83 | 84 | Choose the necessary options: 85 | - network (main); 86 | - input already existing private key or create a new one; 87 | - add IP URL of your droplet (Atlas only) 88 | - enter your email 89 | 90 | Enter manually the required sentence with your name in it (don’t forget to add period in the end of the sentence). 91 | 92 | Afterwards wait till you get whitelisted 93 | 94 | ``` 95 | 2. NB! Please note that you need to have successfully transferred your stake in AMB for it to get whitelisted. 96 | 97 | First you need to connect Metamask to AMB-Net. Instructions are here 98 | https://blog.ambrosus.io/how-to-connect-to-amb-net-9e4d6d002009. 99 | Your address in ETH network = your address in AMB network. 100 | 101 | Amount to transfer = X AMB (your desired stake amount) 102 | + 100 AMB (for challenge before first 28 days period) 103 | 104 | You will be notified once your node gets whitelisted and afterwards you may proceed to step 3. 105 | 106 | 107 | ``` 108 | 3. Once your node whitelisted 109 | 110 | Run ./setup2.sh 111 | 112 | Choose the necessary options on the menu; 113 | Choose “Finish NOP” on menu. 114 | 115 | You are successfully onboarded. 116 | 117 | ## Contribution 118 | We will accept contributions of good code that we can use from anyone. 119 | Please see [CONTRIBUTION.md](CONTRIBUTION.md) 120 | 121 | Before you issue pull request: 122 | * Make sure all tests pass. 123 | * Make sure you have test coverage for any new features. 124 | 125 | ## Troubleshooting 126 | If you are having docker issues [DigitalOcean has a indepth guide for installing docker](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-18-04). 127 | 128 | ## Alternative community guide 129 | 130 | There also detailed alternative intructions made by our community: https://github.com/ambrosus/community-wiki/wiki/Installation-guides 131 | -------------------------------------------------------------------------------- /test/services/validations.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinonChai from 'sinon-chai'; 13 | 14 | import Validations from '../../src/services/validations'; 15 | 16 | chai.use(chaiAsPromised); 17 | chai.use(sinonChai); 18 | const {expect} = chai; 19 | 20 | 21 | describe('Validations', () => { 22 | describe('isValidPrivateKey', () => { 23 | const inputs = { 24 | correct: '0x074976a8D5F07dA5DADa1Eb248AD369a764bB373DADa1Eb248AD369a764bB373', 25 | tooShort: '0x074976a8D5F07dA5DADa1Eb248AD369a764bB373DADa1Eb248AD369a764b373', 26 | tooLong: '0x074976a8D5F07dA5DADa1Eb248AD369a764bB373DADa1Eb248AD369a764b37311', 27 | noPrefix: '074976a8D5F07dA5DADa1Eb248AD369a764bB373DADa1Eb248AD369a764bB373', 28 | notHex: '0x074976a8D5Y07dA5XADa1Eb248AD369a764bB373DADa1Eb248AD369a764bB37Z' 29 | }; 30 | 31 | it('returns true for valid private key', async () => { 32 | expect(Validations.isValidPrivateKey(inputs.correct)).to.be.true; 33 | }); 34 | 35 | it('returns false if private key has wrong length', async () => { 36 | expect(Validations.isValidPrivateKey(inputs.tooShort)).to.be.false; 37 | expect(Validations.isValidPrivateKey(inputs.tooLong)).to.be.false; 38 | }); 39 | 40 | it('returns false if private key has no 0x prefix', async () => { 41 | expect(Validations.isValidPrivateKey(inputs.noPrefix)).to.be.false; 42 | }); 43 | 44 | it('returns false if private key is not a hex value', async () => { 45 | expect(Validations.isValidPrivateKey(inputs.notHex)).to.be.false; 46 | }); 47 | }); 48 | 49 | describe('isValidUrl', () => { 50 | const correctInputs = [ 51 | 'http://ambrosusnode.com', 52 | 'http://ambrosus-node.com', 53 | 'http://ambrosus-node.masternode.com', 54 | 'http://ambrosus-node.com:8080', 55 | 'https://ambrosus-node.com/api', 56 | 'https://ambrosus-node.com/resources/ambnet/app.js' 57 | ]; 58 | 59 | correctInputs.forEach((url) => { 60 | it(`returns true for ${url}`, async () => { 61 | expect(Validations.isValidUrl(url)).to.be.true; 62 | }); 63 | }); 64 | 65 | const incorrectInputs = [ 66 | 'http://ambrosus-node.123', 67 | 'http://ambrosus-node.', 68 | 'http://ambrosus-node', 69 | 'ftp://ambrosus-node.com', 70 | '//ambrosus-node.com/resources/ambnet/accesspoint/app.js', 71 | 'ambrosus-node.com', 72 | 'ambrosus-node' 73 | ]; 74 | 75 | incorrectInputs.forEach((url) => { 76 | it(`returns false for ${url}`, async () => { 77 | expect(Validations.isValidUrl(url)).to.be.false; 78 | }); 79 | }); 80 | }); 81 | 82 | describe('isValidEmail', () => { 83 | const correctInputs = [ 84 | 'amboperator@gmail.com', 85 | 'amb_operator@mail.com', 86 | 'amb.operator@poczta.pl', 87 | 'amb.perator@gmail.subdomain.com', 88 | 'amb-operator@gmail.com', 89 | '123456@gmail.com', 90 | '"amboperator"@gmail.com', 91 | 'amboperator@subdomain.gmail.com', 92 | 'amboperator@gmail.name' 93 | ]; 94 | 95 | correctInputs.forEach((email) => { 96 | it(`returns true for ${email}`, async () => { 97 | expect(Validations.isValidEmail(email)).to.be.true; 98 | }); 99 | }); 100 | 101 | const incorrectInputs = [ 102 | 'amboperator', 103 | '#@%^%#$@#$@#.com', 104 | '@gmail.com', 105 | 'amb operator@gmail.com', 106 | 'amboperator.gmail.com', 107 | 'amboperator@gmail@gmail.com', 108 | '.amboperator@gmail.com', 109 | 'amboperator.@gmail.com', 110 | 'amboperator@gmail..com', 111 | 'amboperator@gmail.com (John Smith)', 112 | 'amboperator@gmail', 113 | 'amb..operator@gmail.com' 114 | ]; 115 | 116 | incorrectInputs.forEach((email) => { 117 | it(`returns false for ${email}`, async () => { 118 | expect(Validations.isValidEmail(email)).to.be.false; 119 | }); 120 | }); 121 | }); 122 | 123 | describe('isValidIP', () => { 124 | const valid = [ 125 | '10.0.0.1', 126 | '192.168.0.1', 127 | '2001:0db8:85a3:0000:0000:8a2e:0370:7334', 128 | '2001:db8:85a3:0:0:8a2e:370:7334', 129 | '2001:db8:85a3::8a2e:370:7334' 130 | ]; 131 | 132 | const invalid = [ 133 | '10.0.0', 134 | '10.0.0.1.2', 135 | '300.0.0.1', 136 | '2001::8a2e::0370:7334', 137 | 'prefix10.0.0.1', 138 | 'prefix 10.0.0.1', 139 | '10.0.0.1sufix', 140 | '10.0.0.1 sufix' 141 | ]; 142 | 143 | valid.forEach((validExample) => 144 | it(`accepts ${validExample}`, () => { 145 | expect(Validations.isValidIP(validExample)).to.be.true; 146 | }) 147 | ); 148 | 149 | invalid.forEach((invalidExample) => 150 | it(`rejects ${invalidExample}`, () => { 151 | expect(Validations.isValidIP(invalidExample)).to.be.false; 152 | }) 153 | ); 154 | }); 155 | 156 | describe('isValidNumber', () => { 157 | const valid = [ 158 | '0', 159 | '5', 160 | '123123', 161 | '3.14', 162 | '1e10', 163 | '-123' 164 | ]; 165 | 166 | const invalid = [ 167 | '', 168 | 'test', 169 | '1a', 170 | 'a1' 171 | ]; 172 | 173 | valid.forEach((validExample) => 174 | it(`accepts ${validExample}`, () => { 175 | expect(Validations.isValidNumber(validExample)).to.be.true; 176 | }) 177 | ); 178 | 179 | invalid.forEach((invalidExample) => 180 | it(`rejects ${invalidExample}`, () => { 181 | expect(Validations.isValidNumber(invalidExample)).to.be.false; 182 | }) 183 | ); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /test/phases/perform_onboarding_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import performOnboardingPhase from '../../src/phases/perform_onboarding_phase'; 16 | import StateModel from '../../src/models/state_model'; 17 | import SmartContractsModel from '../../src/models/smart_contracts_model'; 18 | import Dialog from '../../src/models/dialog_model'; 19 | 20 | chai.use(chaiAsPromised); 21 | chai.use(sinonChai); 22 | const {expect} = chai; 23 | 24 | describe('Perform onboarding Phase', () => { 25 | const exampleAddress = '0xc0ffee'; 26 | const exampleUrl = 'http://url'; 27 | const exampleApolloDeposit = '150'; 28 | let stateModelStub; 29 | let smartContractsModelStub; 30 | let onboardingConfirmationDialogStub; 31 | let askForApolloDepositDialogStub; 32 | 33 | const exampleWhitelistingStatus = { 34 | roleAssigned: 'bar', 35 | requiredDeposit: '123' 36 | }; 37 | 38 | const call = performOnboardingPhase; 39 | 40 | beforeEach(async () => { 41 | stateModelStub = { 42 | getAddress: sinon.stub().resolves(exampleAddress), 43 | getNodeUrl: sinon.stub().resolves(exampleUrl) 44 | }; 45 | StateModel.getAddress = stateModelStub.getAddress; 46 | StateModel.getNodeUrl = stateModelStub.getNodeUrl; 47 | 48 | smartContractsModelStub = { 49 | hasEnoughBalance: sinon.stub().resolves(true), 50 | performOnboarding: sinon.stub(), 51 | getOnboardedRole: sinon.stub().resolves(null) 52 | }; 53 | SmartContractsModel.hasEnoughBalance = smartContractsModelStub.hasEnoughBalance; 54 | SmartContractsModel.performOnboarding = smartContractsModelStub.performOnboarding; 55 | SmartContractsModel.getOnboardedRole = smartContractsModelStub.getOnboardedRole; 56 | 57 | onboardingConfirmationDialogStub = sinon.stub().resolves({onboardingConfirmation: true}); 58 | askForApolloDepositDialogStub = sinon.stub().resolves(exampleApolloDeposit); 59 | Dialog.onboardingConfirmationDialog = onboardingConfirmationDialogStub; 60 | Dialog.askForApolloDepositDialog = askForApolloDepositDialogStub; 61 | Dialog.notEnoughBalanceDialog = sinon.stub().returns(); 62 | Dialog.alreadyOnboardedDialog = sinon.stub().returns(); 63 | Dialog.onboardingSuccessfulDialog = sinon.stub().returns(); 64 | Dialog.insufficientFundsDialog = sinon.stub(); 65 | Dialog.genericErrorDialog = sinon.stub(); 66 | }); 67 | 68 | it('onboarding successful: displays dialogs and calls performOnboarding', async () => { 69 | await call(exampleWhitelistingStatus); 70 | expect(stateModelStub.getAddress).to.be.calledOnce; 71 | expect(smartContractsModelStub.getOnboardedRole).to.be.calledOnceWith(exampleAddress); 72 | expect(smartContractsModelStub.hasEnoughBalance).to.be.calledOnceWith(exampleAddress, exampleWhitelistingStatus.requiredDeposit); 73 | expect(onboardingConfirmationDialogStub).to.be.calledOnceWith(exampleAddress, exampleWhitelistingStatus.roleAssigned, exampleWhitelistingStatus.requiredDeposit); 74 | expect(stateModelStub.getNodeUrl).to.be.calledOnceWith; 75 | expect(smartContractsModelStub.performOnboarding).to.be.calledOnceWith(exampleAddress, exampleWhitelistingStatus.roleAssigned, exampleWhitelistingStatus.requiredDeposit, exampleUrl); 76 | expect(Dialog.onboardingSuccessfulDialog).to.be.calledOnce; 77 | }); 78 | 79 | it('onboarding failed: already onboarded', async () => { 80 | smartContractsModelStub.getOnboardedRole.resolves('Hermes'); 81 | await call(exampleWhitelistingStatus); 82 | expect(Dialog.alreadyOnboardedDialog).to.be.calledOnceWith('Hermes'); 83 | expect(smartContractsModelStub.performOnboarding).to.be.not.called; 84 | }); 85 | 86 | it('onboarding failed: not enough balance', async () => { 87 | smartContractsModelStub.hasEnoughBalance.resolves(false); 88 | await call(exampleWhitelistingStatus); 89 | expect(Dialog.notEnoughBalanceDialog).to.be.calledOnceWith(exampleWhitelistingStatus.requiredDeposit); 90 | expect(smartContractsModelStub.performOnboarding).to.be.not.called; 91 | }); 92 | 93 | it('onboarding failed: not confirmed', async () => { 94 | onboardingConfirmationDialogStub.resolves({onboardingConfirmation: false}); 95 | await call(exampleWhitelistingStatus); 96 | expect(onboardingConfirmationDialogStub).to.be.calledOnce; 97 | expect(smartContractsModelStub.performOnboarding).to.be.not.called; 98 | }); 99 | 100 | it('onboarding failed: insufficient funds', async () => { 101 | smartContractsModelStub.performOnboarding.throws(new Error('Error: Insufficient funds')); 102 | await call(exampleWhitelistingStatus); 103 | expect(Dialog.insufficientFundsDialog).to.be.calledOnce; 104 | expect(Dialog.onboardingSuccessfulDialog).to.be.not.called; 105 | }); 106 | 107 | it('onboarding failed: unknown error', async () => { 108 | smartContractsModelStub.performOnboarding.throws(new Error('Error: Something is not working')); 109 | await call(exampleWhitelistingStatus); 110 | expect(Dialog.genericErrorDialog).to.be.calledOnce; 111 | expect(Dialog.onboardingSuccessfulDialog).to.be.not.called; 112 | }); 113 | 114 | it('asks apollo for a deposit', async () => { 115 | await call({...exampleWhitelistingStatus, roleAssigned: 'Apollo'}); 116 | expect(askForApolloDepositDialogStub).to.be.calledOnceWith(exampleWhitelistingStatus.requiredDeposit); 117 | expect(smartContractsModelStub.hasEnoughBalance).to.be.calledOnceWith(exampleAddress, exampleApolloDeposit); 118 | expect(onboardingConfirmationDialogStub).to.be.calledOnceWith(exampleAddress, 'Apollo', exampleApolloDeposit); 119 | expect(smartContractsModelStub.performOnboarding).to.be.calledOnceWith(exampleAddress, 'Apollo', exampleApolloDeposit, exampleUrl); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | const messages = { 11 | networkQuestion: 'Which network do you want to be onboarded to?', 12 | nodeTypeQuestion: `Which node do you want to run?`, 13 | atlasVersionQuestion: `Which Atlas version do you want to run?`, 14 | apolloName: 'Apollo', 15 | hermesName: 'Hermes', 16 | atlasName: 'Atlas', 17 | atlas1Name: 'Zeta', 18 | atlas2Name: 'Sigma', 19 | atlas3Name: 'Omega', 20 | atlas1Stake: '10k', 21 | atlas2Stake: '30k', 22 | atlas3Stake: '75k', 23 | atlasStakeForm: (amount) => `(${amount} AMB)`, 24 | noPrivateKeyQuestion: `No private key setup yet. What do you want to do?`, 25 | privateKeyManualInputAnswer: 'Input existing key manually', 26 | privateKeyAutoGenerationAnswer: 'Generate new key automatically', 27 | privateKeyInputInstruction: `Please provide your private key (in hex form):`, 28 | privateKeyInputError: (wrongValue) => `${wrongValue} is not a valid private key`, 29 | dockerInstalledInfo: '✅ Docker is installed', 30 | dockerMissingInfo: '⛔ Docker is required, and was not found. Please verify your installation', 31 | privateKeyInfo: (address) => `✅ Private key verified. Your address is ${address}`, 32 | roleSelectionInfo: (role) => `${role} has been selected`, 33 | nodeUrlInputInstruction: 'Please provide URL, which you will be using for your node:', 34 | nodeUrlInputError: (wrongValue) => `${wrongValue} is not a valid URL`, 35 | nodeUrlInfo: (url) => `Node URL defined as ${url}`, 36 | urlPrefixWarning: `start with 'http://' or 'https://'`, 37 | nodeIPInputInstruction: 'Please provide the IP address, which you will be using for your node:', 38 | nodeIPInputError: (wrongValue) => `${wrongValue} is not a valid IP address`, 39 | nodeIPInfo: (ip) => `Node IP defined as ${ip}`, 40 | apolloDepositInputInstruction: (minimalDeposit) => `Please provide the deposit (in AMB). Minimal deposit is: ${minimalDeposit}`, 41 | apolloMinimalDepositInputInstruction: 'Please provide the amount (in AMB) that you would like to deposit.', 42 | depositNumberError: (wrongValue) => `${wrongValue} is not a number`, 43 | depositTooSmallError: (minimalDeposit, wrongValue) => `${wrongValue} must be not smaller than ${minimalDeposit}`, 44 | userEmailInputInstruction: 'Please provide your email address:', 45 | userEmailInputError: (wrongValue) => `${wrongValue} is not a valid email address`, 46 | userEmailInfo: (email) => `Your email address is ${email}`, 47 | submissionInfo: (submissionMail, onboardChannel, tosFilePath) => `To finish requesting process, copy following form and mail it to ${submissionMail} or send to ${onboardChannel} in Slack. 48 | Additionally attach your signed Terms of Service file (${tosFilePath})`, 49 | tosFilePath: 'output/TOS.txt', 50 | submissionMail: 'support@ambrosus.io', 51 | onboardChannel: '#onboarding', 52 | addressNotWhitelisted: 'Address is not whitelisted yet', 53 | addressWhitelisted: (roleAssigned, requiredDeposit) => `✅ Address is whitelisted as ${roleAssigned}. Required deposit is: ${requiredDeposit}`, 54 | unitAmb: 'AMB', 55 | notEnoughBalance: (amount) => `Not enough balance 💸 You need at least ${amount} in order to perform onboarding 💸`, 56 | onboardingInfo: (address, nodeType, warning) => `You will now onboard ${address} as the ${nodeType} node.\n${warning}`, 57 | onboardingWarning: (amount) => `⚠️ WARNING! ⚠️ This operations will cost ${amount}!`, 58 | continueConfirmation: 'Do you want to continue?', 59 | onboardingSuccessful: '🎉 You are now successfully onboarded !!! 🎉', 60 | alreadyOnboarded: (role) => `✅ Onboarded as ${role}`, 61 | networkSelected: (network) => `Network: ${network}`, 62 | healthCheckUrl: () => `After starting your node, you can check its health by going to Explorer: https://explorer.ambrosus.io/`, 63 | dockerComposeInfo: (outputDir, command) => `Your node configuration is ready.\nIn order to start it, enter the ${outputDir} directory from the command line and run ${command}`, 64 | dockerComposeCommand: 'docker-compose up -d', 65 | dockerDownCommand: 'docker-compose down', 66 | yes: 'Yes', 67 | no: 'No', 68 | continue: 'Continue', 69 | continueAtlasRetirement: 'Do you want to continue retirement?', 70 | retirementContinues: 'Retirement continues.', 71 | insufficientFunds: 'You have insufficient funds to perform transaction 💸\n Top up with small amount and retry', 72 | genericError: (message) => `An error occurred: ${message}`, 73 | selectActionQuestion: 'You can now perform one of the following actions', 74 | changeUrlConfirmation: (oldUrl, newUrl) => `You will now change your node URL from ${oldUrl} to ${newUrl}. Do you wish to continue?`, 75 | nectarWarning: '⚠️ WARNING! ⚠️ This operation will cost you some nectar (usually less than 0.05 AMB).', 76 | changeUrlSuccessful: (newUrl) => `Success! Node URL changed to ${newUrl}`, 77 | availablePayouts: (availableAmount) => `You can withdraw ${availableAmount} AMB`, 78 | confirmWithdraw: 'Would you like to withdraw now?', 79 | withdrawalSuccessful: (withdrawnAmount) => `Great! 💰 ${withdrawnAmount} AMB (minus small nectar fee) has been transferred to your account.`, 80 | retirementStartSuccessful: 'Retirement process for you Atlas was started.', 81 | retirementStop: 'You Atlas was switched to normal operational mode.', 82 | confirmRetirement: `After retirement this node will stop being part of the network. 83 | You will still be able to onboard it again. 84 | Do you want to continue?`, 85 | retirementSuccessful: (dockerDownCommand, outputDir) => ` 86 | Your node has retired. 87 | Don't forget to turn off the node. You can do it by running ${dockerDownCommand} inside the ${outputDir} directory. 88 | See you later!`, 89 | warningMessage: '⚠️ WARNING! ⚠️', 90 | dockerRestartRequired: 'Changes in network have been detected. Please restart the docker containers with', 91 | acceptTos: 'In order to get whitelisted you must accept the Terms of Service', 92 | tosConfirmationInputInstruction: 'Please write the following sentence: "I, [firstname lastname], read and agreed with the terms and conditions above."', 93 | tosConfirmationInputError: (wrongValue) => `${wrongValue} is not a valid confirmation`, 94 | actions: { 95 | payouts: 'Payouts', 96 | changeUrl: 'Change node URL', 97 | retire: 'Retire', 98 | quit: 'Finish NOP' 99 | } 100 | }; 101 | 102 | export default messages; 103 | -------------------------------------------------------------------------------- /USING.md: -------------------------------------------------------------------------------- 1 | # Using Ambrosus NOP 2 | 3 | After having successfully installed Ambrosus Node, you have an `ambrosus-nop` 4 | directory that contains all files related to the Node. To run configuration 5 | script you have to change the directory first: 6 | 7 | cd ~/ambrosus-nop 8 | 9 | and then run configuration script: 10 | 11 | yarn start 12 | 13 | The script checks basic node details, shows notifications if something is wrong 14 | and offers several options: 15 | - **Change node URL** - if you hold an Atlas, you can change its URL 16 | - **Payouts** - request Atlas reward payout 17 | - **Retire** - retire Node 18 | - **Finish NOP** - exit configuration console 19 | 20 | Current status of the Node can be viewed at /nodeinfo URL (if you hold an Atlas 21 | or Hermes), for example https://my-cool-amb-node.com/nodeinfo 22 | 23 | To update Ambrosus node software, run the script: 24 | 25 | cd ~/ambrosus-nop 26 | ./update.sh 27 | 28 | ## Directories and important files 29 | 30 | - **state.json** - this file contains all your node configuration, including 31 | private key, _do not share this file with anyone_. 32 | - **output** (directory) - contains files that were created during the 33 | installation process. 34 | - **output/parity\_config.toml** - Parity config. NB! Please change this file 35 | only if you know what you're doing. 36 | - **output/docker-compose.yml** - Docker Compose config. Each part of Ambrosus 37 | Node runs in its own docker container. This file defines what and how will be 38 | run. 39 | - **output/chain.json** - AMB-NET blockchain specification. NB! Please do 40 | not change this file. 41 | - **output/chains** (directory) - Blockchain data. 42 | - **output/data** (directory) - Mongo DB data. 43 | 44 | ## Diagnostics 45 | 46 | We provided a script to diagnose common problems. Run it and follow its 47 | instructions. 48 | 49 | source <(curl -s https://nop.ambrosus.io/check.sh) 50 | 51 | In order to see diagnostics results and try to solve problems by oneself, one 52 | can use the following: 53 | 54 | ### Nodeinfo 55 | 56 | **/nodeinfo** URL (for Atlas and Hermes nodes) is the primary source of 57 | information. If it fails to open or returns some error - the Node is not 58 | operating properly. Example /nodeinfo output: 59 | 60 | ```json 61 | { 62 | "commit": "995579b", 63 | "version": "1.1.1", 64 | "nodeAddress": "0x4b8987bAb7d87bAb7d4b898aa0e7587bAb7d8aa0", 65 | "mode": { 66 | "mode": "normal" 67 | }, 68 | "workerLogs": [ 69 | { 70 | "timestamp": "2019-09-10T11:52:22.734Z", 71 | "message": "Bundle fetched", 72 | "blockNumber": 2399834, 73 | "logIndex": 0, 74 | "transferId": "0x9048cfae83b83bea6a7cf9bf9956237cf9ea6a7cf9bf9956237cf9380d0ac41c", 75 | "donorId": "0x87bAb7d4b89887bAb7d87bA98aa0e7587bAb7d81", 76 | "bundleId": "0x85f430add511074c7a1cb341eadbc5dfdb123f9956237cf9feaa610f894cbd42", 77 | "count": 1 78 | }, 79 | "more messages.." 80 | ] 81 | } 82 | ``` 83 | 84 | **commit** - git commit hash of Ambrosus Node software 85 | **version** - Ambrosus Node version 86 | **workerLogs** - latest log messages. _Please note_, errors in this log don't 87 | mean that something is wrong, this log is mainly for developers. 88 | 89 | ### Logs 90 | 91 | cd ~/ambrosus-nop/output 92 | docker-compose logs --tail 25 93 | 94 | This command shows logs of all components. You can specify number of log 95 | messages to output in **--tail** argument. 96 | 97 | There are several components (for Atlas): 98 | 99 | - atlas\_worker - downloads and stores bundles 100 | - atlas\_server - provides public API, shows /nodeinfo 101 | - mongod - Mongo database stores bundles and other data for Atlas 102 | - parity - blockchain node, atlas\_worker use it to interact with AMB-NET 103 | 104 | Let's talk about Parity logs. 105 | 106 | parity | 2019-09-10 12:32:05 UTC Verifier #0 INFO import Imported #1912475 0xdaf2…be13 (1 txs, 0.02 Mgas, 2 ms, 0.67 KiB) 107 | parity | 2019-09-10 12:32:10 UTC Verifier #0 INFO import Imported #1912476 0x3871…6230 (0 txs, 0.00 Mgas, 4 ms, 0.56 KiB) 108 | 109 | As you can see Parity imports blocks from network. If everything goes fine, you 110 | should see new block every 5 seconds. And the latest block number in your 111 | Parity logs should match the one in 112 | [Ambrosus Explorer](https://explorer.ambrosus.io/). 113 | If it doesn't - your Parity is not in sync and the Node is not operating. 114 | 115 | ## Problems and their fixes 116 | 117 | First of all **check.sh** script provides some fixes. If it doesn't help or you 118 | want to try manual fixes, you can try the following: 119 | 120 | ### Parity not in sync 121 | 122 | When you see in Parity logs that it stoped syncing (the same block number 123 | repeats at least for several minutes), run the following: 124 | 125 | cd ~/ambrosus-nop/output 126 | docker stop parity 127 | rm -rf chains 128 | curl -s https://backup.ambrosus.io/blockchain.tgz | tar zxpf - 129 | docker start parity 130 | 131 | ### Useful docker-compose commands 132 | 133 | Show container status: 134 | 135 | docker-compose ps 136 | 137 | Show container logs: 138 | 139 | docker-compose logs parity 140 | 141 | Watch logs: 142 | 143 | docker-compose logs -f 144 | 145 | Restart all containers: 146 | 147 | docker-compose restart 148 | 149 | Restart specific container: 150 | 151 | docker-compose restart parity 152 | 153 | Recreate containers (safe, but do it if you're sure that you need it): 154 | 155 | docker-compose down 156 | docker-compose up -d 157 | 158 | ### Out of memory 159 | 160 | If logs contain messages about memory issues, it means that your instance or 161 | server don't hold enough memory. Try to increase it. We recommend at least 2GB 162 | of memory to run an Ambrosus Node. 163 | 164 | ### Instance not responding 165 | 166 | You can't access /nodeinfo url or login via ssh - restart your instance. 167 | 168 | ### No challenge resolution on Atlas, no block reward on Apollo 169 | 170 | Atlas: first check if Parity is in sync (see above) 171 | 172 | Apollo: also check Parity. If it's fine, wait for reward to appear (it could 173 | take up to 24 hours). Don't stop and try not to restart Apollo, it has to be 174 | always online. 175 | 176 | ## Terminology 177 | 178 | - **Ambrosus Node** - Ambrosus Atlas, Hermes or Apollo. 179 | - **Atlas** - node that stores bundles and earns ambers. 180 | - **Hermes** - node that generates bundles and spends ambers. 181 | - **Apollo** - validator node, it creates blocks. 182 | - **Bundle** - pack of Assets and Events, produced by Hermes 183 | - **Asset** - object, for example product 184 | - **Events** - events that happen to Assets 185 | - **Challenge** - Atlas "request" to store a bundle, if smart-contract confirms 186 | this request, challenge turns into "resolved". 187 | -------------------------------------------------------------------------------- /test/phases/select_network_phase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import selectNetworkPhase from '../../src/phases/select_network_phase'; 16 | import StateModel from '../../src/models/state_model'; 17 | import Dialog from '../../src/models/dialog_model'; 18 | 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | const {expect} = chai; 22 | 23 | describe('Select Network Phase', () => { 24 | let stateModelStub; 25 | let askForNetworkDialogStub; 26 | let networkSelectedDialogStub; 27 | let dockerRestartRequiredDialogStub; 28 | 29 | const exampleAvailableNetworks = { 30 | test: { 31 | rpc: 'test', 32 | chainspec: 'https://chainspec.ambrosus-test.io', 33 | headContractAddress: '0x0', 34 | dockerTag: '1' 35 | }, 36 | dev: { 37 | rpc: 'dev', 38 | chainspec: 'https://chainspec.ambrosus-dev.io', 39 | headContractAddress: '0x1', 40 | dockerTag: '2' 41 | } 42 | }; 43 | const exampleStoredNetwork = { 44 | ...exampleAvailableNetworks.test, 45 | name: 'test' 46 | }; 47 | 48 | const call = selectNetworkPhase; 49 | 50 | beforeEach(async () => { 51 | stateModelStub = { 52 | storeNetwork: sinon.stub(), 53 | getNetwork: sinon.stub().resolves(null) 54 | }; 55 | StateModel.storeNetwork = stateModelStub.storeNetwork; 56 | StateModel.getNetwork = stateModelStub.getNetwork; 57 | 58 | askForNetworkDialogStub = sinon.stub().resolves({ 59 | network: exampleStoredNetwork.name 60 | }); 61 | networkSelectedDialogStub = sinon.stub(); 62 | dockerRestartRequiredDialogStub = sinon.stub(); 63 | 64 | Dialog.askForNetworkDialog = askForNetworkDialogStub; 65 | Dialog.networkSelectedDialog = networkSelectedDialogStub; 66 | Dialog.dockerRestartRequiredDialog = dockerRestartRequiredDialogStub; 67 | }); 68 | 69 | it('stores selected network', async () => { 70 | const phaseResult = await call(exampleAvailableNetworks); 71 | 72 | expect(stateModelStub.getNetwork).to.have.been.calledOnce; 73 | expect(askForNetworkDialogStub).to.have.been.calledOnceWith(['test', 'dev']); 74 | expect(stateModelStub.storeNetwork).to.have.been.calledOnceWith(exampleStoredNetwork); 75 | expect(networkSelectedDialogStub).to.have.been.calledOnceWith(exampleStoredNetwork.name); 76 | expect(dockerRestartRequiredDialogStub).to.be.not.called; 77 | expect(phaseResult).to.deep.equal(exampleStoredNetwork); 78 | }); 79 | 80 | it('skips selection dialog if correct network is already in the store', async () => { 81 | stateModelStub.getNetwork.resolves(exampleStoredNetwork); 82 | 83 | const phaseResult = await call(exampleAvailableNetworks); 84 | 85 | expect(stateModelStub.getNetwork).to.have.been.calledOnce; 86 | expect(askForNetworkDialogStub).to.not.have.been.called; 87 | expect(stateModelStub.storeNetwork).to.not.have.been.called; 88 | expect(networkSelectedDialogStub).to.have.been.calledOnceWith(exampleStoredNetwork.name); 89 | expect(dockerRestartRequiredDialogStub).to.be.not.called; 90 | expect(phaseResult).to.deep.equal(exampleStoredNetwork); 91 | }); 92 | 93 | for (const field of ['rpc', 'chainspec', 'headContractAddress', 'dockerTag']) { 94 | // eslint-disable-next-line no-loop-func 95 | it(`stores network without showing selection dialog when network without ${field} was stored`, async () => { 96 | stateModelStub.getNetwork.resolves({...exampleStoredNetwork, [field]: null}); 97 | const phaseResult = await call(exampleAvailableNetworks); 98 | expect(askForNetworkDialogStub).to.be.not.called; 99 | expect(stateModelStub.storeNetwork).to.have.been.calledOnceWith(exampleStoredNetwork); 100 | expect(dockerRestartRequiredDialogStub).to.be.calledOnce; 101 | expect(phaseResult).to.deep.equal(exampleStoredNetwork); 102 | }); 103 | } 104 | 105 | for (const field of ['rpc', 'chainspec', 'headContractAddress', 'dockerTag']) { 106 | // eslint-disable-next-line no-loop-func 107 | it(`stores network without showing selection dialog when ${field} has changed in config before`, async () => { 108 | stateModelStub.getNetwork.resolves({...exampleStoredNetwork, [field]: 12}); 109 | const phaseResult = await call(exampleAvailableNetworks); 110 | expect(askForNetworkDialogStub).to.be.not.called; 111 | expect(dockerRestartRequiredDialogStub).to.be.calledOnce; 112 | expect(stateModelStub.storeNetwork).to.have.been.calledOnceWith(exampleStoredNetwork); 113 | expect(phaseResult).to.deep.equal(exampleStoredNetwork); 114 | }); 115 | } 116 | 117 | it('shows selection dialog when network with same name as currently stored one is not available anymore', async () => { 118 | stateModelStub.getNetwork.resolves({...exampleStoredNetwork, name: 'bar'}); 119 | 120 | const phaseResult = await call(exampleAvailableNetworks); 121 | 122 | expect(stateModelStub.getNetwork).to.have.been.calledOnce; 123 | expect(askForNetworkDialogStub).to.have.been.calledOnceWith(['test', 'dev']); 124 | expect(stateModelStub.storeNetwork).to.have.been.calledOnceWith(exampleStoredNetwork); 125 | expect(networkSelectedDialogStub).to.have.been.calledOnceWith(exampleStoredNetwork.name); 126 | expect(dockerRestartRequiredDialogStub).to.be.calledOnce; 127 | expect(phaseResult).to.deep.equal(exampleStoredNetwork); 128 | }); 129 | 130 | it('shows selection dialog when network with missing name was stored', async () => { 131 | stateModelStub.getNetwork.resolves({...exampleStoredNetwork, name: undefined}); 132 | 133 | const phaseResult = await call(exampleAvailableNetworks); 134 | 135 | expect(stateModelStub.getNetwork).to.have.been.calledOnce; 136 | expect(askForNetworkDialogStub).to.have.been.calledOnceWith(['test', 'dev']); 137 | expect(stateModelStub.storeNetwork).to.have.been.calledOnceWith(exampleStoredNetwork); 138 | expect(networkSelectedDialogStub).to.have.been.calledOnceWith(exampleStoredNetwork.name); 139 | expect(dockerRestartRequiredDialogStub).to.be.calledOnce; 140 | expect(phaseResult).to.deep.equal(exampleStoredNetwork); 141 | }); 142 | 143 | it('skips selection dialog if only one network is available', async () => { 144 | const phaseResult = await call({test: exampleAvailableNetworks.test}); 145 | 146 | expect(stateModelStub.getNetwork).to.have.been.calledOnce; 147 | expect(askForNetworkDialogStub).to.not.have.been.called; 148 | expect(stateModelStub.storeNetwork).to.have.been.calledOnceWith(exampleStoredNetwork); 149 | expect(dockerRestartRequiredDialogStub).to.be.not.called; 150 | expect(networkSelectedDialogStub).to.have.been.calledOnceWith(exampleStoredNetwork.name); 151 | expect(phaseResult).to.deep.equal(exampleStoredNetwork); 152 | }); 153 | 154 | it('throws error if no networks are available and network is not stored', async () => { 155 | await expect(call({})).to.be.rejected; 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/models/atlas_mode_model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import chaiAsPromised from 'chai-as-promised'; 12 | import sinon from 'sinon'; 13 | import sinonChai from 'sinon-chai'; 14 | 15 | import Web3 from 'web3'; 16 | import {ATLAS_1, APOLLO, HERMES} from '../../src/consts'; 17 | import AtlasModeModel from '../../src/models/atlas_mode_model'; 18 | import StateModel from '../../src/models/state_model'; 19 | import HttpUtils from '../../src/utils/http_utils'; 20 | import Crypto from '../../src/services/crypto'; 21 | 22 | chai.use(chaiAsPromised); 23 | chai.use(sinonChai); 24 | const {expect} = chai; 25 | 26 | const privateKey = '0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7'; 27 | 28 | describe('Atlas Mode Model', () => { 29 | let web3; 30 | let account; 31 | let httpUtilsMock; 32 | let stateModelStub; 33 | let atlasModeModel; 34 | 35 | before(async () => { 36 | web3 = new Web3(); 37 | account = web3.eth.accounts.privateKeyToAccount(privateKey); 38 | web3.eth.accounts.wallet.add(account); 39 | web3.eth.defaultAccount = account.address; 40 | Crypto.setAccount(privateKey); 41 | }); 42 | 43 | beforeEach(() => { 44 | stateModelStub = { 45 | getRole: sinon.stub(), 46 | getNodeUrl: sinon.stub() 47 | }; 48 | StateModel.getRole = stateModelStub.getRole; 49 | StateModel.getNodeUrl = stateModelStub.getNodeUrl; 50 | 51 | httpUtilsMock = { 52 | httpPost: sinon.stub(), 53 | httpGet: sinon.stub() 54 | }; 55 | HttpUtils.httpGet = httpUtilsMock.httpGet; 56 | HttpUtils.httpPost = httpUtilsMock.httpPost; 57 | 58 | atlasModeModel = AtlasModeModel; 59 | }); 60 | 61 | describe('getMode', () => { 62 | it('returns empty for empty URL', async () => { 63 | stateModelStub.getRole.resolves(ATLAS_1); 64 | stateModelStub.getNodeUrl.resolves(''); 65 | const mode = await atlasModeModel.getMode(); 66 | expect(mode).to.be.deep.equal({}); 67 | expect(httpUtilsMock.httpGet).to.not.be.called; 68 | }); 69 | 70 | it('returns empty for APOLLO', async () => { 71 | stateModelStub.getRole.resolves(APOLLO); 72 | stateModelStub.getNodeUrl.resolves('http://atlas.ambrosus.io'); 73 | const mode = await atlasModeModel.getMode(); 74 | expect(mode).to.be.deep.equal({}); 75 | expect(httpUtilsMock.httpGet).to.not.be.called; 76 | }); 77 | 78 | it('returns empty for HERMES', async () => { 79 | stateModelStub.getRole.resolves(HERMES); 80 | stateModelStub.getNodeUrl.resolves('http://atlas.ambrosus.io'); 81 | const mode = await atlasModeModel.getMode(); 82 | expect(mode).to.be.deep.equal({}); 83 | expect(httpUtilsMock.httpGet).to.not.be.called; 84 | }); 85 | 86 | it('returns "normal" mode by http', async () => { 87 | stateModelStub.getRole.resolves(ATLAS_1); 88 | stateModelStub.getNodeUrl.resolves('http://atlas.ambrosus.io'); 89 | httpUtilsMock.httpGet.resolves(JSON.parse('{"mode":{"mode":"normal"}}')); 90 | const mode = await atlasModeModel.getMode(); 91 | expect(mode).to.be.deep.equal({mode:'normal'}); 92 | }); 93 | 94 | it('returns "normal" mode by https', async () => { 95 | stateModelStub.getRole.resolves(ATLAS_1); 96 | stateModelStub.getNodeUrl.resolves('https://atlas.ambrosus.io'); 97 | httpUtilsMock.httpGet.resolves(JSON.parse('{"mode":{"mode":"normal"}}')); 98 | const mode = await atlasModeModel.getMode(); 99 | expect(mode).to.be.deep.equal({mode:'normal'}); 100 | }); 101 | 102 | it('returns {} by error', async () => { 103 | stateModelStub.getRole.resolves(ATLAS_1); 104 | stateModelStub.getNodeUrl.resolves('https://atlas.ambrosus.io'); 105 | httpUtilsMock.httpGet.throws(); 106 | const mode = await atlasModeModel.getMode(); 107 | expect(mode).to.be.deep.equal({}); 108 | }); 109 | }); 110 | 111 | describe('setMode', () => { 112 | it('returns false for empty URL', async () => { 113 | stateModelStub.getRole.resolves(ATLAS_1); 114 | stateModelStub.getNodeUrl.resolves(''); 115 | expect(await atlasModeModel.setMode()).to.be.false; 116 | expect(httpUtilsMock.httpGet).to.not.be.called; 117 | }); 118 | 119 | it('returns false for APOLLO', async () => { 120 | stateModelStub.getRole.resolves(APOLLO); 121 | stateModelStub.getNodeUrl.resolves('http://atlas.ambrosus.io'); 122 | expect(await atlasModeModel.setMode()).to.be.false; 123 | expect(httpUtilsMock.httpGet).to.not.be.called; 124 | }); 125 | 126 | it('returns false for HERMES', async () => { 127 | stateModelStub.getRole.resolves(HERMES); 128 | stateModelStub.getNodeUrl.resolves('http://atlas.ambrosus.io'); 129 | expect(await atlasModeModel.setMode()).to.be.false; 130 | expect(httpUtilsMock.httpGet).to.not.be.called; 131 | }); 132 | 133 | it('returns false by http error', async () => { 134 | stateModelStub.getRole.resolves(ATLAS_1); 135 | stateModelStub.getNodeUrl.resolves('http://atlas.ambrosus.io'); 136 | httpUtilsMock.httpPost.resolves(false); 137 | expect(await atlasModeModel.setMode('normal')).to.be.false; 138 | expect(httpUtilsMock.httpPost).to.be.called; 139 | }); 140 | 141 | it('returns false by https error', async () => { 142 | stateModelStub.getRole.resolves(ATLAS_1); 143 | stateModelStub.getNodeUrl.resolves('https://atlas.ambrosus.io'); 144 | httpUtilsMock.httpPost.resolves(false); 145 | expect(await atlasModeModel.setMode('normal')).to.be.false; 146 | expect(httpUtilsMock.httpPost).to.be.called; 147 | }); 148 | }); 149 | 150 | describe('serializeForHashing', () => { 151 | it('check serialize', async () => { 152 | expect(atlasModeModel.serializeForHashing({mode: 'normal', 153 | createdBy: '0x00a329c0648769A73afAc7F9381E08FB43dBEA72', 154 | validUntil: 1500010})).to.be.equal('{"createdBy":"0x00a329c0648769A73afAc7F9381E08FB43dBEA72","mode":"normal","validUntil":1500010}'); 155 | }); 156 | }); 157 | 158 | describe('createSetModeToken', () => { 159 | it('create token for normal', async () => { 160 | expect(await atlasModeModel.createSetModeToken('normal', 10, 1500000000)).to.be.equal('eyJpZERhdGEiOnsiY3JlYXRlZEJ5IjoiMHgwMGEzMjljMDY0ODc2OUE3M2FmQWM3RjkzODFFMDhGQjQzZEJFQTcyIiwibW9kZSI6Im5vcm1hbCIsInZhbGlkVW50aWwiOjE1MDAwMTB9LCJzaWduYXR1cmUiOiIweDBmODVjNjU3YzZhNzBiNTRlMWVhYzk2ZjU5OTU1N2U5YWRhYzU0ZGUyNmY4ZWNhOGE1ODkwZmEyNTViOGVlZWI3NTI0N2Y1ZWMxOTRiYTJlOTAwMzc5ODg1YjRhMGFmZjYzYmIxMzQxMDY0YmRkODFiZjFiYzk0MzZmMzU1OWVhMWMifQ'); 161 | }); 162 | 163 | it('create token for retire', async () => { 164 | expect(await atlasModeModel.createSetModeToken('retire', 10, 1500000000)).to.be.equal('eyJpZERhdGEiOnsiY3JlYXRlZEJ5IjoiMHgwMGEzMjljMDY0ODc2OUE3M2FmQWM3RjkzODFFMDhGQjQzZEJFQTcyIiwibW9kZSI6InJldGlyZSIsInZhbGlkVW50aWwiOjE1MDAwMTB9LCJzaWduYXR1cmUiOiIweDU1OTYyNmIxNGQwNzY3MjM3Y2QyOTYxYTNjNjgxNDE2NzU2ZTQ5ODNlY2QwMjA5MmJhNWYwYzg3MzkzMGViNGUzODE4YTY3MzE1ZjczZjU4NWE4Y2VhMTdjOWUyNTg0OTU0YWMxY2Y2ZmJiNDdjZjFmYTE2MTYwOGQzY2M2ZTkwMWIifQ'); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /src/models/state_model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import * as path from 'path'; 11 | import {readFile} from '../utils/file'; 12 | import {HERMES, APOLLO, ATLAS_1, ATLAS_2, ATLAS_3} from '../consts'; 13 | import jsyaml from 'js-yaml'; 14 | import Store from '../services/store'; 15 | import Crypto from '../services/crypto'; 16 | import SetupCreator from '../services/setup_creator'; 17 | 18 | const dockerFileName = 'docker-compose.yml'; 19 | 20 | class StateModel { 21 | async checkMailInfo() { 22 | // write default 'mailInfo' if it doesn't exist 23 | return await Store.safeRead('mailInfo') || await Store.write('mailInfo', { 24 | from: 'from', 25 | orgRegTo: 'orgRegTo', 26 | apiKey: 'apiKey', 27 | templateIds: { 28 | invite: 'invite', 29 | orgReq: 'orgReq', 30 | orgReqApprove: 'orgReqApprove', 31 | orgReqRefuse: 'orgReqRefuse' 32 | } 33 | }); 34 | } 35 | 36 | async checkWorkerInterval() { 37 | // write default workerInterval if it doesn't exist 38 | return await Store.safeRead('workerInterval') || await Store.write('workerInterval', 300); 39 | } 40 | 41 | async checkStateVariables() { 42 | await this.checkWorkerInterval(); 43 | await this.checkMailInfo(); 44 | } 45 | 46 | async getWorkerInterval() { 47 | return Store.safeRead('workerInterval'); 48 | } 49 | 50 | async getNetwork() { 51 | return Store.safeRead('network'); 52 | } 53 | 54 | async storeNetwork(network) { 55 | await Store.write('network', network); 56 | } 57 | 58 | async generateAndStoreNewPrivateKey() { 59 | const privateKey = await Crypto.generatePrivateKey(); 60 | await this.storePrivateKey(privateKey); 61 | return privateKey; 62 | } 63 | 64 | async getPrivateKey() { 65 | return Store.safeRead('privateKey'); 66 | } 67 | 68 | async storePrivateKey(privateKey) { 69 | await Store.write('privateKey', privateKey); 70 | } 71 | 72 | async getAddress() { 73 | const privateKey = await this.getPrivateKey(); 74 | if (privateKey) { 75 | return Crypto.addressForPrivateKey(privateKey); 76 | } 77 | return null; 78 | } 79 | 80 | async storeAddress(address) { 81 | await Store.write('address', address); 82 | } 83 | 84 | async getRole() { 85 | return Store.safeRead('role'); 86 | } 87 | 88 | async storeRole(role) { 89 | await Store.write('role', role); 90 | } 91 | 92 | async removeRole() { 93 | await Store.clear('role'); 94 | } 95 | 96 | async getNodeUrl() { 97 | return Store.safeRead('url'); 98 | } 99 | 100 | async storeNodeUrl(url) { 101 | await Store.write('url', url); 102 | } 103 | 104 | async getNodeIP() { 105 | return Store.safeRead('ip'); 106 | } 107 | 108 | async storeNodeIP(ip) { 109 | await Store.write('ip', ip); 110 | } 111 | 112 | async getUserEmail() { 113 | return Store.safeRead('email'); 114 | } 115 | 116 | async storeUserEmail(email) { 117 | await Store.write('email', email); 118 | } 119 | 120 | async getApolloMinimalDeposit() { 121 | return Store.safeRead('apolloMinimalDeposit'); 122 | } 123 | 124 | async storeApolloMinimalDeposit(deposit) { 125 | await Store.write('apolloMinimalDeposit', deposit); 126 | } 127 | 128 | async getSignedTos() { 129 | return Store.safeRead('termsOfServiceSignature'); 130 | } 131 | 132 | async storeSignedTos(tosSignature) { 133 | await Store.write('termsOfServiceSignature', tosSignature); 134 | } 135 | 136 | async storeTosHash(tosHash) { 137 | await Store.write('termsOfServiceHash', tosHash); 138 | } 139 | 140 | async getTosHash() { 141 | return Store.safeRead('termsOfServiceHash'); 142 | } 143 | 144 | async getMailInfo() { 145 | return Store.safeRead('mailInfo'); 146 | } 147 | 148 | async getExtraData(templateDirectory, nodeTypeName, networkName, dockerFileName) { 149 | const dockerFile = await readFile(path.join(templateDirectory, nodeTypeName, networkName, dockerFileName)); 150 | 151 | const dockerYaml = await jsyaml.load(dockerFile); 152 | 153 | const parityVersion = dockerYaml.services.parity.image.split(':'); 154 | 155 | return `Apollo ${parityVersion[1]}`; 156 | } 157 | 158 | async assembleSubmission() { 159 | const privateKey = await this.getPrivateKey(); 160 | const submissionForm: any = { 161 | network: (await this.getNetwork()).name, 162 | address: await Crypto.addressForPrivateKey(privateKey), 163 | role: await this.getRole(), 164 | email: await this.getUserEmail(), 165 | termsOfServiceHash: await this.getTosHash(), 166 | termsOfServiceSignature: await this.getSignedTos() 167 | }; 168 | if (await this.getNodeUrl()) { 169 | submissionForm.url = await this.getNodeUrl(); 170 | } 171 | if (await this.getNodeIP()) { 172 | submissionForm.ip = await this.getNodeIP(); 173 | } 174 | if (await this.getApolloMinimalDeposit()) { 175 | submissionForm.depositInAMB = await this.getApolloMinimalDeposit(); 176 | } 177 | return submissionForm; 178 | } 179 | 180 | async readTosText() { 181 | return SetupCreator.readTosText(); 182 | } 183 | 184 | async createTosFile(termsOfServiceText) { 185 | await SetupCreator.createTosFile(termsOfServiceText); 186 | } 187 | 188 | async prepareSetupFiles() { 189 | const role = await this.getRole(); 190 | let nodeTypeName; 191 | 192 | if (role === HERMES) { 193 | nodeTypeName = 'hermes'; 194 | } else if (role === APOLLO) { 195 | nodeTypeName = 'apollo'; 196 | } else if (role === ATLAS_1 || role === ATLAS_2 || role === ATLAS_3) { 197 | nodeTypeName = 'atlas'; 198 | } else { 199 | throw new Error('Invalid role'); 200 | } 201 | 202 | const address = await this.getAddress(); 203 | const privateKey = await this.getPrivateKey(); 204 | 205 | const {headContractAddress, chainspec, dockerTag, domain} = await this.getNetwork(); 206 | 207 | const networkName = await SetupCreator.fetchChainJson(chainspec); 208 | 209 | const workerInterval = await this.getWorkerInterval(); 210 | 211 | await SetupCreator.prepareDockerComposeFile( 212 | dockerTag, 213 | nodeTypeName, 214 | address, 215 | privateKey, 216 | headContractAddress, 217 | networkName, 218 | domain, 219 | await this.getNodeUrl(), 220 | await this.getMailInfo(), 221 | workerInterval 222 | ); 223 | 224 | if (role === APOLLO) { 225 | const password = Crypto.getRandomPassword(); 226 | await SetupCreator.createPasswordFile(password); 227 | 228 | const encryptedWallet = Crypto.getEncryptedWallet(privateKey, password); 229 | await SetupCreator.createKeyFile(encryptedWallet); 230 | 231 | const nodeIp = await this.getNodeIP(); 232 | const extraData = await this.getExtraData(SetupCreator.templateDirectory, nodeTypeName, networkName, dockerFileName); 233 | await SetupCreator.copyParityConfiguration(nodeTypeName, networkName, {address, ip: nodeIp, extraData}); 234 | } else { 235 | await SetupCreator.copyParityConfiguration(nodeTypeName, networkName, {}); 236 | } 237 | } 238 | } 239 | 240 | export default new StateModel(); 241 | -------------------------------------------------------------------------------- /test/actions/retire_action.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import sinon from 'sinon'; 12 | import sinonChai from 'sinon-chai'; 13 | import chaiAsPromised from 'chai-as-promised'; 14 | import retireAction from '../../src/menu_actions/retire_action'; 15 | import {constants} from 'ambrosus-node-contracts'; 16 | import Dialog from '../../src/models/dialog_model'; 17 | import AtlasModeModel from '../../src/models/atlas_mode_model'; 18 | import Store from '../../src/services/store'; 19 | import SmartContractsModel from '../../src/models/smart_contracts_model'; 20 | 21 | chai.use(sinonChai); 22 | chai.use(chaiAsPromised); 23 | const {expect} = chai; 24 | 25 | describe('Retire action', () => { 26 | let atlasModeModelMock; 27 | let onboardMock; 28 | let confirmRetirementDialogStub; 29 | let continueAtlasRetirementDialogStub; 30 | let retireActionCall; 31 | 32 | beforeEach(() => { 33 | confirmRetirementDialogStub = sinon.stub().resolves({retirementConfirmation: true}); 34 | continueAtlasRetirementDialogStub = sinon.stub().resolves({atlasRetirementConfirmation: true}); 35 | Dialog.confirmRetirementDialog = confirmRetirementDialogStub; 36 | Dialog.continueAtlasRetirementDialog = continueAtlasRetirementDialogStub; 37 | Dialog.retirementSuccessfulDialog = sinon.stub().resolves(); 38 | Dialog.retirementStartSuccessfulDialog = sinon.stub().resolves(); 39 | Dialog.retirementContinueDialog = sinon.stub().resolves(); 40 | Dialog.retirementStopDialog = sinon.stub().resolves(); 41 | Dialog.genericErrorDialog = sinon.stub().resolves(); 42 | atlasModeModelMock = { 43 | getMode: sinon.stub().resolves({mode:'normal'}), 44 | setMode: sinon.stub().resolves(true) 45 | }; 46 | AtlasModeModel.getMode = atlasModeModelMock.getMode; 47 | AtlasModeModel.setMode = atlasModeModelMock.setMode; 48 | onboardMock = { 49 | rolesWrapper: { 50 | onboardedRole: sinon.stub().resolves(constants.APOLLO), 51 | defaultAddress: '0x1234' 52 | }, 53 | atlasStakeWrapper: { 54 | isShelteringAny: sinon.stub().resolves(false), 55 | defaultAddress: '0x1234' 56 | }, 57 | retire: sinon.stub().resolves() 58 | }; 59 | SmartContractsModel.onboardActions = onboardMock; 60 | Store.clear = sinon.stub(); 61 | retireActionCall = retireAction(); 62 | }); 63 | 64 | it(`returns true and ends NOP on successful retirement`, async () => { 65 | expect(await retireActionCall()).to.be.true; 66 | }); 67 | 68 | it('immediately returns false if confirmation was negative', async () => { 69 | confirmRetirementDialogStub.resolves({retirementConfirmation: false}); 70 | expect(await retireActionCall()).to.be.false; 71 | expect(onboardMock.retire).to.be.not.called; 72 | }); 73 | 74 | it('shows success dialog after retirement', async () => { 75 | expect(await retireActionCall()).to.be.true; 76 | expect(onboardMock.retire).to.be.calledOnce; 77 | expect(Dialog.retirementSuccessfulDialog).to.be.calledAfter(onboardMock.retire); 78 | }); 79 | 80 | it('immediately returns false if confirmation was negative for ATLAS without bundles', async () => { 81 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 82 | confirmRetirementDialogStub.resolves({retirementConfirmation: false}); 83 | expect(await retireActionCall()).to.be.false; 84 | expect(onboardMock.retire).to.be.not.called; 85 | }); 86 | 87 | it('immediately returns false if confirmation was negative for ATLAS with bundles', async () => { 88 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 89 | onboardMock.atlasStakeWrapper.isShelteringAny.resolves(true), 90 | confirmRetirementDialogStub.resolves({retirementConfirmation: false}); 91 | expect(await retireActionCall()).to.be.false; 92 | expect(onboardMock.retire).to.be.not.called; 93 | }); 94 | 95 | it('start retirement and shows success dialog for ATLAS with bundles', async () => { 96 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 97 | onboardMock.atlasStakeWrapper.isShelteringAny.resolves(true), 98 | confirmRetirementDialogStub.resolves({retirementConfirmation: true}); 99 | expect(await retireActionCall()).to.be.true; 100 | expect(AtlasModeModel.setMode).to.be.calledOnceWith('retire'); 101 | expect(Dialog.retirementStartSuccessfulDialog).to.be.calledAfter(AtlasModeModel.setMode); 102 | }); 103 | 104 | it('fail to start retirement and shows error dialog for ATLAS with bundles', async () => { 105 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 106 | onboardMock.atlasStakeWrapper.isShelteringAny.resolves(true), 107 | confirmRetirementDialogStub.resolves({retirementConfirmation: true}); 108 | atlasModeModelMock.setMode.resolves(false), 109 | expect(await retireActionCall()).to.be.false; 110 | expect(AtlasModeModel.setMode).to.be.calledOnceWith('retire'); 111 | expect(Dialog.genericErrorDialog).to.be.calledAfter(AtlasModeModel.setMode); 112 | }); 113 | 114 | it('continue retirement and shows continue dialog for ATLAS with bundles', async () => { 115 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 116 | onboardMock.atlasStakeWrapper.isShelteringAny.resolves(true), 117 | atlasModeModelMock.getMode.resolves({mode:'retire'}), 118 | continueAtlasRetirementDialogStub.resolves({continueConfirmation: true}); 119 | expect(await retireActionCall()).to.be.true; 120 | expect(Dialog.retirementContinueDialog).to.be.calledOnce; 121 | }); 122 | 123 | it('stop retirement and shows stop dialog for ATLAS with bundles', async () => { 124 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 125 | onboardMock.atlasStakeWrapper.isShelteringAny.resolves(true), 126 | atlasModeModelMock.getMode.resolves({mode:'retire'}), 127 | continueAtlasRetirementDialogStub.resolves({continueConfirmation: false}); 128 | expect(await retireActionCall()).to.be.true; 129 | expect(AtlasModeModel.setMode).to.be.calledOnceWith('normal'); 130 | expect(Dialog.retirementStopDialog).to.be.calledAfter(AtlasModeModel.setMode); 131 | }); 132 | 133 | it('fail to stop retirement and shows error dialog for ATLAS with bundles', async () => { 134 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 135 | onboardMock.atlasStakeWrapper.isShelteringAny.resolves(true), 136 | atlasModeModelMock.getMode.resolves({mode:'retire'}), 137 | continueAtlasRetirementDialogStub.resolves({continueConfirmation: false}); 138 | atlasModeModelMock.setMode.resolves(false), 139 | expect(await retireActionCall()).to.be.false; 140 | expect(AtlasModeModel.setMode).to.be.calledOnceWith('normal'); 141 | expect(Dialog.genericErrorDialog).to.be.calledAfter(AtlasModeModel.setMode); 142 | }); 143 | 144 | it('retires atlas and removes role via state & store', async () => { 145 | onboardMock.rolesWrapper.onboardedRole.resolves(constants.ATLAS), 146 | onboardMock.atlasStakeWrapper.isShelteringAny.resolves(false), 147 | confirmRetirementDialogStub.resolves({retirementConfirmation: true}); 148 | expect(await retireActionCall()).to.be.true; 149 | expect(Store.clear).to.be.calledOnceWith('role'); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /test/services/setup_creator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright: Ambrosus Inc. 3 | Email: tech@ambrosus.io 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 8 | */ 9 | 10 | import chai from 'chai'; 11 | import nock from 'nock'; 12 | import chaiAsPromised from 'chai-as-promised'; 13 | 14 | import SetupCreator from '../../src/services/setup_creator'; 15 | import {readFile, writeFile, removeFile, makeDirectory, removeDirectory} from '../../src/utils/file'; 16 | 17 | chai.use(chaiAsPromised); 18 | const {expect} = chai; 19 | 20 | describe('Setup Creator', () => { 21 | const testInputDir = './testInputDir/'; 22 | const testOutputDir = './testOutputDir/'; 23 | 24 | beforeEach(async () => { 25 | await makeDirectory(testInputDir); 26 | await makeDirectory(testOutputDir); 27 | }); 28 | 29 | afterEach(async () => { 30 | await removeDirectory(testInputDir); 31 | await removeDirectory(testOutputDir); 32 | }); 33 | 34 | before(async () => { 35 | SetupCreator.templateDirectory = testInputDir; 36 | SetupCreator.outputDirectory = testOutputDir; 37 | }); 38 | 39 | describe('createPasswordFile', () => { 40 | const examplePassword = '0x1234deadface'; 41 | const passwordFilePath = `${testOutputDir}password.pwds`; 42 | 43 | afterEach(async () => { 44 | await removeFile(passwordFilePath); 45 | }); 46 | 47 | it('creates file correctly', async () => { 48 | await SetupCreator.createPasswordFile(examplePassword); 49 | expect(await readFile(passwordFilePath)).to.equal(examplePassword); 50 | }); 51 | }); 52 | 53 | describe('createTosFile', () => { 54 | const exampleTosText = '0x1234deadface'; 55 | const tosFilePath = `${testOutputDir}TOS.txt`; 56 | 57 | afterEach(async () => { 58 | await removeFile(tosFilePath); 59 | }); 60 | 61 | it('creates file correctly', async () => { 62 | await SetupCreator.createTosFile(exampleTosText); 63 | expect(await readFile(tosFilePath)).to.equal(exampleTosText); 64 | }); 65 | }); 66 | 67 | describe('createKeyFile', () => { 68 | const exampleEncryptedWallet = { 69 | fo: 'o', 70 | ba: 'r' 71 | }; 72 | const keyFilePath = `${testOutputDir}keyfile`; 73 | 74 | afterEach(async () => { 75 | await removeFile(keyFilePath); 76 | }); 77 | 78 | it('creates file correctly', async () => { 79 | await SetupCreator.createKeyFile(exampleEncryptedWallet); 80 | expect(JSON.parse(await readFile(keyFilePath))).to.deep.equal(exampleEncryptedWallet); 81 | }); 82 | }); 83 | 84 | describe('prepareDockerComposeFile', () => { 85 | const tagPlaceholder = ''; 86 | const addressPlaceholder = ''; 87 | const privateKeyPlaceholder = ''; 88 | const headAddressPlaceholder = ''; 89 | const networkNamePlaceholder = ''; 90 | const domainPlaceholder = ''; 91 | 92 | const exampleAddress = '0xadd4eeee'; 93 | const examplePrivateKey = '0xbeefcafe'; 94 | const exampleHeadAddress = '0xdeadface'; 95 | const exampleNetworkName = 'amb-net'; 96 | const exampleTag = '7654321'; 97 | const exampleDomain = 'ambrosus-dev.com'; 98 | 99 | const sampleForm = (arg1, arg2, arg3, arg4, arg5, arg6) => `${arg1} || ${arg2} || ${arg3} || ${arg4} || ${arg5} || ${arg6}`; 100 | 101 | const nodeTypeName = 'atlas'; 102 | 103 | const templateFilePath = `${testInputDir}${nodeTypeName}/docker-compose.yml`; 104 | const destinationFilePath = `${testOutputDir}docker-compose.yml`; 105 | 106 | beforeEach(async () => { 107 | await makeDirectory(`${testInputDir}${nodeTypeName}`); 108 | await writeFile(templateFilePath, sampleForm(tagPlaceholder, addressPlaceholder, privateKeyPlaceholder, headAddressPlaceholder, networkNamePlaceholder, domainPlaceholder)); 109 | }); 110 | 111 | afterEach(async () => { 112 | await removeFile(templateFilePath); 113 | await removeDirectory(`${testInputDir}${nodeTypeName}`); 114 | await removeFile(destinationFilePath); 115 | }); 116 | 117 | it('creates file correctly', async () => { 118 | await SetupCreator.prepareDockerComposeFile(exampleTag, nodeTypeName, exampleAddress, examplePrivateKey, exampleHeadAddress, exampleNetworkName, exampleDomain); 119 | expect(await readFile(destinationFilePath)).to.deep.equal(sampleForm(exampleTag, exampleAddress, examplePrivateKey, exampleHeadAddress, exampleNetworkName, exampleDomain)); 120 | }); 121 | }); 122 | 123 | describe('copyParityConfiguration', () => { 124 | const exampleAddress = '0x123456789'; 125 | const exampleIP = '10.0.0.1'; 126 | 127 | const inputForm = `parity_config_contents... ... `; 128 | const formWithAddressReplaced = `parity_config_contents... ${exampleAddress} ... `; 129 | const formWithIPReplaced = `parity_config_contents... ... ${exampleIP}`; 130 | 131 | const nodeTypeName = 'apollo'; 132 | const templateDir = `${testInputDir}${nodeTypeName}`; 133 | const srcParityConfigPath = `${templateDir}/parity_config.toml`; 134 | const destParityConfigPath = `${testOutputDir}parity_config.toml`; 135 | 136 | beforeEach(async () => { 137 | await makeDirectory(`${testInputDir}${nodeTypeName}`); 138 | await writeFile(srcParityConfigPath, inputForm); 139 | }); 140 | 141 | afterEach(async () => { 142 | await removeFile(destParityConfigPath); 143 | await removeFile(srcParityConfigPath); 144 | await removeDirectory(`${testInputDir}${nodeTypeName}`); 145 | }); 146 | 147 | it('copies files correctly and replaces the TYPE_YOUR_ADDRESS_HERE placeholder if address was provided', async () => { 148 | await SetupCreator.copyParityConfiguration(nodeTypeName, {address: exampleAddress}); 149 | expect(await readFile(destParityConfigPath)).to.equal(formWithAddressReplaced); 150 | }); 151 | 152 | it('copies files correctly and replaces the TYPE_YOUR_IP_HERE placeholder if IP was provided', async () => { 153 | await SetupCreator.copyParityConfiguration(nodeTypeName, {ip: exampleIP}); 154 | expect(await readFile(destParityConfigPath)).to.equal(formWithIPReplaced); 155 | }); 156 | }); 157 | 158 | describe('fetchChainJson', () => { 159 | const chainSpecUrl = 'https://chainspec.ambrosus-dev.com/'; 160 | const chainJsonContent = '{"name": "dev"}'; 161 | const destChainJsonPath = `${testOutputDir}chain.json`; 162 | 163 | beforeEach(() => { 164 | if (!nock.isActive()) { 165 | nock.activate(); 166 | } 167 | }); 168 | 169 | afterEach(async () => { 170 | nock.cleanAll(); 171 | nock.restore(); 172 | await removeFile(destChainJsonPath); 173 | }); 174 | 175 | it('downloads the chainspec from given url', async () => { 176 | nock(chainSpecUrl) 177 | .get('/') 178 | .reply(200, chainJsonContent); 179 | 180 | const result = await SetupCreator.fetchChainJson(chainSpecUrl); 181 | expect(await readFile(destChainJsonPath)).to.equal(chainJsonContent); 182 | expect(result).to.equal('dev'); 183 | }); 184 | 185 | it('throws when the server responds with code different from 200', async () => { 186 | nock(chainSpecUrl) 187 | .get('/') 188 | .reply(500); 189 | 190 | await expect(SetupCreator.fetchChainJson(chainSpecUrl)).to.be.rejected; 191 | }); 192 | }); 193 | }); 194 | --------------------------------------------------------------------------------