├── .eslintrc.js ├── .github └── workflows │ ├── build_and_test.yml │ ├── publish.yml │ └── test_coverage.yml ├── .gitignore ├── .npmignore ├── 7nodes ├── key1 │ ├── acctkeyfile.json │ ├── enode │ ├── key │ ├── nodekey │ ├── password.txt │ ├── tm.key │ └── tm.pub ├── key2 │ ├── acctkeyfile.json │ ├── enode │ ├── key │ ├── nodekey │ ├── password.txt │ ├── tm.key │ └── tm.pub ├── key3 │ ├── acctkeyfile.json │ ├── enode │ ├── key │ ├── nodekey │ ├── password.txt │ ├── tm.key │ └── tm.pub ├── key4 │ ├── acctkeyfile.json │ ├── enode │ ├── key │ ├── nodekey │ ├── password.txt │ ├── tm.key │ └── tm.pub ├── key5 │ ├── acctkeyfile.json │ ├── enode │ ├── key │ ├── nodekey │ ├── password.txt │ ├── tm.key │ └── tm.pub ├── key6 │ ├── acctkeyfile.json │ ├── enode │ ├── key │ ├── nodekey │ ├── password.txt │ ├── tm.key │ └── tm.pub └── key7 │ ├── acctkeyfile.json │ ├── enode │ ├── key │ ├── nodekey │ ├── password.txt │ ├── tm.key │ └── tm.pub ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── WizardTools.png └── quorum-wizard.gif ├── jest.config.js ├── lib ├── docker-compose-definitions-cakeshop.yml ├── docker-compose-definitions-quorum.yml ├── docker-compose-definitions-reporting.yml ├── docker-compose-definitions-tessera.yml ├── public_contract.js ├── reporting-config.toml └── splunk │ └── splunk-config.yml ├── package-lock.json ├── package.json └── src ├── generators ├── __snapshots__ │ └── bashHelper.test.js.snap ├── bashHelper.js ├── bashHelper.test.js ├── binaryHelper.js ├── binaryHelper.test.js ├── cakeshopHelper.js ├── cakeshopHelper.test.js ├── consensusHelper.js ├── consensusHelper.test.js ├── dockerHelper.js ├── dockerHelper.test.js ├── download.js ├── download.test.js ├── keyGen.js ├── keyGen.test.js ├── networkCreator.js ├── networkCreator.test.js ├── networkHelper.js ├── reportingHelper.js ├── scripts │ ├── attach.js │ ├── getEndpoints.js │ ├── index.js │ ├── privateContract.js │ ├── publicContract.js │ ├── runscript.js │ ├── start.js │ ├── stop.js │ └── utils.js ├── splunkHelper.js ├── transactionManager.js └── transactionManger.test.js ├── index.js ├── model ├── CakeshopConfig.js ├── CakeshopConfig.test.js ├── ConsensusConfig.js ├── ConsensusConfig.test.js ├── DockerFile.test.js ├── NetworkConfig.js ├── NetworkConfig.test.js ├── ResourceConfig.js ├── ResourceConfig.test.js ├── TesseraConfig.js └── __snapshots__ │ ├── CakeshopConfig.test.js.snap │ ├── ConsensusConfig.test.js.snap │ ├── DockerFile.test.js.snap │ ├── NetworkConfig.test.js.snap │ └── ResourceConfig.test.js.snap ├── questions ├── customPromptHelper.js ├── customPromptHelper.test.js ├── index.js ├── index.test.js ├── questions.js ├── validators.js └── validators.test.js └── utils ├── accountHelper.js ├── accountHelper.test.js ├── execUtils.js ├── execUtils.test.js ├── fileUtils.js ├── fileUtils.test.js ├── log.js ├── pathUtils.js ├── pathUtils.test.js ├── subnetUtils.js ├── subnetUtils.test.js └── testHelper.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'semi': ['warn', 'never'], 4 | 'no-use-before-define': ['off'], 5 | 'no-await-in-loop': ['off'], 6 | "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js"]}] 7 | }, 8 | parser: 'babel-eslint', 9 | extends: 'airbnb', 10 | env: { 11 | jest: true 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build & Test 5 | 6 | on: 7 | push: 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 10.x 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 10.x 23 | - name: Build wizard 24 | run: | 25 | npm install 26 | npm run build 27 | - name: Test wizard 28 | run: npm run test 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js 10.x 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 10.x 19 | - run: npm install 20 | - run: npm run test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js 10.x 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: 10.x 31 | registry-url: https://registry.npmjs.org/ 32 | - run: npm install 33 | - run: npm run build 34 | - run: npm publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 37 | -------------------------------------------------------------------------------- /.github/workflows/test_coverage.yml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js 10.x 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 10.x 19 | - name: Test wizard 20 | run: | 21 | npm install 22 | npm run test:coverage 23 | - name: Jest Code Coverage Report 24 | uses: romeovs/lcov-reporter-action@v0.2.16 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | .idea 3 | node_modules 4 | network 5 | configs 6 | .DS_Store 7 | build 8 | bin/ 9 | coverage/ 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/.npmignore -------------------------------------------------------------------------------- /7nodes/key1/acctkeyfile.json: -------------------------------------------------------------------------------- 1 | {"address":"ed9d02e382b34818e88b88a309c7fe71e65f419d","crypto":{"cipher":"aes-128-ctr","ciphertext":"4e77046ba3f699e744acb4a89c36a3ea1158a1bd90a076d36675f4c883864377","cipherparams":{"iv":"a8932af2a3c0225ee8e872bc0e462c11"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8ca49552b3e92f79c51f2cd3d38dfc723412c212e702bd337a3724e8937aff0f"},"mac":"6d1354fef5aa0418389b1a5d1f5ee0050d7273292a1171c51fd02f9ecff55264"},"id":"a65d1ac3-db7e-445d-a1cc-b6c5eeaa05e0","version":3} -------------------------------------------------------------------------------- /7nodes/key1/enode: -------------------------------------------------------------------------------- 1 | ac6b1096ca56b9f6d004b779ae3728bf83f8e22453404cc3cef16a3d9b96608bc67c4b30db88e0a5a6c6390213f7acbe1153ff6d23ce57380104288ae19373ef 2 | -------------------------------------------------------------------------------- /7nodes/key1/key: -------------------------------------------------------------------------------- 1 | {"address":"ed9d02e382b34818e88b88a309c7fe71e65f419d","crypto":{"cipher":"aes-128-ctr","ciphertext":"4e77046ba3f699e744acb4a89c36a3ea1158a1bd90a076d36675f4c883864377","cipherparams":{"iv":"a8932af2a3c0225ee8e872bc0e462c11"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8ca49552b3e92f79c51f2cd3d38dfc723412c212e702bd337a3724e8937aff0f"},"mac":"6d1354fef5aa0418389b1a5d1f5ee0050d7273292a1171c51fd02f9ecff55264"},"id":"a65d1ac3-db7e-445d-a1cc-b6c5eeaa05e0","version":3} -------------------------------------------------------------------------------- /7nodes/key1/nodekey: -------------------------------------------------------------------------------- 1 | 1be3b50b31734be48452c29d714941ba165ef0cbf3ccea8ca16c45e3d8d45fb0 2 | -------------------------------------------------------------------------------- /7nodes/key1/password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/7nodes/key1/password.txt -------------------------------------------------------------------------------- /7nodes/key1/tm.key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} -------------------------------------------------------------------------------- /7nodes/key1/tm.pub: -------------------------------------------------------------------------------- 1 | BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= -------------------------------------------------------------------------------- /7nodes/key2/acctkeyfile.json: -------------------------------------------------------------------------------- 1 | {"address":"ca843569e3427144cead5e4d5999a3d0ccf92b8e","crypto":{"cipher":"aes-128-ctr","ciphertext":"01d409941ce57b83a18597058033657182ffb10ae15d7d0906b8a8c04c8d1e3a","cipherparams":{"iv":"0bfb6eadbe0ab7ffaac7e1be285fb4e5"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7b90f455a95942c7c682e0ef080afc2b494ef71e749ba5b384700ecbe6f4a1bf"},"mac":"4cc851f9349972f851d03d75a96383a37557f7c0055763c673e922de55e9e307"},"id":"354e3b35-1fed-407d-a358-889a29111211","version":3} -------------------------------------------------------------------------------- /7nodes/key2/enode: -------------------------------------------------------------------------------- 1 | 0ba6b9f606a43a95edc6247cdb1c1e105145817be7bcafd6b2c0ba15d58145f0dc1a194f70ba73cd6f4cdd6864edc7687f311254c7555cc32e4d45aeb1b80416 2 | -------------------------------------------------------------------------------- /7nodes/key2/key: -------------------------------------------------------------------------------- 1 | {"address":"ca843569e3427144cead5e4d5999a3d0ccf92b8e","crypto":{"cipher":"aes-128-ctr","ciphertext":"01d409941ce57b83a18597058033657182ffb10ae15d7d0906b8a8c04c8d1e3a","cipherparams":{"iv":"0bfb6eadbe0ab7ffaac7e1be285fb4e5"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7b90f455a95942c7c682e0ef080afc2b494ef71e749ba5b384700ecbe6f4a1bf"},"mac":"4cc851f9349972f851d03d75a96383a37557f7c0055763c673e922de55e9e307"},"id":"354e3b35-1fed-407d-a358-889a29111211","version":3} 2 | -------------------------------------------------------------------------------- /7nodes/key2/nodekey: -------------------------------------------------------------------------------- 1 | 9bdd6a2e7cc1ca4a4019029df3834d2633ea6e14034d6dcc3b944396fe13a08b 2 | -------------------------------------------------------------------------------- /7nodes/key2/password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/7nodes/key2/password.txt -------------------------------------------------------------------------------- /7nodes/key2/tm.key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"nDFwJNHSiT1gNzKBy9WJvMhmYRkW3TzFUmPsNzR6oFk="},"type":"unlocked"} -------------------------------------------------------------------------------- /7nodes/key2/tm.pub: -------------------------------------------------------------------------------- 1 | QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= -------------------------------------------------------------------------------- /7nodes/key3/acctkeyfile.json: -------------------------------------------------------------------------------- 1 | {"address":"0fbdc686b912d7722dc86510934589e0aaf3b55a","crypto":{"cipher":"aes-128-ctr","ciphertext":"6b2c72c6793f3da8185e36536e02f574805e41c18f551f24b58346ef4ecf3640","cipherparams":{"iv":"582f27a739f39580410faa108d5cc59f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1a79b0db3f8cb5c2ae4fa6ccb2b5917ce446bd5e42c8d61faeee512b97b4ad4a"},"mac":"cecb44d2797d6946805d5d744ff803805477195fab1d2209eddc3d1158f2e403"},"id":"f7292e90-af71-49af-a5b3-40e8493f4681","version":3} -------------------------------------------------------------------------------- /7nodes/key3/enode: -------------------------------------------------------------------------------- 1 | 579f786d4e2830bbcc02815a27e8a9bacccc9605df4dc6f20bcc1a6eb391e7225fff7cb83e5b4ecd1f3a94d8b733803f2f66b7e871961e7b029e22c155c3a778 2 | -------------------------------------------------------------------------------- /7nodes/key3/key: -------------------------------------------------------------------------------- 1 | {"address":"0fbdc686b912d7722dc86510934589e0aaf3b55a","crypto":{"cipher":"aes-128-ctr","ciphertext":"6b2c72c6793f3da8185e36536e02f574805e41c18f551f24b58346ef4ecf3640","cipherparams":{"iv":"582f27a739f39580410faa108d5cc59f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1a79b0db3f8cb5c2ae4fa6ccb2b5917ce446bd5e42c8d61faeee512b97b4ad4a"},"mac":"cecb44d2797d6946805d5d744ff803805477195fab1d2209eddc3d1158f2e403"},"id":"f7292e90-af71-49af-a5b3-40e8493f4681","version":3} 2 | -------------------------------------------------------------------------------- /7nodes/key3/nodekey: -------------------------------------------------------------------------------- 1 | 722f11686b2277dcbd72713d8a3c81c666b585c337d47f503c3c1f3c17cf001d 2 | -------------------------------------------------------------------------------- /7nodes/key3/password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/7nodes/key3/password.txt -------------------------------------------------------------------------------- /7nodes/key3/tm.key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"tMxUVR8bX7aq/TbpVHc2QV3SN2iUuExBwefAuFsO0Lg="},"type":"unlocked"} -------------------------------------------------------------------------------- /7nodes/key3/tm.pub: -------------------------------------------------------------------------------- 1 | 1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= -------------------------------------------------------------------------------- /7nodes/key4/acctkeyfile.json: -------------------------------------------------------------------------------- 1 | {"address":"9186eb3d20cbd1f5f992a950d808c4495153abd5","crypto":{"cipher":"aes-128-ctr","ciphertext":"d160a630a39be3ff35556055406d8ff2a635f0535fe298d62ccc812d8f7b3bd5","cipherparams":{"iv":"82fce06bc6e1658a5e81ccef3b753329"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8d0c486db4c942721f4f5e96d48e9344805d101dad8159962b8a2008ac718548"},"mac":"4a92bda949068968d470320260ae1a825aa22f6a40fb8567c9f91d700c3f7e91"},"id":"bdb3b4f6-d8d0-4b00-8473-e223ef371b5c","version":3} -------------------------------------------------------------------------------- /7nodes/key4/enode: -------------------------------------------------------------------------------- 1 | 3d9ca5956b38557aba991e31cf510d4df641dce9cc26bfeb7de082f0c07abb6ede3a58410c8f249dabeecee4ad3979929ac4c7c496ad20b8cfdd061b7401b4f5 2 | -------------------------------------------------------------------------------- /7nodes/key4/key: -------------------------------------------------------------------------------- 1 | {"address":"9186eb3d20cbd1f5f992a950d808c4495153abd5","crypto":{"cipher":"aes-128-ctr","ciphertext":"d160a630a39be3ff35556055406d8ff2a635f0535fe298d62ccc812d8f7b3bd5","cipherparams":{"iv":"82fce06bc6e1658a5e81ccef3b753329"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8d0c486db4c942721f4f5e96d48e9344805d101dad8159962b8a2008ac718548"},"mac":"4a92bda949068968d470320260ae1a825aa22f6a40fb8567c9f91d700c3f7e91"},"id":"bdb3b4f6-d8d0-4b00-8473-e223ef371b5c","version":3} 2 | -------------------------------------------------------------------------------- /7nodes/key4/nodekey: -------------------------------------------------------------------------------- 1 | 6af685c4de99d44c620ccd9464d19bdeb62a750b9ae49b1740fb28d68a0e5c7d 2 | -------------------------------------------------------------------------------- /7nodes/key4/password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/7nodes/key4/password.txt -------------------------------------------------------------------------------- /7nodes/key4/tm.key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"grQjd3dBp4qFs8/5Jdq7xjz++aUx/LXAqISFyPWaCRw="},"type":"unlocked"} -------------------------------------------------------------------------------- /7nodes/key4/tm.pub: -------------------------------------------------------------------------------- 1 | oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= -------------------------------------------------------------------------------- /7nodes/key5/acctkeyfile.json: -------------------------------------------------------------------------------- 1 | {"address":"0638e1574728b6d862dd5d3a3e0942c3be47d996","crypto":{"cipher":"aes-128-ctr","ciphertext":"d8119d67cb134bc65c53506577cfd633bbbf5acca976cea12dd507de3eb7fd6f","cipherparams":{"iv":"76e88f3f246d4bf9544448d1a27b06f4"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"6d05ade3ee96191ed73ea019f30c02cceb6fc0502c99f706b7b627158bfc2b0a"},"mac":"b39c2c56b35958c712225970b49238fb230d7981ef47d7c33c730c363b658d06"},"id":"00307b43-53a3-4e03-9d0c-4fcbb3da29df","version":3} -------------------------------------------------------------------------------- /7nodes/key5/enode: -------------------------------------------------------------------------------- 1 | 3701f007bfa4cb26512d7df18e6bbd202e8484a6e11d387af6e482b525fa25542d46ff9c99db87bd419b980c24a086117a397f6d8f88e74351b41693880ea0cb 2 | -------------------------------------------------------------------------------- /7nodes/key5/key: -------------------------------------------------------------------------------- 1 | {"address":"0638e1574728b6d862dd5d3a3e0942c3be47d996","crypto":{"cipher":"aes-128-ctr","ciphertext":"d8119d67cb134bc65c53506577cfd633bbbf5acca976cea12dd507de3eb7fd6f","cipherparams":{"iv":"76e88f3f246d4bf9544448d1a27b06f4"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"6d05ade3ee96191ed73ea019f30c02cceb6fc0502c99f706b7b627158bfc2b0a"},"mac":"b39c2c56b35958c712225970b49238fb230d7981ef47d7c33c730c363b658d06"},"id":"00307b43-53a3-4e03-9d0c-4fcbb3da29df","version":3} 2 | -------------------------------------------------------------------------------- /7nodes/key5/nodekey: -------------------------------------------------------------------------------- 1 | 103bb5d20384b9af9f693d4287822fef6da7d79cb2317ed815f0081c7ea8d17d 2 | -------------------------------------------------------------------------------- /7nodes/key5/password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/7nodes/key5/password.txt -------------------------------------------------------------------------------- /7nodes/key5/tm.key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"W8XcdJfOuxCrLcspSceNg9vh7Cwe8tXiIx5xPJ88OtQ="},"type":"unlocked"} -------------------------------------------------------------------------------- /7nodes/key5/tm.pub: -------------------------------------------------------------------------------- 1 | R56gy4dn24YOjwyesTczYa8m5xhP6hF2uTMCju/1xkY= -------------------------------------------------------------------------------- /7nodes/key6/acctkeyfile.json: -------------------------------------------------------------------------------- 1 | {"address":"ae9bc6cd5145e67fbd1887a5145271fd182f0ee7","crypto":{"cipher":"aes-128-ctr","ciphertext":"013ed4c928bf7ae50dba7c9d8396f2d89d1fccc16a2067fdad56e125a0f5d96c","cipherparams":{"iv":"9fce4f1ab5c9cdaee9432dbc43d28ed8"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"5301d6b0c7bc8ba4ca60256ba524bae57cb078679e0de7d5322ebdc2393849a0"},"mac":"55aabdbc4853a188e8b63a1cec93e5d233a8b5b529ed00c610d1b4a9b27990da"},"id":"025f7cf0-f35b-4988-8a22-2443a08e5d69","version":3} -------------------------------------------------------------------------------- /7nodes/key6/enode: -------------------------------------------------------------------------------- 1 | eacaa74c4b0e7a9e12d2fe5fee6595eda841d6d992c35dbbcc50fcee4aa86dfbbdeff7dc7e72c2305d5a62257f82737a8cffc80474c15c611c037f52db1a3a7b 2 | -------------------------------------------------------------------------------- /7nodes/key6/key: -------------------------------------------------------------------------------- 1 | {"address":"ae9bc6cd5145e67fbd1887a5145271fd182f0ee7","crypto":{"cipher":"aes-128-ctr","ciphertext":"013ed4c928bf7ae50dba7c9d8396f2d89d1fccc16a2067fdad56e125a0f5d96c","cipherparams":{"iv":"9fce4f1ab5c9cdaee9432dbc43d28ed8"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"5301d6b0c7bc8ba4ca60256ba524bae57cb078679e0de7d5322ebdc2393849a0"},"mac":"55aabdbc4853a188e8b63a1cec93e5d233a8b5b529ed00c610d1b4a9b27990da"},"id":"025f7cf0-f35b-4988-8a22-2443a08e5d69","version":3} 2 | -------------------------------------------------------------------------------- /7nodes/key6/nodekey: -------------------------------------------------------------------------------- 1 | 79999aef8d5197446b6051df47f01fd4d6dd1997aec3f5282e77ea27b6727346 2 | -------------------------------------------------------------------------------- /7nodes/key6/password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/7nodes/key6/password.txt -------------------------------------------------------------------------------- /7nodes/key6/tm.key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"N9wH6bG0lWOCJtSnosatAskvzkrDApdrjaWkqjDyDzE="},"type":"unlocked"} -------------------------------------------------------------------------------- /7nodes/key6/tm.pub: -------------------------------------------------------------------------------- 1 | UfNSeSGySeKg11DVNEnqrUtxYRVor4+CvluI8tVv62Y= -------------------------------------------------------------------------------- /7nodes/key7/acctkeyfile.json: -------------------------------------------------------------------------------- 1 | {"address":"cc71c7546429a13796cf1bf9228bff213e7ae9cc","crypto":{"cipher":"aes-128-ctr","ciphertext":"a522d53d5a86405435f6288d4e34b0c038de25f46fa935b0be78fd24d4aa65da","cipherparams":{"iv":"10511f1422825b699718559dcaaa0ff2"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"471dfeff2dc7524d27970e54d8224320cb13f7e580431473a362fe8850ebc120"},"mac":"25ee0b623467350a4245a041a89a6797560ade8a1bd1c8d4d1b67ca5e37c56c0"},"id":"477df10a-9591-4fae-9c30-3aa0bc0ec57b","version":3} -------------------------------------------------------------------------------- /7nodes/key7/enode: -------------------------------------------------------------------------------- 1 | 239c1f044a2b03b6c4713109af036b775c5418fe4ca63b04b1ce00124af00ddab7cc088fc46020cdc783b6207efe624551be4c06a994993d8d70f684688fb7cf 2 | -------------------------------------------------------------------------------- /7nodes/key7/key: -------------------------------------------------------------------------------- 1 | {"address":"cc71c7546429a13796cf1bf9228bff213e7ae9cc","crypto":{"cipher":"aes-128-ctr","ciphertext":"a522d53d5a86405435f6288d4e34b0c038de25f46fa935b0be78fd24d4aa65da","cipherparams":{"iv":"10511f1422825b699718559dcaaa0ff2"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"471dfeff2dc7524d27970e54d8224320cb13f7e580431473a362fe8850ebc120"},"mac":"25ee0b623467350a4245a041a89a6797560ade8a1bd1c8d4d1b67ca5e37c56c0"},"id":"477df10a-9591-4fae-9c30-3aa0bc0ec57b","version":3} 2 | -------------------------------------------------------------------------------- /7nodes/key7/nodekey: -------------------------------------------------------------------------------- 1 | e85dae073b504871ffd7946bf5f45e6fa8dc09eb1536a48c4b6822332008973d 2 | -------------------------------------------------------------------------------- /7nodes/key7/password.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/7nodes/key7/password.txt -------------------------------------------------------------------------------- /7nodes/key7/tm.key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"lesaO6EWbmL1rie1biy851BnN1QsRRDK4kWUimlK0EA="},"type":"unlocked"} -------------------------------------------------------------------------------- /7nodes/key7/tm.pub: -------------------------------------------------------------------------------- 1 | ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc= -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/jpmorganchase/quorum-wizard/workflows/Build%20&%20Test/badge.svg) 2 | # GoQuorum Wizard 3 | 4 | [GoQuorum Wizard](https://github.com/ConsenSys/quorum-wizard) is a command line tool that allows 5 | users to set up a development GoQuorum network on their local machine in less than 2 minutes. 6 | 7 | ## ⚠️ Project Deprecation Notice ⚠️ 8 | 9 | quorum-wizard will be deprecated on December 31st 2021, date from when we will stop supporting the project. 10 | 11 | From now on, we encourage all users to use to [quorum-dev-quickstart](https://github.com/ConsenSys/quorum-dev-quickstart) which is a similar tool offering extra compatibility with Quorum products, in particular Hyperledger Besu and Orchestrate. 12 | 13 | We will continue to support quorum-wizard in particular fixing bugs until the end of 2021. 14 | 15 | If you have any questions or concerns, please reach out to the ConsenSys protocol engineering team on [#Discord](https://chat.consensys.net) or by [email](mailto:quorum@consensys.net). 16 | 17 | ## Using GoQuorum Wizard 18 | 19 | ![](docs/quorum-wizard.gif) 20 | 21 | GoQuorum Wizard is written in Javascript and designed to be run as a global NPM module from the command line. Make sure you have [Node.js/NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed (version 10 or higher). 22 | 23 | Using npx to run the wizard without the need to install: 24 | 25 | ``` 26 | npx quorum-wizard 27 | ``` 28 | 29 | You can also install the wizard globally with npm: 30 | 31 | ```Bash 32 | npm install -g quorum-wizard 33 | 34 | # Once the global module is installed, run: 35 | quorum-wizard 36 | ``` 37 | 38 | Note: Many installations of npm don't have permission to install global modules and will throw an EACCES error. [Here is the recommended solution from NPM](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally) 39 | 40 | ## Dependencies 41 | 42 | Here are the dependencies (in addition to NodeJS 10+) that are required depending on the mode that you run the wizard in: 43 | 44 | Bash: 45 | 46 | - Java (when running Tessera and/or Cakeshop) 47 | 48 | Docker Compose: 49 | 50 | - Docker 51 | - docker-compose 52 | 53 | Kubernetes: 54 | 55 | - Docker (for generating resources during network creation) 56 | - kubectl 57 | - minikube, Docker Desktop with Kubernetes enabled, or some other kubernetes context 58 | 59 | ## Options 60 | 61 | You can also provide these flags when running quorum-wizard: 62 | 63 | | Flags | Effect | 64 | | - | - | 65 | | `-q`, `--quickstart` | Create 3 node raft network with tessera and cakeshop (no user-input required) | 66 | | `generate --config ` | Generate a network from an existing config.json file | 67 | | `-r`, `--registry ` | Use a custom docker registry (instead of registry.hub.docker.com) | 68 | | `-o`, `--outputPath ` | Set the output path. Wizard will place all generated files into this folder. Defaults to the location where Wizard is run | 69 | | `-v`, `--verbose` | Turn on additional logs for debugging | 70 | | `--version` | Show version number | 71 | | `-h`, `--help` | Show help | 72 | 73 | ## Interacting with the Network 74 | 75 | To explore the features of GoQuorum and deploy a private contract, follow the instructions on [Interacting with the Network](https://docs.goquorum.consensys.net/en/stable/HowTo/GetStarted/Wizard/Interacting/). 76 | 77 | ## Tools 78 | 79 | The wizard provides the option to deploy some useful tools alongside your network. Learn more on the [Tools page](https://docs.goquorum.consensys.net/en/stable/HowTo/GetStarted/Wizard/Tools/). 80 | 81 | ## Developing 82 | Clone this repo to your local machine. 83 | 84 | `npm install` to get all the dependencies. 85 | 86 | `npm run test:watch` to automatically run tests on changes 87 | 88 | `npm run start` to automatically build on changes to any files in the src directory 89 | 90 | `npm link` to use your development build when you run the global npm command 91 | 92 | `quorum-wizard` to run (alternatively, you can run `node build/index.js`) 93 | 94 | ## Contributing 95 | GoQuorum Wizard is built on open source and we invite you to contribute enhancements. Upon review you will be required to complete a Contributor License Agreement (CLA) before we are able to merge. If you have any questions about the contribution process, please feel free to send an email to [quorum@consensys.net](mailto:quorum@consensys.net). 96 | 97 | ## Getting Help 98 | Stuck at some step? Please join our Slack community for support. 99 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [["@babel/preset-env", {targets: {node: 'current'}}],["airbnb", {targets: {node: 'current'}}]], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-transform-runtime", 6 | { 7 | "absoluteRuntime": false, 8 | "corejs": false, 9 | "helpers": true, 10 | "regenerator": true, 11 | "useESModules": true 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /docs/WizardTools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/docs/WizardTools.png -------------------------------------------------------------------------------- /docs/quorum-wizard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/quorum-wizard/d17c8a0242d0f6849882c9b1bef933c93a060fd1/docs/quorum-wizard.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/75/_q1gpvcx7d710ym8pxykc9p40000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | collectCoverageFrom: ["src/**/*.{js,jsx}", "!**/node_modules/**", "!src/**/*.test.js"], 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: null, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // A list of reporter names that Jest uses when writing coverage reports 34 | // coverageReporters: [ 35 | // "json", 36 | // "text", 37 | // "lcov", 38 | // "clover" 39 | // ], 40 | 41 | // An object that configures minimum threshold enforcement for coverage results 42 | // coverageThreshold: null, 43 | 44 | // A path to a custom dependency extractor 45 | // dependencyExtractor: null, 46 | 47 | // Make calling deprecated APIs throw helpful error messages 48 | // errorOnDeprecated: false, 49 | 50 | // Force coverage collection from ignored files using an array of glob patterns 51 | // forceCoverageMatch: [], 52 | 53 | // A path to a module which exports an async function that is triggered once before all test suites 54 | // globalSetup: null, 55 | 56 | // A path to a module which exports an async function that is triggered once after all test suites 57 | // globalTeardown: null, 58 | 59 | // A set of global variables that need to be available in all test environments 60 | // globals: {}, 61 | 62 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 63 | // maxWorkers: "50%", 64 | 65 | // An array of directory names to be searched recursively up from the requiring module's location 66 | // moduleDirectories: [ 67 | // "node_modules" 68 | // ], 69 | 70 | // An array of file extensions your modules use 71 | // moduleFileExtensions: [ 72 | // "js", 73 | // "json", 74 | // "jsx", 75 | // "ts", 76 | // "tsx", 77 | // "node" 78 | // ], 79 | 80 | // A map from regular expressions to module names that allow to stub out resources with a single module 81 | // moduleNameMapper: {}, 82 | 83 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 84 | // modulePathIgnorePatterns: [], 85 | 86 | // Activates notifications for test results 87 | // notify: false, 88 | 89 | // An enum that specifies notification mode. Requires { notify: true } 90 | // notifyMode: "failure-change", 91 | 92 | // A preset that is used as a base for Jest's configuration 93 | // preset: null, 94 | 95 | // Run tests from one or more projects 96 | // projects: null, 97 | 98 | // Use this configuration option to add custom reporters to Jest 99 | // reporters: undefined, 100 | 101 | // Automatically reset mock state between every test 102 | // resetMocks: false, 103 | 104 | // Reset the module registry before running each individual test 105 | // resetModules: false, 106 | 107 | // A path to a custom resolver 108 | // resolver: null, 109 | 110 | // Automatically restore mock state between every test 111 | // restoreMocks: false, 112 | 113 | // The root directory that Jest should scan for tests and modules within 114 | // rootDir: null, 115 | 116 | // A list of paths to directories that Jest should use to search for files in 117 | // roots: [ 118 | // "" 119 | // ], 120 | 121 | // Allows you to use a custom runner instead of Jest's default test runner 122 | // runner: "jest-runner", 123 | 124 | // The paths to modules that run some code to configure or set up the testing environment before each test 125 | // setupFiles: [], 126 | 127 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 128 | // setupFilesAfterEnv: [], 129 | 130 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 131 | // snapshotSerializers: [], 132 | 133 | // The test environment that will be used for testing 134 | testEnvironment: "node", 135 | 136 | // Options that will be passed to the testEnvironment 137 | // testEnvironmentOptions: {}, 138 | 139 | // Adds a location field to test results 140 | // testLocationInResults: false, 141 | 142 | // The glob patterns Jest uses to detect test files 143 | testMatch: [ 144 | "**/src/**/*.test.[jt]s?(x)", 145 | // "**/?(*.)+(spec|test).[tj]s?(x)" 146 | ], 147 | 148 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 149 | testPathIgnorePatterns: [ 150 | "/node_modules/", 151 | "/build/", 152 | "/network/", 153 | "/configs/", 154 | "/lib/", 155 | ], 156 | 157 | // The regexp pattern or array of patterns that Jest uses to detect test files 158 | // testRegex: [], 159 | 160 | // This option allows the use of a custom results processor 161 | // testResultsProcessor: null, 162 | 163 | // This option allows use of a custom test runner 164 | // testRunner: "jasmine2", 165 | 166 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 167 | // testURL: "http://localhost", 168 | 169 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 170 | // timers: "real", 171 | 172 | // A map from regular expressions to paths to transformers 173 | // transform: null, 174 | 175 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 176 | // transformIgnorePatterns: [ 177 | // "/node_modules/" 178 | // ], 179 | 180 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 181 | // unmockedModulePathPatterns: undefined, 182 | 183 | // Indicates whether each individual test should be reported during the run 184 | // verbose: null, 185 | 186 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 187 | // watchPathIgnorePatterns: [], 188 | 189 | // Whether to use watchman for file crawling 190 | // watchman: true, 191 | }; 192 | -------------------------------------------------------------------------------- /lib/docker-compose-definitions-cakeshop.yml: -------------------------------------------------------------------------------- 1 | x-cakeshop-def: 2 | &cakeshop-def 3 | image: "${DOCKER_REGISTRY:-}${CAKESHOP_DOCKER_IMAGE:-quorumengineering/cakeshop:0.12.1}" 4 | expose: 5 | - "8999" 6 | restart: "no" 7 | healthcheck: 8 | test: ["CMD", "wget", "--spider", "--proxy=off", "http://localhost:8999/actuator/health"] 9 | interval: 5s 10 | timeout: 5s 11 | retries: 20 12 | start_period: 5s 13 | entrypoint: 14 | - /bin/sh 15 | - -c 16 | - | 17 | DDIR=/qdata/cakeshop/local 18 | rm -rf $${DDIR} 19 | mkdir -p $${DDIR} 20 | cp /examples/cakeshop/local/application.properties $${DDIR}/application.properties 21 | cp /examples/cakeshop/local/cakeshop.json $${DDIR}/cakeshop.json 22 | java -Xms128M -Xmx128M -Dspring.config.additional-location=file:/qdata/cakeshop/local/ -Dcakeshop.config.dir=/qdata/cakeshop -jar /cakeshop/cakeshop.war 23 | ;; 24 | -------------------------------------------------------------------------------- /lib/docker-compose-definitions-quorum.yml: -------------------------------------------------------------------------------- 1 | # The following environment variables are substituted if present 2 | # * QUORUM_CONSENSUS: default to istanbul 3 | # * QUORUM_DOCKER_IMAGE: default to quorumengineering/quorum:2.5.0 4 | # * QUORUM_TX_MANAGER_DOCKER_IMAGE: default to quorumengineering/tessera:0.10.2 5 | # * QUORUM_GETH_ARGS: extra geth arguments to be included when running geth 6 | # To use Constellation, set QUORUM_TX_MANAGER_DOCKER_IMAGE to Constellation docker image, 7 | # e.g.: QUORUM_TX_MANAGER_DOCKER_IMAGE=quorumengineering/constellation:0.3.2 docker-compose up -d 8 | # To use Remix, set QUORUM_GETH_ARGS="--rpccorsdomain https://remix.ethereum.org" 9 | version: "2.3" 10 | x-quorum-def: 11 | &quorum-def 12 | restart: "on-failure" 13 | image: "${DOCKER_REGISTRY:-}${QUORUM_DOCKER_IMAGE:-quorumengineering/quorum:2.5.0}" 14 | healthcheck: 15 | test: ["CMD", "wget", "--spider", "--proxy", "off", "http://localhost:${QUORUM_RPC_PORT:-8545}"] 16 | interval: 3s 17 | timeout: 3s 18 | retries: 10 19 | start_period: 5s 20 | labels: 21 | com.quorum.consensus: ${QUORUM_CONSENSUS:-istanbul} 22 | entrypoint: 23 | - /bin/sh 24 | - -c 25 | - | 26 | UDS_WAIT=10 27 | if [ "$${PRIVATE_CONFIG}" != "ignore" ]; then 28 | for i in $$(seq 1 100) 29 | do 30 | set -e 31 | if [ -S $${PRIVATE_CONFIG} ] && \ 32 | [ "I'm up!" == "$$(wget --timeout $${UDS_WAIT} -qO- --proxy off txmanager$${NODE_ID}:${TESSERA_P2P_PORT:-9000}/upcheck)" ]; 33 | then break 34 | else 35 | echo "Sleep $${UDS_WAIT} seconds. Waiting for TxManager." 36 | sleep $${UDS_WAIT} 37 | fi 38 | done 39 | fi 40 | DDIR=/qdata/dd 41 | rm -rf $${DDIR} 42 | mkdir -p $${DDIR} 43 | cp -r /examples/dd$${NODE_ID}/* $${DDIR} 44 | cp /examples/dd$${NODE_ID}/permissioned-nodes.json $${DDIR}/static-nodes.json 45 | cp $${DDIR}/static-nodes.json $${DDIR}/permissioned-nodes.json 46 | cat $${DDIR}/static-nodes.json 47 | GENESIS_FILE="/examples/dd$${NODE_ID}/genesis.json" 48 | NETWORK_ID=$$(cat $${GENESIS_FILE} | grep chainId | awk -F " " '{print $$2}' | awk -F "," '{print $$1}') 49 | GETH_ARGS_raft="--raft --raftport ${QUORUM_RAFT_PORT:-50400}" 50 | GETH_ARGS_istanbul="--emitcheckpoints --istanbul.blockperiod 1 --mine --minerthreads 1 --syncmode full" 51 | EXTRA_ARGS="${QUORUM_GETH_ARGS:-}" 52 | geth --datadir $${DDIR} init $${GENESIS_FILE} 53 | geth \ 54 | --identity node$${NODE_ID}-${QUORUM_CONSENSUS:-istanbul} \ 55 | --datadir $${DDIR} \ 56 | --permissioned \ 57 | --nodiscover \ 58 | --verbosity 5 \ 59 | --networkid $${NETWORK_ID} \ 60 | --rpc \ 61 | --rpccorsdomain "*" \ 62 | --rpcvhosts "*" \ 63 | --rpcaddr 0.0.0.0 \ 64 | --rpcport ${QUORUM_RPC_PORT:-8545} \ 65 | --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,${QUORUM_CONSENSUS:-istanbul} \ 66 | --ws \ 67 | --wsaddr 0.0.0.0 \ 68 | --wsorigins "*" \ 69 | --wsport ${QUORUM_WS_PORT:-8546} \ 70 | --wsapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,${QUORUM_CONSENSUS:-istanbul} \ 71 | --port ${QUORUM_P2P_PORT:-21000} \ 72 | --allow-insecure-unlock \ 73 | --unlock 0 \ 74 | --password $${DDIR}/keystore/password.txt \ 75 | $${GETH_ARGS_${QUORUM_CONSENSUS:-istanbul}} \ 76 | $$EXTRA_ARGS 77 | expose: 78 | - "${QUORUM_P2P_PORT:-21000}" 79 | - "${QUORUM_RAFT_PORT:-50400}" 80 | -------------------------------------------------------------------------------- /lib/docker-compose-definitions-reporting.yml: -------------------------------------------------------------------------------- 1 | x-reporting-def: 2 | &reporting-def 3 | restart: "on-failure" 4 | image: "${DOCKER_REGISTRY:-}${REPORTING_DOCKER_IMAGE:-quorum-reporting:latest}" 5 | healthcheck: 6 | test: ["CMD", "wget", "--spider", "--proxy", "off", "http://localhost:${REPORTING_UI_PORT:-3000}"] 7 | interval: 3s 8 | timeout: 3s 9 | retries: 10 10 | start_period: 5s 11 | entrypoint: 12 | - /bin/sh 13 | - -c 14 | - | 15 | sleep 30 16 | reporting -config /config/reporting-config.toml 17 | expose: 18 | - "${REPORTING_RPC_PORT:-4000}" 19 | - "${REPORTING_UI_PORT:-3000}" 20 | -------------------------------------------------------------------------------- /lib/docker-compose-definitions-tessera.yml: -------------------------------------------------------------------------------- 1 | x-tx-manager-def: 2 | &tx-manager-def 3 | image: "${DOCKER_REGISTRY:-}${QUORUM_TX_MANAGER_DOCKER_IMAGE:-quorumengineering/tessera:0.10.2}" 4 | restart: "no" 5 | healthcheck: 6 | test: ["CMD-SHELL", "[ -S /qdata/tm/tm.ipc ] || exit 1"] 7 | interval: 3s 8 | timeout: 3s 9 | retries: 20 10 | start_period: 5s 11 | entrypoint: 12 | - /bin/sh 13 | - -c 14 | - | 15 | DDIR=/qdata/tm 16 | rm -rf $${DDIR} 17 | mkdir -p $${DDIR} 18 | DOCKER_IMAGE="${QUORUM_TX_MANAGER_DOCKER_IMAGE:-quorumengineering/tessera:0.10.2}" 19 | TX_MANAGER=$$(echo $${DOCKER_IMAGE} | sed 's/^.*\/\(.*\):.*$$/\1/g') 20 | echo "TxManager: $${TX_MANAGER}" 21 | case $${TX_MANAGER} 22 | in 23 | tessera) 24 | cp -r /examples/c$${NODE_ID}/* $${DDIR} 25 | #extract the tessera version from the jar 26 | TESSERA_VERSION=$$(unzip -p /tessera/tessera-app.jar META-INF/MANIFEST.MF | grep Tessera-Version | cut -d" " -f2) 27 | echo "Tessera version (extracted from manifest file): $${TESSERA_VERSION}" 28 | TESSERA_CONFIG_TYPE="-09" 29 | 30 | echo Config type $${TESSERA_CONFIG_TYPE} 31 | 32 | HOSTNAME=$$(hostname -i) 33 | sed -i "s,%THIS_PRIV_KEY%,$${DDIR}/tm.key,g" $${DDIR}/tessera-config$${TESSERA_CONFIG_TYPE}.json 34 | sed -i "s,%THIS_PUB_KEY%,$${DDIR}/tm.pub,g" $${DDIR}/tessera-config$${TESSERA_CONFIG_TYPE}.json 35 | sed -i 's,etc/quorum/,,g' $${DDIR}/tessera-config$${TESSERA_CONFIG_TYPE}.json 36 | sed -i 's/%QUORUM-NODE\([0-9]\)_SERVICE_HOST%/txmanager\1/g' $${DDIR}/tessera-config$${TESSERA_CONFIG_TYPE}.json 37 | sed -i "s/%THIS_SERVICE_HOST%/$${HOSTNAME}/g" $${DDIR}/tessera-config$${TESSERA_CONFIG_TYPE}.json 38 | 39 | cat $${DDIR}/tessera-config$${TESSERA_CONFIG_TYPE}.json 40 | java -Xms128M -Xmx128M -jar /tessera/tessera-app.jar -configfile $${DDIR}/tessera-config$${TESSERA_CONFIG_TYPE}.json 41 | ;; 42 | *) 43 | echo "Invalid Transaction Manager" 44 | exit 1 45 | ;; 46 | esac 47 | expose: 48 | - "${TESSERA_P2P_PORT:-9000}" 49 | - "${TESSERA_3PARTY_PORT:-9080}" 50 | -------------------------------------------------------------------------------- /lib/public_contract.js: -------------------------------------------------------------------------------- 1 | a = eth.accounts[0] 2 | web3.eth.defaultAccount = a; 3 | 4 | // abi and bytecode generated from simplestorage.sol: 5 | // > solcjs --bin --abi simplestorage.sol 6 | var abi = [{"constant":true,"inputs":[],"name":"storedData","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initVal","type":"uint256"}],"payable":false,"type":"constructor"}]; 7 | 8 | var bytecode = "0x6060604052341561000f57600080fd5b604051602080610149833981016040528080519060200190919050505b806000819055505b505b610104806100456000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a1afcd914605157806360fe47b11460775780636d4ce63c146097575b600080fd5b3415605b57600080fd5b606160bd565b6040518082815260200191505060405180910390f35b3415608157600080fd5b6095600480803590602001909190505060c3565b005b341560a157600080fd5b60a760ce565b6040518082815260200191505060405180910390f35b60005481565b806000819055505b50565b6000805490505b905600a165627a7a72305820d5851baab720bba574474de3d09dbeaabc674a15f4dd93b974908476542c23f00029"; 9 | 10 | var simpleContract = web3.eth.contract(abi); 11 | var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760}, function(e, contract) { 12 | if (e) { 13 | console.log("err creating contract", e); 14 | } else { 15 | if (!contract.address) { 16 | console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined..."); 17 | } else { 18 | console.log("Contract mined! Address: " + contract.address); 19 | console.log(contract); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /lib/splunk/splunk-config.yml: -------------------------------------------------------------------------------- 1 | splunk: 2 | conf: 3 | indexes: 4 | directory: /opt/splunk/etc/apps/search/local 5 | content: 6 | metrics: 7 | coldPath: $SPLUNK_DB/metrics/colddb 8 | datatype: metric 9 | homePath: $SPLUNK_DB/metrics/db 10 | maxTotalDataSizeMB: 512000 11 | thawedPath: $SPLUNK_DB/metrics/thaweddb 12 | ethereum: 13 | coldPath: $SPLUNK_DB/ethereum/colddb 14 | datatype: event 15 | homePath: $SPLUNK_DB/ethereum/db 16 | maxTotalDataSizeMB: 512000 17 | thawedPath: $SPLUNK_DB/ethereum/thaweddb 18 | logs: 19 | coldPath: $SPLUNK_DB/logs/colddb 20 | datatype: event 21 | homePath: $SPLUNK_DB/logs/db 22 | maxTotalDadtaSizeMB: 512000 23 | thawedPath: $SPLUNK_DB/logs/thaweddb 24 | inputs: 25 | directory: /opt/splunk/etc/apps/search/local 26 | content: 27 | "udp://8125": 28 | connection_host: ip 29 | index: metrics 30 | sourcetype: statsd 31 | "logs://cadvisor": 32 | index: logs 33 | sourcetype: cadvisor 34 | disabled: 0 35 | "logs://cakeshop": 36 | index: logs 37 | sourcetype: cakeshop 38 | disabled: 0 39 | props: 40 | directory: /opt/splunk/etc/apps/search/local 41 | content: 42 | ethlogger: 43 | EXTRACT-log_ethlogger: ethlogger-[a-f0-9]{12}.*ethlogger:(?P.*) 44 | docker: 45 | EXTRACT-container_name,container_id: ^(?P\w+)\-(?P[^ ]+) 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quorum-wizard", 3 | "version": "1.3.3", 4 | "description": "Tool for walking through the setup of networks.", 5 | "main": "src/index.js", 6 | "bin": { 7 | "quorum-wizard": "build/index.js" 8 | }, 9 | "preferGlobal": true, 10 | "files": [ 11 | "build", 12 | "lib", 13 | "7nodes", 14 | "package.json", 15 | "README.md" 16 | ], 17 | "scripts": { 18 | "build": "babel src -d build --ignore \"src/**/*.test.js\" --source-maps inline", 19 | "start": "babel src --watch -d build --ignore \"src/**/*.test.js\" --source-maps inline", 20 | "test": "jest", 21 | "test:watch": "jest --watch", 22 | "test:coverage": "jest --coverage=true", 23 | "lint": "eslint \"src/**/*.js\"", 24 | "fix": "eslint --fix \"src/**/*.js\"" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/jpmorganchase/quorum-wizard" 29 | }, 30 | "author": "", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/jpmorganchase/quorum-wizard/issues" 34 | }, 35 | "keywords": [ 36 | "quorum", 37 | "tessera", 38 | "cakeshop", 39 | "ethereum", 40 | "wizard" 41 | ], 42 | "homepage": "http://docs.goquorum.com/en/latest/Wizard/GettingStarted/", 43 | "dependencies": { 44 | "@babel/runtime": "^7.8.3", 45 | "@iarna/toml": "^3.0.0", 46 | "axios": "^0.21.1", 47 | "ethers": "^5.0.7", 48 | "fs-extra": "^9.0.0", 49 | "inquirer": "^7.0.0", 50 | "ip-address": "^6.3.0", 51 | "is-wsl": "^2.2.0", 52 | "js-yaml": "^3.13.1", 53 | "jsbn": "^1.1.0", 54 | "lodash": "^4.17.19", 55 | "react": "^16.12.0", 56 | "sanitize-filename": "^1.6.3", 57 | "semver-compare": "^1.0.0", 58 | "source-map-support": "^0.5.16", 59 | "tar-fs": "^2.0.0", 60 | "winston": "^3.2.1", 61 | "yargs": "^15.1.0" 62 | }, 63 | "devDependencies": { 64 | "@babel/cli": "^7.8.3", 65 | "@babel/core": "^7.8.3", 66 | "@babel/plugin-transform-runtime": "^7.8.3", 67 | "@babel/preset-env": "^7.8.3", 68 | "babel-eslint": "^10.0.3", 69 | "babel-jest": "^25.1.0", 70 | "babel-preset-airbnb": "^4.4.0", 71 | "eslint": "^6.8.0", 72 | "eslint-config-airbnb": "^18.0.1", 73 | "eslint-plugin-import": "^2.18.2", 74 | "eslint-plugin-jsx-a11y": "^6.2.3", 75 | "eslint-plugin-react": "^7.14.3", 76 | "eslint-plugin-react-hooks": "^2.5.1", 77 | "expect": "latest", 78 | "jest": "^25.1.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/generators/bashHelper.js: -------------------------------------------------------------------------------- 1 | import { getFullNetworkPath } from './networkHelper' 2 | import { executeSync } from '../utils/execUtils' 3 | import { buildCakeshopDir, generateCakeshopScript } from './cakeshopHelper' 4 | import { isQuorumVersionAbove, pathToQuorumBinary } from './binaryHelper' 5 | import { isCakeshop, isRaft, isTessera } from '../model/NetworkConfig' 6 | import { info } from '../utils/log' 7 | import { formatTesseraKeysOutput } from './transactionManager' 8 | import { joinPath } from '../utils/pathUtils' 9 | import { scriptHeader, setEnvironmentCommand } from './scripts/utils' 10 | import { generateReportingConfig, generateReportingScript } from './reportingHelper' 11 | 12 | export async function initBash(config) { 13 | const initCommands = [] 14 | const networkPath = getFullNetworkPath(config) 15 | config.nodes.forEach((node, i) => { 16 | const nodeNumber = i + 1 17 | const quorumDir = joinPath('qdata', `dd${nodeNumber}`) 18 | const genesisLocation = joinPath(quorumDir, 'genesis.json') 19 | const initCommand = `cd '${networkPath}' && '${pathToQuorumBinary(config.network.quorumVersion)}' --datadir '${quorumDir}' init '${genesisLocation}' 2>&1` 20 | initCommands.push(initCommand) 21 | }) 22 | 23 | const qdataFolder = joinPath(networkPath, 'qdata') 24 | if (isCakeshop(config.network.cakeshop)) { 25 | buildCakeshopDir(config, qdataFolder) 26 | } 27 | if (config.network.reporting) { 28 | generateReportingConfig(config, qdataFolder) 29 | } 30 | info('Initializing quorum...') 31 | initCommands.forEach((command) => executeSync(command)) 32 | info('Done') 33 | } 34 | 35 | export function startScriptBash(config) { 36 | const commands = createCommands(config) 37 | 38 | const startScript = [ 39 | scriptHeader(), 40 | 'echo "\nStarting Quorum network...\n"', 41 | setEnvironmentCommand(config), 42 | commands.tesseraStart, 43 | waitForTesseraNodesCommand(config), 44 | 'echo "Starting Quorum nodes"', 45 | commands.gethStart, 46 | generateCakeshopScript(config), 47 | generateReportingScript(config), 48 | 'echo "Successfully started Quorum network."', 49 | `echo "${formatTesseraKeysOutput(config)}"`, 50 | ] 51 | 52 | return startScript.join('\n') 53 | } 54 | 55 | export function createCommands(config) { 56 | const startCommands = [] 57 | const tmStartCommands = [] 58 | 59 | config.nodes.forEach((node, i) => { 60 | const nodeNumber = i + 1 61 | const quorumDir = joinPath('qdata', `dd${nodeNumber}`) 62 | const tmDir = joinPath('qdata', `c${nodeNumber}`) 63 | const keyDir = joinPath(quorumDir, 'keystore') 64 | const passwordDestination = joinPath(keyDir, 'password.txt') 65 | const logs = joinPath('qdata', 'logs') 66 | 67 | const tmIpcLocation = isTessera(config.network.transactionManager) 68 | ? joinPath(getFullNetworkPath(config), tmDir, 'tm.ipc') 69 | : 'ignore' 70 | if (tmIpcLocation.length > 104) { 71 | throw new Error(`The full path to your network folder is ${tmIpcLocation.length - 88} character(s) too long. Please re-run the wizard in a different folder with a shorter path.`) 72 | } 73 | const startCommand = createGethStartCommand( 74 | config, 75 | node, 76 | passwordDestination, 77 | nodeNumber, 78 | tmIpcLocation, 79 | ) 80 | startCommands.push(startCommand) 81 | 82 | if (isTessera(config.network.transactionManager)) { 83 | const tmStartCommand = createTesseraStartCommand(config, node, nodeNumber, tmDir, logs) 84 | tmStartCommands.push(tmStartCommand) 85 | } 86 | }) 87 | 88 | return { 89 | tesseraStart: tmStartCommands.join('\n'), 90 | gethStart: startCommands.join('\n'), 91 | } 92 | } 93 | 94 | export function createGethStartCommand(config, node, passwordDestination, nodeNumber, tmIpcPath) { 95 | const { 96 | verbosity, networkId, consensus, quorumVersion, 97 | } = config.network 98 | const { 99 | devP2pPort, rpcPort, raftPort, wsPort, graphQlPort, 100 | } = node.quorum 101 | // in quorum 21.4.0 and higher, addr and port are the same as the rpc addr and port 102 | const legacyGraphQl = isQuorumVersionAbove(quorumVersion, '21.4.0') ? '' : `--graphql.addr 0.0.0.0 --graphql.port ${graphQlPort}` 103 | const args = `--nodiscover --rpc --rpccorsdomain=* --rpcvhosts=* --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,${consensus},quorumPermission --ws --wsaddr 0.0.0.0 --wsorigins=* --wsapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,${consensus},quorumPermission --emitcheckpoints --unlock 0 --password ${passwordDestination} --allow-insecure-unlock --graphql --graphql.corsdomain=* --graphql.vhosts=* ${legacyGraphQl}` 104 | const consensusArgs = isRaft(consensus) 105 | ? `--raft --raftport ${raftPort}` 106 | : '--istanbul.blockperiod 5 --syncmode full --mine --minerthreads 1' 107 | 108 | return `PRIVATE_CONFIG='${tmIpcPath}' nohup "$BIN_GETH" --datadir qdata/dd${nodeNumber} ${args} ${consensusArgs} --permissioned --verbosity ${verbosity} --networkid ${networkId} --rpcport ${rpcPort} --wsport ${wsPort} --port ${devP2pPort} 2>>qdata/logs/${nodeNumber}.log &` 109 | } 110 | 111 | export function createTesseraStartCommand(config, node, nodeNumber, tmDir, logDir) { 112 | // `rm -f ${tmDir}/tm.ipc` 113 | 114 | let DEBUG = '' 115 | if (config.network.remoteDebug) { 116 | DEBUG = '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=500$i -Xdebug' 117 | } 118 | 119 | const MEMORY = '-Xms128M -Xmx128M' 120 | const CMD = `java ${DEBUG} ${MEMORY} -jar "$BIN_TESSERA" -configfile ${tmDir}/tessera-config-09-${nodeNumber}.json >> ${logDir}/tessera${nodeNumber}.log 2>&1 &` 121 | return CMD 122 | } 123 | 124 | function checkTesseraUpcheck(nodes) { 125 | return nodes.map((node, i) => ` 126 | result${i + 1}=$(curl -s http://${node.tm.ip}:${node.tm.p2pPort}/upcheck) 127 | if [ ! "\${result${i + 1}}" == "I'm up!" ]; then 128 | echo "Node ${i + 1} is not yet listening on http" 129 | DOWN=true 130 | fi`) 131 | } 132 | 133 | export function waitForTesseraNodesCommand(config) { 134 | if (!isTessera(config.network.transactionManager)) { 135 | return '' 136 | } 137 | return ` 138 | echo "Waiting until all Tessera nodes are running..." 139 | DOWN=true 140 | k=10 141 | while \${DOWN}; do 142 | sleep 1 143 | DOWN=false 144 | for i in \`seq 1 ${config.nodes.length}\` 145 | do 146 | if [ ! -S "qdata/c\${i}/tm.ipc" ]; then 147 | echo "Node \${i} is not yet listening on tm.ipc" 148 | DOWN=true 149 | fi 150 | done 151 | set +e 152 | #NOTE: if using https, change the scheme 153 | #NOTE: if using the IP whitelist, change the host to an allowed host 154 | ${checkTesseraUpcheck(config.nodes).join('')} 155 | 156 | k=$((k - 1)) 157 | if [ \${k} -le 0 ]; then 158 | echo "Tessera is taking a long time to start. Look at the Tessera logs in qdata/logs/ for help diagnosing the problem." 159 | fi 160 | echo "Waiting until all Tessera nodes are running..." 161 | 162 | sleep 5 163 | done 164 | 165 | echo "All Tessera nodes started" 166 | ` 167 | } 168 | -------------------------------------------------------------------------------- /src/generators/bashHelper.test.js: -------------------------------------------------------------------------------- 1 | import { anything } from 'expect' 2 | import { 3 | initBash, 4 | startScriptBash, 5 | } from './bashHelper' 6 | import { createConfigFromAnswers } from '../model/NetworkConfig' 7 | import { 8 | getOutputPath, 9 | libRootDir, 10 | readFileToString, 11 | writeFile, 12 | createFolder, 13 | writeJsonFile, 14 | wizardHomeDir, 15 | } from '../utils/fileUtils' 16 | import { 17 | TEST_CWD, 18 | TEST_LIB_ROOT_DIR, 19 | TEST_WIZARD_HOME_DIR, 20 | createNetPath, 21 | } from '../utils/testHelper' 22 | import { info } from '../utils/log' 23 | import { generateAccounts } from './consensusHelper' 24 | import { 25 | LATEST_QUORUM, LATEST_TESSERA, 26 | } from './download' 27 | 28 | jest.mock('../utils/fileUtils') 29 | jest.mock('./consensusHelper') 30 | jest.mock('../utils/execUtils') 31 | jest.mock('../utils/log') 32 | getOutputPath.mockReturnValue(TEST_CWD) 33 | libRootDir.mockReturnValue(TEST_LIB_ROOT_DIR) 34 | wizardHomeDir.mockReturnValue(TEST_WIZARD_HOME_DIR) 35 | generateAccounts.mockReturnValue('accounts') 36 | readFileToString.mockReturnValue('publicKey') 37 | info.mockReturnValue('log') 38 | 39 | const baseNetwork = { 40 | numberNodes: '3', 41 | consensus: 'raft', 42 | quorumVersion: LATEST_QUORUM, 43 | transactionManager: LATEST_TESSERA, 44 | tools: [], 45 | deployment: 'bash', 46 | } 47 | 48 | test('creates quickstart config', () => { 49 | const config = createConfigFromAnswers({}) 50 | const bash = startScriptBash(config) 51 | expect(bash).toMatchSnapshot() 52 | }) 53 | 54 | test('creates 3nodes raft bash tessera', () => { 55 | const config = createConfigFromAnswers(baseNetwork) 56 | const bash = startScriptBash(config) 57 | expect(bash).toMatchSnapshot() 58 | }) 59 | 60 | test('creates 3nodes raft bash tessera cakeshop', () => { 61 | const config = createConfigFromAnswers({ 62 | ...baseNetwork, 63 | tools: ['cakeshop'], 64 | }) 65 | const bash = startScriptBash(config) 66 | expect(bash).toMatchSnapshot() 67 | }) 68 | 69 | test('creates 3nodes raft bash no tessera', () => { 70 | const config = createConfigFromAnswers({ 71 | ...baseNetwork, 72 | transactionManager: 'none', 73 | }) 74 | const bash = startScriptBash(config) 75 | expect(bash).toMatchSnapshot() 76 | }) 77 | 78 | test('creates 3nodes raft bash tessera custom', () => { 79 | const config = createConfigFromAnswers({ 80 | ...baseNetwork, 81 | generateKeys: false, 82 | networkId: 10, 83 | genesisLocation: 'none', 84 | customizePorts: false, 85 | nodes: [], 86 | }) 87 | const bash = startScriptBash(config) 88 | expect(bash).toMatchSnapshot() 89 | }) 90 | 91 | test('creates 2nodes istanbul bash tessera cakeshop custom ports', () => { 92 | const nodes = [ 93 | { 94 | quorum: { 95 | ip: '127.0.0.1', 96 | devP2pPort: '55001', 97 | rpcPort: '56000', 98 | wsPort: '57000', 99 | raftPort: '80501', 100 | graphQlPort: '58000', 101 | }, 102 | tm: { 103 | ip: '127.0.0.1', 104 | thirdPartyPort: '5081', 105 | p2pPort: '5001', 106 | }, 107 | }, 108 | { 109 | quorum: { 110 | ip: '127.0.0.1', 111 | devP2pPort: '55001', 112 | rpcPort: '56001', 113 | wsPort: '57001', 114 | raftPort: '80502', 115 | graphQlPort: '58001', 116 | }, 117 | tm: { 118 | ip: '127.0.0.1', 119 | thirdPartyPort: '5082', 120 | p2pPort: '5002', 121 | }, 122 | }, 123 | ] 124 | const config = createConfigFromAnswers({ 125 | numberNodes: '2', 126 | consensus: 'istanbul', 127 | quorumVersion: LATEST_QUORUM, 128 | transactionManager: LATEST_TESSERA, 129 | deployment: 'bash', 130 | tools: ['cakeshop'], 131 | generateKeys: false, 132 | networkId: 10, 133 | genesisLocation: 'none', 134 | customizePorts: true, 135 | cakeshopPort: '7999', 136 | nodes, 137 | }) 138 | const bash = startScriptBash(config) 139 | expect(bash).toMatchSnapshot() 140 | }) 141 | 142 | test('build bash with tessera and cakeshop', () => { 143 | const config = createConfigFromAnswers({ 144 | ...baseNetwork, 145 | tools: ['cakeshop'], 146 | }) 147 | initBash(config) 148 | expect(createFolder).toBeCalledWith(createNetPath(config, 'qdata', 'cakeshop', 'local'), true) 149 | expect(writeJsonFile).toBeCalledWith(createNetPath(config, 'qdata', 'cakeshop', 'local'), 'cakeshop.json', anything()) 150 | expect(writeFile).toBeCalledWith(createNetPath(config, 'qdata', 'cakeshop', 'local', 'application.properties'), anything(), false) 151 | }) 152 | -------------------------------------------------------------------------------- /src/generators/binaryHelper.js: -------------------------------------------------------------------------------- 1 | import cmp from 'semver-compare' 2 | import { wizardHomeDir } from '../utils/fileUtils' 3 | import { executeSync } from '../utils/execUtils' 4 | import { 5 | isBash, 6 | isIstanbul, 7 | isTessera, 8 | isKubernetes, 9 | } from '../model/NetworkConfig' 10 | import { 11 | BINARIES, 12 | downloadIfMissing, 13 | LATEST_BOOTNODE, 14 | LATEST_ISTANBUL_TOOLS, 15 | } from './download' 16 | import { disableIfWrongJavaVersion } from '../questions/validators' 17 | import { info } from '../utils/log' 18 | import { joinPath } from '../utils/pathUtils' 19 | 20 | // This method could be improved, but right now it tries to: 21 | // a. Cache downloads 22 | // b. Only download if necessary for bash deployments 23 | export async function downloadAndCopyBinaries(config) { 24 | info('Downloading dependencies...') 25 | const { 26 | transactionManager, cakeshop, generateKeys, quorumVersion, consensus, 27 | } = config.network 28 | 29 | const downloads = [] 30 | 31 | if (isIstanbul(consensus)) { 32 | downloads.push(downloadIfMissing('istanbul', LATEST_ISTANBUL_TOOLS)) 33 | } 34 | 35 | if (generateKeys) { 36 | downloads.push(downloadIfMissing('bootnode', LATEST_BOOTNODE)) 37 | } 38 | 39 | if (quorumVersion !== 'PATH') { 40 | downloads.push(downloadIfMissing('quorum', quorumVersion)) 41 | } 42 | const tesseraVersion = transactionManager 43 | if (tesseraVersion !== 'PATH' && isTessera(tesseraVersion)) { 44 | downloads.push(downloadIfMissing('tessera', tesseraVersion)) 45 | } 46 | 47 | if (cakeshop !== 'none') { 48 | downloads.push(downloadIfMissing('cakeshop', cakeshop)) 49 | } 50 | 51 | await Promise.all(downloads) 52 | } 53 | 54 | export function getGethOnPath() { 55 | const pathChoices = [] 56 | try { 57 | const gethOnPath = executeSync('which geth').toString().trim() 58 | if (gethOnPath) { 59 | const version = getPathGethVersion() 60 | if (version !== null) { 61 | pathChoices.push({ 62 | name: `Quorum ${version} on path (${gethOnPath})`, 63 | value: 'PATH', 64 | }) 65 | } 66 | } 67 | } catch (e) { 68 | // either no geth or the version call errored, don't include it in choices 69 | } 70 | return pathChoices 71 | } 72 | 73 | export function getPathGethVersion() { 74 | const gethVersionOutput = executeSync('geth version').toString() 75 | const versionMatch = gethVersionOutput.match(/Quorum Version: (\S+)/) 76 | if (versionMatch !== null) { 77 | return versionMatch[1] 78 | } 79 | return null 80 | } 81 | 82 | export function isQuorumVersionAbove(quorumVersion, compareVersion) { 83 | const version = quorumVersion === 'PATH' ? getPathGethVersion() : quorumVersion 84 | return cmp(version, compareVersion) >= 0 85 | } 86 | 87 | export function isLegacyTessera(tesseraVersion) { 88 | if (tesseraVersion === 'PATH') { 89 | // no easy way to get version from tessera jar, assume it is not legacy 90 | return false 91 | } 92 | if (tesseraVersion === 'none') { 93 | // treat no tessera as legacy, since this determines if enhanced privacy is enabled 94 | return true 95 | } 96 | return cmp(tesseraVersion, '1.0.0') < 0 97 | } 98 | 99 | export function getDownloadableGethChoices(deployment) { 100 | let choices = getDownloadableChoices(BINARIES.quorum) 101 | if (isBash(deployment)) { 102 | choices = choices.concat(getGethOnPath()) 103 | } 104 | return choices 105 | } 106 | 107 | export function getDownloadableTesseraChoices(deployment) { 108 | let choices = getDownloadableChoices(BINARIES.tessera) 109 | if (isBash(deployment)) { 110 | choices = choices.concat(getTesseraOnPath()) 111 | } else { 112 | // allow all options in docker compose mode since local jdk version doesn't matter 113 | choices = choices.map((choice) => ({ ...choice, disabled: false })) 114 | } 115 | return isKubernetes(deployment) ? choices : choices.concat('none') 116 | } 117 | 118 | function getDownloadableChoices(versions) { 119 | return Object.entries(versions).map(([key, binaryInfo]) => ({ 120 | name: binaryInfo.description, 121 | value: key, 122 | disabled: disableIfWrongJavaVersion(binaryInfo), 123 | })) 124 | } 125 | 126 | export function getTesseraOnPath() { 127 | const pathChoices = [] 128 | const tesseraJarEnv = process.env.TESSERA_JAR 129 | if (tesseraJarEnv) { 130 | pathChoices.push({ 131 | name: `Tessera at $TESSERA_JAR (${tesseraJarEnv})`, 132 | value: 'PATH', 133 | }) 134 | } 135 | return pathChoices 136 | } 137 | 138 | export function pathToQuorumBinary(quorumVersion) { 139 | if (quorumVersion === 'PATH') { 140 | return 'geth' 141 | } 142 | const binary = BINARIES.quorum[quorumVersion] 143 | return joinPath(wizardHomeDir(), 'bin', 'quorum', quorumVersion, binary.name) 144 | } 145 | 146 | export function pathToTesseraJar(transactionManager) { 147 | if (transactionManager === 'PATH') { 148 | return '$TESSERA_JAR' 149 | } 150 | const binary = BINARIES.tessera[transactionManager] 151 | return joinPath(wizardHomeDir(), 'bin', 'tessera', transactionManager, binary.name) 152 | } 153 | 154 | export function pathToCakeshop(version) { 155 | const binary = BINARIES.cakeshop[version] 156 | return joinPath(wizardHomeDir(), 'bin', 'cakeshop', version, binary.name) 157 | } 158 | 159 | export function pathToIstanbulTools() { 160 | const binary = BINARIES.istanbul[LATEST_ISTANBUL_TOOLS] 161 | return joinPath(wizardHomeDir(), 'bin', 'istanbul', LATEST_ISTANBUL_TOOLS, binary.name) 162 | } 163 | 164 | export function pathToBootnode() { 165 | const binary = BINARIES.bootnode[LATEST_BOOTNODE] 166 | return joinPath(wizardHomeDir(), 'bin', 'bootnode', LATEST_BOOTNODE, binary.name) 167 | } 168 | -------------------------------------------------------------------------------- /src/generators/cakeshopHelper.js: -------------------------------------------------------------------------------- 1 | import { createFolder, formatNewLine, writeFile, writeJsonFile, } from '../utils/fileUtils' 2 | import { generateCakeshopConfig } from '../model/CakeshopConfig' 3 | import { isCakeshop, isDocker, } from '../model/NetworkConfig' 4 | import { joinPath } from '../utils/pathUtils' 5 | 6 | export function buildCakeshopDir(config, qdata) { 7 | const cakeshopDir = joinPath(qdata, 'cakeshop', 'local') 8 | createFolder(cakeshopDir, true) 9 | writeJsonFile(cakeshopDir, 'cakeshop.json', generateCakeshopConfig(config)) 10 | writeFile(joinPath(cakeshopDir, 'application.properties'), buildPropertiesFile(config), false) 11 | } 12 | 13 | function buildPropertiesFile(config) { 14 | const cakeshopConfig = { 15 | 'cakeshop.initialnodes': 'qdata/cakeshop/local/cakeshop.json', 16 | 'cakeshop.selected_node': '1', 17 | 'contract.registry.addr': '', 18 | 'server.port': config.network.cakeshopPort, 19 | } 20 | if(config.network.reporting) { 21 | // docker only, so use 'reporting' hostname, but localhost for the ui 22 | cakeshopConfig['cakeshop.reporting.rpc'] = `http://reporting:${config.network.reportingRpcPort}` 23 | cakeshopConfig['cakeshop.reporting.ui'] = `http://localhost:${config.network.reportingUiPort}` 24 | } 25 | return Object.entries(cakeshopConfig) 26 | .map(([key, value]) => `${key}=${value}`) 27 | .join('\n') 28 | } 29 | 30 | export function generateCakeshopScript(config) { 31 | if (!isCakeshop(config.network.cakeshop)) { 32 | return '' 33 | } 34 | const jvmParams = '-Dcakeshop.config.dir=qdata/cakeshop' 35 | const startCommand = `java ${jvmParams} -jar "$BIN_CAKESHOP" >> qdata/logs/cakeshop.log 2>&1 &` 36 | return [ 37 | 'echo "Starting Cakeshop"', 38 | startCommand, 39 | waitForCakeshopCommand(config.network.cakeshopPort), 40 | ].join('\n') 41 | } 42 | 43 | export function waitForCakeshopCommand(cakeshopPort) { 44 | return ` 45 | DOWN=true 46 | k=10 47 | while \${DOWN}; do 48 | sleep 1 49 | echo "Waiting until Cakeshop is running..." 50 | DOWN=false 51 | set +e 52 | result=$(curl -s http://localhost:${cakeshopPort}/actuator/health) 53 | set -e 54 | if [ ! "\${result}" == "{\\"status\\":\\"UP\\"}" ]; then 55 | echo "Cakeshop is not yet listening on http" 56 | DOWN=true 57 | fi 58 | 59 | k=$((k-1)) 60 | if [ \${k} -le 0 ]; then 61 | echo "Cakeshop is taking a long time to start. Look at logs" 62 | fi 63 | 64 | sleep 5 65 | done 66 | 67 | echo "Cakeshop started at http://localhost:${cakeshopPort}"` 68 | } 69 | -------------------------------------------------------------------------------- /src/generators/cakeshopHelper.test.js: -------------------------------------------------------------------------------- 1 | import { anything } from 'expect' 2 | import { createConfigFromAnswers } from '../model/NetworkConfig' 3 | import { 4 | createFolder, 5 | getOutputPath, 6 | libRootDir, 7 | writeJsonFile, 8 | readFileToString, 9 | writeFile, 10 | } from '../utils/fileUtils' 11 | import { buildCakeshopDir } from './cakeshopHelper' 12 | import { 13 | createNetPath, 14 | createLibPath, 15 | TEST_CWD, 16 | TEST_LIB_ROOT_DIR, 17 | } from '../utils/testHelper' 18 | import { LATEST_QUORUM, LATEST_TESSERA } from './download' 19 | 20 | jest.mock('../utils/fileUtils') 21 | getOutputPath.mockReturnValue(TEST_CWD) 22 | libRootDir.mockReturnValue(TEST_LIB_ROOT_DIR) 23 | 24 | describe('creates a cakeshop directory structure for bash', () => { 25 | const baseNetwork = { 26 | numberNodes: '5', 27 | consensus: 'raft', 28 | quorumVersion: LATEST_QUORUM, 29 | transactionManager: LATEST_TESSERA, 30 | tools: ['cakeshop'], 31 | deployment: 'bash', 32 | } 33 | it('creates directory structure for cakeshop files and moves them in', () => { 34 | const config = createConfigFromAnswers(baseNetwork) 35 | 36 | buildCakeshopDir(config, createNetPath(config, 'qdata')) 37 | expect(createFolder).toBeCalledWith(createNetPath(config, 'qdata/cakeshop/local'), true) 38 | expect(writeJsonFile).toBeCalledWith( 39 | createNetPath(config, 'qdata/cakeshop/local'), 40 | 'cakeshop.json', 41 | anything(), 42 | ) 43 | expect(writeFile).toBeCalledWith( 44 | createNetPath(config, 'qdata/cakeshop/local/application.properties'), 45 | anything(), 46 | false, 47 | ) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/generators/consensusHelper.js: -------------------------------------------------------------------------------- 1 | import { 2 | readFileToString, 3 | writeFile, 4 | } from '../utils/fileUtils' 5 | import { executeSync } from '../utils/execUtils' 6 | import nodekeyToAccount from '../utils/accountHelper' 7 | import { pathToIstanbulTools } from './binaryHelper' 8 | import { joinPath } from '../utils/pathUtils' 9 | 10 | export function generateAccounts(nodes, configDir) { 11 | const numNodes = nodes.length 12 | const accounts = {} 13 | for (let i = 0; i < parseInt(numNodes, 10); i += 1) { 14 | const numNode = i + 1 15 | 16 | const keyDir = joinPath(configDir, `key${numNode}`) 17 | const keyString = readFileToString(joinPath(keyDir, 'acctkeyfile.json')) 18 | const key = `0x${JSON.parse(keyString).address}` 19 | accounts[key] = { balance: '1000000000000000000000000000' } 20 | } 21 | return accounts 22 | } 23 | 24 | export function generateExtraData(nodes, configDir) { 25 | const configLines = ['vanity = "0x00"'] 26 | const validators = nodes.map((node, i) => { 27 | const nodeNumber = i + 1 28 | const keyDir = joinPath(configDir, `key${nodeNumber}`) 29 | return nodekeyToAccount(`0x${readFileToString(joinPath(keyDir, 'nodekey'))}`) 30 | }) 31 | configLines.push(`validators = ${JSON.stringify(validators)}`) 32 | 33 | const istanbulConfigFile = joinPath(configDir, 'istanbul.toml') 34 | writeFile(istanbulConfigFile, configLines.join('\n'), false) 35 | 36 | const extraDataCmd = `cd '${configDir}' && '${pathToIstanbulTools()}' extra encode --config ${istanbulConfigFile} | awk '{print $4}' ` 37 | 38 | return executeSync(extraDataCmd).toString().trim() 39 | } 40 | -------------------------------------------------------------------------------- /src/generators/consensusHelper.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | generateAccounts, 3 | generateExtraData, 4 | } from './consensusHelper' 5 | import nodekeyToAccount from '../utils/accountHelper' 6 | import { 7 | getOutputPath, 8 | libRootDir, 9 | readFileToString, 10 | wizardHomeDir, 11 | writeFile, 12 | } from '../utils/fileUtils' 13 | import { generateNodeConfigs } from '../model/NetworkConfig' 14 | import { executeSync } from '../utils/execUtils' 15 | import { 16 | TEST_CWD, 17 | TEST_LIB_ROOT_DIR, 18 | TEST_WIZARD_HOME_DIR, 19 | } from '../utils/testHelper' 20 | import { joinPath } from '../utils/pathUtils' 21 | 22 | jest.mock('../utils/fileUtils') 23 | jest.mock('../utils/accountHelper') 24 | jest.mock('../utils/execUtils') 25 | getOutputPath.mockReturnValue(TEST_CWD) 26 | libRootDir.mockReturnValue(TEST_LIB_ROOT_DIR) 27 | wizardHomeDir.mockReturnValue(TEST_WIZARD_HOME_DIR) 28 | 29 | describe('generates accounts for genesis', () => { 30 | it('creates allocation account json given nodes', () => { 31 | const nodes = generateNodeConfigs(3) 32 | const accts = '{"0xa5a0b81cbcd2d93bba08b3e27b1437d7bdc42836":{"balance":"1000000000000000000000000000"},"0x0fe3fd1414001b295da621e30698462df06eaad2":{"balance":"1000000000000000000000000000"},"0x8aef5fa7f18ffda8fa98016ec27562ea33743f18":{"balance":"1000000000000000000000000000"}}' 33 | const expected = JSON.parse(accts) 34 | readFileToString 35 | .mockReturnValueOnce('{"address":"a5a0b81cbcd2d93bba08b3e27b1437d7bdc42836"}') 36 | .mockReturnValueOnce('{"address":"0fe3fd1414001b295da621e30698462df06eaad2"}') 37 | .mockReturnValueOnce('{"address":"8aef5fa7f18ffda8fa98016ec27562ea33743f18"}') 38 | 39 | expect(generateAccounts(nodes, 'keyPath')).toEqual(expected) 40 | }) 41 | }) 42 | 43 | describe('generates extraData for istanbul genesis', () => { 44 | it('creates extraData for given nodes', () => { 45 | const nodes = generateNodeConfigs(3) 46 | 47 | readFileToString 48 | .mockReturnValueOnce('0x48B4e05D804254f35e657c46cD25A6C4DaC85446') 49 | .mockReturnValueOnce('0xe63C266CB3b2750E8B210eA4E9154D6a099f6e53') 50 | .mockReturnValueOnce('0xFCF5B48D9972CF125f85c7aa04641942220A46C5') 51 | 52 | nodekeyToAccount 53 | .mockReturnValueOnce('0xAb2a47301ACa1444Ae4e3c2bC7D4B88afc4Af5f2') 54 | .mockReturnValueOnce('0x49C1488d2f8Abf1D7AB0a08f2E1308369fDDFbfE') 55 | .mockReturnValueOnce('0x1760F90FF74aD4d8BBF536D01Fc0afb879c3dCf0') 56 | 57 | executeSync.mockReturnValueOnce('validators') 58 | generateExtraData(nodes, 'testDir', 'keyPath') 59 | expect(writeFile).toBeCalledWith(joinPath('testDir', 'istanbul.toml'), expect.anything(), false) 60 | expect(executeSync).toBeCalledTimes(1) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/generators/download.js: -------------------------------------------------------------------------------- 1 | import { basename } from 'path' 2 | import axios from 'axios' 3 | import { createGunzip } from 'zlib' 4 | import { extract } from 'tar-fs' 5 | import { createWriteStream } from 'fs' 6 | import { 7 | createFolder, 8 | exists, 9 | wizardHomeDir, 10 | } from '../utils/fileUtils' 11 | import { info } from '../utils/log' 12 | import { joinPath } from '../utils/pathUtils' 13 | 14 | // eslint-disable-next-line import/prefer-default-export 15 | export async function downloadIfMissing(name, version) { 16 | if (BINARIES[name] === undefined || BINARIES[name][version] === undefined) { 17 | throw new Error(`Could not find binary info entry for ${name} ${version}`) 18 | } 19 | const binaryInfo = BINARIES[name][version] 20 | const binDir = joinPath(wizardHomeDir(), 'bin', name, version) 21 | const binaryFileLocation = joinPath(binDir, binaryInfo.name) 22 | if (!exists(binaryFileLocation)) { 23 | createFolder(binDir, true) 24 | const url = getPlatformSpecificUrl(binaryInfo) 25 | 26 | info(`Downloading ${name} ${version} from ${url}...`) 27 | const response = await axios({ 28 | url, 29 | method: 'GET', 30 | responseType: 'stream', 31 | }) 32 | 33 | info(`Unpacking to ${binaryFileLocation}`) 34 | if (binaryInfo.type === 'tar.gz') { 35 | const extractorStream = response.data.pipe(createGunzip()) 36 | .pipe(extract(binDir, { 37 | map: (header) => { 38 | const filename = basename(header.name) 39 | if (binaryInfo.files.includes(filename)) { 40 | // don't include folders when extracting files we want 41 | header.name = filename // eslint-disable-line no-param-reassign 42 | } 43 | return header 44 | }, 45 | ignore: (pathName) => !binaryInfo.files.includes(basename(pathName)), 46 | })) 47 | return new Promise((resolve, reject) => { 48 | extractorStream.on('finish', () => { 49 | info(`Saved to ${binaryFileLocation}`) 50 | resolve() 51 | }) 52 | extractorStream.on('error', reject) 53 | }) 54 | } 55 | const writer = createWriteStream(binaryFileLocation, { mode: 0o755 }) 56 | response.data.pipe(writer) 57 | return new Promise((resolve, reject) => { 58 | writer.on('finish', () => { 59 | info(`Saved to ${binaryFileLocation}`) 60 | resolve() 61 | }) 62 | writer.on('error', reject) 63 | }) 64 | } 65 | info(`Using cached ${name} at: ${binaryFileLocation}`) 66 | return binaryFileLocation 67 | } 68 | 69 | export function getPlatformSpecificUrl({ url }) { 70 | if (typeof url === 'string') { 71 | return url 72 | } 73 | const platformUrl = url[process.platform] 74 | if (platformUrl === undefined) { 75 | throw new Error( 76 | `Sorry, your platform (${process.platform}) is not supported.`, 77 | ) 78 | } 79 | return platformUrl 80 | } 81 | 82 | export const LATEST_QUORUM = '21.4.0' 83 | export const LATEST_TESSERA = '21.1.1' 84 | export const LATEST_CAKESHOP = '0.12.1' 85 | export const LATEST_ISTANBUL_TOOLS = '1.0.3' 86 | export const LATEST_BOOTNODE = '1.9.7' 87 | export const QUORUM_PRE_260 = '2.5.0' 88 | export const LATEST_REPORTING = 'latest' 89 | 90 | export const BINARIES = { 91 | quorum: { 92 | '21.4.0': { 93 | name: 'geth', 94 | description: 'Quorum 21.4.0', 95 | url: { 96 | darwin: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v21.4.0/geth_v21.4.0_darwin_amd64.tar.gz', 97 | linux: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v21.4.0/geth_v21.4.0_linux_amd64.tar.gz', 98 | }, 99 | type: 'tar.gz', 100 | files: [ 101 | 'geth', 102 | ], 103 | }, 104 | '21.1.0': { 105 | name: 'geth', 106 | description: 'Quorum 21.1.0', 107 | url: { 108 | darwin: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v21.1.0/geth_v21.1.0_darwin_amd64.tar.gz', 109 | linux: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v21.1.0/geth_v21.1.0_linux_amd64.tar.gz', 110 | }, 111 | type: 'tar.gz', 112 | files: [ 113 | 'geth', 114 | ], 115 | }, 116 | '20.10.0': { 117 | name: 'geth', 118 | description: 'Quorum 20.10.0', 119 | url: { 120 | darwin: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v20.10.0/geth_v20.10.0_darwin_amd64.tar.gz', 121 | linux: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v20.10.0/geth_v20.10.0_linux_amd64.tar.gz', 122 | }, 123 | type: 'tar.gz', 124 | files: [ 125 | 'geth', 126 | ], 127 | }, 128 | '2.7.0': { 129 | name: 'geth', 130 | description: 'Quorum 2.7.0', 131 | url: { 132 | darwin: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v2.7.0/geth_v2.7.0_darwin_amd64.tar.gz', 133 | linux: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v2.7.0/geth_v2.7.0_linux_amd64.tar.gz', 134 | }, 135 | type: 'tar.gz', 136 | files: [ 137 | 'geth', 138 | ], 139 | }, 140 | '2.6.0': { 141 | name: 'geth', 142 | description: 'Quorum 2.6.0', 143 | url: { 144 | darwin: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v2.6.0/geth_v2.6.0_darwin_amd64.tar.gz', 145 | linux: 'https://artifacts.consensys.net/public/go-quorum/raw/versions/v2.6.0/geth_v2.6.0_linux_amd64.tar.gz', 146 | }, 147 | type: 'tar.gz', 148 | files: [ 149 | 'geth', 150 | ], 151 | }, 152 | }, 153 | 154 | tessera: { 155 | '21.1.1': { 156 | name: 'tessera-app.jar', 157 | description: 'Tessera 21.1.1', 158 | url: 'https://oss.sonatype.org/service/local/repositories/releases/content/net/consensys/quorum/tessera/tessera-app/21.1.1/tessera-app-21.1.1-app.jar', 159 | type: 'jar', 160 | }, 161 | '21.1.0': { 162 | name: 'tessera-app.jar', 163 | description: 'Tessera 21.1.0', 164 | url: 'https://oss.sonatype.org/service/local/repositories/releases/content/net/consensys/quorum/tessera/tessera-app/21.1.0/tessera-app-21.1.0-app.jar', 165 | type: 'jar', 166 | }, 167 | '20.10.0': { 168 | name: 'tessera-app.jar', 169 | description: 'Tessera 20.10.0', 170 | url: 'https://oss.sonatype.org/service/local/repositories/releases/content/net/consensys/quorum/tessera/tessera-app/20.10.0/tessera-app-20.10.0-app.jar', 171 | type: 'jar', 172 | }, 173 | '0.11.0': { 174 | name: 'tessera-app.jar', 175 | description: 'Tessera 0.11.0', 176 | url: 'https://oss.sonatype.org/service/local/repositories/releases/content/net/consensys/quorum/tessera/tessera-app/0.11.0/tessera-app-0.11.0-app.jar', 177 | type: 'jar', 178 | }, 179 | '0.10.6': { 180 | name: 'tessera-app.jar', 181 | description: 'Tessera 0.10.6', 182 | url: 'https://oss.sonatype.org/service/local/repositories/releases/content/com/jpmorgan/quorum/tessera-app/0.10.6/tessera-app-0.10.6-app.jar', 183 | type: 'jar', 184 | }, 185 | '0.10.5': { 186 | name: 'tessera-app.jar', 187 | description: 'Tessera 0.10.5', 188 | url: 'https://oss.sonatype.org/service/local/repositories/releases/content/com/jpmorgan/quorum/tessera-app/0.10.5/tessera-app-0.10.5-app.jar', 189 | type: 'jar', 190 | }, 191 | '0.10.4': { 192 | name: 'tessera-app.jar', 193 | description: 'Tessera 0.10.4', 194 | url: 'https://oss.sonatype.org/service/local/repositories/releases/content/com/jpmorgan/quorum/tessera-app/0.10.4/tessera-app-0.10.4-app.jar', 195 | type: 'jar', 196 | }, 197 | }, 198 | 199 | cakeshop: { 200 | '0.12.1': { 201 | name: 'cakeshop.war', 202 | description: 'Cakeshop 0.12.1', 203 | url: 'https://github.com/jpmorganchase/cakeshop/releases/download/v0.12.1/cakeshop-0.12.1.war', 204 | type: 'jar', 205 | }, 206 | }, 207 | 208 | istanbul: { 209 | '1.0.3': { 210 | name: 'istanbul', 211 | url: { 212 | darwin: 'https://artifacts.consensys.net/public/quorum-tools/raw/versions/v1.0.3/istanbul-tools_v1.0.3_darwin_amd64.tar.gz', 213 | linux: 'https://artifacts.consensys.net/public/quorum-tools/raw/versions/v1.0.3/istanbul-tools_v1.0.3_linux_amd64.tar.gz', 214 | }, 215 | type: 'tar.gz', 216 | files: [ 217 | 'istanbul', 218 | ], 219 | }, 220 | }, 221 | 222 | bootnode: { 223 | '1.9.7': { 224 | name: 'bootnode', 225 | url: { 226 | darwin: 'https://artifacts.consensys.net/public/quorum-tools/raw/versions/v1.9.7/bootnode_v1.9.7_darwin_amd64.tar.gz', 227 | linux: 'https://artifacts.consensys.net/public/quorum-tools/raw/versions/v1.9.7/bootnode_v1.9.7_linux_amd64.tar.gz', 228 | }, 229 | type: 'tar.gz', 230 | files: [ 231 | 'bootnode', 232 | ], 233 | }, 234 | }, 235 | } 236 | -------------------------------------------------------------------------------- /src/generators/download.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | overrideProcessValue, 3 | TEST_CWD, 4 | TEST_WIZARD_HOME_DIR, 5 | } from '../utils/testHelper' 6 | import { 7 | getPlatformSpecificUrl, 8 | downloadIfMissing, 9 | LATEST_QUORUM, 10 | LATEST_TESSERA, 11 | LATEST_ISTANBUL_TOOLS, 12 | } from './download' 13 | import { 14 | getOutputPath, 15 | exists, 16 | wizardHomeDir, 17 | } from '../utils/fileUtils' 18 | import { info } from '../utils/log' 19 | 20 | jest.mock('../utils/fileUtils') 21 | jest.mock('../utils/log') 22 | 23 | getOutputPath.mockReturnValue(TEST_CWD) 24 | wizardHomeDir.mockReturnValue(TEST_WIZARD_HOME_DIR) 25 | info.mockReturnValue('info') 26 | 27 | describe('Handles different binary file urls', () => { 28 | let originalPlatform 29 | beforeAll(() => { 30 | originalPlatform = process.platform 31 | }) 32 | afterAll(() => { 33 | overrideProcessValue('platform', originalPlatform) 34 | }) 35 | it('Works with cross-platform single urls', () => { 36 | expect(getPlatformSpecificUrl(crossPlatform)).toEqual('crossplatform_url') 37 | }) 38 | it('Works with multiple platform urls', () => { 39 | overrideProcessValue('platform', 'linux') 40 | expect(getPlatformSpecificUrl(multiplePlatform)).toEqual('linux_url') 41 | overrideProcessValue('platform', 'darwin') 42 | expect(getPlatformSpecificUrl(multiplePlatform)).toEqual('mac_url') 43 | }) 44 | it('Throws an error when using an unsupported platform', () => { 45 | overrideProcessValue('platform', 'windows_nt') 46 | expect(() => getPlatformSpecificUrl(multiplePlatform)) 47 | .toThrow(new Error('Sorry, your platform (windows_nt) is not supported.')) 48 | }) 49 | }) 50 | 51 | const crossPlatform = { 52 | name: 'file.jar', 53 | url: 'crossplatform_url', 54 | type: 'jar', 55 | } 56 | 57 | const multiplePlatform = { 58 | name: 'compiled_bin', 59 | url: { 60 | darwin: 'mac_url', 61 | linux: 'linux_url', 62 | }, 63 | type: 'tar.gz', 64 | files: [ 65 | 'compiled_bin', 66 | ], 67 | } 68 | 69 | describe('tests download if missing function', () => { 70 | it('istanbul that exists', () => { 71 | exists.mockReturnValue(true) 72 | downloadIfMissing('istanbul', LATEST_ISTANBUL_TOOLS) 73 | }) 74 | it('quorum that exists', () => { 75 | exists.mockReturnValue(true) 76 | downloadIfMissing('quorum', LATEST_QUORUM) 77 | }) 78 | it('tessera that exists', () => { 79 | exists.mockReturnValue(true) 80 | downloadIfMissing('tessera', LATEST_TESSERA) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/generators/keyGen.js: -------------------------------------------------------------------------------- 1 | import { info } from '../utils/log' 2 | import { execute } from '../utils/execUtils' 3 | import { 4 | createFolder, 5 | writeFile, 6 | } from '../utils/fileUtils' 7 | import { isTessera } from '../model/NetworkConfig' 8 | import { 9 | pathToBootnode, 10 | pathToQuorumBinary, 11 | pathToTesseraJar, 12 | } from './binaryHelper' 13 | import { joinPath } from '../utils/pathUtils' 14 | 15 | // eslint-disable-next-line import/prefer-default-export 16 | export async function generateKeys(config, keyPath) { 17 | const tesseraKeyMsg = isTessera(config.network.transactionManager) ? ' and Tessera' : '' 18 | info(`Generating ${config.nodes.length} keys for Quorum${tesseraKeyMsg} nodes..`) 19 | const keygenProcesses = config.nodes.map((node, i) => { 20 | const nodeNumber = i + 1 21 | const keyDir = joinPath(keyPath, `key${nodeNumber}`) 22 | createFolder(keyDir, true) 23 | writeFile(joinPath(keyDir, 'password.txt'), '') 24 | 25 | return doExec(keyDir, config) 26 | }) 27 | 28 | await Promise.all(keygenProcesses) 29 | info('Keys were generated') 30 | } 31 | 32 | function doExec(keyDir, config) { 33 | let cmd = `cd '${keyDir}' && '${pathToQuorumBinary(config.network.quorumVersion)}' account new --keystore '${keyDir}' --password password.txt 2>&1 34 | '${pathToBootnode()}' -genkey=nodekey 35 | '${pathToBootnode()}' --nodekey=nodekey --writeaddress > enode 36 | find . -type f -name 'UTC*' -execdir mv {} acctkeyfile.json ';' 37 | ` 38 | if (isTessera(config.network.transactionManager)) { 39 | // blank password for now using `< /dev/null` to automatically answer password prompts 40 | cmd += `java -jar '${pathToTesseraJar(config.network.transactionManager)}' -keygen -filename tm < /dev/null` 41 | } 42 | 43 | return execute(cmd) 44 | } 45 | -------------------------------------------------------------------------------- /src/generators/keyGen.test.js: -------------------------------------------------------------------------------- 1 | import { generateKeys } from './keyGen' 2 | import { info } from '../utils/log' 3 | import { 4 | pathToQuorumBinary, 5 | pathToBootnode, 6 | pathToTesseraJar, 7 | } from './binaryHelper' 8 | import { execute } from '../utils/execUtils' 9 | import { 10 | createFolder, 11 | writeFile, 12 | getOutputPath, 13 | } from '../utils/fileUtils' 14 | import { 15 | TEST_CWD, 16 | createNetPath, 17 | } from '../utils/testHelper' 18 | import { joinPath } from '../utils/pathUtils' 19 | 20 | jest.mock('./binaryHelper') 21 | jest.mock('../utils/execUtils') 22 | jest.mock('../utils/fileUtils') 23 | jest.mock('../utils/log') 24 | getOutputPath.mockReturnValue(TEST_CWD) 25 | info.mockReturnValue('log') 26 | 27 | describe('generates keys', () => { 28 | it('generates quorum keys', async () => { 29 | const config = { 30 | network: { 31 | transactionManager: 'none', 32 | name: 'test', 33 | }, 34 | nodes: ['nodes'], 35 | } 36 | pathToQuorumBinary.mockReturnValueOnce('quorumPath') 37 | pathToBootnode.mockReturnValue('bootnodePath') 38 | await generateKeys(config, joinPath(createNetPath(config), 'keyPath')) 39 | const keyNum = config.nodes.length 40 | 41 | const expected = `cd '${joinPath(createNetPath(config), 'keyPath')}/key${keyNum}' && 'quorumPath' account new --keystore '${joinPath(createNetPath(config), 'keyPath')}/key${keyNum}' --password password.txt 2>&1 42 | 'bootnodePath' -genkey=nodekey 43 | 'bootnodePath' --nodekey=nodekey --writeaddress > enode 44 | find . -type f -name 'UTC*' -execdir mv {} acctkeyfile.json ';' 45 | ` 46 | expect(createFolder).toBeCalledWith(joinPath(createNetPath(config), 'keyPath', `key${keyNum}`), true) 47 | expect(writeFile).toBeCalledWith(joinPath(createNetPath(config), 'keyPath', `key${keyNum}`, 'password.txt'), '') 48 | expect(execute).toHaveBeenCalledWith(expected) 49 | }) 50 | 51 | it('generates quorum and tessera keys', async () => { 52 | const config = { 53 | network: { 54 | transactionManager: 'tessera', 55 | name: 'test', 56 | }, 57 | nodes: ['nodes'], 58 | } 59 | pathToQuorumBinary.mockReturnValueOnce('quorumPath') 60 | pathToBootnode.mockReturnValueOnce('bootnodePath') 61 | pathToTesseraJar.mockReturnValueOnce('tesseraPath') 62 | await generateKeys(config, joinPath(createNetPath(config), 'keyPath')) 63 | const keyNum = config.nodes.length 64 | 65 | const withTessera = `cd '${joinPath(createNetPath(config), 'keyPath')}/key${keyNum}' && 'quorumPath' account new --keystore '${joinPath(createNetPath(config), 'keyPath')}/key${keyNum}' --password password.txt 2>&1 66 | 'bootnodePath' -genkey=nodekey 67 | 'bootnodePath' --nodekey=nodekey --writeaddress > enode 68 | find . -type f -name 'UTC*' -execdir mv {} acctkeyfile.json ';' 69 | java -jar 'tesseraPath' -keygen -filename tm < /dev/null` 70 | 71 | expect(createFolder).toBeCalledWith(joinPath(createNetPath(config), 'keyPath', `key${keyNum}`), true) 72 | expect(writeFile).toBeCalledWith(joinPath(createNetPath(config), 'keyPath', `key${keyNum}`, 'password.txt'), '') 73 | expect(execute).toHaveBeenCalledWith(withTessera) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/generators/networkHelper.js: -------------------------------------------------------------------------------- 1 | import sanitize from 'sanitize-filename' 2 | import { getOutputPath } from '../utils/fileUtils' 3 | import { joinPath } from '../utils/pathUtils' 4 | 5 | // eslint-disable-next-line import/prefer-default-export 6 | export function getFullNetworkPath(config) { 7 | const networkFolderName = sanitize(config.network.name) 8 | if (networkFolderName === '') { 9 | throw new Error('Network name was empty or contained invalid characters') 10 | } 11 | 12 | return joinPath(getOutputPath(), 'network', networkFolderName) 13 | } 14 | -------------------------------------------------------------------------------- /src/generators/reportingHelper.js: -------------------------------------------------------------------------------- 1 | import TOML from '@iarna/toml' 2 | import { libRootDir, readFileToString, writeFile } from '../utils/fileUtils' 3 | import { joinPath } from '../utils/pathUtils' 4 | import { cidrhost } from '../utils/subnetUtils' 5 | import { isBash, isDocker } from '../model/NetworkConfig' 6 | import { isQuorumVersionAbove } from './binaryHelper' 7 | 8 | export function generateReportingConfig(config, qdataFolder) { 9 | const reportingConfig = TOML.parse(readFileToString(joinPath(libRootDir(), 'lib', 'reporting-config.toml'))) 10 | 11 | if (isDocker(config.network.deployment)) { 12 | reportingConfig.server.rpcAddr = `0.0.0.0:${config.containerPorts.reporting.rpcPort}` 13 | reportingConfig.server.uiPort = parseInt(config.containerPorts.reporting.uiPort, 10) 14 | reportingConfig.connection.wsUrl = `ws://node1:${config.containerPorts.quorum.wsPort}` 15 | // graphql is on the rpc port in quorum 21.4.0+ 16 | const graphQlPort = isQuorumVersionAbove(config.network.quorumVersion, '21.4.0') ? config.containerPorts.quorum.rpcPort : config.containerPorts.quorum.graphQlPort 17 | reportingConfig.connection.graphQLUrl = `http://node1:${graphQlPort}/graphql` 18 | } else if (isBash(config.network.deployment)) { 19 | reportingConfig.server.rpcAddr = `localhost:${config.network.reportingRpcPort}` 20 | reportingConfig.server.uiPort = parseInt(config.network.reportingUiPort, 10) 21 | reportingConfig.connection.wsUrl = `ws://localhost:${config.nodes[0].quorum.wsPort}` 22 | // graphql is on the rpc port in quorum 21.4.0+ 23 | const graphQlPort = isQuorumVersionAbove(config.network.quorumVersion, '21.4.0') ? config.nodes[0].quorum.rpcPort : config.nodes[0].quorum.graphQlPort 24 | reportingConfig.connection.graphQLUrl = `http://localhost:${graphQlPort}/graphql` 25 | // use in-memory db in bash mode by deleting database section 26 | delete reportingConfig.database 27 | } 28 | 29 | writeFile(joinPath(qdataFolder, 'reporting-config.toml'), TOML.stringify(reportingConfig)) 30 | } 31 | 32 | export function generateReportingScript(config) { 33 | if (!config.network.reporting) { 34 | return '' 35 | } 36 | return `echo Starting Quorum Reporting... 37 | sleep 10 38 | quorum-report -config qdata/reporting-config.toml > qdata/logs/reporting.log &` 39 | } 40 | 41 | export function generateReportingService(config) { 42 | return ` 43 | reporting: 44 | << : *reporting-def 45 | container_name: reporting-${config.network.name} 46 | hostname: reporting 47 | ports: 48 | - "${config.network.reportingRpcPort}:${config.containerPorts.reporting.rpcPort}" 49 | - "${config.network.reportingUiPort}:${config.containerPorts.reporting.uiPort}" 50 | volumes: 51 | - ./qdata/reporting-config.toml:/config/reporting-config.toml 52 | networks: 53 | ${(config.network.name)}-net: 54 | ipv4_address: ${cidrhost(config.containerPorts.dockerSubnet, 65)}` 55 | } 56 | 57 | export function generateElasticsearchService(config) { 58 | return ` 59 | es: 60 | image: elasticsearch:7.9.2 61 | container_name: es-${config.network.name} 62 | environment: 63 | - node.name=es 64 | - bootstrap.memory_lock=true 65 | - discovery.type=single-node 66 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 67 | ulimits: 68 | memlock: 69 | soft: -1 70 | hard: -1 71 | volumes: 72 | - esvol:/usr/share/elasticsearch/data 73 | expose: 74 | - "9200" 75 | networks: 76 | ${(config.network.name)}-net: 77 | ipv4_address: ${cidrhost(config.containerPorts.dockerSubnet, 66)}` 78 | } 79 | -------------------------------------------------------------------------------- /src/generators/scripts/attach.js: -------------------------------------------------------------------------------- 1 | import { 2 | addScriptExtension, scriptHeader, setEnvironmentCommand, validateNodeNumberInput, 3 | } from './utils' 4 | import { isWin32 } from '../../utils/execUtils' 5 | 6 | export default { 7 | filename: addScriptExtension('attach'), 8 | executable: true, 9 | generate: (config) => `${scriptHeader()} 10 | ${validateNodeNumberInput(config)} 11 | ${attachCommand(config)}`, 12 | } 13 | 14 | function attachCommand(config) { 15 | switch (config.network.deployment) { 16 | case 'bash': 17 | return attachCommandBash(config) 18 | case 'docker-compose': 19 | return isWin32() ? attachCommandDockerWindows() : attachCommandDockerBash() 20 | case 'kubernetes': 21 | return isWin32() ? attachCommandKubernetesWindows() : attachCommandKubernetesBash() 22 | default: 23 | return '' 24 | } 25 | } 26 | 27 | export function attachCommandBash(config) { 28 | return `${setEnvironmentCommand(config)} 29 | "$BIN_GETH" attach qdata/dd$1/geth.ipc` 30 | } 31 | 32 | function attachCommandDockerWindows() { 33 | return 'docker-compose exec node%NODE_NUMBER% /bin/sh -c "geth attach qdata/dd/geth.ipc"' 34 | } 35 | 36 | function attachCommandDockerBash() { 37 | return 'docker-compose exec node$NODE_NUMBER /bin/sh -c "geth attach qdata/dd/geth.ipc"' 38 | } 39 | 40 | function attachCommandKubernetesWindows() { 41 | return ` 42 | FOR /f "delims=" %%g IN ('kubectl get pod --field-selector=status.phase^=Running -o name ^| findstr quorum-node%NODE_NUMBER%') DO set POD=%%g 43 | ECHO ON 44 | kubectl exec -it %POD% -c quorum -- /geth-helpers/geth-attach.sh` 45 | } 46 | 47 | function attachCommandKubernetesBash() { 48 | return `POD=$(kubectl get pod --field-selector=status.phase=Running -o name | grep quorum-node$NODE_NUMBER) 49 | kubectl $NAMESPACE exec -it $POD -c quorum -- /geth-helpers/geth-attach.sh` 50 | } 51 | -------------------------------------------------------------------------------- /src/generators/scripts/getEndpoints.js: -------------------------------------------------------------------------------- 1 | import { addScriptExtension, scriptHeader, validateNodeNumberInput } from './utils' 2 | import { isWin32 } from '../../utils/execUtils' 3 | import { isKubernetes } from '../../model/NetworkConfig' 4 | 5 | export default { 6 | filename: addScriptExtension('getEndpoints'), 7 | executable: true, 8 | generate: (config) => { 9 | if (!isKubernetes(config.network.deployment)) { 10 | throw new Error('getEndpoints script only used for Kubernetes deployments') 11 | } 12 | return endpointScriptKubernetes(config) 13 | }, 14 | } 15 | 16 | export function endpointScriptKubernetes(config) { 17 | return isWin32() ? endpointScriptKubernetesWindows(config) : endpointScriptKubernetesBash(config) 18 | } 19 | 20 | function endpointScriptKubernetesBash(config) { 21 | return `${scriptHeader()} 22 | ${validateNodeNumberInput(config)} 23 | 24 | minikube ip > /dev/null 2>&1 25 | EXIT_CODE=$? 26 | 27 | if [ $EXIT_CODE -ne 0 ]; 28 | then 29 | IP_ADDRESS=localhost 30 | else 31 | IP_ADDRESS=$(minikube ip) 32 | fi 33 | 34 | 35 | QUORUM_PORT=$(kubectl get service quorum-node$1 -o=jsonpath='{range.spec.ports[?(@.name=="rpc-listener")]}{.nodePort}') 36 | 37 | TESSERA_PORT=$(kubectl get service quorum-node$1 -o=jsonpath='{range.spec.ports[?(@.name=="tm-tessera-third-part")]}{.nodePort}') 38 | 39 | echo quorum rpc: http://$IP_ADDRESS:$QUORUM_PORT 40 | echo tessera 3rd party: http://$IP_ADDRESS:$TESSERA_PORT 41 | 42 | kubectl get service cakeshop-service > /dev/null 2>&1 43 | EXIT_CODE=$? 44 | if [ $EXIT_CODE -eq 0 ]; 45 | then 46 | CAKESHOP_PORT=$(kubectl get service cakeshop-service -o=jsonpath='{range.spec.ports[?(@.name=="http")]}{.nodePort}') 47 | echo cakeshop: http://$IP_ADDRESS:$CAKESHOP_PORT 48 | fi 49 | 50 | kubectl get service quorum-monitor > /dev/null 2>&1 51 | EXIT_CODE=$? 52 | if [ $EXIT_CODE -eq 0 ]; 53 | then 54 | PROMETHEUS_PORT=$(kubectl get service quorum-monitor -o=jsonpath='{range.spec.ports[?(@.name=="prometheus")]}{.nodePort}') 55 | echo prometheus: http://$IP_ADDRESS:$PROMETHEUS_PORT 56 | fi 57 | ` 58 | } 59 | 60 | function endpointScriptKubernetesWindows(config) { 61 | return `${scriptHeader()} 62 | ${validateNodeNumberInput(config)} 63 | 64 | minikube ip >nul 2>&1 65 | if ERRORLEVEL 1 ( 66 | FOR /f "delims=" %%g IN ('minikube ip') DO set IP_ADDRESS=%%g 67 | ) else ( 68 | set IP_ADDRESS=localhost 69 | ) 70 | 71 | FOR /f "delims=" %%g IN ('minikube ip 2^>nul ^|^| echo localhost') DO set IP_ADDRESS=%%g 72 | 73 | FOR /F "tokens=* USEBACKQ" %%g IN (\`kubectl get service quorum-node%NODE_NUMBER% -o^=jsonpath^="{range.spec.ports[?(@.name=='rpc-listener')]}{.nodePort}"\`) DO set QUORUM_PORT=%%g 74 | 75 | FOR /F "tokens=* USEBACKQ" %%g IN (\`kubectl get service quorum-node%NODE_NUMBER% -o^=jsonpath^="{range.spec.ports[?(@.name=='tm-tessera-third-part')]}{.nodePort}"\`) DO set TESSERA_PORT=%%g 76 | 77 | echo quorum rpc: http://%IP_ADDRESS%:%QUORUM_PORT% 78 | echo tessera 3rd party: http://%IP_ADDRESS%:%TESSERA_PORT% 79 | 80 | kubectl get service cakeshop-service >nul 2>&1 81 | if ERRORLEVEL 1 ( 82 | FOR /F "tokens=* USEBACKQ" %%g IN (\`kubectl get service cakeshop-service -o^=jsonpath^="{range.spec.ports[?(@.name=='http')]}{.nodePort}"\`) DO set CAKESHOP_PORT=%%g 83 | echo cakeshop: http://%IP_ADDRESS%:%CAKESHOP_PORT% 84 | ) 85 | 86 | kubectl get service quorum-monitor >nul 2>&1 87 | if ERRORLEVEL 1 ( 88 | FOR /F "tokens=* USEBACKQ" %%g IN (\`kubectl get service quorum-monitor -o^=jsonpath^="{range.spec.ports[?(@.name=='prometheus')]}{.nodePort}"\`) DO set PROMETHEUS_PORT=%%g 89 | echo prometheus: http://%IP_ADDRESS%:%PROMETHEUS_PORT% 90 | )` 91 | } 92 | -------------------------------------------------------------------------------- /src/generators/scripts/index.js: -------------------------------------------------------------------------------- 1 | import attach from './attach' 2 | import getEndpoints from './getEndpoints' 3 | import privateContract from './privateContract' 4 | import publicContract from './publicContract' 5 | import runscript from './runscript' 6 | import start from './start' 7 | import stop from './stop' 8 | 9 | export default { 10 | attach, 11 | getEndpoints, 12 | privateContract, 13 | publicContract, 14 | runscript, 15 | start, 16 | stop, 17 | } 18 | -------------------------------------------------------------------------------- /src/generators/scripts/privateContract.js: -------------------------------------------------------------------------------- 1 | import { loadTesseraPublicKey } from '../transactionManager' 2 | 3 | export default { 4 | filename: 'private_contract.js', 5 | executable: false, 6 | generate: (config) => { 7 | const nodeTwoPublicKey = loadTesseraPublicKey(config, 2) 8 | return ` 9 | a = eth.accounts[0] 10 | web3.eth.defaultAccount = a; 11 | 12 | // abi and bytecode generated from simplestorage.sol: 13 | // > solcjs --bin --abi simplestorage.sol 14 | var abi = [{"constant":true,"inputs":[],"name":"storedData","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initVal","type":"uint256"}],"payable":false,"type":"constructor"}]; 15 | 16 | var bytecode = "0x6060604052341561000f57600080fd5b604051602080610149833981016040528080519060200190919050505b806000819055505b505b610104806100456000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a1afcd914605157806360fe47b11460775780636d4ce63c146097575b600080fd5b3415605b57600080fd5b606160bd565b6040518082815260200191505060405180910390f35b3415608157600080fd5b6095600480803590602001909190505060c3565b005b341560a157600080fd5b60a760ce565b6040518082815260200191505060405180910390f35b60005481565b806000819055505b50565b6000805490505b905600a165627a7a72305820d5851baab720bba574474de3d09dbeaabc674a15f4dd93b974908476542c23f00029"; 17 | 18 | var simpleContract = web3.eth.contract(abi); 19 | var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760, privateFor: ["${nodeTwoPublicKey}"]}, function(e, contract) { 20 | if (e) { 21 | console.log("err creating contract", e); 22 | } else { 23 | if (!contract.address) { 24 | console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined..."); 25 | } else { 26 | console.log("Contract mined! Address: " + contract.address); 27 | console.log(contract); 28 | } 29 | } 30 | });` 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /src/generators/scripts/publicContract.js: -------------------------------------------------------------------------------- 1 | export default { 2 | filename: 'public_contract.js', 3 | executable: false, 4 | generate: () => `a = eth.accounts[0] 5 | web3.eth.defaultAccount = a; 6 | 7 | // abi and bytecode generated from simplestorage.sol: 8 | // > solcjs --bin --abi simplestorage.sol 9 | var abi = [{"constant":true,"inputs":[],"name":"storedData","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initVal","type":"uint256"}],"payable":false,"type":"constructor"}]; 10 | 11 | var bytecode = "0x6060604052341561000f57600080fd5b604051602080610149833981016040528080519060200190919050505b806000819055505b505b610104806100456000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a1afcd914605157806360fe47b11460775780636d4ce63c146097575b600080fd5b3415605b57600080fd5b606160bd565b6040518082815260200191505060405180910390f35b3415608157600080fd5b6095600480803590602001909190505060c3565b005b341560a157600080fd5b60a760ce565b6040518082815260200191505060405180910390f35b60005481565b806000819055505b50565b6000805490505b905600a165627a7a72305820d5851baab720bba574474de3d09dbeaabc674a15f4dd93b974908476542c23f00029"; 12 | 13 | var simpleContract = web3.eth.contract(abi); 14 | var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760}, function(e, contract) { 15 | if (e) { 16 | console.log("err creating contract", e); 17 | } else { 18 | if (!contract.address) { 19 | console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined..."); 20 | } else { 21 | console.log("Contract mined! Address: " + contract.address); 22 | console.log(contract); 23 | } 24 | } 25 | });`, 26 | } 27 | -------------------------------------------------------------------------------- /src/generators/scripts/runscript.js: -------------------------------------------------------------------------------- 1 | import { 2 | addScriptExtension, filenameCheck, scriptHeader, setEnvironmentCommand, 3 | } from './utils' 4 | import { isWin32 } from '../../utils/execUtils' 5 | 6 | export default { 7 | filename: addScriptExtension('runscript'), 8 | executable: true, 9 | generate: (config) => `${scriptHeader()} 10 | ${filenameCheck()} 11 | ${runScript(config)} 12 | `, 13 | } 14 | 15 | function runScript(config) { 16 | switch (config.network.deployment) { 17 | case 'bash': 18 | return runscriptCommandBash(config) 19 | case 'docker-compose': 20 | return isWin32() ? runScriptCommandDockerWindows() : runScriptCommandDockerBash() 21 | case 'kubernetes': 22 | return isWin32() ? runscriptCommandKubernetesWindows() : runscriptCommandKubernetesBash() 23 | default: 24 | return '' 25 | } 26 | } 27 | 28 | export function runscriptCommandBash(config) { 29 | return `${setEnvironmentCommand(config)} 30 | "$BIN_GETH" --exec "loadScript(\\"$1\\")" attach qdata/dd1/geth.ipc` 31 | } 32 | 33 | function runScriptCommandDockerWindows() { 34 | return `FOR /F "tokens=* USEBACKQ" %%g IN (\`docker-compose ps -q node1\`) DO set DOCKER_CONTAINER=%%g 35 | docker cp %1 %DOCKER_CONTAINER%:/%1 36 | docker-compose exec node1 /bin/sh -c "geth --exec 'loadScript(\\"%1\\")' attach qdata/dd/geth.ipc" 37 | ` 38 | } 39 | 40 | function runScriptCommandDockerBash() { 41 | return `docker cp $1 "$(docker-compose ps -q node1)":/$1 42 | docker-compose exec node1 /bin/sh -c "geth --exec 'loadScript(\\"$1\\")' attach qdata/dd/geth.ipc" 43 | ` 44 | } 45 | 46 | // TODO is there a way to copy the script in like we do for docker-compose runscript? 47 | function runscriptCommandKubernetesWindows() { 48 | return ` 49 | SET NODE_NUMBER=1 50 | FOR /f "delims=" %%g IN ('kubectl get pod --field-selector=status.phase^=Running -o name ^| findstr quorum-node%NODE_NUMBER%') DO set POD=%%g 51 | ECHO ON 52 | kubectl exec -it %POD% -c quorum -- /etc/quorum/qdata/contracts/runscript.sh /etc/quorum/qdata/contracts/%1 53 | ` 54 | } 55 | 56 | function runscriptCommandKubernetesBash() { 57 | return ` 58 | NODE_NUMBER=1 59 | POD=$(kubectl get pod --field-selector=status.phase=Running -o name | grep quorum-node$NODE_NUMBER) 60 | kubectl $NAMESPACE exec -it $POD -c quorum -- /etc/quorum/qdata/contracts/runscript.sh /etc/quorum/qdata/contracts/$1` 61 | } 62 | -------------------------------------------------------------------------------- /src/generators/scripts/start.js: -------------------------------------------------------------------------------- 1 | import { startScriptBash } from '../bashHelper' 2 | import { addScriptExtension, scriptHeader } from './utils' 3 | import { isWin32 } from '../../utils/execUtils' 4 | 5 | export default { 6 | filename: addScriptExtension('start'), 7 | executable: true, 8 | generate: (config) => { 9 | switch (config.network.deployment) { 10 | case 'bash': 11 | return startScriptBash(config) 12 | case 'docker-compose': 13 | return startScriptDocker(config) 14 | case 'kubernetes': 15 | return isWin32() ? startScriptKubernetesWindows() : startScriptKubernetesBash() 16 | default: 17 | return '' 18 | } 19 | }, 20 | } 21 | 22 | function startSplunkDocker(config) { 23 | return `docker-compose -f docker-compose-splunk.yml up -d 24 | 25 | sleep 3 26 | 27 | echo -n 'Waiting for splunk to start.' 28 | until docker logs splunk-${config.network.name} | grep -m 1 'Ansible playbook complete' 29 | do 30 | echo -n "." 31 | sleep 5 32 | done 33 | echo " " 34 | echo "Splunk started!" 35 | 36 | echo "Starting quorum stack..."` 37 | } 38 | 39 | function startScriptDocker(config) { 40 | const startSplunk = config.network.splunk ? startSplunkDocker(config) : '' 41 | return `${scriptHeader()} 42 | ${startSplunk} 43 | docker-compose up -d` 44 | } 45 | 46 | function startScriptKubernetesBash() { 47 | return `${scriptHeader()} 48 | 49 | echo "Checking if Kubernetes is running" 50 | kubectl > /dev/null 2>&1 51 | EXIT_CODE=$? 52 | 53 | if [ $EXIT_CODE -ne 0 ]; 54 | then 55 | printf "Error: kubectl not found, please install kubectl before running this script.\n" 56 | printf "For more information, see our qubernetes project: https://github.com/jpmorganchase/qubernetes\n" 57 | exit $EXIT_CODE 58 | fi 59 | 60 | kubectl cluster-info > /dev/null 2>&1 61 | EXIT_CODE=$? 62 | 63 | if [ $EXIT_CODE -ne 0 ]; 64 | then 65 | printf "Could not connect to a kubernetes cluster. Please make sure you have minikube or another local kubernetes cluster running.\n" 66 | printf "For more information, see our qubernetes project: https://github.com/jpmorganchase/qubernetes\n" 67 | exit $EXIT_CODE 68 | fi 69 | 70 | echo "Setting up network" 71 | kubectl apply -f out -f out/deployments 72 | echo "\nRun 'kubectl get pods' to check status of pods\n" 73 | ` 74 | } 75 | 76 | function startScriptKubernetesWindows() { 77 | return `${scriptHeader()} 78 | echo Checking if Kubernetes is running 79 | kubectl >nul 2>&1 80 | if ERRORLEVEL 1 ( 81 | echo kubectl not found on your machine. Please make sure you have Kubernetes installed && EXIT /B 1 82 | ) 83 | 84 | kubectl cluster-info >nul 2>&1 85 | if ERRORLEVEL 1 ( 86 | echo Could not connect to a kubernetes cluster. Please make sure you have minikube or another local kubernetes cluster running. && EXIT /B 1 87 | ) 88 | 89 | echo Setting up network 90 | kubectl apply -f out -f out/deployments 91 | echo Run 'kubectl get pods' to check status of pods` 92 | } 93 | -------------------------------------------------------------------------------- /src/generators/scripts/stop.js: -------------------------------------------------------------------------------- 1 | import { addScriptExtension, scriptHeader } from './utils' 2 | 3 | export default { 4 | filename: addScriptExtension('stop'), 5 | executable: true, 6 | generate: (config) => { 7 | switch (config.network.deployment) { 8 | case 'bash': 9 | return stopScriptBash(config) 10 | case 'docker-compose': 11 | return stopScriptDocker(config) 12 | case 'kubernetes': 13 | return stopScriptKubernetes() 14 | default: 15 | return '' 16 | } 17 | }, 18 | } 19 | 20 | export function stopScriptBash() { 21 | return `#!/bin/bash 22 | killall -INT geth 23 | killall -INT quorum-report 24 | killall constellation-node 25 | 26 | if [ "\`ps -ef | grep tessera-app.jar | grep -v grep\`" != "" ] 27 | then 28 | ps -ef | grep tessera-app.jar | grep -v grep | awk '{print $2}' | xargs kill 29 | else 30 | echo "tessera: no process found" 31 | fi 32 | 33 | if [ "\`ps -ef | grep cakeshop.war | grep -v grep\`" != "" ] 34 | then 35 | ps -ef | grep cakeshop.war | grep -v grep | awk '{print $2}' | xargs kill 36 | else 37 | echo "cakeshop: no process found" 38 | fi 39 | ` 40 | } 41 | 42 | export function stopScriptDocker(config) { 43 | const stopSplunk = config.network.splunk ? 'docker-compose -f docker-compose-splunk.yml down' : '' 44 | return `${scriptHeader()} 45 | ${stopSplunk} 46 | docker-compose down` 47 | } 48 | 49 | function stopScriptKubernetes() { 50 | return `${scriptHeader()} 51 | kubectl delete -f out -f out/deployments` 52 | } 53 | -------------------------------------------------------------------------------- /src/generators/scripts/utils.js: -------------------------------------------------------------------------------- 1 | import { pathToCakeshop, pathToQuorumBinary, pathToTesseraJar } from '../binaryHelper' 2 | import { isCakeshop, isTessera } from '../../model/NetworkConfig' 3 | import { isWin32 } from '../../utils/execUtils' 4 | 5 | export function setEnvironmentCommand(config) { 6 | const lines = [] 7 | lines.push(`BIN_GETH='${pathToQuorumBinary(config.network.quorumVersion)}'`) 8 | if (isTessera(config.network.transactionManager)) { 9 | lines.push(`BIN_TESSERA='${pathToTesseraJar(config.network.transactionManager)}'`) 10 | } 11 | if (isCakeshop(config.network.cakeshop)) { 12 | lines.push(`BIN_CAKESHOP='${pathToCakeshop(config.network.cakeshop)}'`) 13 | } 14 | lines.push('') 15 | return lines.join('\n') 16 | } 17 | 18 | export function addScriptExtension(filename) { 19 | return `${filename}${isWin32() ? '.cmd' : '.sh'}` 20 | } 21 | 22 | export function scriptHeader() { 23 | return isWin32() ? scriptHeaderWindows() : scriptHeaderBash() 24 | } 25 | function scriptHeaderWindows() { 26 | return '@ECHO OFF\nSETLOCAL' 27 | } 28 | 29 | function scriptHeaderBash() { 30 | return '#!/bin/bash' 31 | } 32 | 33 | export function validateNodeNumberInput(config) { 34 | return isWin32() ? validateEnvNodeNumberWindows(config) : validateEnvNodeNumberBash(config) 35 | } 36 | 37 | function validateEnvNodeNumberWindows(config) { 38 | return `SET NUMBER_OF_NODES=${config.nodes.length} 39 | SET /A NODE_NUMBER=%1 40 | 41 | if "%1"=="" ( 42 | echo Please provide the number of the node to attach to (i.e. attach.cmd 2^) && EXIT /B 1 43 | ) 44 | 45 | if %NODE_NUMBER% EQU 0 ( 46 | echo Please provide the number of the node to attach to (i.e. attach.cmd 2^) && EXIT /B 1 47 | ) 48 | 49 | if %NODE_NUMBER% GEQ %NUMBER_OF_NODES%+1 ( 50 | echo %1 is not a valid node number. Must be between 1 and %NUMBER_OF_NODES%. && EXIT /B 1 51 | )` 52 | } 53 | 54 | function validateEnvNodeNumberBash(config) { 55 | return `NUMBER_OF_NODES=${config.nodes.length} 56 | NODE_NUMBER=$1 57 | case "$NODE_NUMBER" in ("" | *[!0-9]*) 58 | echo 'Please provide the number of the node to attach to (i.e. ./attach.sh 2)' >&2 59 | exit 1 60 | esac 61 | 62 | if [ "$NODE_NUMBER" -lt 1 ] || [ "$NODE_NUMBER" -gt $NUMBER_OF_NODES ]; then 63 | echo "$NODE_NUMBER is not a valid node number. Must be between 1 and $NUMBER_OF_NODES." >&2 64 | exit 1 65 | fi` 66 | } 67 | 68 | export function filenameCheck() { 69 | return isWin32() ? filenameCheckWindows() : filenameCheckBash() 70 | } 71 | 72 | function filenameCheckWindows() { 73 | return `if "%1"=="" ( 74 | echo Please provide a valid script file to execute as the first parameter (i.e. private_contract.js^) && EXIT /B 1 75 | ) 76 | if NOT EXIST %1 ( 77 | echo Please provide a valid script file to execute as the first parameter (i.e. private_contract.js^) && EXIT /B 1 78 | )` 79 | } 80 | 81 | function filenameCheckBash() { 82 | return `if [ -z $1 ] || [ ! -f $1 ]; then 83 | echo "Please provide a valid script file to execute as the first parameter (i.e. private_contract.js)" >&2 84 | exit 1 85 | fi` 86 | } 87 | -------------------------------------------------------------------------------- /src/generators/splunkHelper.js: -------------------------------------------------------------------------------- 1 | import { info } from '../utils/log' 2 | 3 | export function buildSplunkDockerCompose(config) { 4 | const version = `version: "3.6" 5 | ` 6 | const services = [buildSplunkService(config)] 7 | info('Splunk>') 8 | 9 | return [ 10 | version, 11 | 'services:', 12 | services.join(''), 13 | buildSplunkEndService(config), 14 | ].join('') 15 | } 16 | 17 | function buildSplunkService(config) { 18 | const networkName = config.network.name 19 | return ` 20 | splunk: 21 | image: splunk/splunk:8.0.4-debian 22 | container_name: splunk-${networkName} 23 | hostname: splunk 24 | environment: 25 | - SPLUNK_START_ARGS=--accept-license 26 | - SPLUNK_HEC_TOKEN=11111111-1111-1111-1111-1111111111113 27 | - SPLUNK_PASSWORD=changeme 28 | - SPLUNK_APPS_URL=https://github.com/splunk/ethereum-basics/releases/download/latest/ethereum-basics.tgz,https://github.com/splunkdlt/splunk-app-quorum/releases/download/1.0.7/splunk-app-quorum-v1.0.7.tgz 29 | expose: 30 | - "8000" 31 | - "8088" 32 | healthcheck: 33 | test: ['CMD', 'curl', '-f', 'http://localhost:8000'] 34 | interval: 5s 35 | timeout: 5s 36 | retries: 20 37 | ports: 38 | - "${config.network.splunkPort}:8000" 39 | - "${config.network.splunkHecPort}:8088" 40 | volumes: 41 | - splunk-var:/opt/splunk/var 42 | - splunk-etc:/opt/splunk/etc 43 | - ./out/config/splunk/splunk-config.yml:/tmp/defaults/default.yml 44 | networks: 45 | ${networkName}-net: 46 | ipv4_address: ${config.network.splunkIp}` 47 | } 48 | 49 | function buildSplunkEndService(config) { 50 | const networkName = config.network.name 51 | return ` 52 | networks: 53 | ${networkName}-net: 54 | name: ${networkName}-net 55 | driver: bridge 56 | ipam: 57 | driver: default 58 | config: 59 | - subnet: ${config.containerPorts.dockerSubnet} 60 | volumes: 61 | "splunk-var": 62 | "splunk-etc":` 63 | } 64 | 65 | export function buildCadvisorService(config) { 66 | const networkName = config.network.name 67 | return ` 68 | cadvisor: 69 | image: google/cadvisor:latest 70 | container_name: cadvisor-${networkName} 71 | hostname: cadvisor 72 | command: 73 | - --storage_driver=statsd 74 | - --storage_driver_host=${config.network.splunkIp}:8125 75 | - --docker_only=true 76 | user: root 77 | volumes: 78 | - /:/rootfs:ro 79 | - /var/run:/var/run:ro 80 | - /sys:/sys:ro 81 | - /var/lib/docker/:/var/lib/docker:ro 82 | networks: 83 | - ${networkName}-net 84 | logging: *default-logging` 85 | } 86 | 87 | export function buildEthloggerService(config) { 88 | const networkName = config.network.name 89 | let ethloggers = '' 90 | 91 | config.nodes.forEach((node, i) => { 92 | const instance = i + 1 93 | ethloggers += ` 94 | ethlogger${instance}: 95 | image: splunkdlt/ethlogger:latest 96 | container_name: ethlogger${instance}-${networkName} 97 | hostname: ethlogger${instance} 98 | environment: 99 | - ETH_RPC_URL=http://node${instance}:${config.containerPorts.quorum.rpcPort} 100 | - NETWORK_NAME=quorum 101 | - START_AT_BLOCK=genesis 102 | - SPLUNK_HEC_URL=https://${config.network.splunkIp}:8088 103 | - SPLUNK_HEC_TOKEN=11111111-1111-1111-1111-1111111111113 104 | - SPLUNK_EVENTS_INDEX=ethereum 105 | - SPLUNK_METRICS_INDEX=metrics 106 | - SPLUNK_INTERNAL_INDEX=metrics 107 | - SPLUNK_HEC_REJECT_INVALID_CERTS=false 108 | - COLLECT_PEER_INFO=true 109 | depends_on: 110 | - node${instance} 111 | restart: unless-stopped 112 | volumes: 113 | - ethlogger-state${instance}:/app 114 | networks: 115 | - ${networkName}-net 116 | logging: *default-logging` 117 | }) 118 | return ethloggers 119 | } 120 | 121 | export function getSplunkDefinitions(config) { 122 | return ` 123 | x-logging: 124 | &default-logging 125 | driver: splunk 126 | options: 127 | splunk-token: 11111111-1111-1111-1111-1111111111113 128 | splunk-url: https://localhost:${config.network.splunkHecPort} 129 | splunk-index: logs 130 | splunk-sourcetype: docker 131 | splunk-insecureskipverify: "true" 132 | splunk-verify-connection: "false" 133 | splunk-format: "raw" 134 | tag: "{{.Name}}-{{.ID}}" 135 | ` 136 | } 137 | -------------------------------------------------------------------------------- /src/generators/transactionManager.js: -------------------------------------------------------------------------------- 1 | import { readFileToString } from '../utils/fileUtils' 2 | import { 3 | isTessera, 4 | isKubernetes, 5 | } from '../model/NetworkConfig' 6 | import { getFullNetworkPath } from './networkHelper' 7 | import { joinPath } from '../utils/pathUtils' 8 | 9 | export function formatTesseraKeysOutput(config) { 10 | if (!isTessera(config.network.transactionManager)) { 11 | return '' 12 | } 13 | const output = config.nodes 14 | .map((node, i) => loadTesseraPublicKey(config, i + 1)) 15 | .map((publicKey, i) => `Tessera Node ${i + 1} public key:\n${publicKey}`) 16 | 17 | return `-------------------------------------------------------------------------------- 18 | 19 | ${output.join('\n\n')} 20 | 21 | -------------------------------------------------------------------------------- 22 | ` 23 | } 24 | 25 | export function loadTesseraPublicKey(config, nodeNumber) { 26 | const keyPath = isKubernetes(config.network.deployment) 27 | ? joinPath(getFullNetworkPath(config), 'out', 'config', `key${nodeNumber}`) 28 | : joinPath(getFullNetworkPath(config), 'qdata', `c${nodeNumber}`) 29 | return readFileToString(joinPath(keyPath, 'tm.pub')) 30 | } 31 | -------------------------------------------------------------------------------- /src/generators/transactionManger.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | loadTesseraPublicKey, 3 | formatTesseraKeysOutput, 4 | } from './transactionManager' 5 | import { readFileToString } from '../utils/fileUtils' 6 | import { getFullNetworkPath } from './networkHelper' 7 | import { TEST_CWD } from '../utils/testHelper' 8 | 9 | jest.mock('../utils/fileUtils') 10 | jest.mock('../generators/networkHelper') 11 | getFullNetworkPath.mockReturnValue(`${TEST_CWD}/test-network`) 12 | readFileToString.mockReturnValue('pubkey') 13 | 14 | const CONFIG = { 15 | network: { 16 | name: 'test', 17 | deployment: 'bash', 18 | }, 19 | nodes: ['node1'], 20 | } 21 | 22 | const EXPECTED = `-------------------------------------------------------------------------------- 23 | 24 | Tessera Node 1 public key: 25 | pubkey 26 | 27 | -------------------------------------------------------------------------------- 28 | ` 29 | 30 | describe('load tessera public keys', () => { 31 | it('load tessera public keys for bash', () => { 32 | expect(loadTesseraPublicKey(CONFIG, 1)).toEqual('pubkey') 33 | }) 34 | it('load tessera public keys for docker', () => { 35 | const config = { 36 | ...CONFIG, 37 | network: { 38 | ...CONFIG.network, 39 | deployment: 'docker-compose', 40 | }, 41 | } 42 | 43 | expect(loadTesseraPublicKey(config, 1)).toEqual('pubkey') 44 | }) 45 | it('load tessera public keys for kubernetes', () => { 46 | const config = { 47 | ...CONFIG, 48 | network: { 49 | ...CONFIG.network, 50 | deployment: 'kubernetes', 51 | }, 52 | } 53 | 54 | expect(loadTesseraPublicKey(config, 1)).toEqual('pubkey') 55 | }) 56 | }) 57 | 58 | describe('format tessera public keys', () => { 59 | it('empty string no tessera', () => { 60 | const config = { 61 | ...CONFIG, 62 | network: { 63 | ...CONFIG.network, 64 | transactionManager: 'none', 65 | }, 66 | } 67 | 68 | expect(formatTesseraKeysOutput(config, 1)).toEqual('') 69 | }) 70 | it('bash with tessera', () => { 71 | expect(formatTesseraKeysOutput(CONFIG, 1)).toEqual(EXPECTED) 72 | }) 73 | it('docker with tessera', () => { 74 | const config = { 75 | ...CONFIG, 76 | network: { 77 | ...CONFIG.network, 78 | deployment: 'docker-compose', 79 | }, 80 | } 81 | expect(formatTesseraKeysOutput(config, 1)).toEqual(EXPECTED) 82 | }) 83 | it('kubernetes with tessera', () => { 84 | const config = { 85 | ...CONFIG, 86 | network: { 87 | ...CONFIG.network, 88 | deployment: 'kubernetes', 89 | }, 90 | } 91 | expect(formatTesseraKeysOutput(config, 1)).toEqual(EXPECTED) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import 'source-map-support/register' 4 | import inquirer from 'inquirer' 5 | import { 6 | createLogger, 7 | debug, 8 | error, 9 | info, 10 | } from './utils/log' 11 | import { 12 | promptUser, 13 | promptGenerate, 14 | } from './questions' 15 | import { INITIAL_MODE } from './questions/questions' 16 | import { 17 | createConfigFromAnswers, isBash, isDocker, isKubernetes, isTessera, isCakeshop, 18 | } from './model/NetworkConfig' 19 | import { 20 | createNetwork, 21 | createQdataDirectory, 22 | createScripts, 23 | generateResourcesLocally, 24 | generateResourcesRemote, 25 | } from './generators/networkCreator' 26 | import { getFullNetworkPath } from './generators/networkHelper' 27 | import { initBash } from './generators/bashHelper' 28 | import { initDockerCompose, setDockerRegistry } from './generators/dockerHelper' 29 | import { formatTesseraKeysOutput, loadTesseraPublicKey } from './generators/transactionManager' 30 | import { downloadAndCopyBinaries } from './generators/binaryHelper' 31 | import { setOutputPath, readJsonFile } from './utils/fileUtils' 32 | import { wrapScript } from './utils/pathUtils' 33 | import SCRIPTS from './generators/scripts' 34 | 35 | const yargs = require('yargs') 36 | 37 | const { argv } = yargs 38 | .boolean('q') 39 | .alias('q', 'quickstart') 40 | .describe('q', 'create 3 node raft network with tessera and cakeshop') 41 | .boolean('v') 42 | .alias('v', 'verbose') 43 | .describe('v', 'Turn on additional logs for debugging') 44 | .alias('o', 'outputPath') 45 | .describe('o', 'Set the output path. Wizard will place all generated files into this folder. Defaults to the location where Wizard is run.') 46 | .string('o') 47 | .command('generate', '--config path to config.json', () => yargs.option('config', { 48 | desc: 'path to config.json', 49 | }) 50 | .coerce('config', (configPath) => { 51 | const config = readJsonFile(configPath) 52 | checkValidConfig(config) 53 | return config 54 | })) 55 | .string('r') 56 | .alias('r', 'registry') 57 | .describe('r', 'Use a custom docker registry (instead of registry.hub.docker.com)') 58 | .help() 59 | .alias('h', 'help') 60 | .version() 61 | .strict() 62 | 63 | createLogger(argv.v) 64 | debug('Showing debug logs') 65 | setDockerRegistry(argv.r) 66 | 67 | setOutputPath(argv.o) 68 | 69 | if (argv.q) { 70 | buildNetwork('quickstart') 71 | } else if (argv.config) { 72 | generateNetwork(argv.config) 73 | } else if (argv._[0] === 'generate') { 74 | regenerateNetwork() 75 | } else { 76 | inquirer.prompt([INITIAL_MODE]) 77 | .then(async ({ mode }) => { 78 | if (mode === 'exit') { 79 | info('Exiting...') 80 | return 81 | } if (mode === 'generate') { 82 | regenerateNetwork() 83 | return 84 | } 85 | buildNetwork(mode) 86 | }) 87 | } 88 | 89 | async function regenerateNetwork() { 90 | const ans = await promptGenerate() 91 | try { 92 | const config = readJsonFile(ans.configLocation) 93 | config.network.name = ans.name 94 | checkValidConfig(config) 95 | generateNetwork(config) 96 | } catch (e) { 97 | error(e.message) 98 | process.exit(1) 99 | } 100 | } 101 | 102 | function checkValidConfig(config) { 103 | if (!isBash(config.network.deployment) && Object.keys(config.containerPorts).length === 0) { 104 | throw new Error('Invalid config: containerPorts object is required for docker and kubernetes') 105 | } 106 | } 107 | 108 | async function generateNetwork(config) { 109 | if (isBash(config.network.deployment)) { 110 | await downloadAndCopyBinaries(config) 111 | } 112 | await createDirectory(config) 113 | createScripts(config) 114 | printInstructions(config) 115 | } 116 | 117 | async function buildNetwork(mode) { 118 | const answers = await promptUser(mode) 119 | const config = createConfigFromAnswers(answers) 120 | generateNetwork(config) 121 | } 122 | 123 | async function createDirectory(config) { 124 | createNetwork(config) 125 | if (isBash(config.network.deployment)) { 126 | await generateResourcesLocally(config) 127 | createQdataDirectory(config) 128 | await initBash(config) 129 | } else if (isDocker(config.network.deployment)) { 130 | generateResourcesRemote(config) 131 | createQdataDirectory(config) 132 | await initDockerCompose(config) 133 | } else if (isKubernetes(config.network.deployment)) { 134 | generateResourcesRemote(config) 135 | } else { 136 | throw new Error('Only bash, docker, and kubernetes deployments are supported') 137 | } 138 | } 139 | 140 | function printInstructions(config) { 141 | info(formatTesseraKeysOutput(config)) 142 | info('') 143 | info('Quorum network created') 144 | info('') 145 | if (isKubernetes(config.network.deployment)) { 146 | info('Before starting the network please make sure kubectl is installed and setup properly') 147 | info('Check out our qubernetes project docs for more info: https://github.com/jpmorganchase/qubernetes') 148 | info('') 149 | } 150 | info('Run the following commands to start your network:') 151 | info('') 152 | info(`cd ${getFullNetworkPath(config)}`) 153 | info(`${wrapScript(SCRIPTS.start.filename)}`) 154 | info('') 155 | info('A sample simpleStorage contract is provided to deploy to your network') 156 | info(`To use run ${wrapScript(SCRIPTS.runscript.filename)} ${SCRIPTS.publicContract.filename} from the network folder`) 157 | info('') 158 | if (isTessera(config.network.transactionManager)) { 159 | info(`A private simpleStorage contract was created with privateFor set to use Node 2's public key: ${loadTesseraPublicKey(config, 2)}`) 160 | info(`To use run ${wrapScript(SCRIPTS.runscript.filename)} ${SCRIPTS.privateContract.filename} from the network folder`) 161 | info('') 162 | } 163 | if (isKubernetes(config.network.deployment)) { 164 | info('A script to retrieve the quorum rpc and tessera 3rd party endpoints to use with remix or cakeshop is provided') 165 | info(`To use run ${wrapScript(SCRIPTS.getEndpoints.filename)} from the network folder after starting`) 166 | info('') 167 | } 168 | if (isCakeshop(config.network.cakeshop)) { 169 | if (isKubernetes(config.network.deployment)) { 170 | info('After starting, run ./getEndpoints on any node to get the Cakeshop url') 171 | } else { 172 | info(`After starting, Cakeshop will be accessible here: http://localhost:${config.network.cakeshopPort}`) 173 | } 174 | info('') 175 | } 176 | if (config.network.prometheus) { 177 | info('After starting, run ./getEndpoints on any node to get the Prometheus url') 178 | info('') 179 | } 180 | if (config.network.reporting) { 181 | info(`After starting, the Reporting Tool will be accessible here: http://localhost:${config.network.reportingUiPort}`) 182 | info('') 183 | } 184 | if (config.network.splunk) { 185 | info(`After starting, Splunk will be accessible here: http://localhost:${config.network.splunkPort}`) 186 | info('The default credentials are admin:changeme') 187 | info('') 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/model/CakeshopConfig.js: -------------------------------------------------------------------------------- 1 | import { 2 | isDocker, 3 | isTessera, 4 | } from './NetworkConfig' 5 | 6 | // eslint-disable-next-line import/prefer-default-export 7 | export function generateCakeshopConfig(config) { 8 | const nodes = [] 9 | 10 | config.nodes.forEach((node, i) => { 11 | const nodeData = { 12 | name: `node${i + 1}`, 13 | rpcUrl: isDocker(config.network.deployment) 14 | // docker-compose creates dns entries for the container's ip using container name (nodeX) 15 | ? `http://node${i + 1}:${config.containerPorts.quorum.rpcPort}` 16 | : `http://${node.quorum.ip}:${node.quorum.rpcPort}`, 17 | transactionManagerUrl: createTransactionManagerUrl(config, node, i), 18 | } 19 | nodes.push(nodeData) 20 | }) 21 | return nodes 22 | } 23 | 24 | function createTransactionManagerUrl(config, node, i) { 25 | if (isTessera(config.network.transactionManager)) { 26 | return isDocker(config.network.deployment) 27 | // docker-compose creates dns entries for the container's ip using container name (txmanagerX) 28 | ? `http://txmanager${i + 1}:${config.containerPorts.tm.thirdPartyPort}` 29 | : `http://${node.tm.ip}:${node.tm.thirdPartyPort}` 30 | } 31 | return '' 32 | } 33 | -------------------------------------------------------------------------------- /src/model/CakeshopConfig.test.js: -------------------------------------------------------------------------------- 1 | import { createConfigFromAnswers } from './NetworkConfig' 2 | import { generateCakeshopConfig } from './CakeshopConfig' 3 | import { LATEST_QUORUM, LATEST_TESSERA } from '../generators/download' 4 | 5 | const baseNetwork = { 6 | numberNodes: '3', 7 | consensus: 'raft', 8 | quorumVersion: LATEST_QUORUM, 9 | transactionManager: LATEST_TESSERA, 10 | deployment: 'bash', 11 | tools: ['cakeshop'], 12 | } 13 | 14 | const containerPortInfo = { 15 | quorum: { 16 | rpcPort: 8545, 17 | p2pPort: 21000, 18 | raftPort: 50400, 19 | wsPort: 8546, 20 | graphQlPort: 8547, 21 | }, 22 | tm: { 23 | p2pPort: 9000, 24 | thirdPartyPort: 9080, 25 | }, 26 | } 27 | 28 | test('creates 3nodes raft dockerFile tessera cakeshop', () => { 29 | const config = createConfigFromAnswers({ 30 | ...baseNetwork, 31 | deployment: 'docker-compose', 32 | containerPorts: { 33 | dockerSubnet: '172.16.239.0/24', 34 | ...containerPortInfo, 35 | }, 36 | }) 37 | const cakeshop = generateCakeshopConfig(config) 38 | expect(cakeshop).toMatchSnapshot() 39 | }) 40 | 41 | test('creates 3nodes istanbul bash tessera cakeshop', () => { 42 | const config = createConfigFromAnswers({ 43 | ...baseNetwork, 44 | consensus: 'istanbul', 45 | }) 46 | const cakeshop = generateCakeshopConfig(config) 47 | expect(cakeshop).toMatchSnapshot() 48 | }) 49 | 50 | test('creates 3nodes raft dockerFile no tessera cakeshop', () => { 51 | const config = createConfigFromAnswers({ 52 | ...baseNetwork, 53 | transactionManager: 'none', 54 | deployment: 'docker-compose', 55 | containerPorts: { 56 | dockerSubnet: '172.16.239.0/24', 57 | ...containerPortInfo, 58 | }, 59 | }) 60 | const cakeshop = generateCakeshopConfig(config) 61 | expect(cakeshop).toMatchSnapshot() 62 | }) 63 | 64 | test('creates 3nodes istanbul bash no tessera cakeshop', () => { 65 | const config = createConfigFromAnswers({ 66 | ...baseNetwork, 67 | consensus: 'istanbul', 68 | transactionManager: 'none', 69 | }) 70 | const cakeshop = generateCakeshopConfig(config) 71 | expect(cakeshop).toMatchSnapshot() 72 | }) 73 | -------------------------------------------------------------------------------- /src/model/ConsensusConfig.js: -------------------------------------------------------------------------------- 1 | import { writeJsonFile } from '../utils/fileUtils' 2 | import { 3 | generateAccounts, 4 | generateExtraData, 5 | } from '../generators/consensusHelper' 6 | import { isRaft } from './NetworkConfig' 7 | import { isLegacyTessera } from '../generators/binaryHelper' 8 | 9 | export function generateConsensusConfig(configDir, consensus, nodes, networkId, tessera) { 10 | writeJsonFile( 11 | configDir, 12 | 'genesis.json', 13 | isRaft(consensus) 14 | ? generateRaftConfig(nodes, configDir, networkId, tessera) 15 | : generateIstanbulConfig(nodes, configDir, networkId, tessera), 16 | ) 17 | } 18 | 19 | export function generateRaftConfig(nodes, configDir, networkId, tessera) { 20 | const alloc = generateAccounts(nodes, configDir) 21 | const privacyEnhancementsBlock = isLegacyTessera(tessera) ? undefined : 0 22 | return { 23 | alloc, 24 | coinbase: '0x0000000000000000000000000000000000000000', 25 | config: { 26 | homesteadBlock: 0, 27 | byzantiumBlock: 0, 28 | constantinopleBlock: 0, 29 | istanbulBlock: 0, 30 | petersburgBlock: 0, 31 | privacyEnhancementsBlock, 32 | chainId: parseInt(networkId, 10), 33 | eip150Block: 0, 34 | eip155Block: 0, 35 | eip150Hash: '0x0000000000000000000000000000000000000000000000000000000000000000', 36 | eip158Block: 0, 37 | maxCodeSizeConfig: [ 38 | { 39 | block: 0, 40 | size: 32, 41 | }, 42 | ], 43 | isQuorum: true, 44 | }, 45 | difficulty: '0x0', 46 | extraData: '0x0000000000000000000000000000000000000000000000000000000000000000', 47 | gasLimit: '0xE0000000', 48 | mixhash: '0x00000000000000000000000000000000000000647572616c65787365646c6578', 49 | nonce: '0x0', 50 | parentHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 51 | timestamp: '0x00', 52 | } 53 | } 54 | 55 | export function generateIstanbulConfig(nodes, configDir, networkId, tessera) { 56 | const alloc = generateAccounts(nodes, configDir) 57 | const extraData = generateExtraData(nodes, configDir) 58 | const privacyEnhancementsBlock = isLegacyTessera(tessera) ? undefined : 0 59 | return { 60 | alloc, 61 | coinbase: '0x0000000000000000000000000000000000000000', 62 | config: { 63 | homesteadBlock: 0, 64 | byzantiumBlock: 0, 65 | constantinopleBlock: 0, 66 | istanbulBlock: 0, 67 | petersburgBlock: 0, 68 | privacyEnhancementsBlock, 69 | chainId: parseInt(networkId, 10), 70 | eip150Block: 0, 71 | eip155Block: 0, 72 | eip150Hash: '0x0000000000000000000000000000000000000000000000000000000000000000', 73 | eip158Block: 0, 74 | maxCodeSizeConfig: [ 75 | { 76 | block: 0, 77 | size: 32, 78 | }, 79 | ], 80 | isQuorum: true, 81 | istanbul: { 82 | epoch: 30000, 83 | policy: 0, 84 | ceil2Nby3Block: 0, 85 | }, 86 | }, 87 | difficulty: '0x1', 88 | extraData, 89 | gasLimit: '0xE0000000', 90 | mixhash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365', 91 | nonce: '0x0', 92 | parentHash: '0x0000000000000000000000000000000000000000000000000000000000000000', 93 | timestamp: '0x00', 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/model/ConsensusConfig.test.js: -------------------------------------------------------------------------------- 1 | import { anything } from 'expect' 2 | import { generateNodeConfigs } from './NetworkConfig' 3 | import { 4 | generateIstanbulConfig, 5 | generateRaftConfig, 6 | generateConsensusConfig, 7 | } from './ConsensusConfig' 8 | import { 9 | generateAccounts, 10 | generateExtraData, 11 | } from '../generators/consensusHelper' 12 | import { writeJsonFile } from '../utils/fileUtils' 13 | import { LATEST_TESSERA } from '../generators/download' 14 | 15 | jest.mock('../generators/consensusHelper') 16 | jest.mock('../utils/fileUtils') 17 | 18 | test('creates 3nodes raft genesis', () => { 19 | const accts = '{"0xa5a0b81cbcd2d93bba08b3e27b1437d7bdc42836":{"balance":"1000000000000000000000000000"},"0x0fe3fd1414001b295da621e30698462df06eaad2":{"balance":"1000000000000000000000000000"},"0x8aef5fa7f18ffda8fa98016ec27562ea33743f18":{"balance":"1000000000000000000000000000"}}' 20 | generateAccounts 21 | .mockReturnValueOnce(JSON.parse(accts)) 22 | 23 | const genesis = generateRaftConfig(3, 'keyPath', 10, LATEST_TESSERA) 24 | expect(genesis).toMatchSnapshot() 25 | }) 26 | 27 | test('creates 3nodes raft genesis without privacy enhancements', () => { 28 | const accts = '{"0xa5a0b81cbcd2d93bba08b3e27b1437d7bdc42836":{"balance":"1000000000000000000000000000"},"0x0fe3fd1414001b295da621e30698462df06eaad2":{"balance":"1000000000000000000000000000"},"0x8aef5fa7f18ffda8fa98016ec27562ea33743f18":{"balance":"1000000000000000000000000000"}}' 29 | generateAccounts 30 | .mockReturnValueOnce(JSON.parse(accts)) 31 | 32 | const genesis = generateRaftConfig(3, 'keyPath', 10, '0.10.6') 33 | expect(genesis).toMatchSnapshot() 34 | }) 35 | 36 | test('creates 5nodes istanbul genesis', () => { 37 | const accts = '{"0x59b64581638fd8311423e007c5131b0d9287d069":{"balance":"1000000000000000000000000000"},"0x4e03a788769f04bb8ec13826d1efe6cd9ca46190":{"balance":"1000000000000000000000000000"},"0xb01a34cca09374a58068eda1c2d7e472f39ef413":{"balance":"1000000000000000000000000000"}, "0xdbd868dd04daf492b783587f50bdd82fbf9bda3f":{"balance":"1000000000000000000000000000"}, "0x62fdbfa6bebe19c812f3bceb3d2d16b00563862c":{"balance":"1000000000000000000000000000"}}' 38 | generateAccounts 39 | .mockReturnValueOnce(JSON.parse(accts)) 40 | 41 | generateExtraData.mockReturnValueOnce( 42 | '0x0000000000000000000000000000000000000000000000000000000000000000f8aff86994dea501aa3315db296f1ce0f7d264c6c812b2088a942da3b70ed4e94ad02641ef83d33727d86da41e78945a4a874f95cd8f0758dea8f6719cd686aedc30e994d34cb7199598f100f76eed6fdfe962b37a66b7dc9499fc6e8ac3f567ae9ee11559e0fa686513aa9e74b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0', 43 | ) 44 | 45 | const genesis = generateIstanbulConfig(5, 'testDir', 10, LATEST_TESSERA) 46 | expect(genesis).toMatchSnapshot() 47 | }) 48 | 49 | test('generates raft consensus file', () => { 50 | const nodes = generateNodeConfigs(5) 51 | const accts = '{"0x59b64581638fd8311423e007c5131b0d9287d069":{"balance":"1000000000000000000000000000"},"0x4e03a788769f04bb8ec13826d1efe6cd9ca46190":{"balance":"1000000000000000000000000000"},"0xb01a34cca09374a58068eda1c2d7e472f39ef413":{"balance":"1000000000000000000000000000"}, "0xdbd868dd04daf492b783587f50bdd82fbf9bda3f":{"balance":"1000000000000000000000000000"}, "0x62fdbfa6bebe19c812f3bceb3d2d16b00563862c":{"balance":"1000000000000000000000000000"}}' 52 | generateAccounts 53 | .mockReturnValueOnce(JSON.parse(accts)) 54 | generateConsensusConfig('testConfigDir', 'raft', nodes, 10, LATEST_TESSERA) 55 | expect(writeJsonFile).toBeCalledWith('testConfigDir', 'genesis.json', anything()) 56 | }) 57 | 58 | test('generates istanbul consensus file', () => { 59 | const nodes = generateNodeConfigs(5) 60 | const accts = '{"0x59b64581638fd8311423e007c5131b0d9287d069":{"balance":"1000000000000000000000000000"},"0x4e03a788769f04bb8ec13826d1efe6cd9ca46190":{"balance":"1000000000000000000000000000"},"0xb01a34cca09374a58068eda1c2d7e472f39ef413":{"balance":"1000000000000000000000000000"}, "0xdbd868dd04daf492b783587f50bdd82fbf9bda3f":{"balance":"1000000000000000000000000000"}, "0x62fdbfa6bebe19c812f3bceb3d2d16b00563862c":{"balance":"1000000000000000000000000000"}}' 61 | generateAccounts 62 | .mockReturnValueOnce(JSON.parse(accts)) 63 | generateConsensusConfig('testConfigDir', 'istanbul', nodes, 10, LATEST_TESSERA) 64 | expect(writeJsonFile).toBeCalledWith('testConfigDir', 'genesis.json', anything()) 65 | }) 66 | -------------------------------------------------------------------------------- /src/model/DockerFile.test.js: -------------------------------------------------------------------------------- 1 | import { buildDockerCompose } from '../generators/dockerHelper' 2 | import { createConfigFromAnswers } from './NetworkConfig' 3 | import { LATEST_QUORUM, LATEST_TESSERA } from '../generators/download' 4 | 5 | const baseNetwork = { 6 | numberNodes: '3', 7 | consensus: 'raft', 8 | quorumVersion: LATEST_QUORUM, 9 | transactionManager: LATEST_TESSERA, 10 | deployment: 'bash', 11 | tools: [], 12 | containerPorts: { 13 | dockerSubnet: '172.16.239.0/24', 14 | quorum: { 15 | rpcPort: 8545, 16 | p2pPort: 21000, 17 | raftPort: 50400, 18 | wsPort: 8546, 19 | graphQlPort: 8547, 20 | }, 21 | tm: { 22 | p2pPort: 9000, 23 | thirdPartyPort: 9080, 24 | }, 25 | }, 26 | } 27 | 28 | test('creates 3nodes raft dockerFile tessera no cakeshop', () => { 29 | const config = createConfigFromAnswers({ 30 | ...baseNetwork, 31 | deployment: 'docker-compose', 32 | }) 33 | const docker = buildDockerCompose(config) 34 | expect(docker).toMatchSnapshot() 35 | }) 36 | 37 | test('creates 5nodes istanbul dockerFile no tessera no cakeshop', () => { 38 | const config = createConfigFromAnswers({ 39 | ...baseNetwork, 40 | numberNodes: '5', 41 | consensus: 'istanbul', 42 | transactionManager: 'none', 43 | deployment: 'docker-compose', 44 | }) 45 | const docker = buildDockerCompose(config) 46 | expect(docker).toMatchSnapshot() 47 | }) 48 | 49 | test('creates 3nodes raft dockerFile tessera cakeshop', () => { 50 | const config = createConfigFromAnswers({ 51 | ...baseNetwork, 52 | deployment: 'docker-compose', 53 | tools: ['cakeshop'], 54 | }) 55 | const docker = buildDockerCompose(config) 56 | expect(docker).toMatchSnapshot() 57 | }) 58 | 59 | test('creates 5nodes istanbul dockerFile no tessera cakeshop', () => { 60 | const config = createConfigFromAnswers({ 61 | ...baseNetwork, 62 | numberNodes: '5', 63 | consensus: 'istanbul', 64 | transactionManager: 'none', 65 | deployment: 'docker-compose', 66 | tools: ['cakeshop'], 67 | }) 68 | const docker = buildDockerCompose(config) 69 | expect(docker).toMatchSnapshot() 70 | }) 71 | 72 | test('creates 3nodes raft docker tessera custom', () => { 73 | const config = createConfigFromAnswers({ 74 | ...baseNetwork, 75 | deployment: 'docker-compose', 76 | generateKeys: false, 77 | networkId: 10, 78 | genesisLocation: 'none', 79 | customizePorts: false, 80 | nodes: [], 81 | }) 82 | const docker = buildDockerCompose(config) 83 | expect(docker).toMatchSnapshot() 84 | }) 85 | 86 | test('creates 2nodes raft docker tessera cakeshop custom ports', () => { 87 | const nodes = [ 88 | { 89 | quorum: { 90 | ip: '172.16.239.11', 91 | devP2pPort: '55001', 92 | rpcPort: '56000', 93 | wsPort: '57000', 94 | raftPort: '80501', 95 | graphQlPort: '58000', 96 | }, 97 | tm: { 98 | ip: '172.16.239.101', 99 | thirdPartyPort: '5081', 100 | p2pPort: '5001', 101 | }, 102 | }, 103 | { 104 | quorum: { 105 | ip: '172.16.239.12', 106 | devP2pPort: '55001', 107 | rpcPort: '56001', 108 | wsPort: '57001', 109 | raftPort: '80502', 110 | graphQlPort: '58001', 111 | }, 112 | tm: { 113 | ip: '172.16.239.102', 114 | thirdPartyPort: '5082', 115 | p2pPort: '5002', 116 | }, 117 | }, 118 | ] 119 | 120 | const config = createConfigFromAnswers({ 121 | ...baseNetwork, 122 | numberNodes: '2', 123 | deployment: 'docker-compose', 124 | tools: ['cakeshop'], 125 | generateKeys: false, 126 | networkId: 10, 127 | genesisLocation: 'none', 128 | customizePorts: true, 129 | cakeshopPort: '7999', 130 | nodes, 131 | }) 132 | const bash = buildDockerCompose(config) 133 | expect(bash).toMatchSnapshot() 134 | }) 135 | -------------------------------------------------------------------------------- /src/model/NetworkConfig.js: -------------------------------------------------------------------------------- 1 | import { 2 | LATEST_CAKESHOP, LATEST_QUORUM, LATEST_REPORTING, LATEST_TESSERA, 3 | } from '../generators/download' 4 | import { cidrhost, getDockerSubnet } from '../utils/subnetUtils' 5 | 6 | export function createConfigFromAnswers(answers) { 7 | const { 8 | name, 9 | numberNodes = 3, 10 | consensus = 'raft', 11 | quorumVersion = LATEST_QUORUM, 12 | transactionManager = LATEST_TESSERA, 13 | deployment = 'bash', 14 | tools = ['cakeshop'], 15 | generateKeys = false, 16 | networkId = '10', 17 | genesisLocation = 'none', 18 | customizePorts = false, 19 | nodes = [], 20 | cakeshopPort = isKubernetes(deployment) ? '30108' : '8999', 21 | splunkPort = '8000', 22 | splunkHecPort = '8088', 23 | reportingRpcPort = '4000', 24 | reportingUiPort = '3000', 25 | remoteDebug = false, 26 | containerPorts = undefined, 27 | } = answers 28 | const networkFolder = name 29 | || defaultNetworkName(numberNodes, consensus, transactionManager, deployment) 30 | const dockerSubnet = (isDocker(deployment) && containerPorts !== undefined) ? containerPorts.dockerSubnet : '' 31 | const cakeshop = tools.includes('cakeshop') ? LATEST_CAKESHOP : 'none' 32 | const splunk = tools.includes('splunk') 33 | const prometheus = tools.includes('prometheus') 34 | const reporting = tools.includes('reporting') 35 | return { 36 | network: { 37 | name: networkFolder, 38 | verbosity: 5, 39 | consensus, 40 | quorumVersion, 41 | transactionManager, 42 | permissioned: true, 43 | genesisFile: genesisLocation, 44 | generateKeys, 45 | configDir: `network/${networkFolder}/resources`, 46 | deployment, 47 | cakeshop, 48 | splunk, 49 | prometheus, 50 | reporting: reporting && LATEST_REPORTING, 51 | reportingRpcPort, 52 | reportingUiPort, 53 | networkId, 54 | customizePorts, 55 | cakeshopPort, 56 | remoteDebug, 57 | splunkIp: (splunk) ? cidrhost(dockerSubnet, 66) : '127.0.0.1', 58 | splunkPort, 59 | splunkHecPort, 60 | }, 61 | nodes: (customizePorts && nodes.length > 0) ? nodes : generateNodeConfigs( 62 | numberNodes, 63 | transactionManager, 64 | deployment, 65 | dockerSubnet, 66 | ), 67 | containerPorts, 68 | } 69 | } 70 | 71 | export function defaultNetworkName(numberNodes, consensus, transactionManager, deployment) { 72 | const transactionManagerName = !isTessera(transactionManager) 73 | ? '' 74 | : 'tessera-' 75 | return `${numberNodes}-nodes-${consensus}-${transactionManagerName}${deployment}` 76 | } 77 | 78 | export function generateNodeConfigs( 79 | numberNodes, 80 | transactionManager, 81 | deployment, 82 | dockerSubnet, 83 | ) { 84 | const devP2pPort = 21000 85 | const rpcPort = 22000 86 | const wsPort = 23000 87 | const graphQlPort = 24000 88 | const raftPort = 50401 89 | const thirdPartyPort = 9081 90 | const p2pPort = 9001 91 | const nodes = [] 92 | 93 | for (let i = 0; i < parseInt(numberNodes, 10); i += 1) { 94 | const node = { 95 | quorum: { 96 | ip: isDocker(deployment) ? cidrhost(dockerSubnet, i + 1 + 10) : '127.0.0.1', 97 | devP2pPort: devP2pPort + i, 98 | rpcPort: rpcPort + i, 99 | wsPort: wsPort + i, 100 | raftPort: raftPort + i, 101 | graphQlPort: graphQlPort + i, 102 | }, 103 | } 104 | if (isTessera(transactionManager)) { 105 | node.tm = { 106 | ip: isDocker(deployment) ? cidrhost(dockerSubnet, i + 1 + 100) : '127.0.0.1', 107 | thirdPartyPort: thirdPartyPort + i, 108 | p2pPort: p2pPort + i, 109 | } 110 | } 111 | nodes.push(node) 112 | } 113 | return nodes 114 | } 115 | 116 | export function getContainerPorts(deployment) { 117 | const dockerSubnet = isDocker(deployment) ? getDockerSubnet() : '' 118 | return { 119 | dockerSubnet, 120 | quorum: { 121 | rpcPort: 8545, 122 | p2pPort: 30303, 123 | raftPort: 50401, 124 | wsPort: 8546, 125 | graphQlPort: 8547, 126 | }, 127 | tm: { 128 | p2pPort: 9001, 129 | thirdPartyPort: 9080, 130 | }, 131 | reporting: { 132 | rpcPort: 4000, 133 | uiPort: 3000, 134 | }, 135 | } 136 | } 137 | 138 | export function isTessera(tessera) { 139 | return tessera !== 'none' 140 | } 141 | 142 | export function isDocker(deployment) { 143 | return deployment === 'docker-compose' 144 | } 145 | 146 | export function isBash(deployment) { 147 | return deployment === 'bash' 148 | } 149 | 150 | export function isKubernetes(deployment) { 151 | return deployment === 'kubernetes' 152 | } 153 | 154 | export function isIstanbul(consensus) { 155 | return consensus === 'istanbul' 156 | } 157 | 158 | export function isRaft(consensus) { 159 | return consensus === 'raft' 160 | } 161 | 162 | export function isCakeshop(cakeshop) { 163 | return cakeshop !== 'none' 164 | } 165 | 166 | export const CUSTOM_CONFIG_LOCATION = 'Enter path to config.json' 167 | -------------------------------------------------------------------------------- /src/model/NetworkConfig.test.js: -------------------------------------------------------------------------------- 1 | import { createConfigFromAnswers } from './NetworkConfig' 2 | import { LATEST_QUORUM, LATEST_TESSERA } from '../generators/download' 3 | 4 | jest.mock('../utils/execUtils') 5 | 6 | // rather than having big test jsons that we match to, we can just use snapshot 7 | // tests, where it will compare against the last time you ran and if it's 8 | // different you can review to make sure the changes were intended 9 | const baseNetwork = { 10 | numberNodes: '3', 11 | consensus: 'raft', 12 | quorumVersion: LATEST_QUORUM, 13 | transactionManager: LATEST_TESSERA, 14 | deployment: 'bash', 15 | tools: [], 16 | } 17 | 18 | const containerPortInfo = { 19 | quorum: { 20 | rpcPort: 8545, 21 | p2pPort: 21000, 22 | raftPort: 50400, 23 | wsPort: 8546, 24 | graphQlPort: 8547, 25 | }, 26 | tm: { 27 | p2pPort: 9000, 28 | thirdPartyPort: 9080, 29 | }, 30 | } 31 | 32 | test('creates quickstart config', () => { 33 | const config = createConfigFromAnswers({}) 34 | expect(config).toMatchSnapshot() 35 | }) 36 | 37 | test('creates 7nodes istanbul config', () => { 38 | const config = createConfigFromAnswers({ 39 | ...baseNetwork, 40 | numberNodes: '7', 41 | consensus: 'istanbul', 42 | }) 43 | expect(config).toMatchSnapshot() 44 | }) 45 | 46 | test('creates 5nodes raft no-TM config', () => { 47 | const config = createConfigFromAnswers({ 48 | ...baseNetwork, 49 | numberNodes: '5', 50 | transactionManager: 'none', 51 | }) 52 | expect(config).toMatchSnapshot() 53 | }) 54 | 55 | test('creates 7nodes istanbul docker config', () => { 56 | const config = createConfigFromAnswers({ 57 | ...baseNetwork, 58 | numberNodes: '7', 59 | consensus: 'istanbul', 60 | deployment: 'docker-compose', 61 | containerPorts: { 62 | dockerSubnet: '172.16.239.0/24', 63 | ...containerPortInfo, 64 | }, 65 | }) 66 | expect(config).toMatchSnapshot() 67 | }) 68 | 69 | test('creates 5nodes raft no-TM cakeshop docker config', () => { 70 | const config = createConfigFromAnswers({ 71 | ...baseNetwork, 72 | numberNodes: '5', 73 | transactionManager: 'none', 74 | deployment: 'docker-compose', 75 | tools: ['cakeshop'], 76 | containerPorts: { 77 | dockerSubnet: '172.16.239.0/24', 78 | ...containerPortInfo, 79 | }, 80 | }) 81 | expect(config).toMatchSnapshot() 82 | }) 83 | 84 | test('creates 7nodes istanbul kubernetes config', () => { 85 | const config = createConfigFromAnswers({ 86 | ...baseNetwork, 87 | numberNodes: '7', 88 | consensus: 'istanbul', 89 | deployment: 'kubernetes', 90 | containerPorts: { 91 | dockerSubnet: '', 92 | ...containerPortInfo, 93 | }, 94 | }) 95 | expect(config).toMatchSnapshot() 96 | }) 97 | 98 | test('creates 7nodes raft kubernetes custom config', () => { 99 | const config = createConfigFromAnswers({ 100 | ...baseNetwork, 101 | numberNodes: '7', 102 | deployment: 'kubernetes', 103 | generateKeys: true, 104 | networkId: 14, 105 | genesisLocation: '', 106 | customizePorts: false, 107 | containerPorts: { 108 | dockerSubnet: '', 109 | ...containerPortInfo, 110 | }, 111 | }) 112 | expect(config).toMatchSnapshot() 113 | }) 114 | 115 | test('creates 7nodes istanbul cakeshop config', () => { 116 | const config = createConfigFromAnswers({ 117 | ...baseNetwork, 118 | numberNodes: '7', 119 | consensus: 'istanbul', 120 | tools: ['cakeshop'], 121 | }) 122 | expect(config).toMatchSnapshot() 123 | }) 124 | 125 | test('creates 6nodes raft custom config', () => { 126 | const config = createConfigFromAnswers({ 127 | ...baseNetwork, 128 | numberNodes: '6', 129 | generateKeys: true, 130 | networkId: 10, 131 | genesisLocation: '', 132 | customizePorts: false, 133 | }) 134 | expect(config).toMatchSnapshot() 135 | }) 136 | 137 | test('creates 7nodes istanbul no-TM custom config', () => { 138 | const config = createConfigFromAnswers({ 139 | ...baseNetwork, 140 | numberNodes: '7', 141 | consensus: 'istanbul', 142 | transactionManager: 'none', 143 | generateKeys: true, 144 | networkId: 12, 145 | genesisLocation: '', 146 | customizePorts: false, 147 | }) 148 | expect(config).toMatchSnapshot() 149 | }) 150 | 151 | test('creates 6nodes raft custom docker config', () => { 152 | const config = createConfigFromAnswers({ 153 | ...baseNetwork, 154 | numberNodes: '6', 155 | deployment: 'docker-compose', 156 | generateKeys: true, 157 | networkId: 10, 158 | genesisLocation: '', 159 | customizePorts: false, 160 | containerPorts: { 161 | dockerSubnet: '172.16.239.0/24', 162 | ...containerPortInfo, 163 | }, 164 | }) 165 | expect(config).toMatchSnapshot() 166 | }) 167 | 168 | test('creates 7nodes istanbul no-TM custom docker config', () => { 169 | const config = createConfigFromAnswers({ 170 | ...baseNetwork, 171 | numberNodes: '7', 172 | consensus: 'istanbul', 173 | transactionManager: 'none', 174 | deployment: 'docker-compose', 175 | generateKeys: false, 176 | networkId: 10, 177 | genesisLocation: '', 178 | customizePorts: false, 179 | containerPorts: { 180 | dockerSubnet: '172.16.239.0/24', 181 | ...containerPortInfo, 182 | }, 183 | }) 184 | expect(config).toMatchSnapshot() 185 | }) 186 | -------------------------------------------------------------------------------- /src/model/ResourceConfig.js: -------------------------------------------------------------------------------- 1 | import { getDockerRegistry } from '../generators/dockerHelper' 2 | import { isCakeshop, isKubernetes } from './NetworkConfig' 3 | 4 | export const LATEST_QUBERNETES = 'v0.2.2-rc1' 5 | 6 | // eslint-disable-next-line import/prefer-default-export 7 | export function buildKubernetesResource(config) { 8 | return [ 9 | buildKubernetesDetails(config), 10 | buildCakeshopDetails(config), 11 | buildPrometheusDetails(config), 12 | buildGenesisDetails(config), 13 | buildNodesDetails(config), 14 | ].join('') 15 | } 16 | 17 | function buildGenesisDetails(config) { 18 | // even when not using tessera, qubernetes generates the resources for it anyways, so pass the 19 | // pre-1.0.0 version so that qubernetes does not include the privacyEnhancementsBlock in the 20 | // genesis file. Including this flag in the config without a compatible version of tessera will 21 | // cause Quorum to fail to start 22 | const tesseraVersion = config.network.transactionManager === 'none' ? '0.11.0' : config.network.transactionManager 23 | return ` 24 | genesis: 25 | consensus: ${config.network.consensus} 26 | Quorum_Version: ${config.network.quorumVersion} 27 | Tm_Version: ${tesseraVersion} 28 | Chain_Id: ${config.network.networkId}` 29 | } 30 | 31 | function buildGeneralDetails(config, i) { 32 | const nodeName = !isKubernetes(config.network.deployment) ? `"%QUORUM-NODE${i + 1}_SERVICE_HOST%"` : `quorum-node${i + 1}` 33 | return ` 34 | - Node_UserIdent: ${nodeName} 35 | Key_Dir_Base: ${config.network.generateKeys ? 'out/config' : '7nodes'} 36 | Key_Dir: key${i + 1} 37 | Permissioned_Nodes_File: out/config/permissioned-nodes.json` 38 | } 39 | 40 | function buildNodeDetails(config, i) { 41 | return [ 42 | buildGeneralDetails(config, i), 43 | buildQuorumDetails(config), 44 | buildTesseraDetails(config), 45 | buildGethDetails(config), 46 | ].join('') 47 | } 48 | 49 | function buildNodesDetails(config) { 50 | const numNodes = config.nodes.length 51 | let nodeString = '' 52 | for (let i = 0; i < parseInt(numNodes, 10); i += 1) { 53 | nodeString += buildNodeDetails(config, i) 54 | } 55 | return ` 56 | nodes: 57 | ${nodeString}` 58 | } 59 | 60 | function buildQuorumDetails(config) { 61 | return ` 62 | quorum: 63 | quorum: 64 | consensus: ${config.network.consensus} 65 | Quorum_Version: ${config.network.quorumVersion} 66 | Docker_Repo: ${getDockerRegistry()}quorumengineering` 67 | } 68 | 69 | function buildGethDetails(config) { 70 | return ` 71 | geth: 72 | network: 73 | id: ${config.network.networkId} 74 | public: false 75 | verbosity: 9 76 | Geth_Startup_Params: --rpccorsdomain=\\"*\\" --rpcvhosts=\\"*\\" --wsorigins=\\"*\\"` 77 | } 78 | 79 | function buildTesseraDetails(config) { 80 | return ` 81 | tm: 82 | Name: tessera 83 | Tm_Version: ${config.network.transactionManager} 84 | Docker_Repo: ${getDockerRegistry()}quorumengineering 85 | Tessera_Config_Dir: out/config` 86 | } 87 | 88 | function buildCakeshopDetails(config) { 89 | if (!isCakeshop(config.network.cakeshop)) { 90 | return '' 91 | } 92 | return ` 93 | cakeshop: 94 | version: ${config.network.cakeshop} 95 | Docker_Repo: ${getDockerRegistry()}quorumengineering 96 | service: 97 | type: NodePort 98 | nodePort: ${config.network.cakeshopPort}` 99 | } 100 | 101 | function buildPrometheusDetails(config) { 102 | if (!config.network.prometheus) { 103 | return '' 104 | } 105 | return ` 106 | prometheus: 107 | # override the default monitor startup params --metrics --metrics.expensive --pprof --pprofaddr=0.0.0.0. 108 | #monitor_params_geth: --metrics --metrics.expensive --pprof --pprofaddr=0.0.0.0 109 | nodePort_prom: 31323` 110 | } 111 | 112 | function buildKubernetesDetails() { 113 | return ` 114 | k8s: 115 | sep_deployment_files: true 116 | service: 117 | type: NodePort 118 | storage: 119 | Type: PVC 120 | Capacity: 200Mi` 121 | } 122 | -------------------------------------------------------------------------------- /src/model/ResourceConfig.test.js: -------------------------------------------------------------------------------- 1 | import { getFullNetworkPath } from '../generators/networkHelper' 2 | import { createConfigFromAnswers } from './NetworkConfig' 3 | import { 4 | TEST_CWD, 5 | } from '../utils/testHelper' 6 | import { buildKubernetesResource } from './ResourceConfig' 7 | import { LATEST_QUORUM, LATEST_TESSERA } from '../generators/download' 8 | import { getDockerRegistry } from '../generators/dockerHelper' 9 | 10 | jest.mock('../utils/fileUtils') 11 | jest.mock('../generators/networkHelper') 12 | jest.mock('../generators/dockerHelper') 13 | getFullNetworkPath.mockReturnValue(`${TEST_CWD}/test-network`) 14 | getDockerRegistry.mockReturnValue('') 15 | 16 | const containerPortInfo = { 17 | quorum: { 18 | rpcPort: 8545, 19 | p2pPort: 21000, 20 | raftPort: 50400, 21 | wsPort: 8546, 22 | graphQlPort: 8547, 23 | }, 24 | tm: { 25 | p2pPort: 9000, 26 | thirdPartyPort: 9080, 27 | }, 28 | } 29 | const baseNetwork = { 30 | numberNodes: '5', 31 | consensus: 'raft', 32 | quorumVersion: LATEST_QUORUM, 33 | transactionManager: LATEST_TESSERA, 34 | tools: [], 35 | deployment: 'kubernetes', 36 | containerPorts: { 37 | dockerSubnet: '', 38 | ...containerPortInfo, 39 | }, 40 | } 41 | 42 | test('creates 5nodes raft kubernetes tessera', () => { 43 | const config = createConfigFromAnswers(baseNetwork) 44 | const kubernetes = buildKubernetesResource(config) 45 | expect(kubernetes).toMatchSnapshot() 46 | }) 47 | 48 | test('creates 7nodes istanbul kubernetes generate keys', () => { 49 | const config = createConfigFromAnswers({ 50 | ...baseNetwork, 51 | numberNodes: '7', 52 | consensus: 'istanbul', 53 | generateKeys: true, 54 | }) 55 | const kubernetes = buildKubernetesResource(config) 56 | expect(kubernetes).toMatchSnapshot() 57 | }) 58 | 59 | test('creates 7nodes istanbul docker generate keys', () => { 60 | const config = createConfigFromAnswers({ 61 | ...baseNetwork, 62 | numberNodes: '7', 63 | deployment: 'docker-compose', 64 | consensus: 'istanbul', 65 | generateKeys: true, 66 | containerPorts: { 67 | dockerSubnet: '172.16.239.0/24', 68 | ...containerPortInfo, 69 | }, 70 | }) 71 | const kubernetes = buildKubernetesResource(config) 72 | expect(kubernetes).toMatchSnapshot() 73 | }) 74 | 75 | test('creates 7nodes istanbul docker generate keys no tessera', () => { 76 | const config = createConfigFromAnswers({ 77 | ...baseNetwork, 78 | numberNodes: '7', 79 | deployment: 'docker-compose', 80 | consensus: 'istanbul', 81 | transactionManger: 'none', 82 | generateKeys: true, 83 | containerPorts: { 84 | dockerSubnet: '172.16.239.0/24', 85 | ...containerPortInfo, 86 | }, 87 | }) 88 | const kubernetes = buildKubernetesResource(config) 89 | expect(kubernetes).toMatchSnapshot() 90 | }) 91 | 92 | test('creates 7nodes istanbul docker generate keys with cakeshop', () => { 93 | const config = createConfigFromAnswers({ 94 | ...baseNetwork, 95 | numberNodes: '7', 96 | deployment: 'docker-compose', 97 | consensus: 'istanbul', 98 | tools: ['cakeshop'], 99 | cakeshopPort: 8999, 100 | generateKeys: true, 101 | containerPorts: { 102 | dockerSubnet: '172.16.239.0/24', 103 | ...containerPortInfo, 104 | }, 105 | }) 106 | const kubernetes = buildKubernetesResource(config) 107 | expect(kubernetes).toMatchSnapshot() 108 | }) 109 | 110 | test('creates 7nodes istanbul kubernetes generate keys with cakeshop and prometheus', () => { 111 | const config = createConfigFromAnswers({ 112 | ...baseNetwork, 113 | numberNodes: '7', 114 | deployment: 'kubernetes', 115 | consensus: 'istanbul', 116 | tools: ['cakeshop', 'prometheus'], 117 | cakeshopPort: 8999, 118 | generateKeys: true, 119 | containerPorts: { 120 | dockerSubnet: '172.16.239.0/24', 121 | ...containerPortInfo, 122 | }, 123 | }) 124 | const kubernetes = buildKubernetesResource(config) 125 | expect(kubernetes).toMatchSnapshot() 126 | }) 127 | -------------------------------------------------------------------------------- /src/model/TesseraConfig.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export function createConfig(DDIR, i, ip, serverPortThirdParty, serverPortP2P, peerList) { 3 | return { 4 | useWhiteList: false, 5 | jdbc: { 6 | username: 'sa', 7 | password: '', 8 | url: `jdbc:h2:${DDIR}/db${i};MODE=Oracle;TRACE_LEVEL_SYSTEM_OUT=0`, 9 | autoCreateTables: true, 10 | }, 11 | serverConfigs: [ 12 | { 13 | app: 'ThirdParty', 14 | serverAddress: `http://${ip}:${serverPortThirdParty}`, 15 | cors: { 16 | allowedMethods: ['GET', 'OPTIONS'], 17 | allowedOrigins: ['*'], 18 | }, 19 | communicationType: 'REST', 20 | }, 21 | { 22 | app: 'Q2T', 23 | // Tessera uses URI.parse() for this, url encode any spaces in the path 24 | serverAddress: `unix:${DDIR.replace(/\s/g, '%20')}/tm.ipc`, 25 | communicationType: 'REST', 26 | }, 27 | { 28 | app: 'P2P', 29 | serverAddress: `http://${ip}:${serverPortP2P}`, 30 | sslConfig: { 31 | tls: 'OFF', 32 | generateKeyStoreIfNotExisted: true, 33 | serverKeyStore: `${DDIR}/server${i}-keystore`, 34 | serverKeyStorePassword: 'quorum', 35 | serverTrustStore: `${DDIR}/server-truststore`, 36 | serverTrustStorePassword: 'quorum', 37 | serverTrustMode: 'TOFU', 38 | knownClientsFile: `${DDIR}/knownClients`, 39 | clientKeyStore: `${DDIR}/client${i}-keystore`, 40 | clientKeyStorePassword: 'quorum', 41 | clientTrustStore: `${DDIR}/client-truststore`, 42 | clientTrustStorePassword: 'quorum', 43 | clientTrustMode: 'TOFU', 44 | knownServersFile: `${DDIR}/knownServers`, 45 | }, 46 | communicationType: 'REST', 47 | }, 48 | ], 49 | peer: peerList, 50 | keys: { 51 | passwords: [], 52 | keyData: [ 53 | { 54 | privateKeyPath: `${DDIR}/tm.key`, 55 | publicKeyPath: `${DDIR}/tm.pub`, 56 | }, 57 | ], 58 | }, 59 | alwaysSendTo: [], 60 | features: { 61 | enablePrivacyEnhancements: true, 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/model/__snapshots__/CakeshopConfig.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`creates 3nodes istanbul bash no tessera cakeshop 1`] = ` 4 | Array [ 5 | Object { 6 | "name": "node1", 7 | "rpcUrl": "http://127.0.0.1:22000", 8 | "transactionManagerUrl": "", 9 | }, 10 | Object { 11 | "name": "node2", 12 | "rpcUrl": "http://127.0.0.1:22001", 13 | "transactionManagerUrl": "", 14 | }, 15 | Object { 16 | "name": "node3", 17 | "rpcUrl": "http://127.0.0.1:22002", 18 | "transactionManagerUrl": "", 19 | }, 20 | ] 21 | `; 22 | 23 | exports[`creates 3nodes istanbul bash tessera cakeshop 1`] = ` 24 | Array [ 25 | Object { 26 | "name": "node1", 27 | "rpcUrl": "http://127.0.0.1:22000", 28 | "transactionManagerUrl": "http://127.0.0.1:9081", 29 | }, 30 | Object { 31 | "name": "node2", 32 | "rpcUrl": "http://127.0.0.1:22001", 33 | "transactionManagerUrl": "http://127.0.0.1:9082", 34 | }, 35 | Object { 36 | "name": "node3", 37 | "rpcUrl": "http://127.0.0.1:22002", 38 | "transactionManagerUrl": "http://127.0.0.1:9083", 39 | }, 40 | ] 41 | `; 42 | 43 | exports[`creates 3nodes raft dockerFile no tessera cakeshop 1`] = ` 44 | Array [ 45 | Object { 46 | "name": "node1", 47 | "rpcUrl": "http://node1:8545", 48 | "transactionManagerUrl": "", 49 | }, 50 | Object { 51 | "name": "node2", 52 | "rpcUrl": "http://node2:8545", 53 | "transactionManagerUrl": "", 54 | }, 55 | Object { 56 | "name": "node3", 57 | "rpcUrl": "http://node3:8545", 58 | "transactionManagerUrl": "", 59 | }, 60 | ] 61 | `; 62 | 63 | exports[`creates 3nodes raft dockerFile tessera cakeshop 1`] = ` 64 | Array [ 65 | Object { 66 | "name": "node1", 67 | "rpcUrl": "http://node1:8545", 68 | "transactionManagerUrl": "http://txmanager1:9080", 69 | }, 70 | Object { 71 | "name": "node2", 72 | "rpcUrl": "http://node2:8545", 73 | "transactionManagerUrl": "http://txmanager2:9080", 74 | }, 75 | Object { 76 | "name": "node3", 77 | "rpcUrl": "http://node3:8545", 78 | "transactionManagerUrl": "http://txmanager3:9080", 79 | }, 80 | ] 81 | `; 82 | -------------------------------------------------------------------------------- /src/model/__snapshots__/ConsensusConfig.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`creates 3nodes raft genesis 1`] = ` 4 | Object { 5 | "alloc": Object { 6 | "0x0fe3fd1414001b295da621e30698462df06eaad2": Object { 7 | "balance": "1000000000000000000000000000", 8 | }, 9 | "0x8aef5fa7f18ffda8fa98016ec27562ea33743f18": Object { 10 | "balance": "1000000000000000000000000000", 11 | }, 12 | "0xa5a0b81cbcd2d93bba08b3e27b1437d7bdc42836": Object { 13 | "balance": "1000000000000000000000000000", 14 | }, 15 | }, 16 | "coinbase": "0x0000000000000000000000000000000000000000", 17 | "config": Object { 18 | "byzantiumBlock": 0, 19 | "chainId": 10, 20 | "constantinopleBlock": 0, 21 | "eip150Block": 0, 22 | "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", 23 | "eip155Block": 0, 24 | "eip158Block": 0, 25 | "homesteadBlock": 0, 26 | "isQuorum": true, 27 | "istanbulBlock": 0, 28 | "maxCodeSizeConfig": Array [ 29 | Object { 30 | "block": 0, 31 | "size": 32, 32 | }, 33 | ], 34 | "petersburgBlock": 0, 35 | "privacyEnhancementsBlock": 0, 36 | }, 37 | "difficulty": "0x0", 38 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", 39 | "gasLimit": "0xE0000000", 40 | "mixhash": "0x00000000000000000000000000000000000000647572616c65787365646c6578", 41 | "nonce": "0x0", 42 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 43 | "timestamp": "0x00", 44 | } 45 | `; 46 | 47 | exports[`creates 3nodes raft genesis without privacy enhancements 1`] = ` 48 | Object { 49 | "alloc": Object { 50 | "0x0fe3fd1414001b295da621e30698462df06eaad2": Object { 51 | "balance": "1000000000000000000000000000", 52 | }, 53 | "0x8aef5fa7f18ffda8fa98016ec27562ea33743f18": Object { 54 | "balance": "1000000000000000000000000000", 55 | }, 56 | "0xa5a0b81cbcd2d93bba08b3e27b1437d7bdc42836": Object { 57 | "balance": "1000000000000000000000000000", 58 | }, 59 | }, 60 | "coinbase": "0x0000000000000000000000000000000000000000", 61 | "config": Object { 62 | "byzantiumBlock": 0, 63 | "chainId": 10, 64 | "constantinopleBlock": 0, 65 | "eip150Block": 0, 66 | "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", 67 | "eip155Block": 0, 68 | "eip158Block": 0, 69 | "homesteadBlock": 0, 70 | "isQuorum": true, 71 | "istanbulBlock": 0, 72 | "maxCodeSizeConfig": Array [ 73 | Object { 74 | "block": 0, 75 | "size": 32, 76 | }, 77 | ], 78 | "petersburgBlock": 0, 79 | "privacyEnhancementsBlock": undefined, 80 | }, 81 | "difficulty": "0x0", 82 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", 83 | "gasLimit": "0xE0000000", 84 | "mixhash": "0x00000000000000000000000000000000000000647572616c65787365646c6578", 85 | "nonce": "0x0", 86 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 87 | "timestamp": "0x00", 88 | } 89 | `; 90 | 91 | exports[`creates 5nodes istanbul genesis 1`] = ` 92 | Object { 93 | "alloc": Object { 94 | "0x4e03a788769f04bb8ec13826d1efe6cd9ca46190": Object { 95 | "balance": "1000000000000000000000000000", 96 | }, 97 | "0x59b64581638fd8311423e007c5131b0d9287d069": Object { 98 | "balance": "1000000000000000000000000000", 99 | }, 100 | "0x62fdbfa6bebe19c812f3bceb3d2d16b00563862c": Object { 101 | "balance": "1000000000000000000000000000", 102 | }, 103 | "0xb01a34cca09374a58068eda1c2d7e472f39ef413": Object { 104 | "balance": "1000000000000000000000000000", 105 | }, 106 | "0xdbd868dd04daf492b783587f50bdd82fbf9bda3f": Object { 107 | "balance": "1000000000000000000000000000", 108 | }, 109 | }, 110 | "coinbase": "0x0000000000000000000000000000000000000000", 111 | "config": Object { 112 | "byzantiumBlock": 0, 113 | "chainId": 10, 114 | "constantinopleBlock": 0, 115 | "eip150Block": 0, 116 | "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", 117 | "eip155Block": 0, 118 | "eip158Block": 0, 119 | "homesteadBlock": 0, 120 | "isQuorum": true, 121 | "istanbul": Object { 122 | "ceil2Nby3Block": 0, 123 | "epoch": 30000, 124 | "policy": 0, 125 | }, 126 | "istanbulBlock": 0, 127 | "maxCodeSizeConfig": Array [ 128 | Object { 129 | "block": 0, 130 | "size": 32, 131 | }, 132 | ], 133 | "petersburgBlock": 0, 134 | "privacyEnhancementsBlock": 0, 135 | }, 136 | "difficulty": "0x1", 137 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f8aff86994dea501aa3315db296f1ce0f7d264c6c812b2088a942da3b70ed4e94ad02641ef83d33727d86da41e78945a4a874f95cd8f0758dea8f6719cd686aedc30e994d34cb7199598f100f76eed6fdfe962b37a66b7dc9499fc6e8ac3f567ae9ee11559e0fa686513aa9e74b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", 138 | "gasLimit": "0xE0000000", 139 | "mixhash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", 140 | "nonce": "0x0", 141 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 142 | "timestamp": "0x00", 143 | } 144 | `; 145 | -------------------------------------------------------------------------------- /src/questions/customPromptHelper.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import { cidrhost } from '../utils/subnetUtils' 3 | 4 | function buildBashQuestions(numberNodes, hasTessera, i, prevAnswers, isRaft) { 5 | const questions = [] 6 | questions.push({ 7 | type: 'input', 8 | name: 'quorumIP', 9 | message: `input quorum node ${i + 1} ip`, 10 | default: prevAnswers !== undefined ? prevAnswers.quorumIP : '127.0.0.1', 11 | }) 12 | questions.push({ 13 | type: 'input', 14 | name: 'quorumP2P', 15 | message: `input quorum node ${i + 1} p2p port`, 16 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumP2P, 1) : '21000', 17 | }) 18 | questions.push({ 19 | type: 'input', 20 | name: 'quorumRpc', 21 | message: `input quorum node ${i + 1} rpc port`, 22 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumRpc, 1) : '22000', 23 | }) 24 | questions.push({ 25 | type: 'input', 26 | name: 'quorumWs', 27 | message: `input quorum node ${i + 1} ws port`, 28 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumWs, 1) : '23000', 29 | }) 30 | questions.push({ 31 | type: 'input', 32 | name: 'quorumGraphQl', 33 | message: `input quorum node ${i + 1} graphql port`, 34 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumGraphQl, 1) : '24000', 35 | }) 36 | if (isRaft) { 37 | questions.push({ 38 | type: 'input', 39 | name: 'quorumRaft', 40 | message: `input quorum node ${i + 1} raft port`, 41 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumRaft, 1) : '50401', 42 | }) 43 | } 44 | 45 | if (hasTessera) { 46 | questions.push({ 47 | type: 'input', 48 | name: 'tesseraIP', 49 | message: `input tessera node ${i + 1} ip`, 50 | default: prevAnswers !== undefined ? prevAnswers.tesseraIP : '127.0.0.1', 51 | }) 52 | questions.push({ 53 | type: 'input', 54 | name: 'tesseraP2P', 55 | message: `input tessera node ${i + 1} p2p port`, 56 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.tesseraP2P, 1) : '9001', 57 | }) 58 | questions.push({ 59 | type: 'input', 60 | name: 'tessera3Party', 61 | message: `input tessera node ${i + 1} third party port`, 62 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.tessera3Party, 1) : '9081', 63 | }) 64 | } 65 | return questions 66 | } 67 | 68 | export async function getCustomizedBashNodes(numberNodes, hasTessera, isRaft) { 69 | let answers 70 | const nodes = [] 71 | // eslint-disable no-await-in-loop 72 | for (let i = 0; i < parseInt(numberNodes, 10); i += 1) { 73 | answers = await inquirer.prompt(buildBashQuestions(numberNodes, hasTessera, i, answers, isRaft)) 74 | const node = { 75 | quorum: { 76 | ip: answers.quorumIP, 77 | devP2pPort: answers.quorumP2P, 78 | rpcPort: answers.quorumRpc, 79 | wsPort: answers.quorumWs, 80 | graphQlPort: answers.quorumGraphQl, 81 | raftPort: answers.quorumRaft === undefined ? incrementPort(50401, i) : answers.quorumRaft, 82 | }, 83 | } 84 | if (hasTessera) { 85 | node.tm = { 86 | ip: answers.tesseraIP, 87 | p2pPort: answers.tesseraP2P, 88 | thirdPartyPort: answers.tessera3Party, 89 | } 90 | } 91 | nodes.push(node) 92 | } 93 | return nodes 94 | } 95 | 96 | function buildDockerQuestions(numberNodes, hasTessera, i, prevAnswers) { 97 | const questions = [] 98 | questions.push({ 99 | type: 'input', 100 | name: 'quorumRpc', 101 | message: `input quorum node ${i + 1} rpc port`, 102 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumRpc, 1) : '22000', 103 | }) 104 | questions.push({ 105 | type: 'input', 106 | name: 'quorumWs', 107 | message: `input quorum node ${i + 1} ws port`, 108 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumWs, 1) : '23000', 109 | }) 110 | questions.push({ 111 | type: 'input', 112 | name: 'quorumGraphQl', 113 | message: `input quorum node ${i + 1} graphql port`, 114 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.quorumGraphQl, 1) : '24000', 115 | }) 116 | if (hasTessera) { 117 | questions.push({ 118 | type: 'input', 119 | name: 'tessera3Party', 120 | message: `input tessera node ${i + 1} third party port`, 121 | default: prevAnswers !== undefined ? incrementPort(prevAnswers.tessera3Party, 1) : '9081', 122 | }) 123 | } 124 | return questions 125 | } 126 | 127 | export async function getCustomizedDockerPorts(numberNodes, hasTessera, dockerSubnet) { 128 | let answers 129 | const nodes = [] 130 | const devP2pPort = 21000 131 | const raftPort = 50401 132 | const p2pPort = 9001 133 | 134 | // eslint-disable no-await-in-loop 135 | for (let i = 0; i < parseInt(numberNodes, 10); i += 1) { 136 | answers = await inquirer.prompt(buildDockerQuestions(numberNodes, hasTessera, i, answers)) 137 | const node = { 138 | quorum: { 139 | ip: cidrhost(dockerSubnet, i + 1 + 10), 140 | devP2pPort: numToString(devP2pPort + i), 141 | rpcPort: answers.quorumRpc, 142 | wsPort: answers.quorumWs, 143 | graphQlPort: answers.quorumGraphQl, 144 | raftPort: numToString(raftPort + i), 145 | }, 146 | } 147 | if (hasTessera) { 148 | node.tm = { 149 | ip: cidrhost(dockerSubnet, i + 1 + 100), 150 | thirdPartyPort: answers.tessera3Party, 151 | p2pPort: numToString(p2pPort + i), 152 | } 153 | } 154 | nodes.push(node) 155 | } 156 | return nodes 157 | } 158 | 159 | export async function getCustomizedCakeshopPort() { 160 | const question = { 161 | type: 'input', 162 | name: 'cakeshopPort', 163 | message: 'input cakeshop port', 164 | default: '8999', 165 | } 166 | const answer = await inquirer.prompt(question) 167 | 168 | return answer.cakeshopPort 169 | } 170 | 171 | export async function getCustomizedReportingPorts() { 172 | const questions = [{ 173 | type: 'input', 174 | name: 'reportingRpcPort', 175 | message: 'input reporting rpc port', 176 | default: '4000', 177 | }, 178 | { 179 | type: 'input', 180 | name: 'reportingUiPort', 181 | message: 'input reporting ui port', 182 | default: '3000', 183 | }, 184 | ] 185 | return inquirer.prompt(questions) 186 | } 187 | 188 | export async function getCustomizedSplunkPort() { 189 | const questions = [{ 190 | type: 'input', 191 | name: 'splunkPort', 192 | message: 'input splunk port', 193 | default: '8000', 194 | }, 195 | { 196 | type: 'input', 197 | name: 'splunkHecPort', 198 | message: 'input splunk hec port', 199 | default: '8088', 200 | }, 201 | ] 202 | return inquirer.prompt(questions) 203 | } 204 | 205 | function numToString(num) { 206 | return num.toString() 207 | } 208 | 209 | function incrementPort(num, i) { 210 | return numToString(parseInt(num, 10) + i) 211 | } 212 | -------------------------------------------------------------------------------- /src/questions/customPromptHelper.test.js: -------------------------------------------------------------------------------- 1 | import { prompt } from 'inquirer' 2 | import { promptUser } from './index' 3 | import { getCustomizedBashNodes, getCustomizedCakeshopPort, getCustomizedDockerPorts } from './customPromptHelper' 4 | import { CUSTOM_ANSWERS, QUESTIONS } from './questions' 5 | import { exists } from '../utils/fileUtils' 6 | import { LATEST_QUORUM, LATEST_TESSERA } from '../generators/download' 7 | import { cidrhost } from '../utils/subnetUtils' 8 | 9 | jest.mock('inquirer') 10 | jest.mock('../generators/networkHelper') 11 | jest.mock('../generators/networkCreator') 12 | jest.mock('../utils/fileUtils') 13 | jest.mock('../utils/subnetUtils') 14 | exists.mockReturnValue(false) 15 | 16 | const SIMPLE_CONFIG = { 17 | numberNodes: '5', 18 | consensus: 'istanbul', 19 | quorumVersion: LATEST_QUORUM, 20 | transactionManager: LATEST_TESSERA, 21 | deployment: 'bash', 22 | tools: [], 23 | } 24 | 25 | const CUSTOM_CONFIG = { 26 | ...SIMPLE_CONFIG, 27 | generateKeys: false, 28 | networkId: 10, 29 | genesisLocation: 'testDir', 30 | customizePorts: true, 31 | nodes: ['nodes'], 32 | } 33 | 34 | const QUORUM_DOCKER_NODE1 = { 35 | quorumRpc: '32000', 36 | quorumWs: '33000', 37 | quorumGraphQl: '34000', 38 | } 39 | 40 | const TESSERA_DOCKER_NODE1 = { 41 | tessera3Party: '8081', 42 | } 43 | 44 | const QUORUM_DOCKER_NODE2 = { 45 | quorumRpc: '32001', 46 | quorumWs: '33001', 47 | quorumGraphQl: '34001', 48 | } 49 | 50 | const TESSERA_DOCKER_NODE2 = { 51 | tessera3Party: '8082', 52 | } 53 | 54 | const QUORUM_BASH_NODE1 = { 55 | quorumIP: '127.0.0.1', 56 | quorumP2P: '31000', 57 | quorumRpc: '32000', 58 | quorumWs: '33000', 59 | quorumRaft: '40401', 60 | quorumGraphQl: '34000', 61 | } 62 | 63 | const TESSERA_BASH_NODE1 = { 64 | tesseraIP: '127.0.0.1', 65 | tesseraP2P: '8001', 66 | tessera3Party: '8081', 67 | } 68 | 69 | const QUORUM_BASH_NODE2 = { 70 | quorumIP: '127.0.0.1', 71 | quorumP2P: '31001', 72 | quorumRpc: '32001', 73 | quorumWs: '33001', 74 | quorumRaft: '40402', 75 | quorumGraphQl: '34001', 76 | } 77 | 78 | const TESSERA_BASH_NODE2 = { 79 | tesseraIP: '127.0.0.1', 80 | tesseraP2P: '8002', 81 | tessera3Party: '8082', 82 | } 83 | 84 | const CAKESHOP = { 85 | cakeshopPort: '7999', 86 | } 87 | 88 | describe('build customized node info from custom prompts', () => { 89 | beforeEach(() => { 90 | prompt.mockClear() 91 | }) 92 | it('build bash quorum and tessera with raft', async () => { 93 | prompt.mockResolvedValueOnce({ 94 | ...QUORUM_BASH_NODE1, 95 | ...TESSERA_BASH_NODE1, 96 | }) 97 | prompt.mockClear() 98 | prompt.mockResolvedValueOnce({ 99 | ...QUORUM_BASH_NODE2, 100 | ...TESSERA_BASH_NODE2, 101 | }) 102 | 103 | const expected = [{ 104 | quorum: { 105 | devP2pPort: '31000', 106 | ip: '127.0.0.1', 107 | raftPort: '40401', 108 | rpcPort: '32000', 109 | wsPort: '33000', 110 | graphQlPort: '34000', 111 | }, 112 | tm: { 113 | ip: '127.0.0.1', p2pPort: '8001', thirdPartyPort: '8081', 114 | }, 115 | }, { 116 | quorum: { 117 | devP2pPort: '31001', 118 | ip: '127.0.0.1', 119 | raftPort: '40402', 120 | rpcPort: '32001', 121 | wsPort: '33001', 122 | graphQlPort: '34001', 123 | }, 124 | tm: { 125 | ip: '127.0.0.1', p2pPort: '8002', thirdPartyPort: '8082', 126 | }, 127 | }] 128 | 129 | const nodes = await getCustomizedBashNodes(2, true, true) 130 | expect(nodes).toEqual(expected) 131 | }) 132 | 133 | it('build bash quorum no tessera with istanbul', async () => { 134 | prompt.mockResolvedValueOnce({ 135 | ...QUORUM_BASH_NODE1, 136 | quorumRaft: undefined, 137 | }) 138 | prompt.mockClear() 139 | prompt.mockResolvedValueOnce({ 140 | ...QUORUM_BASH_NODE2, 141 | quorumRaft: undefined, 142 | }) 143 | 144 | const expected = [{ 145 | quorum: { 146 | devP2pPort: '31000', 147 | ip: '127.0.0.1', 148 | raftPort: '50401', 149 | rpcPort: '32000', 150 | wsPort: '33000', 151 | graphQlPort: '34000', 152 | }, 153 | }, { 154 | quorum: { 155 | devP2pPort: '31001', 156 | ip: '127.0.0.1', 157 | raftPort: '50402', 158 | rpcPort: '32001', 159 | wsPort: '33001', 160 | graphQlPort: '34001', 161 | }, 162 | }] 163 | 164 | const nodes = await getCustomizedBashNodes(2, false, false) 165 | expect(nodes).toEqual(expected) 166 | }) 167 | 168 | it('build docker quorum and tessera with raft', async () => { 169 | prompt.mockResolvedValueOnce({ 170 | ...QUORUM_DOCKER_NODE1, 171 | ...TESSERA_DOCKER_NODE1, 172 | }) 173 | prompt.mockClear() 174 | prompt.mockResolvedValueOnce({ 175 | ...QUORUM_DOCKER_NODE2, 176 | ...TESSERA_DOCKER_NODE2, 177 | }) 178 | 179 | cidrhost.mockReturnValueOnce('172.16.239.11') 180 | cidrhost.mockReturnValueOnce('172.16.239.101') 181 | cidrhost.mockReturnValueOnce('172.16.239.12') 182 | cidrhost.mockReturnValueOnce('172.16.239.102') 183 | 184 | const expected = [{ 185 | quorum: { 186 | devP2pPort: '21000', 187 | ip: '172.16.239.11', 188 | raftPort: '50401', 189 | rpcPort: '32000', 190 | wsPort: '33000', 191 | graphQlPort: '34000', 192 | }, 193 | tm: { 194 | ip: '172.16.239.101', p2pPort: '9001', thirdPartyPort: '8081', 195 | }, 196 | }, { 197 | quorum: { 198 | devP2pPort: '21001', 199 | ip: '172.16.239.12', 200 | raftPort: '50402', 201 | rpcPort: '32001', 202 | wsPort: '33001', 203 | graphQlPort: '34001', 204 | }, 205 | tm: { 206 | ip: '172.16.239.102', p2pPort: '9002', thirdPartyPort: '8082', 207 | }, 208 | }] 209 | 210 | const nodes = await getCustomizedDockerPorts(2, true) 211 | expect(nodes).toEqual(expected) 212 | }) 213 | 214 | it('build docker quorum no tessera with istanbul', async () => { 215 | prompt.mockResolvedValueOnce({ 216 | ...QUORUM_DOCKER_NODE1, 217 | }) 218 | prompt.mockClear() 219 | prompt.mockResolvedValueOnce({ 220 | ...QUORUM_DOCKER_NODE2, 221 | }) 222 | 223 | cidrhost.mockReturnValueOnce('172.16.239.11') 224 | cidrhost.mockReturnValueOnce('172.16.239.12') 225 | 226 | const expected = [{ 227 | quorum: { 228 | devP2pPort: '21000', 229 | ip: '172.16.239.11', 230 | raftPort: '50401', 231 | rpcPort: '32000', 232 | wsPort: '33000', 233 | graphQlPort: '34000', 234 | }, 235 | }, { 236 | quorum: { 237 | devP2pPort: '21001', 238 | ip: '172.16.239.12', 239 | raftPort: '50402', 240 | rpcPort: '32001', 241 | wsPort: '33001', 242 | graphQlPort: '34001', 243 | }, 244 | }] 245 | 246 | const nodes = await getCustomizedDockerPorts(2, false) 247 | expect(nodes).toEqual(expected) 248 | }) 249 | 250 | it('build cakeshop customized port', async () => { 251 | prompt.mockResolvedValueOnce({ 252 | ...CAKESHOP, 253 | }) 254 | 255 | const port = await getCustomizedCakeshopPort() 256 | expect(port).toEqual('7999') 257 | }) 258 | 259 | describe('prompts the user with different sets of questions based on first choice', () => { 260 | beforeEach(() => { 261 | prompt.mockClear() 262 | }) 263 | 264 | it('customize, bash ports', async () => { 265 | prompt.mockResolvedValue({ 266 | ...CUSTOM_CONFIG, 267 | tools: ['cakeshop'], 268 | consensus: 'raft', 269 | }) 270 | await promptUser('custom') 271 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, CUSTOM_ANSWERS) 272 | }) 273 | it('customize, docker ports', async () => { 274 | prompt.mockResolvedValue({ 275 | ...CUSTOM_CONFIG, 276 | deployment: 'docker-compose', 277 | }) 278 | await promptUser('custom') 279 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, CUSTOM_ANSWERS) 280 | }) 281 | }) 282 | }) 283 | -------------------------------------------------------------------------------- /src/questions/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import inquirer from 'inquirer' 3 | import { 4 | isBash, 5 | isTessera, 6 | isRaft, 7 | isDocker, 8 | getContainerPorts, 9 | CUSTOM_CONFIG_LOCATION, 10 | } from '../model/NetworkConfig' 11 | import { 12 | getCustomizedBashNodes, 13 | getCustomizedDockerPorts, 14 | getCustomizedCakeshopPort, 15 | getCustomizedSplunkPort, 16 | getCustomizedReportingPorts, 17 | } from './customPromptHelper' 18 | import { 19 | getPrefilledAnswersForMode, 20 | NETWORK_CONFIRM, 21 | NETWORK_NAME, 22 | QUESTIONS, 23 | GENERATE_QUESTIONS, 24 | GENERATE_NAME, 25 | } from './questions' 26 | import { 27 | getConfigsPath, 28 | } from '../generators/networkCreator' 29 | import { exists } from '../utils/fileUtils' 30 | import { getFullNetworkPath } from '../generators/networkHelper' 31 | 32 | // eslint-disable-next-line import/prefer-default-export 33 | export async function promptUser(mode) { 34 | const answers = await inquirer.prompt(QUESTIONS, getPrefilledAnswersForMode(mode)) 35 | 36 | answers.containerPorts = !isBash(answers.deployment) ? getContainerPorts(answers.deployment) : {} 37 | 38 | if (answers.customizePorts) { 39 | await promptForCustomPorts(answers) 40 | } 41 | 42 | await confirmNetworkName(answers) 43 | 44 | return answers 45 | } 46 | 47 | async function promptForCustomPorts(answers) { 48 | if (isBash(answers.deployment)) { 49 | answers.nodes = await getCustomizedBashNodes( 50 | answers.numberNodes, 51 | isTessera(answers.transactionManager), 52 | isRaft(answers.consensus), 53 | ) 54 | } else if (isDocker(answers.deployment)) { 55 | answers.nodes = await getCustomizedDockerPorts( 56 | answers.numberNodes, 57 | isTessera(answers.transactionManager), 58 | answers.containerPorts.dockerSubnet, 59 | ) 60 | } 61 | 62 | if (answers.tools.includes('cakeshop')) { 63 | answers.cakeshopPort = await getCustomizedCakeshopPort() 64 | } 65 | 66 | if (answers.tools.includes('reporting')) { 67 | const { reportingRpcPort, reportingUiPort } = await getCustomizedReportingPorts() 68 | answers.reportingRpcPort = reportingRpcPort 69 | answers.reportingUiPort = reportingUiPort 70 | } 71 | 72 | if (answers.tools.includes('splunk')) { 73 | const { splunkPort, splunkHecPort } = await getCustomizedSplunkPort() 74 | answers.splunkPort = splunkPort 75 | answers.splunkHecPort = splunkHecPort 76 | } 77 | } 78 | 79 | export async function promptGenerate() { 80 | const answers = await inquirer.prompt(GENERATE_QUESTIONS) 81 | 82 | if (answers.name) { 83 | await confirmGenerateNetworkName(answers) 84 | } 85 | if (answers.generate !== CUSTOM_CONFIG_LOCATION) { 86 | answers.configLocation = getConfigsPath(answers.generate) 87 | } 88 | 89 | return answers 90 | } 91 | 92 | async function confirmNetworkName(answers) { 93 | let overwrite = false 94 | while (networkExists(answers.name) && !overwrite) { 95 | overwrite = (await inquirer.prompt([NETWORK_CONFIRM], answers)).overwrite 96 | if (overwrite === false) { 97 | delete answers.name 98 | answers.name = (await inquirer.prompt([NETWORK_NAME], answers)).name 99 | } 100 | } 101 | } 102 | 103 | async function confirmGenerateNetworkName(answers) { 104 | let overwrite = false 105 | while (networkExists(answers.name) && !overwrite) { 106 | overwrite = (await inquirer.prompt([NETWORK_CONFIRM], answers)).overwrite 107 | if (overwrite === false) { 108 | delete answers.name 109 | answers.name = (await inquirer.prompt([GENERATE_NAME], answers)).name 110 | } 111 | } 112 | } 113 | 114 | function networkExists(networkName) { 115 | return exists(getFullNetworkPath({ network: { name: networkName } })) 116 | } 117 | -------------------------------------------------------------------------------- /src/questions/index.test.js: -------------------------------------------------------------------------------- 1 | import { prompt } from 'inquirer' 2 | import { 3 | promptUser, 4 | promptGenerate, 5 | } from './index' 6 | import { 7 | CUSTOM_ANSWERS, NETWORK_CONFIRM, NETWORK_NAME, 8 | QUESTIONS, 9 | QUICKSTART_ANSWERS, 10 | SIMPLE_ANSWERS, 11 | GENERATE_QUESTIONS, 12 | } from './questions' 13 | import { 14 | getCustomizedBashNodes, 15 | getCustomizedDockerPorts, 16 | } from './customPromptHelper' 17 | import { exists } from '../utils/fileUtils' 18 | import { LATEST_QUORUM, LATEST_TESSERA } from '../generators/download' 19 | import { CUSTOM_CONFIG_LOCATION } from '../model/NetworkConfig' 20 | 21 | jest.mock('inquirer') 22 | jest.mock('./customPromptHelper') 23 | jest.mock('../generators/networkHelper') 24 | jest.mock('../generators/networkCreator') 25 | jest.mock('../utils/fileUtils') 26 | exists.mockReturnValue(false) 27 | 28 | const QUICKSTART_CONFIG = { 29 | numberNodes: '3', 30 | consensus: 'raft', 31 | quorumVersion: LATEST_QUORUM, 32 | transactionManager: LATEST_TESSERA, 33 | deployment: 'bash', 34 | tools: ['cakeshop'], 35 | } 36 | 37 | const SIMPLE_CONFIG = { 38 | numberNodes: '5', 39 | consensus: 'istanbul', 40 | quorumVersion: LATEST_QUORUM, 41 | transactionManager: LATEST_TESSERA, 42 | deployment: 'bash', 43 | tools: [], 44 | } 45 | 46 | const CUSTOM_CONFIG = { 47 | ...SIMPLE_CONFIG, 48 | generateKeys: false, 49 | networkId: 10, 50 | genesisLocation: 'testDir', 51 | customizePorts: true, 52 | nodes: ['nodes'], 53 | } 54 | 55 | const GENERATE_ANSWERS = { 56 | generate: '1-config.json', 57 | configLocation: undefined, 58 | name: '1', 59 | } 60 | 61 | const GENERATE_CUSTOM_CONFIG_LOCATION_ANSWERS = { 62 | generate: CUSTOM_CONFIG_LOCATION, 63 | configLocation: 'custom/config/location.json', 64 | name: '2', 65 | } 66 | 67 | const GENERATE_NO_NAME_ANSWERS = { 68 | generate: '1-config.json', 69 | } 70 | 71 | describe('prompts the user with different sets of questions based on first choice', () => { 72 | beforeEach(() => { 73 | prompt.mockClear() 74 | getCustomizedBashNodes.mockClear() 75 | getCustomizedDockerPorts.mockClear() 76 | }) 77 | it('quickstart', async () => { 78 | prompt.mockResolvedValueOnce(QUICKSTART_CONFIG) 79 | await promptUser('quickstart') 80 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, QUICKSTART_ANSWERS()) 81 | expect(prompt).not.toHaveBeenCalledWith([NETWORK_CONFIRM], expect.anything()) 82 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(0) 83 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(0) 84 | }) 85 | 86 | it('quickstart, overwrite network', async () => { 87 | prompt.mockResolvedValueOnce(QUICKSTART_CONFIG) 88 | prompt.mockResolvedValueOnce({ ...QUICKSTART_CONFIG, overwrite: true }) 89 | exists.mockReturnValueOnce(true) 90 | await promptUser('quickstart') 91 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, QUICKSTART_ANSWERS()) 92 | expect(prompt).toHaveBeenCalledWith([NETWORK_CONFIRM], QUICKSTART_CONFIG) 93 | expect(prompt).not.toHaveBeenCalledWith([NETWORK_NAME], QUICKSTART_CONFIG) 94 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(0) 95 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(0) 96 | }) 97 | 98 | it('quickstart, do not overwrite network, change name', async () => { 99 | prompt.mockResolvedValueOnce(QUICKSTART_CONFIG) 100 | prompt.mockResolvedValueOnce({ ...QUICKSTART_CONFIG, overwrite: false }) 101 | prompt.mockResolvedValueOnce({ ...QUICKSTART_CONFIG, name: 'newname' }) 102 | exists.mockReturnValueOnce(true) 103 | await promptUser('quickstart') 104 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, QUICKSTART_ANSWERS()) 105 | expect(prompt).toHaveBeenCalledWith([NETWORK_CONFIRM], QUICKSTART_CONFIG) 106 | expect(prompt).toHaveBeenCalledWith([NETWORK_NAME], QUICKSTART_CONFIG) 107 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(0) 108 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(0) 109 | }) 110 | 111 | it('simple', async () => { 112 | prompt.mockResolvedValue(SIMPLE_CONFIG) 113 | await promptUser('simple') 114 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, SIMPLE_ANSWERS) 115 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(0) 116 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(0) 117 | }) 118 | 119 | it('customize, but not ports', async () => { 120 | prompt.mockResolvedValue({ 121 | ...CUSTOM_CONFIG, 122 | customizePorts: false, 123 | }) 124 | await promptUser('custom') 125 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, CUSTOM_ANSWERS) 126 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(0) 127 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(0) 128 | }) 129 | it('customize, bash ports', async () => { 130 | prompt.mockResolvedValue({ 131 | ...CUSTOM_CONFIG, 132 | tools: ['cakeshop'], 133 | consensus: 'raft', 134 | }) 135 | await promptUser('custom') 136 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, CUSTOM_ANSWERS) 137 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(1) 138 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(0) 139 | }) 140 | it('customize, docker ports', async () => { 141 | prompt.mockResolvedValue({ 142 | ...CUSTOM_CONFIG, 143 | deployment: 'docker-compose', 144 | }) 145 | await promptUser('custom') 146 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, CUSTOM_ANSWERS) 147 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(0) 148 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(1) 149 | }) 150 | it('customize, kubernetes ports', async () => { 151 | prompt.mockResolvedValue({ 152 | ...CUSTOM_CONFIG, 153 | deployment: 'kubernetes', 154 | }) 155 | await promptUser('custom') 156 | expect(prompt).toHaveBeenCalledWith(QUESTIONS, CUSTOM_ANSWERS) 157 | expect(getCustomizedBashNodes).toHaveBeenCalledTimes(0) 158 | expect(getCustomizedDockerPorts).toHaveBeenCalledTimes(0) 159 | }) 160 | it('regenerate', async () => { 161 | prompt.mockResolvedValue(GENERATE_ANSWERS) 162 | await promptGenerate() 163 | expect(prompt).toHaveBeenCalledWith(GENERATE_QUESTIONS) 164 | }) 165 | it('regenerate - user input config file', async () => { 166 | prompt.mockResolvedValue(GENERATE_CUSTOM_CONFIG_LOCATION_ANSWERS) 167 | await promptGenerate() 168 | expect(prompt).toHaveBeenCalledWith(GENERATE_QUESTIONS) 169 | }) 170 | it('regenerate - no name config file', async () => { 171 | prompt.mockResolvedValue(GENERATE_NO_NAME_ANSWERS) 172 | await promptGenerate() 173 | expect(prompt).toHaveBeenCalledWith(GENERATE_QUESTIONS) 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /src/questions/validators.js: -------------------------------------------------------------------------------- 1 | import { isJava11Plus } from '../utils/execUtils' 2 | import { getFullNetworkPath } from '../generators/networkHelper' 3 | import { isBash } from '../model/NetworkConfig' 4 | 5 | export function validateNumberStringInRange(input, low, high) { 6 | const number = parseInt(input, 10) 7 | if (number >= low && number <= high) { 8 | return true 9 | } 10 | return `Number must be between ${low} and ${high} (inclusive)` 11 | } 12 | 13 | export function validateNetworkId(input) { 14 | const parsedNumber = parseInt(input, 10) 15 | if (Number.isNaN(parsedNumber)) { 16 | return 'Network ID must be a number' 17 | } 18 | if (parsedNumber === 1) { 19 | return 'Ethereum Mainnet has a network id of 1. Please choose another id' 20 | } 21 | if (parsedNumber < 0) { 22 | return 'Network ID must be positive' 23 | } 24 | return true 25 | } 26 | 27 | export function disableIfWrongJavaVersion({ type }) { 28 | if (type === 'jar' && !isJava11Plus()) { 29 | return 'Disabled, requires Java 11+' 30 | } 31 | return false 32 | } 33 | 34 | export function validatePathLength(name, deployment) { 35 | const trimmed = name.trim() 36 | if (trimmed === '') { 37 | return 'Network name must not be blank.' 38 | } 39 | const fullPath = getFullNetworkPath({ network: { name: trimmed } }) 40 | if (isBash(deployment) && fullPath.length > 88) { 41 | // the max path length for unix sockets is 104-108 characters, depending on the os 42 | // 88 characters plus /qdata/c1/tm.ipc gets us to 104 43 | return `The full path to your network folder is ${fullPath.length - 88} character(s) too long. Please choose a shorter name or re-run the wizard in a different folder with a shorter path.` 44 | } 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /src/questions/validators.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | disableIfWrongJavaVersion, 3 | validateNetworkId, 4 | validateNumberStringInRange, 5 | validatePathLength, 6 | } from './validators' 7 | import { isJava11Plus } from '../utils/execUtils' 8 | import { getOutputPath } from '../utils/fileUtils' 9 | import { TEST_CWD } from '../utils/testHelper' 10 | 11 | jest.mock('../utils/execUtils') 12 | jest.mock('../utils/fileUtils') 13 | 14 | getOutputPath.mockReturnValue(TEST_CWD) 15 | 16 | test('accepts answer bottom of range', () => { 17 | expect(validateNumberStringInRange('2', 2, 3)).toBe(true) 18 | }) 19 | 20 | test('accepts answer top of range', () => { 21 | expect(validateNumberStringInRange('3', 2, 3)).toBe(true) 22 | }) 23 | 24 | test('rejects answer outside of range', () => { 25 | expect(validateNumberStringInRange('1', 2, 3)) 26 | .toEqual('Number must be between 2 and 3 (inclusive)') 27 | }) 28 | 29 | test('rejects answer that is not a number string', () => { 30 | expect(validateNumberStringInRange('on', 2, 3)) 31 | .toEqual('Number must be between 2 and 3 (inclusive)') 32 | }) 33 | 34 | test('allows network id that is not 1', () => { 35 | expect(validateNetworkId('0')).toEqual(true) 36 | expect(validateNetworkId('2')).toEqual(true) 37 | expect(validateNetworkId('10')).toEqual(true) 38 | expect(validateNetworkId('19283847')).toEqual(true) 39 | }) 40 | 41 | test('rejects network id of 1', () => { 42 | expect(validateNetworkId('1')) 43 | .toEqual('Ethereum Mainnet has a network id of 1. Please choose another id') 44 | }) 45 | 46 | test('rejects network id of less than 0', () => { 47 | expect(validateNetworkId('-1')).toEqual('Network ID must be positive') 48 | }) 49 | 50 | test('rejects network id that is not a number', () => { 51 | expect(validateNetworkId('n')).toEqual('Network ID must be a number') 52 | }) 53 | 54 | test('Disables java choices based on java version', () => { 55 | isJava11Plus.mockReturnValue(true) 56 | expect(disableIfWrongJavaVersion({ type: 'jar' })).toEqual(false) 57 | isJava11Plus.mockReturnValue(false) 58 | expect(disableIfWrongJavaVersion({ type: 'jar' })).toEqual('Disabled, requires Java 11+') 59 | }) 60 | 61 | test('Validates network name and path length', () => { 62 | expect(validatePathLength('', 'bash')).toEqual('Network name must not be blank.') 63 | expect(validatePathLength('valid_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bash')) 64 | .toEqual(true) 65 | expect(validatePathLength('invalid_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bash')) 66 | .toEqual('The full path to your network folder is 1 character(s) too long. Please choose a shorter name or re-run the wizard in a different folder with a shorter path.') 67 | expect(validatePathLength('invalid_but_is_docker_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'docker-compose')) 68 | .toEqual(true) 69 | }) 70 | -------------------------------------------------------------------------------- /src/utils/accountHelper.js: -------------------------------------------------------------------------------- 1 | import { Wallet } from 'ethers' 2 | 3 | export default function nodekeyToAccount(nodekey) { 4 | return new Wallet(nodekey).address 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/accountHelper.test.js: -------------------------------------------------------------------------------- 1 | import nodekeyToAccount from './accountHelper' 2 | 3 | test('converts node key to ethereum address', () => { 4 | expect(nodekeyToAccount('0x1be3b50b31734be48452c29d714941ba165ef0cbf3ccea8ca16c45e3d8d45fb0')) 5 | .toEqual('0xd8Dba507e85F116b1f7e231cA8525fC9008A6966') 6 | }) 7 | -------------------------------------------------------------------------------- /src/utils/execUtils.js: -------------------------------------------------------------------------------- 1 | import { exec, execSync } from 'child_process' 2 | import isWsl from 'is-wsl' 3 | 4 | export function executeSync(command, options) { 5 | return execSync(command, options) 6 | } 7 | 8 | export async function execute(command, options) { 9 | return new Promise((resolve, reject) => { 10 | exec(command, options, (e, stdout, stderr) => { 11 | if (e) { 12 | reject(e, stderr) 13 | } else { 14 | resolve(stdout) 15 | } 16 | }) 17 | }) 18 | } 19 | 20 | let JAVA_VERSION 21 | 22 | export function runJavaVersionLookup() { 23 | let versionText 24 | try { 25 | versionText = executeSync('java -version 2>&1') 26 | .toString() 27 | .trim() 28 | } catch (e) { 29 | // java probably doesn't exist 30 | return 0 31 | } 32 | const versionMatch = versionText 33 | .match(/[0-9]+\.?[0-9]+?/) 34 | 35 | if (versionMatch === null) { 36 | // java exists but output is unrecognized 37 | throw new Error(`Could not read Java version number in output: ${versionText}`) 38 | } 39 | 40 | const version = versionMatch[0] 41 | 42 | if (version === '1.8') { 43 | return 8 44 | } 45 | return parseInt(version, 10) 46 | } 47 | 48 | export function getJavaVersion() { 49 | if (!JAVA_VERSION) { 50 | JAVA_VERSION = runJavaVersionLookup() 51 | } 52 | return JAVA_VERSION 53 | } 54 | 55 | export function isJava11Plus() { 56 | return getJavaVersion() >= 11 57 | } 58 | 59 | export function isWindows() { 60 | return isWin32() || isWindowsSubsystemLinux() 61 | } 62 | 63 | export function isWin32() { 64 | return process.platform === 'win32' 65 | } 66 | 67 | export function isWindowsSubsystemLinux() { 68 | return isWsl 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/execUtils.test.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import { 3 | isJava11Plus, 4 | runJavaVersionLookup, 5 | } from './execUtils' 6 | 7 | jest.mock('child_process') 8 | 9 | const OpenJDK14 = `openjdk version "14.0.1" 2020-04-14 10 | OpenJDK Runtime Environment (build 14.0.1+14) 11 | OpenJDK 64-Bit Server VM (build 14.0.1+14, mixed mode, sharing) 12 | ` 13 | const OpenJDK11 = `openjdk version "11.0.7" 2020-04-14 14 | OpenJDK Runtime Environment (build 11.0.7+10) 15 | OpenJDK 64-Bit Server VM (build 11.0.7+10, mixed mode) 16 | ` 17 | 18 | const OpenJDK8 = `openjdk version "1.8.0_262" 19 | OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_262-b10) 20 | OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.262-b10, mixed mode) 21 | ` 22 | 23 | const OracleJDK8 = `java version "1.8.0_262" 24 | Java(TM) SE Runtime Environment (build 1.8.0_262-b15) 25 | Java HotSpot(TM) 64-Bit Server VM (build 25.262-b10, mixed mode) 26 | ` 27 | 28 | describe('Gets java versions', () => { 29 | it('parses java 1.8 correctly', () => { 30 | execSync.mockReturnValueOnce(OracleJDK8) 31 | expect(runJavaVersionLookup()).toEqual(8) 32 | }) 33 | it('parses openjdk 1.8 correctly', () => { 34 | execSync.mockReturnValueOnce(OpenJDK8) 35 | expect(runJavaVersionLookup()).toEqual(8) 36 | }) 37 | it('parses java 11.x correctly', () => { 38 | execSync.mockReturnValueOnce(OpenJDK11) 39 | expect(runJavaVersionLookup()).toEqual(11) 40 | }) 41 | it('parses java 14 (no decimal) correctly', () => { 42 | execSync.mockReturnValueOnce(OpenJDK14) 43 | expect(runJavaVersionLookup()).toEqual(14) 44 | }) 45 | it('sets java version to 0 when java -version throws an exception', () => { 46 | execSync.mockImplementation(() => { 47 | throw new Error('bash: ava: command not found') 48 | }) 49 | expect(runJavaVersionLookup()).toEqual(0) 50 | }) 51 | it('throws error on unrecognized version text', () => { 52 | execSync.mockReturnValueOnce('fail') 53 | expect(() => runJavaVersionLookup()).toThrow(new Error('Could not read Java version number in output: fail')) 54 | }) 55 | it('caches getJavaVersion', () => { 56 | execSync.mockReturnValueOnce('1.8') 57 | expect(isJava11Plus()).toEqual(false) 58 | execSync.mockReturnValueOnce('fail') 59 | expect(isJava11Plus()).toEqual(false) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/utils/fileUtils.js: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os' 2 | import { 3 | chmodSync, 4 | copyFileSync, 5 | copySync, 6 | readdirSync, 7 | existsSync, 8 | mkdirSync, 9 | readFileSync, 10 | removeSync, 11 | writeFileSync, 12 | } from 'fs-extra' 13 | import { resolve } from 'path' 14 | import { joinPath, verifyPathInsideDirectory } from './pathUtils' 15 | 16 | // this file is in $ROOT/build/utils/ and the bin, lib, and 7nodes folders are in $ROOT. 17 | // Go up two folders and cache that path for later use 18 | const libRoot = resolve(__dirname, '../..') 19 | 20 | let outputPath 21 | 22 | export function setOutputPath(path) { 23 | outputPath = path 24 | } 25 | 26 | export function getOutputPath() { 27 | return (outputPath !== undefined) ? outputPath : cwd() 28 | } 29 | 30 | export function cwd() { 31 | return process.cwd() 32 | } 33 | 34 | export function libRootDir() { 35 | return libRoot 36 | } 37 | 38 | export function wizardHomeDir() { 39 | return joinPath(homedir(), '.quorum-wizard') 40 | } 41 | 42 | export function exists(path) { 43 | return existsSync(path) 44 | } 45 | 46 | export function writeJsonFile(folder, filename, object, space = 2) { 47 | writeFileSync(joinPath(folder, filename), JSON.stringify(object, null, space)) 48 | } 49 | 50 | export function readJsonFile(file) { 51 | return JSON.parse(readFileSync(file, 'utf8')) 52 | } 53 | 54 | export function writeFile(filePath, contents, executable = false) { 55 | writeFileSync(filePath, contents) 56 | if (executable) { 57 | chmodSync(filePath, '755') 58 | } 59 | } 60 | 61 | export function writeScript(networkPath, config, script) { 62 | writeFile( 63 | joinPath(networkPath, script.filename), 64 | script.generate(config), 65 | script.executable, 66 | ) 67 | } 68 | 69 | export function removeFolder(networkPath = '') { 70 | verifyPathInsideDirectory(getOutputPath(), networkPath) 71 | 72 | if (exists(networkPath)) { 73 | removeSync(networkPath) 74 | } 75 | } 76 | 77 | export function createFolder(path, recursive = false) { 78 | mkdirSync(path, { recursive }) 79 | } 80 | 81 | export function copyScript(src, dest) { 82 | copyFile(src, dest) 83 | chmodSync(dest, '755') 84 | } 85 | 86 | export function copyFile(src, dest) { 87 | copyFileSync(src, dest) 88 | } 89 | 90 | export function copyDirectory(src, dest) { 91 | copySync(src, dest) 92 | } 93 | 94 | export function readFileToString(file) { 95 | return readFileSync(file, 'utf8').trim() 96 | } 97 | 98 | export function formatNewLine(file) { 99 | return file !== '' ? `${file}\n` : file 100 | } 101 | 102 | export function readDir(dir) { 103 | return exists(dir) ? readdirSync(dir) : undefined 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/fileUtils.test.js: -------------------------------------------------------------------------------- 1 | import { join, normalize } from 'path' 2 | import { existsSync, removeSync } from 'fs-extra' 3 | import { removeFolder, wizardHomeDir } from './fileUtils' 4 | import { joinPath, verifyPathInsideDirectory } from './pathUtils' 5 | 6 | jest.mock('fs-extra') 7 | jest.mock('os', () => ({ 8 | homedir: jest.fn(() => '/path/to/user/home'), 9 | // is-wsl uses os.release() during initialization, must be mocked here, not using .mockReturnValue 10 | release: jest.fn(() => '19.5.0'), 11 | platform: jest.fn(() => 'darwin'), 12 | })) 13 | jest.mock('./pathUtils') 14 | existsSync.mockReturnValue(true) 15 | 16 | describe('safely removes network folder', () => { 17 | it('does not try to remove when verification throws error', () => { 18 | const networkPath = '/fake/path' 19 | verifyPathInsideDirectory.mockImplementation(() => { 20 | throw new Error('some error') 21 | }) 22 | expect(() => removeFolder(networkPath)).toThrow(new Error('some error')) 23 | expect(removeSync).not.toHaveBeenCalled() 24 | }) 25 | it('does try to remove when verification succeeds', () => { 26 | verifyPathInsideDirectory.mockImplementation(() => { 27 | // do nothing 28 | }) 29 | const networkPath = '/fake/path' 30 | expect(() => removeFolder(networkPath)).not.toThrow(new Error('some error')) 31 | expect(removeSync).toHaveBeenCalledWith(networkPath) 32 | }) 33 | }) 34 | 35 | test('returns the quorum wizard dot folder inside the user\'s home folder', () => { 36 | joinPath.mockImplementation((...segments) => join(...segments)) 37 | expect(wizardHomeDir()).toEqual(normalize('/path/to/user/home/.quorum-wizard')) 38 | }) 39 | -------------------------------------------------------------------------------- /src/utils/log.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | 3 | let logger 4 | 5 | export function createLogger(verbose) { 6 | let logLevel 7 | if (verbose) { 8 | logLevel = 'debug' 9 | } else { 10 | logLevel = process.env.NODE_ENV === 'test' ? 'error' : 'info' 11 | } 12 | logger = winston.createLogger({ 13 | level: logLevel, 14 | transports: [ 15 | new winston.transports.Console({ 16 | format: winston.format.combine( 17 | winston.format.colorize(), 18 | winston.format.printf( 19 | (msg) => { 20 | if (msg.level.indexOf('info') >= 0) { 21 | // no log level label for info messages 22 | return `${msg.message}` 23 | } 24 | return `${msg.level}: ${msg.message}` 25 | }, 26 | ), 27 | ), 28 | handleExceptions: true, 29 | }), 30 | ], 31 | }) 32 | } 33 | 34 | export function info(message) { 35 | logger.info(message) 36 | } 37 | 38 | export function debug(message) { 39 | logger.debug(message) 40 | } 41 | 42 | export function error(message, e = '') { 43 | logger.error(`${message}\n${e}`) 44 | } 45 | 46 | // call directly into the winston log method in case you need to do something complicated 47 | export function log(...args) { 48 | logger.log(...args) 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/pathUtils.js: -------------------------------------------------------------------------------- 1 | import { join, parse, posix } from 'path' 2 | import { isWin32 } from './execUtils' 3 | 4 | // C:\\ on windows, for example 5 | const ROOT = isWin32() ? parse(process.cwd()).root : '/' 6 | 7 | export function joinPath(root, ...segments) { 8 | const path = join(root, ...segments) 9 | verifyPathInsideDirectory(root, path) 10 | return path 11 | } 12 | 13 | export function verifyPathInsideDirectory(root, path) { 14 | if (path === '' 15 | || path === ROOT 16 | || removeTrailingSlash(root) === removeTrailingSlash(path) 17 | || path.indexOf(root) !== 0) { 18 | throw new Error('Path was outside of working directory') 19 | } 20 | } 21 | 22 | export function removeTrailingSlash(path) { 23 | return path.replace(/\/$/, '').replace(/\\$/, '') 24 | } 25 | 26 | export function wrapScript(script) { 27 | // `./start.sh` in shell, just `start.cmd` in windows 28 | return isWin32() ? script : `./${script}` 29 | } 30 | 31 | // convert C:\folder\ to /c/folder/ for docker on windows 32 | export function unixifyPath(path) { 33 | if (!isWin32()) { 34 | return path 35 | } 36 | const unixifiedPath = path.replace(/\\/g, '/') 37 | const matcher = unixifiedPath.match(/^(\w):(.*)$/) 38 | if (matcher) { 39 | return posix.join('/', matcher[1].toLowerCase(), matcher[2]) 40 | } 41 | return unixifiedPath 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/pathUtils.test.js: -------------------------------------------------------------------------------- 1 | import { join, normalize } from 'path' 2 | import { 3 | joinPath, removeTrailingSlash, unixifyPath, verifyPathInsideDirectory, 4 | } from './pathUtils' 5 | import { cwd } from './fileUtils' 6 | import { isWin32 } from './execUtils' 7 | 8 | jest.mock('./execUtils') 9 | isWin32.mockReturnValue(false) 10 | 11 | describe('joins paths safely', () => { 12 | it('joins regular paths', () => { 13 | expect(joinPath(normalize('/home/test'), 'path', 'to', 'something')).toEqual(normalize('/home/test/path/to/something')) 14 | }) 15 | it('joins relative paths inside root', () => { 16 | expect(joinPath(normalize('/home/test'), 'path', 'to', 'something', '../somethingelse')).toEqual(normalize('/home/test/path/to/somethingelse')) 17 | }) 18 | it('does not join paths outside of root directory', () => { 19 | expect(() => joinPath(normalize('/home/test'), '../path', 'to', 'something')).toThrow(new Error('Path was outside of working directory')) 20 | }) 21 | it('does not join paths that are equal to the root directory', () => { 22 | expect(() => joinPath(normalize('/home/test'), '.')).toThrow(new Error('Path was outside of working directory')) 23 | }) 24 | it('does not join paths that are equal to the root directory with a trailing slash', () => { 25 | expect(() => joinPath(normalize('/home/test'), '/./')).toThrow(new Error('Path was outside of working directory')) 26 | }) 27 | }) 28 | 29 | describe('verifies that path is inside of specified root folder', () => { 30 | it('does not allow empty folder', () => { 31 | expect(() => verifyPathInsideDirectory(cwd(), '')).toThrow(new Error('Path was outside of working directory')) 32 | }) 33 | it('does not allow root path', () => { 34 | expect(() => verifyPathInsideDirectory(cwd(), '/')).toThrow(new Error('Path was outside of working directory')) 35 | }) 36 | it('does not allow path outside cwd', () => { 37 | const oneFolderUp = join(cwd(), '..') 38 | expect(() => verifyPathInsideDirectory(cwd(), oneFolderUp)).toThrow(new Error('Path was outside of working directory')) 39 | }) 40 | it('does not allow path that is exactly cwd', () => { 41 | const path = join(cwd(), '') 42 | expect(() => verifyPathInsideDirectory(cwd(), path)).toThrow(new Error('Path was outside of working directory')) 43 | }) 44 | it('does not allow path that is exactly cwd with a trailing slash', () => { 45 | const path = join(cwd(), '/') 46 | expect(() => verifyPathInsideDirectory(cwd(), path)).toThrow(new Error('Path was outside of working directory')) 47 | }) 48 | it('allows path inside cwd', () => { 49 | const path = join(cwd(), 'test') 50 | expect(() => verifyPathInsideDirectory(cwd(), path)).not.toThrow(new Error('Path was outside of working directory')) 51 | }) 52 | }) 53 | 54 | it('removes trailing slash from path strings', () => { 55 | expect(removeTrailingSlash(normalize('/home/test/'))).toEqual(normalize('/home/test')) 56 | expect(removeTrailingSlash(normalize('/home/test'))).toEqual(normalize('/home/test')) 57 | }) 58 | 59 | it('does not change paths on non-windows os', () => { 60 | isWin32.mockReturnValue(false) 61 | expect(unixifyPath('/home/test')).toEqual(normalize('/home/test')) 62 | // backslash is a valid path character on unix machines 63 | expect(unixifyPath('/ho\\me/test')).toEqual(normalize('/ho\\me/test')) 64 | }) 65 | 66 | it('switches windows paths from C:\\test\\ to /c/test/', () => { 67 | isWin32.mockReturnValue(true) 68 | expect(unixifyPath('/home/test')).toEqual(normalize('/home/test')) 69 | expect(unixifyPath('C:\\home\\test')).toEqual(normalize('/c/home/test')) 70 | // keep relative paths but convert backslashes 71 | expect(unixifyPath('home\\test')).toEqual(normalize('home/test')) 72 | }) 73 | -------------------------------------------------------------------------------- /src/utils/subnetUtils.js: -------------------------------------------------------------------------------- 1 | const ipaddress = require('ip-address') 2 | const { BigInteger } = require('jsbn') 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export function cidrsubnet(cidr, newBits, netNum) { 6 | const ipv4 = new ipaddress.Address4(cidr) 7 | 8 | const newMask = ipv4.subnetMask + newBits 9 | 10 | if (newMask > 32) { 11 | throw new Error(`Requested ${newBits} new bits, but only ${32 - ipv4.subnetMask} are available.`) 12 | } 13 | 14 | const maxNetNum = 1 << newBits // eslint-disable-line no-bitwise 15 | 16 | if (netNum > maxNetNum) { 17 | throw new Error(`prefix extension of ${newBits} does not accommodate a subnet numbered ${netNum}`) 18 | } 19 | 20 | const addrBig = ipv4.bigInteger() 21 | const netNumBig = new BigInteger(netNum.toString()) 22 | const addToIp = netNumBig << (32 - newMask) // eslint-disable-line no-bitwise 23 | const newAddrBig = addrBig.add(new BigInteger(addToIp.toString())) 24 | const newAddr = ipaddress.Address4.fromBigInteger(newAddrBig).address 25 | return `${newAddr}/${newMask}` 26 | } 27 | 28 | export function cidrhost(cidr, hostNum) { 29 | const ipv4 = new ipaddress.Address4(cidr) 30 | const hostNumBig = new BigInteger(hostNum.toString()) 31 | 32 | const parentLen = ipv4.subnetMask 33 | const hostLen = 32 - parentLen 34 | 35 | // calculate max host num 36 | let maxHostNum = new BigInteger('1') 37 | maxHostNum <<= new BigInteger(hostLen.toString()) // eslint-disable-line no-bitwise 38 | maxHostNum -= new BigInteger('1') 39 | 40 | if (hostNum > maxHostNum) { 41 | throw new Error(`prefix of ${parentLen} does not accommodate a host numbered ${hostNum}`) 42 | } 43 | const startingAddrBig = ipv4.startAddress().bigInteger() 44 | const newAddrBig = startingAddrBig.add(hostNumBig) 45 | 46 | return ipaddress.Address4.fromBigInteger(newAddrBig).address 47 | } 48 | 49 | export function getRandomInt(min, max) { 50 | // The maximum is exclusive and the minimum is inclusive 51 | const minCeil = Math.ceil(min) 52 | const maxFloor = Math.floor(max) 53 | return Math.floor(Math.random() * (maxFloor - minCeil)) + minCeil 54 | } 55 | 56 | export function getDockerSubnet() { 57 | // min=1, max=254 58 | const randomNetNum = getRandomInt(1, 255) 59 | return cidrsubnet('172.16.0.0/16', 8, randomNetNum) 60 | } 61 | 62 | export function buildDockerIp(ip, end) { 63 | const ipArray = ip.split('.').slice(0, 3) 64 | ipArray.push(end) 65 | const dockerIp = ipArray.join('.') 66 | return dockerIp 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/subnetUtils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | buildDockerIp, 3 | cidrsubnet, 4 | cidrhost, 5 | } from './subnetUtils' 6 | 7 | describe('build docker ip', () => { 8 | it('build docker ip from docker subnet', () => { 9 | expect(buildDockerIp('172.16.239.0/24', '10')).toEqual('172.16.239.10') 10 | expect(buildDockerIp('172.16.239.0/24', '1')).toEqual('172.16.239.1') 11 | }) 12 | }) 13 | 14 | describe('test cidrsubnet to function like terraform', () => { 15 | it('cidrsubnet error - new mask too large', () => { 16 | expect(() => cidrsubnet('172.16.0.0/24', 16, 15)) 17 | .toThrow(new Error('Requested 16 new bits, but only 8 are available.')) 18 | }) 19 | it('cidrsubnet error - prefix extension not large enough', () => { 20 | expect(() => cidrsubnet('172.16.0.0/24', 8, 520)) 21 | .toThrow(new Error('prefix extension of 8 does not accommodate a subnet numbered 520')) 22 | }) 23 | it('cidrsubnet success', () => { 24 | expect(cidrsubnet('172.16.0.0/16', 8, 15)).toEqual('172.16.15.0/24') 25 | }) 26 | }) 27 | 28 | describe('test cidrhost to function like terraform', () => { 29 | it('cidrhost error - ', () => { 30 | expect(() => cidrhost('172.16.15.0/24', 256)) 31 | .toThrow(new Error('prefix of 24 does not accommodate a host numbered 256')) 32 | }) 33 | it('cidrhost success', () => { 34 | expect(cidrhost('172.16.15.0/24', 0)).toEqual('172.16.15.0') 35 | expect(cidrhost('172.16.15.0/24', 8)).toEqual('172.16.15.8') 36 | expect(cidrhost('172.16.15.0/24', 255)).toEqual('172.16.15.255') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/utils/testHelper.js: -------------------------------------------------------------------------------- 1 | import { normalize } from 'path' 2 | import { getOutputPath, libRootDir } from './fileUtils' 3 | import { joinPath } from './pathUtils' 4 | 5 | export const TEST_CWD = '/current/working/dir' 6 | export const TEST_LIB_ROOT_DIR = '/npm/global/module/dir' 7 | export const TEST_WIZARD_HOME_DIR = '/path/to/user/home/.quorum-wizard' 8 | 9 | export function overrideProcessValue(key, value) { 10 | // process.platform is read-only, use this workaround to set it 11 | Object.defineProperty(process, key, { value }) 12 | } 13 | 14 | export function createNetPath(config, ...relativePaths) { 15 | return joinPath(normalize(getOutputPath()), 'network', config.network.name, ...relativePaths) 16 | } 17 | 18 | export function createLibPath(...relativePaths) { 19 | return joinPath(normalize(libRootDir()), ...relativePaths) 20 | } 21 | 22 | export function createConfigPath(...relativePaths) { 23 | return joinPath(normalize(getOutputPath()), 'configs', ...relativePaths) 24 | } 25 | --------------------------------------------------------------------------------