├── .dockerignore ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── push.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── app.js ├── app.json ├── app ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── babel.config.js ├── dist │ ├── 3rdpartylicenses.txt │ ├── assets │ │ ├── browserconfig.xml │ │ ├── favicon-114.png │ │ ├── favicon-120.png │ │ ├── favicon-144.png │ │ ├── favicon-150.png │ │ ├── favicon-152.png │ │ ├── favicon-16.png │ │ ├── favicon-160.png │ │ ├── favicon-180.png │ │ ├── favicon-192.png │ │ ├── favicon-310.png │ │ ├── favicon-32.png │ │ ├── favicon-57.png │ │ ├── favicon-60.png │ │ ├── favicon-64.png │ │ ├── favicon-70.png │ │ ├── favicon-72.png │ │ ├── favicon-76.png │ │ ├── favicon-96.png │ │ ├── favicon.ico │ │ └── fox.svg │ ├── index.html │ ├── main-es2015.bcb27b1605de99dc7931.js │ ├── main-es5.bcb27b1605de99dc7931.js │ ├── polyfills-es2015.021d89b697d9a813f680.js │ ├── polyfills-es5.6f153d70f1503b243296.js │ ├── runtime-es2015.409e6590615fb48d139f.js │ ├── runtime-es5.409e6590615fb48d139f.js │ └── styles.822b7d56b25de77cdcbe.css ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src │ ├── .browserslistrc │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── blocks-won-list │ │ │ ├── blocks-won-list.component.html │ │ │ ├── blocks-won-list.component.scss │ │ │ └── blocks-won-list.component.ts │ │ ├── local-storage.service.ts │ │ ├── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ └── login.component.ts │ │ ├── main │ │ │ ├── main.component.html │ │ │ ├── main.component.scss │ │ │ └── main.component.ts │ │ ├── menu │ │ │ ├── menu.component.html │ │ │ ├── menu.component.scss │ │ │ └── menu.component.ts │ │ ├── net-diff-chart │ │ │ ├── net-diff-chart.component.html │ │ │ ├── net-diff-chart.component.scss │ │ │ └── net-diff-chart.component.ts │ │ ├── new-version-snackbar │ │ │ ├── new-version-snackbar.component.html │ │ │ ├── new-version-snackbar.component.scss │ │ │ └── new-version-snackbar.component.ts │ │ ├── proxy-info │ │ │ ├── proxy-info.component.html │ │ │ ├── proxy-info.component.scss │ │ │ └── proxy-info.component.ts │ │ ├── proxy │ │ │ ├── proxy.component.html │ │ │ ├── proxy.component.scss │ │ │ └── proxy.component.ts │ │ ├── round-stats │ │ │ ├── round-stats.component.html │ │ │ ├── round-stats.component.scss │ │ │ └── round-stats.component.ts │ │ ├── settings │ │ │ ├── settings.component.html │ │ │ ├── settings.component.scss │ │ │ └── settings.component.ts │ │ ├── stats.service.spec.ts │ │ ├── stats.service.ts │ │ ├── upstream-info │ │ │ ├── upstream-info.component.html │ │ │ ├── upstream-info.component.scss │ │ │ └── upstream-info.component.ts │ │ ├── upstream │ │ │ ├── upstream.component.html │ │ │ ├── upstream.component.scss │ │ │ └── upstream.component.ts │ │ ├── websocket.service.spec.ts │ │ └── websocket.service.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── browserconfig.xml │ │ ├── favicon-114.png │ │ ├── favicon-120.png │ │ ├── favicon-144.png │ │ ├── favicon-150.png │ │ ├── favicon-152.png │ │ ├── favicon-16.png │ │ ├── favicon-160.png │ │ ├── favicon-180.png │ │ ├── favicon-192.png │ │ ├── favicon-310.png │ │ ├── favicon-32.png │ │ ├── favicon-57.png │ │ ├── favicon-60.png │ │ ├── favicon-64.png │ │ ├── favicon-70.png │ │ ├── favicon-72.png │ │ ├── favicon-76.png │ │ ├── favicon-96.png │ │ ├── favicon.ico │ │ └── fox.svg │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── tsconfig.app.json │ └── tslint.json ├── tsconfig.base.json ├── tsconfig.json └── tslint.json ├── ecosystem.config.js.dist ├── lib ├── cli-dashboard.js ├── coin-util.js ├── config.js ├── currentRound.js ├── currentRoundManager.js ├── miningInfo.js ├── output-util.js ├── processing-queue.js ├── proxy.js ├── services │ ├── cache.js │ ├── coin-gecko.js │ ├── coin-paprika.js │ ├── event-bus.js │ ├── foxy-pool-gateway.js │ ├── latest-version-service.js │ ├── logger.js │ ├── mail-service.js │ ├── profitability-service.js │ ├── round-populator.js │ ├── self-update-service.js │ ├── store.js │ ├── submission-processor.js │ └── usage-statistics-service.js ├── startup-message.js ├── submission.js ├── transports │ ├── http-multiple-ports.js │ ├── http-single-port.js │ ├── http-transport-mixin.js │ ├── index.js │ └── socketio.js ├── upstream │ ├── base.js │ ├── foxy-pool-multi.js │ ├── foxypool.js │ ├── generic.js │ ├── mixins │ │ ├── cli-color-mixin.js │ │ ├── config-mixin.js │ │ ├── connection-quality-mixin.js │ │ ├── estimated-capacity-mixin.js │ │ ├── stats-mixin.js │ │ └── submit-probability-mixin.js │ ├── socketio.js │ └── util.js ├── util.js └── version.js ├── main.js ├── models ├── config.js ├── index.js ├── plotter.js └── round.js ├── package-lock.json ├── package.json └── shared └── capacity.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ for more info 2 | 3 | * @felixbrucker 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: felixbrucker 4 | custom: https://www.paypal.me/felixbrucker 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: felixbrucker 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **System (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - NodeJs Version [e.g. 10] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: felixbrucker 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Build and Publish 3 | jobs: 4 | auditAndPublish: 5 | name: Build and Publish 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Install dependencies 10 | uses: felixbrucker/npm@master 11 | with: 12 | args: ci 13 | - name: Install Web UI dependencies 14 | uses: felixbrucker/npm@master 15 | with: 16 | args: run install-web 17 | - name: Build Web UI 18 | uses: felixbrucker/npm@master 19 | with: 20 | args: run build-web 21 | - name: Publish to npm 22 | if: startsWith(github.ref, 'refs/tags/') 23 | uses: felixbrucker/npm@master 24 | env: 25 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 26 | with: 27 | args: publish --access public 28 | - name: Create Github Release 29 | if: startsWith(github.ref, 'refs/tags/') 30 | uses: felixbrucker/github-actions/publish-release@master 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | args: --name Foxy-Proxy 35 | - name: Post to Discord 36 | if: startsWith(github.ref, 'refs/tags/') 37 | uses: felixbrucker/github-actions/post-release-in-discord@master 38 | env: 39 | FOXY_DISCORD_WEBHOOK_ID: ${{ secrets.FOXY_DISCORD_WEBHOOK_ID }} 40 | FOXY_DISCORD_WEBHOOK_TOKEN: ${{ secrets.FOXY_DISCORD_WEBHOOK_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .idea 64 | config.yaml 65 | *.sqlite 66 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@felixbrucker.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | WORKDIR /app 3 | COPY . . 4 | RUN npm ci 5 | VOLUME ["/conf"] 6 | ENTRYPOINT ["node", "main"] 7 | CMD ["--config", "/conf/config.yaml", "--db", "/conf/db.sqlite", "--no-colors"] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Foxy-Proxy 4 | ====== 5 | 6 | [![Software License](https://img.shields.io/badge/license-GPL--3.0-brightgreen.svg?style=flat-square)](LICENSE) 7 | [![Discord](https://img.shields.io/discord/582180216747720723.svg?label=Discord&style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAN1wAADdcBQiibeAAABUlJREFUWMO911uMXWUVB/Dft89lOreWMrT0RgutabFWi1JoaxM1EIk2mjQUbeSpKCQqiVFjovbFxBijKMUXo5AgxUu8oWlQgmk0PghW6IPQcHGMrRULDNSZ6UzndDpzZu/lw57pTDO3qtSV7OyTvb5v/f/rv9a39j4JYt9uuBl34+s4giJ99RfeCIvdq6lUM82RrSK+QPo2cSgdfEUaB4fP4Rt4CQ/gQfTAf0skPryWxgD11hWiuFPEXVglpc8r8nt0Xi6bsv7a8ftqfBmPYg/aYt9uU4jOD3zHO8SulYw129VablcUj4r4ElaNL3mzze+hOXKeQB3Lp8TIcAMewgFsRXYxJOLWqzjTV8F2Rf6wiAeJ68djTtgyLz5VV+Sq4w8qaJ8hXis+hHeNl+T+2Le7gfWkdZrnupx4PtMc6SUdk1K3iA5jox8XcQeWzswy2uRjVYxOEAjkcyR2Jb6IXRjBm4h2ZCKICKIh0jGiBRuQZg+XCikFzivQRO886iZsnMPXQWyet0bl6j7V+qgiP1+XHN0XtfmNsW6NgVylKptyxJ7E8P8BfJj0hFpd+unfLujMP41fl9jS07J0WCqhpxI4jf3ov4Top6W0Xz7Wr1afJDClDI/hO5cu+XS/1vZfq9Sknx2fpgDlQLrmEipwtdFz9akPMkwds9uw85LBR7xfnm9X5OJjN86owG1Y9D+izOVcSNzmilXJwGvTCKzATbPtbBTJnxsVI3PEH47M4dFOQ5HNvijiJr2vrJDn0wi8FWtny+mx/qr9r9b9qr+qGdNXjETmB2e77Btc6ZHhxXOpcI2It4miJBCf3ml8nm8RsaD8feGOhPWthYWVcLCv5plGZdqkf2K0ww/PXq4rG7Opem6mzCmCiBZii2ZTfHSzqj0fZM3KzJGjmwyeoX+A04OcGyk3pRLpurbcXUs5ejazvrWYRnJz7axbW0/bUR+ypd6YdBRBtcLCdpZcxpLFXNa5yY4tFSdezlM8ewA6tdQPSWmb5hhDDXpO8Y+TnOojz0lJoFC+u0FzhL8/V96VL5RsXDERtNRZs5z1q1lxBe2tVDPCU5rFLaTBqiwrCUR0oWS7eFF5rVvNyR6e/yu9/VJKk+Az2AW+Ncu5YSOrllKrlIpNlIEuKXVisFpKHO2mfpDEuL61GmtXs7SLZ17g+EuTvllZZLx9Aze+hbaWErCYtqdd0gFVRYF4Tco+JcXVUtqAzVK6FgtF0NHO1uuoVuk+Njt4SmzZyLZNpZKTwINCN46iWzghj56JBp9M/MgDjIwmCzsXqmSbpGyPLN0+LlnZmH94mpd7SrCpPRBR1vp926nXJpTqE34s/EThOSMxoCrSLfsvOGEzWjx7gKFGZvGij8iy76JDSvS8zu8PM9qcJDB6jrYF7Ho3K5dMZN4QPmE4fqSeinTzvTPizDqy0ua9dHYUmmMHRTx5vjeWdLFsyYW9EMFVy7iya1L28Ee5X84FPicBlMextaUh4hHlCSxru3zp+flQRkmsWVb6SivwczUNw8WcEHMSSO/8ZCl1Xjwu4oXxzDQXLfJ61LzazPwzrzmZtRldvMiU6fSiwuNy0gfum5NA1Xw2MsqSrpcNNb4npW9Ksh513+pZYHAonOpbpbXGV7I26yayDw9pSyediXnDZ/MtSNffyeAZ8vz7In4DedCfJ31jmd6ioreoTP6pCIcUHjYc0nvvnS/8/ATAwBmq1V5F8VkRv0WRkITyTpR1/53wGZl/zVf7/4hA2nE3g0NUKt3qlb2DQ+e+ljieUspTSkXixMDQ2D1aKntl/iKvSDvvu5jQ/g1HtBu+eMyiaAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wNC0zMFQxMjozNzoxMC0wNDowMH0cFvgAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDQtMzBUMTI6Mzc6MTAtMDQ6MDAMQa5EAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg==)](https://discord.gg/gNHhn9y) 8 | 9 | ## This software is considered legacy and not maintained anymore, please use [Foxy-Miner](https://github.com/felixbrucker/foxy-miner) instead! 10 | 11 | ## License 12 | 13 | GNU GPLv3 (see [LICENSE](https://github.com/felixbrucker/foxy-proxy/blob/master/LICENSE)) 14 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Foxy-Proxy", 3 | "description": "A BHD, LHD, DISC, BOOM and BURST proxy which supports solo and pool mining upstreams.", 4 | "logo": "https://raw.githubusercontent.com/felixbrucker/foxy-proxy/master/app/src/assets/favicon-310.png", 5 | "keywords": [ 6 | "burst", 7 | "bhd", 8 | "lhd", 9 | "boom", 10 | "disc", 11 | "proxy", 12 | "multi-chain", 13 | "collision-free", 14 | "web-ui", 15 | "foxyproxy", 16 | "foxy" 17 | ], 18 | "env": { 19 | "CONFIG": { 20 | "description": "The config which is usually stored as file as JSON string. You can use https://www.json2yaml.com to convert from yaml to JSON.", 21 | "value": "{}" 22 | } 23 | }, 24 | "addons": [ 25 | "heroku-postgresql" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /tmp 5 | /out-tsc 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # profiling files 11 | chrome-profiler-events.json 12 | speed-measure-plugin.json 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # misc 32 | /.sass-cache 33 | /connect.lock 34 | /coverage 35 | /libpeerconnection.log 36 | npm-debug.log 37 | yarn-error.log 38 | testem.log 39 | /typings 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # MyApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.2.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "aot": true, 21 | "outputPath": "dist", 22 | "index": "src/index.html", 23 | "main": "src/main.ts", 24 | "polyfills": "src/polyfills.ts", 25 | "tsConfig": "src/tsconfig.app.json", 26 | "assets": [ 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 31 | "src/styles.scss" 32 | ], 33 | "scripts": [], 34 | "allowedCommonJsDependencies": [ 35 | "socket.io-client", 36 | "socket.io-parser", 37 | "hash.js", 38 | "chart.js", 39 | "debug" 40 | ] 41 | }, 42 | "configurations": { 43 | "production": { 44 | "fileReplacements": [ 45 | { 46 | "replace": "src/environments/environment.ts", 47 | "with": "src/environments/environment.prod.ts" 48 | } 49 | ], 50 | "optimization": true, 51 | "outputHashing": "all", 52 | "sourceMap": false, 53 | "extractCss": true, 54 | "namedChunks": false, 55 | "aot": true, 56 | "extractLicenses": true, 57 | "vendorChunk": false, 58 | "buildOptimizer": true, 59 | "budgets": [ 60 | { 61 | "type": "initial", 62 | "maximumWarning": "2mb", 63 | "maximumError": "5mb" 64 | }, 65 | { 66 | "type": "anyComponentStyle", 67 | "maximumWarning": "6kb" 68 | } 69 | ] 70 | } 71 | } 72 | }, 73 | "serve": { 74 | "builder": "@angular-devkit/build-angular:dev-server", 75 | "options": { 76 | "browserTarget": "app:build" 77 | }, 78 | "configurations": { 79 | "production": { 80 | "browserTarget": "app:build:production" 81 | } 82 | } 83 | }, 84 | "extract-i18n": { 85 | "builder": "@angular-devkit/build-angular:extract-i18n", 86 | "options": { 87 | "browserTarget": "app:build" 88 | } 89 | }, 90 | "lint": { 91 | "builder": "@angular-devkit/build-angular:tslint", 92 | "options": { 93 | "tsConfig": [ 94 | "src/tsconfig.app.json", 95 | "src/tsconfig.spec.json" 96 | ], 97 | "exclude": [ 98 | "**/node_modules/**" 99 | ] 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "defaultProject": "app" 106 | } 107 | -------------------------------------------------------------------------------- /app/babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | "@babel/env", 4 | { 5 | targets: { 6 | edge: "17", 7 | firefox: "60", 8 | chrome: "67", 9 | safari: "11.1", 10 | ie: "11", 11 | }, 12 | useBuiltIns: "usage", 13 | corejs: '3.4.8', 14 | }, 15 | ], 16 | ]; 17 | 18 | module.exports = { presets }; 19 | -------------------------------------------------------------------------------- /app/dist/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #FFFFFF 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/dist/assets/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-114.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-120.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-144.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-150.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-152.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-16.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-160.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-180.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-192.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-310.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-32.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-57.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-60.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-64.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-70.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-72.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-76.png -------------------------------------------------------------------------------- /app/dist/assets/favicon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon-96.png -------------------------------------------------------------------------------- /app/dist/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/dist/assets/favicon.ico -------------------------------------------------------------------------------- /app/dist/assets/fox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 19 | 20 | 21 | 28 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Foxy-Proxy 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/dist/runtime-es2015.409e6590615fb48d139f.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],p=r[2],c=0,s=[];c 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /app/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import {MainComponent} from './main/main.component'; 4 | import {LoginComponent} from './login/login.component'; 5 | import {SettingsComponent} from './settings/settings.component'; 6 | 7 | const routes: Routes = [ 8 | { path: '', component: MainComponent, pathMatch: 'full' }, 9 | { path: 'login', component: LoginComponent, pathMatch: 'full' }, 10 | { path: 'settings', component: SettingsComponent, pathMatch: 'full' }, 11 | { path: '**', redirectTo: '' } 12 | ]; 13 | 14 | 15 | @NgModule({ 16 | imports: [RouterModule.forRoot(routes)], 17 | exports: [RouterModule] 18 | }) 19 | export class AppRoutingModule { } 20 | -------------------------------------------------------------------------------- /app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Made with favorite by felixbrucker 5 |
6 | -------------------------------------------------------------------------------- /app/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/app/app.component.scss -------------------------------------------------------------------------------- /app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | } 10 | -------------------------------------------------------------------------------- /app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { MainComponent } from './main/main.component'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | import { NetDiffChartComponent } from './net-diff-chart/net-diff-chart.component'; 9 | import { FlexLayoutModule } from '@angular/flex-layout'; 10 | import { RoundStatsComponent } from './round-stats/round-stats.component'; 11 | import { ProxyComponent } from './proxy/proxy.component'; 12 | import { UpstreamComponent } from './upstream/upstream.component'; 13 | import { UpstreamInfoComponent } from './upstream-info/upstream-info.component'; 14 | import { ProxyInfoComponent } from './proxy-info/proxy-info.component'; 15 | import { BlocksWonListComponent } from './blocks-won-list/blocks-won-list.component'; 16 | import { LoginComponent } from './login/login.component'; 17 | import { FormsModule } from '@angular/forms'; 18 | import { MenuComponent } from './menu/menu.component'; 19 | import { NewVersionSnackbarComponent } from './new-version-snackbar/new-version-snackbar.component'; 20 | import { SettingsComponent } from './settings/settings.component'; 21 | import {MatCardModule} from "@angular/material/card"; 22 | import {MatButtonModule} from "@angular/material/button"; 23 | import {MatIconModule} from "@angular/material/icon"; 24 | import {MatProgressBarModule} from "@angular/material/progress-bar"; 25 | import {MatToolbarModule} from "@angular/material/toolbar"; 26 | import {MatTooltipModule} from "@angular/material/tooltip"; 27 | import {MatTabsModule} from "@angular/material/tabs"; 28 | import {MatListModule} from "@angular/material/list"; 29 | import {MatFormFieldModule} from "@angular/material/form-field"; 30 | import {MatInputModule} from "@angular/material/input"; 31 | import {MatSnackBarModule} from "@angular/material/snack-bar"; 32 | import {MatMenuModule} from "@angular/material/menu"; 33 | import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; 34 | import {MatCheckboxModule} from "@angular/material/checkbox"; 35 | import {MatRadioModule} from "@angular/material/radio"; 36 | 37 | @NgModule({ 38 | declarations: [ 39 | AppComponent, 40 | MainComponent, 41 | NetDiffChartComponent, 42 | RoundStatsComponent, 43 | ProxyComponent, 44 | UpstreamComponent, 45 | UpstreamInfoComponent, 46 | ProxyInfoComponent, 47 | BlocksWonListComponent, 48 | LoginComponent, 49 | MenuComponent, 50 | NewVersionSnackbarComponent, 51 | SettingsComponent 52 | ], 53 | imports: [ 54 | BrowserModule, 55 | AppRoutingModule, 56 | BrowserAnimationsModule, 57 | MatCardModule, 58 | MatButtonModule, 59 | MatIconModule, 60 | MatProgressBarModule, 61 | MatToolbarModule, 62 | MatTooltipModule, 63 | MatTabsModule, 64 | MatListModule, 65 | MatFormFieldModule, 66 | MatInputModule, 67 | MatSnackBarModule, 68 | MatMenuModule, 69 | FormsModule, 70 | MatProgressSpinnerModule, 71 | FlexLayoutModule, 72 | MatCheckboxModule, 73 | MatRadioModule, 74 | ], 75 | providers: [], 76 | bootstrap: [AppComponent], 77 | }) 78 | export class AppModule { } 79 | -------------------------------------------------------------------------------- /app/src/app/blocks-won-list/blocks-won-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
7 | Latest Blocks won 8 |
9 |
10 | × 11 |
12 |
13 |
14 | 15 |
16 |
17 | BlockHeight 18 |
19 |
20 | Timestamp 21 |
22 |
23 | Deadline 24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 |
34 |
35 | {{round.blockHeight}} 36 | {{round.blockHeight}} 37 |
38 |
39 | {{getTimeDiff(round.createdAt)}} 40 |
41 |
42 | {{round.bestDL}} 43 |
44 |
45 |
46 | 47 |
48 | 49 | 50 |
51 | None 52 |
53 |
54 |
55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /app/src/app/blocks-won-list/blocks-won-list.component.scss: -------------------------------------------------------------------------------- 1 | .scalable { 2 | width: 260px; 3 | } 4 | 5 | @media (max-width: 684px) { 6 | .scalable { 7 | width: 320px; 8 | } 9 | } 10 | 11 | .scalable-item { 12 | width: 290px !important; 13 | } 14 | 15 | @media (max-width: 684px) { 16 | .scalable-item { 17 | width: 350px !important; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/app/blocks-won-list/blocks-won-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | import * as moment from 'moment'; 3 | import {LocalStorageService} from '../local-storage.service'; 4 | 5 | @Component({ 6 | selector: 'app-blocks-won-list', 7 | templateUrl: './blocks-won-list.component.html', 8 | styleUrls: ['./blocks-won-list.component.scss'] 9 | }) 10 | export class BlocksWonListComponent implements OnInit { 11 | 12 | @Input() historicalRounds: any; 13 | @Input() isBHD: boolean; 14 | @Input() upstreamFullName: string; 15 | @Input() coin: string; 16 | 17 | constructor(private localStorageService: LocalStorageService) { } 18 | 19 | ngOnInit() { 20 | if (this.coin === undefined) { 21 | this.coin = this.isBHD ? 'BHD' : 'BURST'; 22 | } 23 | } 24 | 25 | getLastFourBlockWins() { 26 | return this.historicalRounds 27 | .filter(round => round.roundWon) 28 | .reverse() 29 | .slice(0, 4); 30 | } 31 | 32 | getTimeDiff(date) { 33 | return moment.duration(moment(date).diff(moment())).humanize(true); 34 | } 35 | 36 | hideCard() { 37 | this.localStorageService.hideItem('blocks-won-list', this.upstreamFullName); 38 | } 39 | 40 | getBlockExplorerLink(height) { 41 | const coin = this.coin && this.coin.toUpperCase(); 42 | switch (coin) { 43 | case 'BHD': 44 | return `https://www.btchd.org/explorer/block/${height}`; 45 | case 'BURST': 46 | return `https://explorer.burstcoin.network/?action=block_inspect&height=${height}`; 47 | case 'BOOM': 48 | return `https://explorer.boomcoin.org/block/${height}`; 49 | case 'LHD': 50 | return `https://ltchd.io/explorer/block/${height}`; 51 | case 'HDD': 52 | return `https://www.hdd.cash/block.html?height=${height}`; 53 | case 'XHD': 54 | return `https://explorer.xrphd.org/block/${height}`; 55 | case 'LAVA': 56 | return `http://explorer.lavatech.org/block-height/${height}`; 57 | default: 58 | return null; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/app/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class LocalStorageService { 7 | 8 | constructor() {} 9 | 10 | hideItem(identifier, upstreamFullName = null) { 11 | this.setHideItem(identifier, upstreamFullName, true); 12 | } 13 | 14 | showItem(identifier, upstreamFullName = null) { 15 | this.setHideItem(identifier, upstreamFullName, false); 16 | } 17 | 18 | setHideItem(identifier, upstreamFullName = null, hide) { 19 | let str = 'hide'; 20 | if (upstreamFullName) { 21 | str += `/${upstreamFullName}`; 22 | } 23 | localStorage.setItem(`${str}/${identifier}`, hide.toString()); 24 | } 25 | 26 | setProxyHidden(name, hide) { 27 | this.setItem(`proxy/${name}/hide`, hide.toString()); 28 | } 29 | 30 | showProxy(name) { 31 | return this.getItem(`proxy/${name}/hide`) !== 'true'; 32 | } 33 | 34 | shouldShowItem(identifier, upstreamFullName = null) { 35 | if (upstreamFullName) { 36 | return localStorage.getItem(`hide/${identifier}`) !== 'true' && localStorage.getItem(`hide/${upstreamFullName}/${identifier}`) !== 'true'; 37 | } 38 | return localStorage.getItem(`hide/${identifier}`) !== 'true'; 39 | } 40 | 41 | getItem(key) { 42 | return localStorage.getItem(key); 43 | } 44 | 45 | setItem(key, value) { 46 | localStorage.setItem(key, value); 47 | } 48 | 49 | removeItem(key) { 50 | localStorage.removeItem(key); 51 | } 52 | 53 | clearHideItems() { 54 | const keysToRemove = []; 55 | for (let i = 0; i < localStorage.length; i++) { 56 | const key = localStorage.key(i); 57 | if (key.startsWith('hide')) { 58 | keysToRemove.push(key); 59 | } 60 | if (key.startsWith('layout')) { 61 | keysToRemove.push(key); 62 | } 63 | if (key.startsWith('proxy')) { 64 | keysToRemove.push(key); 65 | } 66 | } 67 | keysToRemove.forEach(key => localStorage.removeItem(key)); 68 | } 69 | 70 | getHiddenCards() { 71 | const hiddenCards = []; 72 | for (let i = 0; i < localStorage.length; i++) { 73 | const key = localStorage.key(i); 74 | if (key.startsWith('hide')) { 75 | hiddenCards.push(key); 76 | } 77 | } 78 | return hiddenCards; 79 | } 80 | 81 | getAuthData() { 82 | const auth = localStorage.getItem('auth'); 83 | if (!auth) { 84 | return null; 85 | } 86 | 87 | return JSON.parse(auth); 88 | } 89 | 90 | setAuthData(username, passHash) { 91 | localStorage.setItem('auth', JSON.stringify({ 92 | username, 93 | passHash, 94 | })); 95 | } 96 | 97 | clearAuthData() { 98 | localStorage.removeItem('auth'); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |  {{getTitle()}} 4 | 5 | 6 | favorite 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | Please login 15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | Username 23 | 24 |
25 |
26 | 27 | 28 | Enter your password 29 | {{hide ? 'visibility_off' : 'visibility'}} 30 | 31 |
32 | 38 | The username or password is invalid! 39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /app/src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .flex-container { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | } 6 | 7 | .placeholder { 8 | color: #cfd0d1; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {StatsService} from '../stats.service'; 3 | import {Router} from '@angular/router'; 4 | import {LocalStorageService} from '../local-storage.service'; 5 | import {sha256} from 'hash.js'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'] 11 | }) 12 | export class LoginComponent implements OnInit { 13 | 14 | username: ''; 15 | password: ''; 16 | hide = true; 17 | invalidAuth = false; 18 | private runningVersion: any; 19 | 20 | constructor( 21 | private statsService: StatsService, 22 | private localStorageService: LocalStorageService, 23 | private router: Router 24 | ) { } 25 | 26 | async ngOnInit() { 27 | this.updateRunningVersion(); 28 | this.statsService.getAuthenticatedObservable().subscribe(async authenticated => { 29 | if (!authenticated) { 30 | return; 31 | } 32 | await this.router.navigate(['/']); 33 | }); 34 | } 35 | 36 | async updateRunningVersion() { 37 | const versionInfo: any = await this.statsService.getVersionInfo(); 38 | this.runningVersion = versionInfo.runningVersion; 39 | } 40 | 41 | getTitle() { 42 | const versionAppend = this.runningVersion ? ` ${this.runningVersion}` : ''; 43 | return `Foxy-Proxy${versionAppend}`; 44 | } 45 | 46 | async login() { 47 | const passHash = await sha256().update(this.password).digest('hex'); 48 | const result = await this.statsService.authenticate(this.username, passHash); 49 | if (!result) { 50 | this.invalidAuth = true; 51 | return; 52 | } else { 53 | this.localStorageService.setAuthData(this.username, passHash); 54 | } 55 | this.statsService.init(); 56 | } 57 | 58 | clearInvalidAuth() { 59 | this.invalidAuth = false; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/app/main/main.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /app/src/app/main/main.component.scss: -------------------------------------------------------------------------------- 1 | .flex-container { 2 | display: flex; /* or inline-flex */ 3 | flex-direction: column; 4 | justify-content: center; 5 | } 6 | 7 | .larger-tooltip { 8 | font-size: 0.7em; 9 | } 10 | 11 | .custom-menu .mat-menu-content { 12 | background-color: #cccccc; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/app/main/main.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewEncapsulation} from '@angular/core'; 2 | import {StatsService} from '../stats.service'; 3 | import {NewVersionSnackbarComponent} from '../new-version-snackbar/new-version-snackbar.component'; 4 | import {LocalStorageService} from '../local-storage.service'; 5 | import {MatSnackBar} from "@angular/material/snack-bar"; 6 | 7 | @Component({ 8 | selector: 'app-main', 9 | templateUrl: './main.component.html', 10 | styleUrls: ['./main.component.scss'], 11 | encapsulation: ViewEncapsulation.None, 12 | }) 13 | export class MainComponent implements OnInit { 14 | 15 | private stats = []; 16 | private _currentProxy: any; 17 | private latestVersion = null; 18 | private runningVersion = null; 19 | 20 | constructor( 21 | private statsService: StatsService, 22 | private localStorageService: LocalStorageService, 23 | private snackBar: MatSnackBar, 24 | ) { } 25 | 26 | async ngOnInit() { 27 | this.statsService.getStatsObservable().subscribe((stats => { 28 | const proxies = stats.filter(proxy => this.showProxy(proxy)); 29 | if (proxies.length > 0) { 30 | let selectProxy = proxies[0]; 31 | if (this.currentProxy) { 32 | const foundProxy = proxies.find(proxy => proxy.name === this.currentProxy.name); 33 | if (foundProxy) { 34 | selectProxy = foundProxy; 35 | } 36 | } 37 | this.currentProxy = selectProxy; 38 | } 39 | this.stats = proxies; 40 | this.detectVersionUpdate(); 41 | })); 42 | setInterval(this.detectVersionUpdate.bind(this), 10 * 60 * 1000); 43 | } 44 | 45 | async detectVersionUpdate() { 46 | const versionInfo: any = await this.statsService.getVersionInfo(); 47 | this.runningVersion = versionInfo.runningVersion; 48 | if (this.latestVersion === versionInfo.latestVersion) { 49 | return; 50 | } 51 | this.latestVersion = versionInfo.latestVersion; 52 | if (versionInfo.latestVersion === versionInfo.runningVersion) { 53 | return; 54 | } 55 | const snackBarRef = this.snackBar.openFromComponent(NewVersionSnackbarComponent, { 56 | verticalPosition: 'top', 57 | horizontalPosition: 'right', 58 | data: versionInfo, 59 | panelClass: 'mat-simple-snackbar', 60 | }); 61 | snackBarRef.onAction().subscribe(() => { 62 | this.statsService.updateProxy(); 63 | this.snackBar.open('Updating the proxy ..', '', { 64 | verticalPosition: 'top', 65 | horizontalPosition: 'right', 66 | }); 67 | }); 68 | } 69 | 70 | getStats() { 71 | return this.stats; 72 | } 73 | 74 | get currentProxy() { 75 | return this._currentProxy; 76 | } 77 | 78 | set currentProxy(proxy: any) { 79 | this._currentProxy = proxy; 80 | } 81 | 82 | showProxy(proxy) { 83 | return this.localStorageService.showProxy(proxy.name); 84 | } 85 | 86 | get layout() { 87 | return this.localStorageService.getItem('layout') || 'Default'; 88 | } 89 | 90 | getRunningVersion() { 91 | return this.runningVersion; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/app/menu/menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{getTitle()}} 4 | 5 | 6 | | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 22 | 23 | 27 | 31 | 32 | 36 | 37 |
38 | info 39 | Version {{getRunningVersion()}} 40 |
41 |
42 | 43 | favorite 44 | 45 |
46 |
47 | -------------------------------------------------------------------------------- /app/src/app/menu/menu.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/app/menu/menu.component.scss -------------------------------------------------------------------------------- /app/src/app/menu/menu.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; 2 | import {StatsService} from '../stats.service'; 3 | import {LocalStorageService} from '../local-storage.service'; 4 | import {MatSnackBar} from "@angular/material/snack-bar"; 5 | 6 | @Component({ 7 | selector: 'app-menu', 8 | templateUrl: './menu.component.html', 9 | styleUrls: ['./menu.component.scss'] 10 | }) 11 | export class MenuComponent implements OnInit { 12 | 13 | @Input() proxies: any[]; 14 | @Input() runningVersion: any; 15 | 16 | @Input() 17 | get currentlySelectedProxy() { 18 | return this._currentlySelectedProxy; 19 | } 20 | @Output() currentlySelectedProxyChange = new EventEmitter(); 21 | set currentlySelectedProxy(proxy: any) { 22 | this._currentlySelectedProxy = proxy; 23 | this.currentlySelectedProxyChange.emit(proxy); 24 | } 25 | 26 | private _currentlySelectedProxy = null; 27 | 28 | constructor( 29 | private statsService: StatsService, 30 | private localStorageService: LocalStorageService, 31 | private snackBar: MatSnackBar, 32 | ) { } 33 | 34 | ngOnInit() { 35 | } 36 | 37 | getProxies() { 38 | return this.proxies; 39 | } 40 | 41 | async logout() { 42 | this.localStorageService.clearAuthData(); 43 | await this.statsService.reconnect(); 44 | } 45 | 46 | update() { 47 | this.statsService.updateProxy(); 48 | this.snackBar.open('Updating the proxy ..', '', { 49 | verticalPosition: 'top', 50 | horizontalPosition: 'right', 51 | }); 52 | } 53 | 54 | showSideBySide() { 55 | const usableSpace = window.innerWidth - 355; 56 | const proxyCount = this.proxies.length; 57 | 58 | return proxyCount * 120 <= usableSpace; 59 | } 60 | 61 | getRunningVersion() { 62 | return this.runningVersion; 63 | } 64 | 65 | getTitle() { 66 | const showVersion = this.showSideBySide(); 67 | const versionAppend = this.runningVersion ? ` ${this.runningVersion}` : ''; 68 | return `Foxy-Proxy${showVersion ? versionAppend : ''}`; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/app/net-diff-chart/net-diff-chart.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | Historical Difficulty 7 |
8 | × 9 |
10 |
11 |
12 |
13 |
14 | 15 | {{ netDiffChart }} 16 | 17 |
18 | -------------------------------------------------------------------------------- /app/src/app/net-diff-chart/net-diff-chart.component.scss: -------------------------------------------------------------------------------- 1 | .scalable { 2 | width: 400px; 3 | } 4 | 5 | @media (max-width: 460px) { 6 | .scalable { 7 | width: 320px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/app/net-diff-chart/net-diff-chart.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from '@angular/core'; 2 | import { Chart } from 'chart.js'; 3 | import {LocalStorageService} from '../local-storage.service'; 4 | 5 | @Component({ 6 | selector: 'app-net-diff-chart', 7 | templateUrl: './net-diff-chart.component.html', 8 | styleUrls: ['./net-diff-chart.component.scss'] 9 | }) 10 | export class NetDiffChartComponent implements OnInit, OnChanges { 11 | static getScaledRounds(rounds) { 12 | let scaledRounds = rounds; 13 | while (scaledRounds.length > 1000) { 14 | scaledRounds = scaledRounds.filter((round, index) => index % 10 !== 0); 15 | } 16 | 17 | return scaledRounds; 18 | } 19 | 20 | @Input() historicalRounds; 21 | @Input() upstreamFullName: string; 22 | 23 | @ViewChild('netDiffChart', {static: true}) private netDiffChartRef; 24 | private netDiffChart:any = {}; 25 | 26 | constructor(private localStorageService: LocalStorageService) { } 27 | 28 | ngOnInit() { 29 | const scaledRounds = NetDiffChartComponent.getScaledRounds(this.historicalRounds); 30 | this.netDiffChart = new Chart(this.netDiffChartRef.nativeElement, { 31 | type: 'line', 32 | data: { 33 | labels: scaledRounds.map(round => round.blockHeight), 34 | datasets: [{ 35 | data: scaledRounds.map(round => round.netDiff), 36 | pointRadius: 0, 37 | backgroundColor: [ 38 | 'rgba(33, 224, 132, 0.5)', 39 | ], 40 | borderColor: [ 41 | 'rgb(51, 51, 51, 1)', 42 | ], 43 | }], 44 | }, 45 | options: { 46 | legend: { 47 | display: false 48 | }, 49 | scales: { 50 | xAxes: [{ 51 | display: true, 52 | ticks: { 53 | fontColor: "#dcddde", 54 | }, 55 | }], 56 | yAxes: [{ 57 | display: true, 58 | ticks: { 59 | fontColor: "#dcddde", 60 | }, 61 | }], 62 | }, 63 | } 64 | }); 65 | } 66 | 67 | ngOnChanges(changes: SimpleChanges) { 68 | if (Object.keys(this.netDiffChart).length === 0) { 69 | return; 70 | } 71 | const scaledRounds = NetDiffChartComponent.getScaledRounds(changes.historicalRounds.currentValue); 72 | this.netDiffChart.data.labels = scaledRounds.map(round => round.blockHeight); 73 | this.netDiffChart.data.datasets[0].data = scaledRounds.map(round => round.netDiff); 74 | this.netDiffChart.update(); 75 | } 76 | 77 | hideCard() { 78 | this.localStorageService.hideItem('net-diff-chart', this.upstreamFullName); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/app/new-version-snackbar/new-version-snackbar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Newer version {{ data.latestVersion }} is available! 4 | 5 | 6 |
7 |
8 | Changelog: 9 |
10 | 11 |
12 | {{changelog}} 13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /app/src/app/new-version-snackbar/new-version-snackbar.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/app/new-version-snackbar/new-version-snackbar.component.scss -------------------------------------------------------------------------------- /app/src/app/new-version-snackbar/new-version-snackbar.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject} from '@angular/core'; 2 | import {MAT_SNACK_BAR_DATA, MatSnackBarRef} from "@angular/material/snack-bar"; 3 | 4 | @Component({ 5 | selector: 'app-new-version-snackbar', 6 | templateUrl: './new-version-snackbar.component.html', 7 | styleUrls: ['./new-version-snackbar.component.scss'] 8 | }) 9 | export class NewVersionSnackbarComponent { 10 | 11 | public showChangelog = false; 12 | 13 | constructor( 14 | private snackBarRef: MatSnackBarRef, 15 | @Inject(MAT_SNACK_BAR_DATA) public data: any 16 | ) {} 17 | 18 | dismiss(): void { 19 | this.snackBarRef.dismiss(); 20 | } 21 | 22 | update(): void { 23 | this.snackBarRef.dismissWithAction(); 24 | } 25 | 26 | toggleChangelog() { 27 | this.showChangelog = !this.showChangelog; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/app/proxy-info/proxy-info.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{name}} 5 | 6 | 7 | Proxy 8 | 9 | 10 | 11 | Scan progress 12 | 13 | 14 | 20 | 21 |  {{scanProgress}}% 22 | 23 | 24 |
25 |
26 |   27 |
28 |
29 | 30 | 34 | 35 | {{miner.id}} 36 | 37 | 38 | 39 | 45 | 46 |  {{getCapacityString(miner.capacity)}} 47 | 48 | 49 |
50 |
51 |
52 |
53 | Total 54 | {{getCapacityString(totalCapacity)}} 55 |
56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /app/src/app/proxy-info/proxy-info.component.scss: -------------------------------------------------------------------------------- 1 | .purple ::ng-deep .mat-progress-bar-fill::after { 2 | background-color: rgb(124, 0, 196); 3 | } 4 | 5 | .green ::ng-deep .mat-progress-bar-fill::after { 6 | background-color: rgba(33, 224, 132, 0.5); 7 | } 8 | 9 | .dot { 10 | height: 10px; 11 | width: 10px; 12 | background-color: #bbb; 13 | border-radius: 50%; 14 | display: inline-block; 15 | } 16 | span[positive] { 17 | background-color: rgb(75, 210, 143); 18 | } 19 | span[intermediary] { 20 | background-color: rgb(255, 170, 0); 21 | } 22 | span[negative] { 23 | background-color: rgb(255, 77, 77); 24 | } 25 | 26 | .ellipsis { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | -o-text-overflow: ellipsis; 30 | white-space: nowrap; 31 | max-width: 172px; 32 | } 33 | .ellipsis:hover { 34 | overflow: visible; 35 | max-width: none; 36 | width: auto; 37 | } 38 | -------------------------------------------------------------------------------- /app/src/app/proxy-info/proxy-info.component.ts: -------------------------------------------------------------------------------- 1 | import * as Capacity from '../../../../shared/capacity'; 2 | import * as moment from 'moment'; 3 | import {Component, Input, OnInit} from '@angular/core'; 4 | import {Observable, Subscription, interval} from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'app-proxy-info', 8 | templateUrl: './proxy-info.component.html', 9 | styleUrls: ['./proxy-info.component.scss'] 10 | }) 11 | export class ProxyInfoComponent implements OnInit { 12 | 13 | @Input() name: string; 14 | @Input() maxScanTime: number; 15 | @Input() totalCapacity: number; 16 | @Input() miners: any; 17 | @Input() currentBlockHeights: any; 18 | 19 | public scanProgress = 100; 20 | private counter: Observable; 21 | private subscription: Subscription; 22 | 23 | constructor() { } 24 | 25 | ngOnInit() { 26 | this.counter = interval(1000); 27 | this.subscription = this.counter.subscribe(() => this.scanProgress = this.getScanProgress()); 28 | } 29 | 30 | getMiner() { 31 | return Object.keys(this.miners).sort().map(minerId => { 32 | let miner = this.miners[minerId]; 33 | miner.id = minerId; 34 | miner.progress = this.getProgressForMiner(miner); 35 | 36 | return miner; 37 | }); 38 | } 39 | 40 | getCapacityString(capacityInGiB) { 41 | return (new Capacity(capacityInGiB)).toString(); 42 | } 43 | 44 | getScanProgress() { 45 | const miners = Object.keys(this.miners).map(key => this.miners[key]); 46 | if (miners.length === 0) { 47 | return 100; 48 | } 49 | 50 | const scanProgress = miners.map(miner => { 51 | const progress = this.getProgressForMiner(miner); 52 | if (!miner.capacity) { 53 | return progress / miners.length; 54 | } 55 | const capacityShare = miner.capacity / this.totalCapacity; 56 | 57 | return capacityShare * progress; 58 | }).reduce((acc, curr) => acc + curr, 0); 59 | 60 | return Math.min(Math.round(scanProgress), 100); 61 | } 62 | 63 | getProgressForMiner(miner) { 64 | const maxScanTime = miner.maxScanTime || this.maxScanTime; 65 | if (!miner.startedAt) { 66 | return 100; 67 | } 68 | const elapsed = moment().diff(miner.startedAt, 'seconds'); 69 | 70 | return Math.min(1, elapsed / maxScanTime) * 100; 71 | } 72 | 73 | getState(miner) { 74 | const lastActiveDiffMin = moment().diff(miner.lastTimeActive, 'minutes'); 75 | const lastBlockActive = miner.lastBlockActive; 76 | const lastActiveError = this.currentBlockHeights.every(height => Math.abs(lastBlockActive - height) > 7); 77 | if (lastActiveDiffMin >= 5 && lastActiveError) { 78 | return 0; 79 | } 80 | const lastActiveWarn = this.currentBlockHeights.some(height => { 81 | const diff = Math.abs(lastBlockActive - height); 82 | 83 | return diff >= 2 && diff < 7; 84 | }); 85 | if (lastActiveDiffMin >= 5 && lastActiveWarn) { 86 | return 1; 87 | } 88 | 89 | return 2; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/app/proxy/proxy.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 | 10 |
11 |
12 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /app/src/app/proxy/proxy.component.scss: -------------------------------------------------------------------------------- 1 | .flex-container-col { 2 | display: flex; /* or inline-flex */ 3 | flex-direction: column; 4 | } 5 | 6 | .flex-container-row { 7 | display: flex; /* or inline-flex */ 8 | flex-direction: row; 9 | flex-wrap: wrap; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | 14 | .item { 15 | flex-grow: 1; /* default 0 */ 16 | } 17 | -------------------------------------------------------------------------------- /app/src/app/proxy/proxy.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-proxy', 5 | templateUrl: './proxy.component.html', 6 | styleUrls: ['./proxy.component.scss'] 7 | }) 8 | export class ProxyComponent implements OnInit { 9 | 10 | @Input() proxy: any; 11 | 12 | constructor() { } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | getCurrentBlockHeights() { 18 | return this.proxy.upstreamStats.map(upstream => upstream.blockNumber); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/app/round-stats/round-stats.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
Rounds
7 |
8 | × 9 |
10 |
11 |
12 |
13 |
14 | 15 | Submitted 16 | {{getSubmitPercent()}}% {{roundsSubmitted}}/{{totalRounds}} 17 |
18 | Won 19 | {{roundsWon}} 20 |
21 | {{roundsSubmittedChart}} 22 |
23 |
24 | -------------------------------------------------------------------------------- /app/src/app/round-stats/round-stats.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/app/round-stats/round-stats.component.scss -------------------------------------------------------------------------------- /app/src/app/round-stats/round-stats.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, ViewChild} from '@angular/core'; 2 | import Chart from 'chart.js'; 3 | import {LocalStorageService} from '../local-storage.service'; 4 | 5 | @Component({ 6 | selector: 'app-round-stats', 7 | templateUrl: './round-stats.component.html', 8 | styleUrls: ['./round-stats.component.scss'] 9 | }) 10 | export class RoundStatsComponent implements OnInit { 11 | 12 | @Input() totalRounds: number; 13 | @Input() roundsWithDLs: number; 14 | @Input() roundsSubmitted: number; 15 | @Input() roundsWon: number; 16 | @Input() upstreamFullName: string; 17 | 18 | @ViewChild('roundsSubmittedChart', {static: true}) private roundsSubmittedChartRef; 19 | private roundsSubmittedChart = []; 20 | 21 | constructor(private localStorageService: LocalStorageService) { } 22 | 23 | ngOnInit() { 24 | this.roundsSubmittedChart = new Chart(this.roundsSubmittedChartRef.nativeElement, { 25 | type: 'pie', 26 | data: { 27 | labels: ['Rounds Won', 'Rounds Submitted', 'Rounds with DLs', 'Rounds without DLs'], 28 | datasets: [{ 29 | data: [ 30 | this.roundsWon, 31 | this.roundsSubmitted - this.roundsWon, 32 | this.roundsWithDLs - this.roundsSubmitted, 33 | this.totalRounds - this.roundsWithDLs, 34 | ], 35 | pointRadius: 0, 36 | backgroundColor: [ 37 | 'rgba(21, 242, 40, 0.5)', 38 | 'rgba(114, 14, 237, 0.5)', 39 | 'rgba(61, 120, 204, 0.5)', 40 | ], 41 | borderColor: [ 42 | 'rgb(51, 51, 51, 1)', 43 | 'rgb(51, 51, 51, 1)', 44 | 'rgb(51, 51, 51, 1)', 45 | 'rgb(51, 51, 51, 1)', 46 | ], 47 | }], 48 | }, 49 | options: { 50 | legend: { 51 | display: false 52 | }, 53 | } 54 | }); 55 | } 56 | 57 | getSubmitPercent() { 58 | if (this.totalRounds === 0) { 59 | return 0; 60 | } 61 | 62 | return (this.roundsSubmitted / this.totalRounds * 100).toFixed(2); 63 | } 64 | 65 | hideCard() { 66 | this.localStorageService.hideItem('round-stats', this.upstreamFullName); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/app/settings/settings.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |  {{getTitle()}}  |  Settings 4 | 5 | 8 | 9 | 13 | 17 | 18 | 22 | 23 |
24 | info 25 | Version {{getRunningVersion()}} 26 |
27 |
28 | 29 | favorite 30 | 31 |
32 |
33 |
34 |

35 |
36 |
Show Proxies:
37 |
38 |
39 | 40 | {{proxy.name}} 41 | 42 |
43 |
44 |
45 |

46 |
47 |
Layout:
48 |
49 | 50 | 51 | {{layout}} 52 |   53 | 54 | 55 |
56 |
57 |

58 |
59 |
Hidden cards:
60 |
61 |
62 | 65 | {{card.split('/').slice(1).join(' > ')}} 66 |
67 |
68 |
69 |

70 |
71 |
72 | 76 |
77 |
78 |

79 |
80 | -------------------------------------------------------------------------------- /app/src/app/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | .flex-container { 2 | display: flex; /* or inline-flex */ 3 | flex-direction: column; 4 | justify-content: center; 5 | } 6 | 7 | .flex-container-row { 8 | display: flex; /* or inline-flex */ 9 | flex-direction: row; 10 | flex-wrap: wrap; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/app/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {StatsService} from '../stats.service'; 3 | import {LocalStorageService} from '../local-storage.service'; 4 | import {MatSnackBar} from "@angular/material/snack-bar"; 5 | 6 | @Component({ 7 | selector: 'app-settings', 8 | templateUrl: './settings.component.html', 9 | styleUrls: ['./settings.component.scss'] 10 | }) 11 | export class SettingsComponent implements OnInit { 12 | 13 | private proxies: any[]; 14 | private runningVersion: any; 15 | 16 | constructor( 17 | private statsService: StatsService, 18 | private localStorageService: LocalStorageService, 19 | private snackBar: MatSnackBar, 20 | ) { } 21 | 22 | ngOnInit() { 23 | this.updateRunningVersion(); 24 | this.statsService.getStatsObservable().subscribe((proxies => { 25 | this.proxies = proxies; 26 | })); 27 | } 28 | 29 | async updateRunningVersion() { 30 | const versionInfo: any = await this.statsService.getVersionInfo(); 31 | this.runningVersion = versionInfo.runningVersion; 32 | } 33 | 34 | getProxies() { 35 | return this.proxies; 36 | } 37 | 38 | async logout() { 39 | this.localStorageService.clearAuthData(); 40 | await this.statsService.reconnect(); 41 | } 42 | 43 | update() { 44 | this.statsService.updateProxy(); 45 | this.snackBar.open('Updating the proxy ..', '', { 46 | verticalPosition: 'top', 47 | horizontalPosition: 'right', 48 | }); 49 | } 50 | 51 | resetLocalConfig() { 52 | this.localStorageService.clearHideItems(); 53 | } 54 | 55 | getRunningVersion() { 56 | return this.runningVersion; 57 | } 58 | 59 | getTitle() { 60 | const versionAppend = this.runningVersion ? ` ${this.runningVersion}` : ''; 61 | return `Foxy-Proxy${versionAppend}`; 62 | } 63 | 64 | showProxy(proxy) { 65 | return this.localStorageService.showProxy(proxy.name); 66 | } 67 | 68 | setShowProxy(proxy, show) { 69 | this.localStorageService.setProxyHidden(proxy.name, !show); 70 | } 71 | 72 | get hiddenCards() { 73 | return this.localStorageService.getHiddenCards(); 74 | } 75 | 76 | clearHiddenCard(card) { 77 | this.localStorageService.removeItem(card); 78 | } 79 | 80 | get layouts() { 81 | return [ 82 | 'Default', 83 | 'Condensed', 84 | ]; 85 | } 86 | 87 | get selectedLayout() { 88 | return this.localStorageService.getItem('layout') || 'Default'; 89 | } 90 | 91 | set selectedLayout(layout) { 92 | this.localStorageService.setItem('layout', layout); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/app/stats.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { StatsService } from './stats.service'; 4 | 5 | describe('StatsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: StatsService = TestBed.get(StatsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/app/stats.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {WebsocketService} from './websocket.service'; 3 | import {BehaviorSubject, Observable} from 'rxjs'; 4 | import {Router} from '@angular/router'; 5 | import {LocalStorageService} from './local-storage.service'; 6 | import {MatSnackBar} from "@angular/material/snack-bar"; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class StatsService { 12 | 13 | private stats = new BehaviorSubject([]); 14 | private statsObservable: any; 15 | private authenticated = new BehaviorSubject(false); 16 | private authenticatedObservable: Observable; 17 | private reconnecting = false; 18 | 19 | constructor( 20 | private websocketService: WebsocketService, 21 | private localStorageService: LocalStorageService, 22 | private router: Router, 23 | private snackBar: MatSnackBar, 24 | ) { 25 | this.statsObservable = this.stats.asObservable(); 26 | this.authenticatedObservable = this.authenticated.asObservable(); 27 | this.websocketService.subscribe('connect', this.onConnected.bind(this)); 28 | this.websocketService.subscribe('disconnect', this.onDisconnected.bind(this)); 29 | this.websocketService.subscribe('reconnect', this.onReconnected.bind(this)); 30 | this.websocketService.subscribe('unauthorized', this.onUnauthorized.bind(this)); 31 | this.websocketService.subscribe('stats/proxy', this.onNewProxyStats.bind(this)); 32 | this.websocketService.subscribe('stats/current-round', this.onNewUpstreamStats.bind(this)); 33 | this.websocketService.subscribe('stats/connection-stats', this.onNewUpstreamStats.bind(this)); 34 | this.websocketService.subscribe('stats/historical', this.onNewUpstreamStats.bind(this)); 35 | } 36 | 37 | init() { 38 | this.websocketService.publish('stats/init', (stats) => { 39 | this.stats.next(stats); 40 | if (this.authenticated.getValue() === false) { 41 | this.authenticated.next(true); 42 | } 43 | }); 44 | } 45 | 46 | onConnected() { 47 | this.authenticated.next(false); 48 | const authData = this.localStorageService.getAuthData(); 49 | if (authData) { 50 | this.authenticate(authData.username, authData.passHash); 51 | } 52 | this.init(); 53 | } 54 | 55 | onDisconnected() { 56 | if (this.reconnecting) { 57 | return; 58 | } 59 | this.snackBar.open('Lost the connection to the proxy, reconnecting..', null, { 60 | duration: 2 * 1000, 61 | verticalPosition: 'top', 62 | horizontalPosition: 'right', 63 | }); 64 | } 65 | 66 | onReconnected() { 67 | if (this.reconnecting) { 68 | return; 69 | } 70 | this.snackBar.open('Re-established the connection to the proxy', null, { 71 | duration: 2 * 1000, 72 | verticalPosition: 'top', 73 | horizontalPosition: 'right', 74 | }); 75 | } 76 | 77 | getVersionInfo() { 78 | return new Promise(resolve => this.websocketService.publish('version/info', (result) => resolve(result))); 79 | } 80 | 81 | updateProxy() { 82 | this.websocketService.publish('version/update'); 83 | } 84 | 85 | authenticate(username, passHash) { 86 | return new Promise(resolve => { 87 | this.websocketService.publish('authenticate', { 88 | username, 89 | passHash, 90 | }, (result) => { 91 | if (result) { 92 | this.authenticated.next(true); 93 | } 94 | resolve(result); 95 | }); 96 | }); 97 | } 98 | 99 | async onUnauthorized() { 100 | await this.router.navigate(['/login']); 101 | } 102 | 103 | onNewProxyStats(proxyName, proxyStats) { 104 | const stats = this.stats.getValue(); 105 | if (!stats) { 106 | return; 107 | } 108 | const proxy = stats.find(proxy => proxy.name === proxyName); 109 | if (!proxy) { 110 | return; 111 | } 112 | Object.keys(proxyStats).forEach(key => { 113 | proxy[key] = proxyStats[key]; 114 | }); 115 | } 116 | 117 | onNewUpstreamStats(fullUpstreamName, upstreamStats) { 118 | const stats = this.stats.getValue(); 119 | if (!stats) { 120 | return; 121 | } 122 | const upstream = stats 123 | .map(proxy => proxy.upstreamStats) 124 | .reduce((acc, curr) => acc.concat(curr), []) 125 | .find(upstream => upstream.fullName === fullUpstreamName); 126 | if (!upstream) { 127 | return; 128 | } 129 | Object.keys(upstreamStats).forEach(key => { 130 | upstream[key] = upstreamStats[key]; 131 | }); 132 | } 133 | 134 | getStatsObservable() { 135 | return this.statsObservable; 136 | } 137 | 138 | getAuthenticatedObservable() { 139 | return this.authenticatedObservable; 140 | } 141 | 142 | async reconnect() { 143 | this.reconnecting = true; 144 | this.websocketService.reconnect(); 145 | await new Promise(resolve => setTimeout(resolve, 5000)); 146 | this.reconnecting = false; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/src/app/upstream-info/upstream-info.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | {{name}} 13 | 14 | 15 | 16 | {{name}} 17 | 18 | 19 | 20 | 26 | 27 |  {{scanProgress}}% 28 | 29 | 30 | 31 | Start time: {{getStartTime()}} | Elapsed: {{getElapsedSinceStart()}} 32 | 33 | 34 | 35 | Best deadline 36 | {{getBestDLString()}} 37 |
38 | Current block 39 | {{currentBlock}} 40 |
41 | Difficulty 42 | {{netDiff ? netDiff.toFixed(0) : 'N/A'}} 43 |
44 |
45 |   46 |
47 | Avg DL (historical) 48 | {{getAvgDL()}} 49 |
50 | Best DL (historical) 51 | {{getBestDL()}} 52 |
53 | Performance 54 | {{getEstimatedCapacity()}} 55 |
56 |
57 |   58 |
59 | Connection quality 60 | {{connectionQuality.toFixed(2)}} % 61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /app/src/app/upstream-info/upstream-info.component.scss: -------------------------------------------------------------------------------- 1 | .purple ::ng-deep .mat-progress-bar-fill::after { 2 | background-color: rgb(124, 0, 196); 3 | } 4 | 5 | .green ::ng-deep .mat-progress-bar-fill::after { 6 | background-color: rgba(33, 224, 132, 0.5); 7 | } 8 | 9 | .dot { 10 | height: 10px; 11 | width: 10px; 12 | background-color: #bbb; 13 | border-radius: 50%; 14 | display: inline-block; 15 | } 16 | span[positive] { 17 | background-color: rgb(75, 210, 143); 18 | } 19 | span[intermediary] { 20 | background-color: rgb(255, 170, 0); 21 | } 22 | span[negative] { 23 | background-color: rgb(255, 77, 77); 24 | } 25 | 26 | .ellipsis { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | -o-text-overflow: ellipsis; 30 | white-space: nowrap; 31 | max-width: 162px; 32 | } 33 | .ellipsis:hover { 34 | overflow: visible; 35 | max-width: none; 36 | width: auto; 37 | } 38 | -------------------------------------------------------------------------------- /app/src/app/upstream/upstream.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 18 | 19 |
20 | 27 | 28 | 33 | 34 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /app/src/app/upstream/upstream.component.scss: -------------------------------------------------------------------------------- 1 | .flex-container-row { 2 | display: flex; /* or inline-flex */ 3 | flex-direction: row; 4 | align-items: center; 5 | flex-wrap: wrap; 6 | justify-content: center; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/app/upstream/upstream.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | import {LocalStorageService} from '../local-storage.service'; 3 | 4 | @Component({ 5 | selector: 'app-upstream', 6 | templateUrl: './upstream.component.html', 7 | styleUrls: ['./upstream.component.scss'] 8 | }) 9 | export class UpstreamComponent implements OnInit { 10 | 11 | @Input() upstream: any; 12 | @Input() miners: any; 13 | @Input() maxScanTime: number; 14 | 15 | constructor(private localStorageService: LocalStorageService) { } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | showCard(identifier) { 21 | return this.localStorageService.shouldShowItem(identifier, this.upstream.fullName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/app/websocket.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { WebsocketService } from './websocket.service'; 4 | 5 | describe('WebsocketService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: WebsocketService = TestBed.get(WebsocketService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/app/websocket.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { io } from 'socket.io-client'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class WebsocketService { 8 | 9 | private socket; 10 | 11 | constructor() { 12 | this.socket = io('/web-ui'); 13 | } 14 | 15 | subscribe(topic, cb) { 16 | this.socket.on(topic, cb); 17 | } 18 | 19 | publish(topic, ...args) { 20 | this.socket.emit(topic, ...args); 21 | } 22 | 23 | unsubscribeAll(topic) { 24 | this.socket.removeAllListeners(topic); 25 | } 26 | 27 | reconnect() { 28 | this.socket.disconnect(); 29 | this.socket.connect(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/.gitkeep -------------------------------------------------------------------------------- /app/src/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #FFFFFF 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/assets/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-114.png -------------------------------------------------------------------------------- /app/src/assets/favicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-120.png -------------------------------------------------------------------------------- /app/src/assets/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-144.png -------------------------------------------------------------------------------- /app/src/assets/favicon-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-150.png -------------------------------------------------------------------------------- /app/src/assets/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-152.png -------------------------------------------------------------------------------- /app/src/assets/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-16.png -------------------------------------------------------------------------------- /app/src/assets/favicon-160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-160.png -------------------------------------------------------------------------------- /app/src/assets/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-180.png -------------------------------------------------------------------------------- /app/src/assets/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-192.png -------------------------------------------------------------------------------- /app/src/assets/favicon-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-310.png -------------------------------------------------------------------------------- /app/src/assets/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-32.png -------------------------------------------------------------------------------- /app/src/assets/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-57.png -------------------------------------------------------------------------------- /app/src/assets/favicon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-60.png -------------------------------------------------------------------------------- /app/src/assets/favicon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-64.png -------------------------------------------------------------------------------- /app/src/assets/favicon-70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-70.png -------------------------------------------------------------------------------- /app/src/assets/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-72.png -------------------------------------------------------------------------------- /app/src/assets/favicon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-76.png -------------------------------------------------------------------------------- /app/src/assets/favicon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon-96.png -------------------------------------------------------------------------------- /app/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixbrucker/foxy-proxy/5e8b590901791bc75e6e288a60e4ce3ca9caf1d5/app/src/assets/favicon.ico -------------------------------------------------------------------------------- /app/src/assets/fox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 19 | 20 | 21 | 28 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Foxy-Proxy 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills. 22 | * This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot 23 | */ 24 | 25 | import 'core-js/es6/symbol'; 26 | import 'core-js/es6/object'; 27 | import 'core-js/es6/function'; 28 | import 'core-js/es6/parse-int'; 29 | import 'core-js/es6/parse-float'; 30 | import 'core-js/es6/number'; 31 | import 'core-js/es6/math'; 32 | import 'core-js/es6/string'; 33 | import 'core-js/es6/date'; 34 | import 'core-js/es6/array'; 35 | import 'core-js/es6/regexp'; 36 | import 'core-js/es6/map'; 37 | import 'core-js/es6/weak-map'; 38 | import 'core-js/es6/set'; 39 | import 'core-js/es7/string'; 40 | 41 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 42 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 43 | 44 | /** IE10 and IE11 requires the following for the Reflect API. */ 45 | // import 'core-js/es6/reflect'; 46 | 47 | /** 48 | * Web Animations `@angular/platform-browser/animations` 49 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 50 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 51 | */ 52 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 53 | 54 | /** 55 | * By default, zone.js will patch all possible macroTask and DomEvents 56 | * user can disable parts of macroTask/DomEvents patch by setting following flags 57 | * because those flags need to be set before `zone.js` being loaded, and webpack 58 | * will put import in the top of bundle, so user need to create a separate file 59 | * in this directory (for example: zone-flags.ts), and put the following flags 60 | * into that file, and then add the following code before importing zone.js. 61 | * import './zone-flags.ts'; 62 | * 63 | * The flags allowed in zone-flags.ts are listed here. 64 | * 65 | * The following flags will work for all browsers. 66 | * 67 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 68 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 69 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 70 | * 71 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 72 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 73 | * 74 | * (window as any).__Zone_enable_cross_context_check = true; 75 | * 76 | */ 77 | 78 | /*************************************************************************************************** 79 | * Zone JS is required by default for Angular itself. 80 | */ 81 | import 'zone.js/dist/zone'; // Included with Angular CLI. 82 | 83 | 84 | /*************************************************************************************************** 85 | * APPLICATION IMPORTS 86 | */ 87 | -------------------------------------------------------------------------------- /app/src/styles.scss: -------------------------------------------------------------------------------- 1 | html, body { height: 100%; } 2 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 3 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 4 | 5 | .dark-theme { 6 | background-color: #36393f; 7 | color: #cfd0d1; 8 | } 9 | 10 | .custom-card { 11 | background-color: #333; 12 | color: #dcddde; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es6", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "core-js/es6/*": ["node_modules/core-js/es"], 23 | "core-js/es7/string": ["node_modules/core-js/es/string"] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./src/tsconfig.app.json" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ecosystem.config.js.dist: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'foxy-proxy', 5 | script: './main.js', 6 | args: ['--no-colors'], 7 | watch: true, 8 | ignore_watch: ['db.sqlite', 'db.sqlite-journal', 'node_modules', '.git'], 9 | } 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /lib/cli-dashboard.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const logUpdate = require('log-update'); 3 | const moment = require('moment'); 4 | const Table = require('cli-table3'); 5 | const eventBus = require('./services/event-bus'); 6 | const store = require('./services/store'); 7 | const version = require('./version'); 8 | const Capacity = require('../shared/capacity'); 9 | const outputUtil = require('./output-util'); 10 | 11 | class Dashboard { 12 | static getTimeElapsedSinceLastBlock(blockStart) { 13 | const duration = moment.duration(moment().diff(moment(blockStart))); 14 | 15 | return `${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 16 | } 17 | 18 | static getBestDeadlineString(bestDL) { 19 | if (bestDL === null) { 20 | return 'N/A'; 21 | } 22 | const duration = moment.duration(parseInt(bestDL, 10), 'seconds'); 23 | if (duration.months() > 0) { 24 | return `${duration.months()}m ${duration.days()}d ${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 25 | } else if (duration.days() > 0) { 26 | return `${duration.days()}d ${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 27 | } 28 | 29 | return `${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 30 | }; 31 | 32 | constructor() { 33 | this.maxLogLines = 12; 34 | this.lastLogLines= []; 35 | this.proxyStats = []; 36 | eventBus.subscribe('log/info', (msg) => { 37 | this.lastLogLines.push(`${moment().format('YYYY-MM-DD HH:mm:ss.SSS')} [INFO] | ${msg}`); 38 | if (this.lastLogLines.length > this.maxLogLines) { 39 | this.lastLogLines = this.lastLogLines.slice(this.maxLogLines * -1); 40 | } 41 | }); 42 | eventBus.subscribe('log/debug', (msg) => { 43 | this.lastLogLines.push(chalk.grey(`${moment().format('YYYY-MM-DD HH:mm:ss.SSS')} [DEBUG] | ${msg}`)); 44 | if (this.lastLogLines.length > this.maxLogLines) { 45 | this.lastLogLines = this.lastLogLines.slice(this.maxLogLines * -1); 46 | } 47 | }); 48 | eventBus.subscribe('log/error', (msg) => { 49 | this.lastLogLines.push(chalk.red(`${moment().format('YYYY-MM-DD HH:mm:ss.SSS')} [ERROR] | ${msg}`)); 50 | if (this.lastLogLines.length > this.maxLogLines) { 51 | this.lastLogLines = this.lastLogLines.slice(this.maxLogLines * -1); 52 | } 53 | }); 54 | 55 | eventBus.subscribe('stats/proxy', this.onNewProxyStats.bind(this)); 56 | eventBus.subscribe('stats/current-round', this.onNewUpstreamStats.bind(this)); 57 | eventBus.subscribe('stats/historical', this.onNewUpstreamStats.bind(this)); 58 | } 59 | 60 | async initStats() { 61 | this.proxyStats = await Promise.all(store.getProxies().map((proxy) => proxy.getStats())); 62 | } 63 | 64 | onNewProxyStats(proxyName, proxyStats) { 65 | const stats = this.proxyStats; 66 | if (!stats) { 67 | return; 68 | } 69 | const proxy = stats.find(proxy => proxy.name === proxyName); 70 | if (!proxy) { 71 | return; 72 | } 73 | Object.keys(proxyStats).forEach(key => { 74 | proxy[key] = proxyStats[key]; 75 | }); 76 | } 77 | 78 | onNewUpstreamStats(fullUpstreamName, upstreamStats) { 79 | const stats = this.proxyStats; 80 | if (!stats) { 81 | return; 82 | } 83 | const upstream = stats 84 | .map(proxy => proxy.upstreamStats) 85 | .reduce((acc, curr) => acc.concat(curr), []) 86 | .find(upstream => upstream.fullName === fullUpstreamName); 87 | if (!upstream) { 88 | return; 89 | } 90 | Object.keys(upstreamStats).forEach(key => { 91 | upstream[key] = upstreamStats[key]; 92 | }); 93 | } 94 | 95 | buildTable() { 96 | const table = new Table({ 97 | head: ['Proxy', 'Upstream', 'Block #', 'NetDiff', 'Elapsed', 'Best DL', 'EC', 'Plot size'], 98 | style: { 99 | head: ['cyan'], 100 | }, 101 | }); 102 | this.proxyStats.map(proxy => { 103 | return proxy.upstreamStats.map(upstream => { 104 | table.push([ 105 | outputUtil.getName(proxy), 106 | outputUtil.getName(upstream), 107 | upstream.blockNumber, 108 | upstream.netDiff ? `${Capacity.fromTiB(upstream.netDiff).toString(2)}` : 'N/A', 109 | Dashboard.getTimeElapsedSinceLastBlock(upstream.roundStart), 110 | Dashboard.getBestDeadlineString(upstream.bestDL), 111 | upstream.estimatedCapacityInTB ? Capacity.fromTiB(upstream.estimatedCapacityInTB).toString() : 'N/A', 112 | proxy.totalCapacity ? (new Capacity(proxy.totalCapacity)).toString() : 'N/A', 113 | ]); 114 | }); 115 | }); 116 | 117 | return table.toString(); 118 | } 119 | 120 | buildLogs() { 121 | return this.lastLogLines.join('\n'); 122 | } 123 | 124 | render() { 125 | logUpdate([ 126 | chalk.bold.magenta(`Foxy-Proxy ${version}`), 127 | this.buildTable(), 128 | '', 129 | 'Last log lines:', 130 | this.buildLogs(), 131 | ].join('\n')); 132 | } 133 | 134 | start() { 135 | this.render(); 136 | this.timer = setInterval(this.render.bind(this), 1000); 137 | } 138 | 139 | stop() { 140 | clearInterval(this.timer); 141 | } 142 | } 143 | 144 | module.exports = Dashboard; 145 | -------------------------------------------------------------------------------- /lib/coin-util.js: -------------------------------------------------------------------------------- 1 | const coinUtil = { 2 | blockTime(coin) { 3 | switch (coin) { 4 | case 'BHD': 5 | return 180; 6 | case 'LHD': 7 | case 'HDD': 8 | case 'XHD': 9 | return 300; 10 | default: 11 | return 240; 12 | } 13 | }, 14 | blockZeroBaseTarget(coin) { 15 | switch (coin) { 16 | case 'BHD': 17 | return 24433591728; 18 | case 'LHD': 19 | case 'HDD': 20 | case 'XHD': 21 | return 14660155037; 22 | default: 23 | return 18325193796; 24 | } 25 | }, 26 | modifyDeadline(deadline, coin) { 27 | if (coin !== 'BURST') { 28 | return deadline; 29 | } 30 | if (!deadline) { 31 | return deadline; 32 | } 33 | 34 | return Math.floor(Math.log(deadline) * (this.blockTime(coin) / Math.log(this.blockTime(coin)))); 35 | }, 36 | modifyNetDiff(netDiff, coin) { 37 | if (coin !== 'BURST') { 38 | return netDiff; 39 | } 40 | 41 | return Math.round(netDiff / 1.83); 42 | } 43 | }; 44 | 45 | module.exports = coinUtil; -------------------------------------------------------------------------------- /lib/currentRound.js: -------------------------------------------------------------------------------- 1 | class CurrentRound { 2 | 3 | constructor(upstream, miningInfo, eventEmitter, maxScanTime) { 4 | this.upstream = upstream; 5 | this.miningInfo = miningInfo; 6 | this.weight = (upstream && (upstream.upstreamConfig.weight || upstream.upstreamConfig.prio || upstream.weight)) || 10; 7 | this.maxScanTime = maxScanTime; 8 | this.eventEmitter = eventEmitter; 9 | this.scanDone = false; 10 | this.startedAt = null; 11 | } 12 | 13 | start() { 14 | this.startedAt = new Date(); 15 | this.timeoutId = setTimeout(() => { 16 | this.scanDone = true; 17 | this.timeoutId = null; 18 | this.eventEmitter.emit('scan-done'); 19 | }, this.maxScanTime * 1000); 20 | } 21 | 22 | cancel() { 23 | if (!this.timeoutId) { 24 | return; 25 | } 26 | clearTimeout(this.timeoutId); 27 | this.timeoutId = null; 28 | this.startedAt = null; 29 | } 30 | 31 | getStartedAt() { 32 | return this.startedAt; 33 | } 34 | 35 | getHeight() { 36 | return this.miningInfo.height; 37 | } 38 | } 39 | 40 | module.exports = CurrentRound; 41 | -------------------------------------------------------------------------------- /lib/currentRoundManager.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const CurrentRound = require('./currentRound'); 3 | 4 | class CurrentRoundManager { 5 | 6 | constructor(maxScanTime, currentRoundEmitter) { 7 | this.maxScanTime = maxScanTime; 8 | this.roundQueue = []; 9 | this.currentRound = {scanDone: true, prio: 9999, startedAt: new Date(), miningInfo: {height: 0, toObject: () => ({height: 0})}}; 10 | this.eventEmitter = new EventEmitter(); 11 | this.eventEmitter.on('scan-done', this.updateCurrentRound.bind(this)); 12 | this.currentRoundEmitter = currentRoundEmitter; 13 | } 14 | 15 | getMiningInfo() { 16 | if (!this.currentRound.miningInfo) { 17 | return { 18 | error: 'No miningInfo available!', 19 | }; 20 | } 21 | return this.currentRound.miningInfo.toObject(); 22 | } 23 | 24 | updateCurrentRound() { 25 | if (this.roundQueue.length === 0) { 26 | return; 27 | } 28 | this.currentRound = this.roundQueue.shift(); 29 | this.currentRound.start(); 30 | this.eventEmitter.emit('new-round', this.currentRound.miningInfo.toObject()); 31 | this.currentRoundEmitter.emit('current-round/new', this.currentRound); 32 | } 33 | 34 | addNewRound(upstream, miningInfo) { 35 | const currentRound = new CurrentRound(upstream, miningInfo, this.eventEmitter, this.maxScanTime); 36 | this.roundQueue = this.roundQueue.filter(currRound => currRound.upstream !== upstream); 37 | 38 | this.roundQueue.push(currentRound); 39 | this.roundQueue.sort((a, b) => b.weight - a.weight); 40 | 41 | // overwrite old round directly if from same upstream 42 | if (this.currentRound.upstream === this.roundQueue[0].upstream) { 43 | if (!this.currentRound.scanDone) { 44 | this.currentRound.cancel(); 45 | } 46 | this.currentRound = this.roundQueue.shift(); 47 | this.currentRound.start(); 48 | this.eventEmitter.emit('new-round', this.currentRound.miningInfo.toObject()); 49 | this.currentRoundEmitter.emit('current-round/new', this.currentRound); 50 | return; 51 | } 52 | 53 | // new high prio round, use it now and get back to the other one later if it was still running 54 | if (this.currentRound.weight < this.roundQueue[0].weight) { 55 | if (!this.currentRound.scanDone) { 56 | this.currentRound.cancel(); 57 | this.roundQueue.push(this.currentRound); 58 | this.roundQueue.sort((a, b) => b.weight - a.weight); 59 | } 60 | this.currentRound = this.roundQueue.shift(); 61 | this.currentRound.start(); 62 | this.eventEmitter.emit('new-round', this.currentRound.miningInfo.toObject()); 63 | this.currentRoundEmitter.emit('current-round/new', this.currentRound); 64 | } 65 | 66 | // init 67 | if (this.currentRound.scanDone) { 68 | this.updateCurrentRound(); 69 | } 70 | } 71 | 72 | copyRoundsFromManager(currentRoundManager) { 73 | this.addNewRound( 74 | currentRoundManager.currentRound.upstream, 75 | currentRoundManager.currentRound.miningInfo 76 | ); 77 | currentRoundManager.roundQueue.forEach(round => { 78 | this.addNewRound( 79 | round.upstream, 80 | round.miningInfo 81 | ); 82 | }); 83 | } 84 | 85 | getCurrentRound() { 86 | return this.currentRound; 87 | } 88 | } 89 | 90 | module.exports = CurrentRoundManager; 91 | -------------------------------------------------------------------------------- /lib/miningInfo.js: -------------------------------------------------------------------------------- 1 | const Capacity = require('../shared/capacity'); 2 | const coinUtil = require('./coin-util'); 3 | 4 | module.exports = class MiningInfo { 5 | constructor(height, baseTarget, generationSignature, targetDeadline = null, coin = null) { 6 | this._height = parseInt(height, 10); 7 | this._baseTarget = parseInt(baseTarget, 10); 8 | this._generationSignature = generationSignature; 9 | this._targetDeadline = targetDeadline; 10 | this._coin = coin; 11 | } 12 | 13 | get blockZeroBaseTarget() { 14 | return coinUtil.blockZeroBaseTarget(this._coin); 15 | } 16 | 17 | get height() { 18 | return this._height; 19 | } 20 | 21 | get baseTarget() { 22 | return this._baseTarget; 23 | } 24 | 25 | get generationSignature() { 26 | return this._generationSignature; 27 | } 28 | 29 | get targetDeadline() { 30 | return this._targetDeadline; 31 | } 32 | 33 | get netDiff() { 34 | return Math.round(this.blockZeroBaseTarget / this.baseTarget); 35 | } 36 | 37 | get modifiedNetDiff() { 38 | return coinUtil.modifyNetDiff(this.netDiff, this._coin); 39 | } 40 | 41 | get modifiedNetDiffFormatted() { 42 | return Capacity.fromTiB(this.netDiff).toString(); 43 | } 44 | 45 | toObject() { 46 | const obj = { 47 | height: this.height, 48 | baseTarget: this.baseTarget, 49 | generationSignature: this.generationSignature, 50 | }; 51 | if (this.targetDeadline) { 52 | obj.targetDeadline = this.targetDeadline; 53 | } 54 | 55 | return obj; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /lib/output-util.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const store = require('./services/store'); 3 | 4 | function getFullUpstreamNameLogs(proxyConfig, upstreamConfig) { 5 | if (proxyConfig.hideUpstreamName) { 6 | if (!store.getUseColors()) { 7 | return proxyConfig.name; 8 | } 9 | 10 | return getName(proxyConfig); 11 | } 12 | 13 | if (!store.getUseColors()) { 14 | return `${proxyConfig.name} | ${upstreamConfig.name}`; 15 | } 16 | 17 | return `${getName(proxyConfig)} | ${getName(upstreamConfig)}`; 18 | } 19 | 20 | function getName(config) { 21 | if (!store.getUseColors()) { 22 | return config.name; 23 | } 24 | 25 | return `${config.color ? chalk.hex(config.color)(config.name) : config.name}`; 26 | } 27 | 28 | function getString(text, color) { 29 | if (!store.getUseColors() || !color) { 30 | return text; 31 | } 32 | if (!color.startsWith('#')) { 33 | return chalk[color](text); 34 | } 35 | 36 | return chalk.hex(color)(text); 37 | } 38 | 39 | function getAccountColor(accountId, upstream) { 40 | if (upstream.accountColors[accountId]) { 41 | return upstream.accountColors[accountId]; 42 | } 43 | 44 | return upstream.accountColor; 45 | } 46 | 47 | module.exports = { 48 | getFullUpstreamNameLogs, 49 | getName, 50 | getString, 51 | getAccountColor, 52 | }; -------------------------------------------------------------------------------- /lib/processing-queue.js: -------------------------------------------------------------------------------- 1 | const { priorityQueue } = require('async'); 2 | 3 | class ProcessingQueue { 4 | constructor() { 5 | this.queue = priorityQueue(this.queueHandler.bind(this), 1); 6 | this.handlers = []; 7 | } 8 | 9 | push({type, data, priority = 50}) { 10 | return new Promise((resolve, reject) => this.queue.push({type, data}, priority, (err, res) => { 11 | if (err) { 12 | return reject(err); 13 | } 14 | resolve(res); 15 | })); 16 | } 17 | 18 | registerHandler(type, handler) { 19 | this.handlers.push({type, handler}); 20 | } 21 | 22 | async queueHandler({type, data}) { 23 | const handler = this.handlers.find(({type: handlerType}) => type === handlerType); 24 | if (!handler) { 25 | throw new Error(`Received unknown type: ${type}`); 26 | } 27 | return handler.handler(data); 28 | } 29 | } 30 | 31 | module.exports = ProcessingQueue; 32 | -------------------------------------------------------------------------------- /lib/services/cache.js: -------------------------------------------------------------------------------- 1 | const { debounce } = require('lodash'); 2 | 3 | const database = require('../../models'); 4 | const ProcessingQueue = require('../processing-queue'); 5 | 6 | class Cache { 7 | constructor() { 8 | this.rounds = {}; 9 | this.plotter = {}; 10 | 11 | this.debouncedSaveRound = {}; 12 | 13 | this.processingQueue = new ProcessingQueue(); 14 | this.processingQueue.registerHandler('create-or-update-round', this.createOrUpdateRoundHandler.bind(this)); 15 | this.processingQueue.registerHandler('remove-old-rounds', this.removeOldRoundsHandler.bind(this)); 16 | this.processingQueue.registerHandler('save-entity', this.saveEntityHandler.bind(this)); 17 | this.processingQueue.registerHandler('find-or-create-plotter', this.findOrCreatePlotterHandler.bind(this)); 18 | } 19 | 20 | async ensureRoundIsCached(upstream, height) { 21 | if (!this.rounds[upstream.fullUpstreamName]) { 22 | this.rounds[upstream.fullUpstreamName] = {}; 23 | } 24 | if (!this.rounds[upstream.fullUpstreamName][height]) { 25 | this.rounds[upstream.fullUpstreamName][height] = await database().round.findOne({ 26 | where: { 27 | upstream: upstream.fullUpstreamName, 28 | blockHeight: height, 29 | }, 30 | }); 31 | } 32 | 33 | return this.rounds[upstream.fullUpstreamName][height]; 34 | } 35 | 36 | async ensurePlotterIsCached(upstream, plotterId) { 37 | if (!this.plotter[upstream.fullUpstreamName]) { 38 | this.plotter[upstream.fullUpstreamName] = {}; 39 | } 40 | if (!this.plotter[upstream.fullUpstreamName][plotterId]) { 41 | this.plotter[upstream.fullUpstreamName][plotterId] = await this.findOrCreatePlotter(upstream, plotterId); 42 | } 43 | 44 | return this.plotter[upstream.fullUpstreamName][plotterId]; 45 | } 46 | 47 | roundWasUpdated(round) { 48 | if (!this.debouncedSaveRound[round.id]) { 49 | this.debouncedSaveRound[round.id] = debounce(this.saveEntity.bind(this), 3 * 1000, { maxWait: 5 * 1000 }); 50 | } 51 | this.debouncedSaveRound[round.id](round); 52 | } 53 | 54 | async findOrCreatePlotter(upstream, plotterId) { 55 | return this.processingQueue.push({type: 'find-or-create-plotter', data: { upstream, plotterId, priority: 20 }}); 56 | } 57 | 58 | async createOrUpdateRound(roundPrototype) { 59 | return this.processingQueue.push({type: 'create-or-update-round', data: roundPrototype, priority: 10}); 60 | } 61 | 62 | async removeOldRounds({upstream, roundsToKeep}) { 63 | return this.processingQueue.push({type: 'remove-old-rounds', data: {upstream, roundsToKeep}}); 64 | } 65 | 66 | async saveEntity(entity) { 67 | return this.processingQueue.push({type: 'save-entity', data: entity, priority: 30}); 68 | } 69 | 70 | async saveEntityHandler(entity) { 71 | await entity.save(); 72 | } 73 | 74 | async findOrCreatePlotterHandler({ upstream, plotterId }) { 75 | const [plotter] = await database().plotter.findOrCreate({ 76 | where: { 77 | upstream: upstream.fullUpstreamName, 78 | pid: plotterId, 79 | }, 80 | }); 81 | 82 | return plotter; 83 | } 84 | 85 | async createOrUpdateRoundHandler(roundPrototype) { 86 | const [round, created] = await database().round.findOrCreate({ 87 | where: { 88 | upstream: roundPrototype.upstream, 89 | blockHeight: roundPrototype.blockHeight, 90 | }, 91 | defaults: roundPrototype, 92 | }); 93 | if (!created && round.baseTarget !== roundPrototype.baseTarget) { 94 | round.baseTarget = roundPrototype.baseTarget; 95 | round.netDiff = roundPrototype.netDiff; 96 | round.bestDL = null; 97 | round.bestDLSubmitted = null; 98 | round.roundWon = null; 99 | await round.save(); 100 | } 101 | 102 | return round; 103 | } 104 | 105 | async removeOldRoundsHandler({upstream, roundsToKeep}) { 106 | const rounds = await database().round.findAll({ 107 | where: { 108 | upstream, 109 | }, 110 | order: [ 111 | ['blockHeight', 'DESC'], 112 | ], 113 | offset: roundsToKeep, 114 | }); 115 | for (let round of rounds) { 116 | await round.destroy(); 117 | } 118 | } 119 | 120 | removeOldCachedRounds(upstream, currentHeight) { 121 | this.removeCachedRoundsBelow(upstream, currentHeight - 10); 122 | } 123 | 124 | removeCachedRoundsBelow(upstream, height) { 125 | const roundHeights = Object.keys(this.rounds[upstream.fullUpstreamName] || {}); 126 | const heightsToRemove = roundHeights.filter(roundHeight => roundHeight < height); 127 | heightsToRemove 128 | .map(heightToRemove => this.rounds[upstream.fullUpstreamName][heightToRemove]) 129 | .filter(round => !!round) 130 | .forEach(round => this.invalidateCachedRound(round)); 131 | } 132 | 133 | invalidateCachedRound(round) { 134 | if (this.rounds[round.upstream] && this.rounds[round.upstream][round.blockHeight]) { 135 | delete this.rounds[round.upstream][round.blockHeight]; 136 | } 137 | if (this.debouncedSaveRound[round.id]) { 138 | delete this.debouncedSaveRound[round.id]; 139 | } 140 | } 141 | } 142 | 143 | module.exports = new Cache(); 144 | -------------------------------------------------------------------------------- /lib/services/coin-gecko.js: -------------------------------------------------------------------------------- 1 | const superagent = require('superagent'); 2 | 3 | class CoinGecko { 4 | constructor(currency = 'usd') { 5 | this.currency = currency; 6 | this.baseUrl = 'https://api.coingecko.com/api/v3'; 7 | } 8 | 9 | async getRates(symbols) { 10 | return this.doApiCall('simple/price', {vs_currencies: this.currency, ids: symbols.join(',')}); 11 | } 12 | 13 | async doApiCall(endpoint, params = {}) { 14 | const res = await superagent.get(`${this.baseUrl}/${endpoint}`).query(params); 15 | 16 | return res.body; 17 | } 18 | } 19 | 20 | module.exports = CoinGecko; 21 | -------------------------------------------------------------------------------- /lib/services/coin-paprika.js: -------------------------------------------------------------------------------- 1 | const superagent = require('superagent'); 2 | 3 | class CoinPaprika { 4 | constructor(currency = 'USD') { 5 | this.currency = currency; 6 | this.baseUrl = 'https://api.coinpaprika.com/v1'; 7 | } 8 | 9 | async getRate(coinId) { 10 | return this.doApiCall(`tickers/${coinId}`, {quotes: this.currency}); 11 | } 12 | 13 | async doApiCall(endpoint, params = {}) { 14 | const {body} = await superagent.get(`${this.baseUrl}/${endpoint}`).query(params); 15 | 16 | return body; 17 | } 18 | } 19 | 20 | module.exports = CoinPaprika; 21 | -------------------------------------------------------------------------------- /lib/services/event-bus.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | class EventBus { 4 | constructor() { 5 | this.emitter = new EventEmitter(); 6 | } 7 | 8 | publish(topic, ...msg) { 9 | this.emitter.emit(topic, ...msg); 10 | } 11 | 12 | subscribe(topic, cb) { 13 | this.emitter.on(topic, cb); 14 | } 15 | } 16 | 17 | module.exports = new EventBus(); 18 | -------------------------------------------------------------------------------- /lib/services/foxy-pool-gateway.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const io = require('socket.io-client'); 3 | 4 | const eventBus = require('./event-bus'); 5 | 6 | class FoxyPoolGateway { 7 | constructor() { 8 | this.url = 'http://miner.foxypool.io/mining'; 9 | this.coins = []; 10 | this.emitter = new EventEmitter(); 11 | this.emitter.setMaxListeners(0); 12 | this.connected = false; 13 | } 14 | 15 | async init({ allowLongPolling = false }) { 16 | const options = { rejectUnauthorized : false }; 17 | if (!allowLongPolling) { 18 | options.transports = ['websocket']; 19 | } 20 | this.client = io(this.url, options); 21 | 22 | this.client.on('connect', async () => { 23 | this.connected = true; 24 | this.emitter.emit('connection-state-change'); 25 | eventBus.publish('log/debug', `Foxy-Pool-Gateway | url=${this.url} | Socket.IO connected`); 26 | const result = await this.subscribeToCoins(); 27 | if (result.error) { 28 | eventBus.publish('log/error', `Foxy-Pool-Gateway | Error: ${result.error}`); 29 | } 30 | await Promise.all(this.coins.map(async coin => { 31 | const miningInfo = await this.getMiningInfo(coin); 32 | this.emitter.emit(`${coin}:miningInfo`, miningInfo); 33 | })); 34 | }); 35 | this.client.on('disconnect', () => { 36 | this.connected = false; 37 | this.emitter.emit('connection-state-change'); 38 | eventBus.publish('log/debug', `Foxy-Pool-Gateway | url=${this.url} | Socket.IO disconnected`); 39 | }); 40 | 41 | this.client.on('miningInfo', (coin, miningInfo) => { 42 | this.connected = true; 43 | this.emitter.emit('connection-state-change'); 44 | this.emitter.emit(`${coin}:miningInfo`, miningInfo); 45 | }); 46 | } 47 | 48 | async subscribeToCoins() { 49 | return new Promise(resolve => this.client.emit('subscribe', this.coins, resolve)); 50 | } 51 | 52 | onNewMiningInfo(coin, handler) { 53 | this.emitter.on(`${coin}:miningInfo`, handler); 54 | } 55 | 56 | onConnectionStateChange(handler) { 57 | this.emitter.on('connection-state-change', handler); 58 | } 59 | 60 | async getMiningInfo(coin) { 61 | return new Promise(resolve => this.client.emit('getMiningInfo', coin, resolve)); 62 | } 63 | 64 | async submitNonce(coin, submission, options) { 65 | return new Promise(resolve => this.client.emit('submitNonce', coin, submission, options, resolve)); 66 | } 67 | } 68 | 69 | module.exports = new FoxyPoolGateway(); 70 | -------------------------------------------------------------------------------- /lib/services/latest-version-service.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | const superagent = require('superagent'); 3 | const runningVersion = require('../version'); 4 | const eventBus = require('./event-bus'); 5 | 6 | class LatestVersionService { 7 | constructor() { 8 | this.repo = 'felixbrucker/foxy-proxy'; 9 | this.latestVersion = null; 10 | this.changelog = null; 11 | } 12 | 13 | async init() { 14 | await this.updateLatestVersion(); 15 | setInterval(this.updateLatestVersion.bind(this), 30 * 60 * 1000); 16 | } 17 | 18 | async updateLatestVersion() { 19 | try { 20 | const {body: data} = await superagent.get(`https://api.github.com/repos/${this.repo}/releases`).set('User-Agent', `Foxy-Proxy ${runningVersion}`); 21 | const validReleases = data.filter(release => semver.valid(release.tag_name)).sort((v1, v2) => semver.compare(v2.tag_name, v1.tag_name)); 22 | const newReleases = validReleases.filter(release => semver.gt(release.tag_name, runningVersion)); 23 | 24 | if (this.latestVersion === validReleases[0].tag_name) { 25 | return; 26 | } 27 | 28 | this.latestVersion = validReleases[0].tag_name; 29 | this.changelog = validReleases[0].body.replace('Changelog:\n', '').split('\n'); 30 | if (newReleases.length > 0) { 31 | this.changelog = newReleases.reduce((acc, curr) => acc.concat(curr.body.replace('Changelog:\n', '').split('\n')), []); 32 | } 33 | if (this.latestVersion === runningVersion) { 34 | return; 35 | } 36 | eventBus.publish('version/new', this.latestVersion); 37 | } catch (err) { 38 | const errorText = err.response ? err.response.error ? err.response.error.text : '' : ''; 39 | eventBus.publish('log/debug', `Latest-Version-Service | Failed checking for latest version: ${errorText}${err.stack}`); 40 | } 41 | } 42 | 43 | getLatestVersion() { 44 | return this.latestVersion; 45 | } 46 | 47 | getChangelog() { 48 | return this.changelog; 49 | } 50 | } 51 | 52 | module.exports = new LatestVersionService(); 53 | -------------------------------------------------------------------------------- /lib/services/logger.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const moment = require('moment'); 3 | const rfs = require('rotating-file-stream'); 4 | const eventBus = require('./event-bus'); 5 | const store = require('./store'); 6 | 7 | class Logger { 8 | static getLogLevelNumber(logLevel) { 9 | switch (logLevel) { 10 | case 'trace': return 1; 11 | case 'debug': return 2; 12 | case 'info': return 3; 13 | case 'error': return 4; 14 | } 15 | } 16 | 17 | constructor() { 18 | eventBus.subscribe('log/info', (msg) => this.onLogs('info', msg)); 19 | eventBus.subscribe('log/debug', (msg) => this.onLogs('debug', msg)); 20 | eventBus.subscribe('log/trace', (msg) => this.onLogs('trace', msg)); 21 | eventBus.subscribe('log/error', (msg) => this.onLogs('error', msg)); 22 | } 23 | 24 | onLogs(logLevel, msg) { 25 | if (store.getUseLiveDashboard()) { 26 | return; 27 | } 28 | if (Logger.getLogLevelNumber(store.logging.level) > Logger.getLogLevelNumber(logLevel)) { 29 | return; 30 | } 31 | const logLine = `${moment().format('YYYY-MM-DD HH:mm:ss.SSS')} [${logLevel.toUpperCase()}] | ${msg}`; 32 | if (this.logWriter) { 33 | this.logWriter.write(`${logLine}\n`); 34 | } 35 | switch (logLevel) { 36 | case 'trace': 37 | case 'debug': 38 | console.log(store.getUseColors() ? chalk.grey(logLine) : logLine); 39 | break; 40 | case 'info': 41 | console.log(logLine); 42 | break; 43 | case 'error': 44 | console.error(store.getUseColors() ? chalk.red(logLine) : logLine); 45 | break; 46 | } 47 | } 48 | 49 | enableFileLogging() { 50 | if (this.logWriter) { 51 | return; 52 | } 53 | 54 | const loggerOptions = { 55 | size: '10M', 56 | interval: '1d', 57 | path: store.logging.dir, 58 | }; 59 | if (store.logging.maxFiles) { 60 | loggerOptions.maxFiles = store.logging.maxFiles; 61 | } 62 | 63 | this.logWriter = rfs.createStream(Logger.logFileGenerator, loggerOptions); 64 | } 65 | 66 | static logFileGenerator(time, index) { 67 | const fileName = 'proxy.log'; 68 | if (!time) { 69 | return fileName; 70 | } 71 | 72 | return `${moment(time).format('YYYY-MM-DD')}-${index}-${fileName}`; 73 | } 74 | } 75 | 76 | module.exports = new Logger(); 77 | -------------------------------------------------------------------------------- /lib/services/mail-service.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const nodemailer = require('nodemailer'); 3 | const moment = require('moment'); 4 | const eventBus = require('./event-bus'); 5 | const store = require('./store'); 6 | 7 | class MailService { 8 | async init() { 9 | this.mailSettings = store.getMailSettings(); 10 | if (!this.mailSettings) { 11 | return; 12 | } 13 | 14 | if (!this.validateMailSettings()) { 15 | return; 16 | } 17 | 18 | const options = { 19 | host: this.mailSettings.host, 20 | port: this.mailSettings.port, 21 | secure: this.mailSettings.secure, 22 | auth: { 23 | user: this.mailSettings.user, 24 | pass: this.mailSettings.pass, 25 | }, 26 | }; 27 | if (this.mailSettings.trustAllCertificates) { 28 | options.tls = { 29 | rejectUnauthorized: false, // do not fail on invalid certs 30 | }; 31 | } 32 | 33 | this.transport = nodemailer.createTransport(options); 34 | 35 | const successful = await this.verifyTransport(); 36 | if (!successful) { 37 | this.transport = null; 38 | return; 39 | } 40 | 41 | eventBus.subscribe('miner/online', this.onMinerOnline.bind(this)); 42 | eventBus.subscribe('miner/offline', this.onMinerOffline.bind(this)); 43 | 44 | const startupLine = 'Mail | Initialized'; 45 | eventBus.publish('log/info', store.getUseColors() ? chalk.green(startupLine) : startupLine); 46 | } 47 | 48 | async onMinerOnline(minerId, offlineSince) { 49 | await this.sendMail({ 50 | from: this.mailSettings.mailFrom || this.mailSettings.user, 51 | to: this.mailSettings.mailTo, 52 | subject: `[Foxy-Proxy] ${minerId} has recovered`, 53 | text: `${minerId} has recovered after ${moment(offlineSince).fromNow(true)} of downtime`, 54 | }); 55 | } 56 | 57 | async onMinerOffline(minerId, miner) { 58 | await this.sendMail({ 59 | from: this.mailSettings.mailFrom || this.mailSettings.user, 60 | to: this.mailSettings.mailTo, 61 | subject: `[Foxy-Proxy] ${minerId} looks offline`, 62 | text: `${minerId} seems to be offline.\nLast active: ${moment(miner.lastTimeActive).fromNow()}\nLast active block: ${miner.lastBlockActive}`, 63 | }); 64 | } 65 | 66 | validateMailSettings() { 67 | if (!this.mailSettings.host) { 68 | eventBus.publish('log/error', 'Mail | Validation error: host missing'); 69 | return false; 70 | } 71 | if (!this.mailSettings.port) { 72 | eventBus.publish('log/error', 'Mail | Validation error: port missing'); 73 | return false; 74 | } 75 | if (this.mailSettings.secure === undefined) { 76 | eventBus.publish('log/error', 'Mail | Validation error: useTLS missing'); 77 | return false; 78 | } 79 | if (!this.mailSettings.user) { 80 | eventBus.publish('log/error', 'Mail | Validation error: user missing'); 81 | return false; 82 | } 83 | if (!this.mailSettings.pass) { 84 | eventBus.publish('log/error', 'Mail | Validation error: pass missing'); 85 | return false; 86 | } 87 | if (!this.mailSettings.mailTo) { 88 | eventBus.publish('log/error', 'Mail | Validation error: mailTo missing'); 89 | return false; 90 | } 91 | 92 | return true; 93 | } 94 | 95 | async verifyTransport(){ 96 | try { 97 | await this.transport.verify(); 98 | } catch(err) { 99 | eventBus.publish('log/error', `Mail | Connection Verification failed: ${err.message}`); 100 | return false; 101 | } 102 | 103 | return true; 104 | } 105 | 106 | async sendMail(options) { 107 | let result = null; 108 | try { 109 | result = await this.transport.sendMail(options); 110 | } catch(err) { 111 | eventBus.publish('log/error', `Mail | Sending mail failed: ${err.message}`); 112 | } 113 | 114 | return result; 115 | } 116 | } 117 | 118 | module.exports = new MailService(); 119 | -------------------------------------------------------------------------------- /lib/services/profitability-service.js: -------------------------------------------------------------------------------- 1 | const superagent = require('superagent'); 2 | const CoinGecko = require('./coin-gecko'); 3 | const CoinPaprika = require('./coin-paprika'); 4 | const { BitmartRestApi } = require('bitmart-api'); 5 | 6 | class ProfitabilityService { 7 | constructor() { 8 | this.coinGecko = new CoinGecko(); 9 | this.coinPaprika = new CoinPaprika(); 10 | this.bitmartApi = new BitmartRestApi(); 11 | this.rates = {}; 12 | } 13 | 14 | getBlockReward(miningInfo, coin) { 15 | switch(coin) { 16 | case 'bhd': return this.useEcoBlockRewards ? 4.5 : 14.25; 17 | case 'burst': 18 | const month = Math.floor(miningInfo.height / 10800); 19 | return Math.floor(10000 * Math.pow(95, month) / Math.pow(100, month)); 20 | case 'lhd': return this.useEcoBlockRewards ? 10 : 92; 21 | case 'hdd': return this.useEcoBlockRewards ? 110 : 2200; 22 | case 'xhd': return this.useEcoBlockRewards ? 1500 : 150000; 23 | } 24 | 25 | return 0; 26 | } 27 | 28 | async init(useEcoBlockRewards) { 29 | this.useEcoBlockRewards = useEcoBlockRewards; 30 | await this.updateRates(); 31 | setInterval(this.updateRates.bind(this), 5 * 60 * 1000); 32 | } 33 | 34 | async updateRates() { 35 | try { 36 | const rates = await this.coinGecko.getRates(['bitcoin-hd', 'burst']); 37 | this.rates.bhd = rates['bitcoin-hd'].usd; 38 | this.rates.burst = rates.burst.usd; 39 | } catch (err) {} 40 | 41 | try { 42 | const {quotes: {USD: {price: bhdPriceInUsd}}} = await this.coinPaprika.getRate('bhd-bitcoin-hd'); 43 | this.rates.bhd = bhdPriceInUsd; 44 | } catch (err) {} 45 | 46 | try { 47 | const tickerHDD = await this.bitmartApi.getTicker('HDD_BHD'); 48 | this.rates.hdd = parseFloat(tickerHDD.current_price) * (this.rates.bhd || 0); 49 | } catch (err) {} 50 | 51 | try { 52 | const tickerXHD = await this.bitmartApi.getTicker('XHD_BHD'); 53 | this.rates.xhd = parseFloat(tickerXHD.current_price) * (this.rates.bhd || 0); 54 | } catch (err) {} 55 | 56 | try { 57 | const tickerLHD = await this.bitmartApi.getTicker('LHD_BHD'); 58 | this.rates.lhd = parseFloat(tickerLHD.current_price) * (this.rates.bhd || 0); 59 | } catch (err) {} 60 | } 61 | 62 | getRate(symbol) { 63 | return this.rates[symbol]; 64 | } 65 | 66 | getProfitability(miningInfo, coin, blockReward) { 67 | const rate = this.getRate(coin); 68 | if (!rate) { 69 | return 0; 70 | } 71 | 72 | if (!blockReward) { 73 | blockReward = this.getBlockReward(miningInfo, coin); 74 | } 75 | 76 | return Math.round((Math.pow(1024, 2) / miningInfo.modifiedNetDiff) * 100 * blockReward * rate); 77 | } 78 | } 79 | 80 | module.exports = new ProfitabilityService(); 81 | -------------------------------------------------------------------------------- /lib/services/round-populator.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | const cache = require('./cache'); 4 | const database = require('../../models'); 5 | const eventBus = require('./event-bus'); 6 | 7 | class RoundPopulator { 8 | constructor() { 9 | this.blockInfoUnavailable = new Map(); 10 | } 11 | 12 | async populateRound(upstream, height) { 13 | const lastUnavailable = this.blockInfoUnavailable.get(upstream.fullUpstreamName); 14 | if (lastUnavailable && moment().diff(lastUnavailable, 'minutes') < 5) { 15 | return; 16 | } 17 | 18 | eventBus.publish('log/debug', `Round-Populator | ${upstream.fullUpstreamNameLogs} | Populating round ${height}`); 19 | const blockInfo = await upstream.getBlockInfo(height); 20 | if (!blockInfo) { 21 | this.blockInfoUnavailable.set(upstream.fullUpstreamName, new Date()); 22 | } 23 | if (!blockInfo || !blockInfo.hash || !blockInfo.plotterId) { 24 | return; 25 | } 26 | if (this.blockInfoUnavailable.has(upstream.fullUpstreamName)) { 27 | this.blockInfoUnavailable.delete(upstream.fullUpstreamName); 28 | } 29 | const round = await cache.ensureRoundIsCached(upstream, height); 30 | round.blockHash = blockInfo.hash; 31 | const activePlotter = await upstream.getActivePlotter(round.blockHeight); 32 | round.roundWon = activePlotter.some(plotter => plotter.pid === blockInfo.plotterId); 33 | 34 | await cache.saveEntity(round); 35 | } 36 | 37 | async populateUnpopulatedRounds(upstream, maxHeight) { 38 | const lastUnavailable = this.blockInfoUnavailable.get(upstream.fullUpstreamName); 39 | if (lastUnavailable && moment().diff(lastUnavailable, 'minutes') < 5) { 40 | return; 41 | } 42 | 43 | const unpopulatedRounds = await database().round.findAll({ 44 | where: { 45 | upstream: upstream.fullUpstreamName, 46 | [database().Op.or]: [{ 47 | roundWon: null, 48 | }, { 49 | blockHash: null, 50 | }], 51 | blockHeight: { 52 | [database().Op.lt]: maxHeight, 53 | }, 54 | }, 55 | order: [ 56 | ['blockHeight', 'ASC'], 57 | ], 58 | }); 59 | for (let round of unpopulatedRounds) { 60 | await this.populateRound(upstream, round.blockHeight); 61 | } 62 | } 63 | } 64 | 65 | module.exports = new RoundPopulator(); 66 | -------------------------------------------------------------------------------- /lib/services/self-update-service.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require('fs'); 2 | const { resolve } = require('path'); 3 | const spawn = require('cross-spawn'); 4 | const eventBus = require('./event-bus'); 5 | const database = require('../../models'); 6 | const store = require('./store'); 7 | 8 | class SelfUpdateService { 9 | constructor() { 10 | eventBus.subscribe('version/update', this.update.bind(this)); 11 | this.rootDir = resolve(`${__dirname}/../../`); 12 | } 13 | 14 | detectInstallMethod() { 15 | const gitPath = resolve(`${this.rootDir}/.git`); 16 | if (existsSync(gitPath)) { 17 | return 'git'; 18 | } 19 | if (store.isInstalledGlobally()) { 20 | return 'npm'; 21 | } 22 | 23 | return null; 24 | } 25 | 26 | async update() { 27 | const installMethod = this.detectInstallMethod(); 28 | let result = null; 29 | switch (installMethod) { 30 | case 'git': 31 | result = await this.updateUsingGit(); 32 | break; 33 | case 'npm': 34 | result = await this.updateUsingNpm(); 35 | break; 36 | default: 37 | eventBus.publish('log/error', 'SelfUpdater | Could not determine install method, no automatic update possible!'); 38 | return; 39 | } 40 | if (!result) { 41 | return; 42 | } 43 | if (typeof process.send !== 'function') { 44 | return; 45 | } 46 | eventBus.publish('log/info', 'SelfUpdater | Successfully updated, restarting now ..'); 47 | await database().sequelize.close(); 48 | process.send('restart'); 49 | } 50 | 51 | async updateUsingGit() { 52 | let git = spawn('git', ['checkout', 'package-lock.json'], { 53 | cwd: this.rootDir, 54 | stdio: 'pipe', 55 | }); 56 | git.stdout.on('data', (data) => eventBus.publish('log/info', `SelfUpdater | GIT ==> ${data.toString().trim()}`)); 57 | git.stderr.on('data', (data) => eventBus.publish('log/error', `SelfUpdater | GIT ==> ${data.toString().trim()}`)); 58 | let success = await new Promise((resolve) => { 59 | git.on('close', (code) => resolve(code === 0)); 60 | }); 61 | if (!success) { 62 | eventBus.publish('log/error', `SelfUpdater | GIT ==> Error reverting package-lock.json`); 63 | return; 64 | } 65 | git = spawn('git', ['pull'], { 66 | cwd: this.rootDir, 67 | stdio: 'pipe', 68 | }); 69 | git.stdout.on('data', (data) => eventBus.publish('log/info', `SelfUpdater | GIT ==> ${data.toString().trim()}`)); 70 | git.stderr.on('data', (data) => eventBus.publish('log/error', `SelfUpdater | GIT ==> ${data.toString().trim()}`)); 71 | success = await new Promise((resolve) => { 72 | git.on('close', (code) => resolve(code === 0)); 73 | }); 74 | if (!success) { 75 | eventBus.publish('log/error', `SelfUpdater | GIT ==> Error pulling`); 76 | return; 77 | } 78 | const npm = spawn('npm', ['update', '--no-save'], { 79 | cwd: this.rootDir, 80 | stdio: 'pipe', 81 | }); 82 | npm.stdout.on('data', (data) => eventBus.publish('log/info', `SelfUpdater | NPM ==> ${data.toString().trim()}`)); 83 | npm.stderr.on('data', (data) => eventBus.publish('log/error', `SelfUpdater | NPM ==> ${data.toString().trim()}`)); 84 | success = await new Promise((resolve) => { 85 | npm.on('close', (code) => resolve(code === 0)); 86 | }); 87 | if (!success) { 88 | eventBus.publish('log/error', `SelfUpdater | NPM ==> Error installing dependencies`); 89 | return; 90 | } 91 | 92 | return true; 93 | } 94 | 95 | async updateUsingNpm() { 96 | const npm = spawn('npm', ['update', '-g', 'foxy-proxy'], { 97 | stdio: 'pipe', 98 | }); 99 | npm.stdout.on('data', (data) => eventBus.publish('log/info', `SelfUpdater | NPM ==> ${data.toString().trim()}`)); 100 | npm.stderr.on('data', (data) => eventBus.publish('log/error', `SelfUpdater | NPM ==> ${data.toString().trim()}`)); 101 | const success = await new Promise((resolve) => { 102 | npm.on('close', (code) => resolve(code === 0)); 103 | }); 104 | if (!success) { 105 | eventBus.publish('log/error', `SelfUpdater | NPM ==> Error updating`); 106 | return; 107 | } 108 | 109 | return true; 110 | } 111 | } 112 | 113 | module.exports = new SelfUpdateService(); 114 | -------------------------------------------------------------------------------- /lib/services/store.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | 3 | class Store { 4 | constructor() { 5 | this.configFilePath = util.detectFilePath('config.yaml'); 6 | this.dbFilePath = util.detectFilePath('db.sqlite'); 7 | this.useLiveDashboard = false; 8 | this.proxies = []; 9 | this.logging = { 10 | level: 'info', 11 | dir: null, 12 | maxFiles: null, 13 | }; 14 | this.useColors = true; 15 | this._isInstalledGlobally = false; 16 | } 17 | 18 | setConfigFilePath(filePath) { 19 | this.configFilePath = filePath; 20 | } 21 | 22 | setDbFilePath(filePath) { 23 | this.dbFilePath = filePath; 24 | } 25 | 26 | getConfigFilePath() { 27 | return this.configFilePath; 28 | } 29 | 30 | getDbFilePath() { 31 | return this.dbFilePath; 32 | } 33 | 34 | getUseLiveDashboard() { 35 | return this.useLiveDashboard; 36 | } 37 | 38 | setUseLiveDashboard(useLiveDashboard) { 39 | this.useLiveDashboard = useLiveDashboard; 40 | } 41 | 42 | getProxies() { 43 | return this.proxies; 44 | } 45 | 46 | setProxies(proxies) { 47 | this.proxies = proxies; 48 | } 49 | 50 | getMailSettings() { 51 | return this.mailSettings; 52 | } 53 | 54 | setMailSettings(mailSettings) { 55 | this.mailSettings = mailSettings; 56 | } 57 | 58 | getUseColors() { 59 | return this.useColors; 60 | } 61 | 62 | setUseColors(useColors) { 63 | this.useColors = useColors; 64 | } 65 | 66 | isInstalledGlobally() { 67 | return this._isInstalledGlobally; 68 | } 69 | 70 | setIsInstalledGlobally(isInstalledGlobally) { 71 | this._isInstalledGlobally = isInstalledGlobally; 72 | } 73 | } 74 | 75 | module.exports = new Store(); 76 | -------------------------------------------------------------------------------- /lib/services/submission-processor.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | 3 | const cache = require('./cache'); 4 | const eventBus = require('./event-bus'); 5 | 6 | class SubmissionProcessor { 7 | async processSubmission({upstream, submission, isSubmitted = true}) { 8 | eventBus.publish('log/trace', `Submission-Processor | ${upstream.fullUpstreamNameLogs} | Processing ${isSubmitted ? 'submitted' : 'received'} submission for height ${submission.height} with DL ${submission.deadline}`); 9 | const round = await cache.ensureRoundIsCached(upstream, submission.height); 10 | let roundUpdated = false; 11 | if (round.bestDL === null || (new BigNumber(round.bestDL)).isGreaterThan(submission.deadline)) { 12 | round.bestDL = submission.deadline; 13 | roundUpdated = true; 14 | } 15 | if (isSubmitted && (round.bestDLSubmitted === null || (new BigNumber(round.bestDLSubmitted)).isGreaterThan(submission.deadline))) { 16 | round.bestDLSubmitted = submission.deadline; 17 | roundUpdated = true; 18 | } 19 | if (roundUpdated) { 20 | cache.roundWasUpdated(round); 21 | } 22 | 23 | const plotter = await cache.ensurePlotterIsCached(upstream, submission.accountId); 24 | let plotterUpdated = false; 25 | if (!plotter.lastSubmitHeight || plotter.lastSubmitHeight < submission.height) { 26 | plotter.lastSubmitHeight = submission.height; 27 | plotterUpdated = true; 28 | } 29 | 30 | if (plotterUpdated) { 31 | await cache.saveEntity(plotter); 32 | } 33 | } 34 | } 35 | 36 | module.exports = new SubmissionProcessor(); 37 | -------------------------------------------------------------------------------- /lib/services/usage-statistics-service.js: -------------------------------------------------------------------------------- 1 | const ua = require('universal-analytics'); 2 | const { v4: uuidv4 } = require('uuid'); 3 | const database = require('../../models'); 4 | 5 | class UsageStatisticsService { 6 | static async getUUID() { 7 | const [config] = await database().config.findOrCreate({ 8 | where: { 9 | id: 1, 10 | }, 11 | defaults: { 12 | id: 1, 13 | uuid: uuidv4(), 14 | }, 15 | }); 16 | 17 | return config.uuid; 18 | } 19 | 20 | async init() { 21 | const uuid = await UsageStatisticsService.getUUID(); 22 | this.client = ua('UA-119575195-14', uuid); 23 | this.keepaliveInterval = setInterval(this.sendKeepalive.bind(this), 30 * 1000); 24 | } 25 | 26 | sendKeepalive() { 27 | this.client.event('util', 'keepalive').send(); 28 | } 29 | } 30 | 31 | module.exports = new UsageStatisticsService(); 32 | -------------------------------------------------------------------------------- /lib/startup-message.js: -------------------------------------------------------------------------------- 1 | const outputUtil = require('./output-util'); 2 | 3 | module.exports = () => { 4 | console.log(` ${outputUtil.getString('______ ______ __ __ __ __', '#ff4f19')} ${outputUtil.getString('______ ______ ______ __ __ __ __', '#ff7a53')} \n` + 5 | `${outputUtil.getString('/\\ ___\\/\\ __ \\ /\\_\\_\\_\\ /\\ \\_\\ \\', '#ff4f19')} ${outputUtil.getString('/\\ == \\/\\ == \\ /\\ __ \\ /\\_\\_\\_\\ /\\ \\_\\ \\', '#ff7a53')} \n` + 6 | `${outputUtil.getString('\\ \\ __\\\\ \\ \\/\\ \\\\/_/\\_\\/_\\ \\____ \\', '#ff4f19')} ${outputUtil.getString('\\ \\ _-/\\ \\ __< \\ \\ \\/\\ \\\\/_/\\_\\/_\\ \\____ \\', '#ff7a53')} \n` + 7 | ` ${outputUtil.getString('\\ \\_\\ \\ \\_____\\ /\\_\\/\\_\\\\/\\_____\\', '#ff4f19')} ${outputUtil.getString('\\ \\_\\ \\ \\_\\ \\_\\\\ \\_____\\ /\\_\\/\\_\\\\/\\_____\\', '#ff7a53')} \n` + 8 | ` ${outputUtil.getString('\\/_/ \\/_____/ \\/_/\\/_/ \\/_____/', '#ff4f19')} ${outputUtil.getString('\\/_/ \\/_/ /_/ \\/_____/ \\/_/\\/_/ \\/_____/', '#ff7a53')}\n\n` + 9 | ` ${outputUtil.getString('BHD: 33fKEwAHxVwnrhisREFdSNmZkguo76a2ML', '#f99320')}\n` + 10 | ` ${outputUtil.getString('BURST: BURST-BVUD-7VWE-HD7F-6RX4P', '#00579d')}\n` + 11 | ` ${outputUtil.getString('ETH: 0xfEc6F48633A7c557b4ac5c37B4519C55CD701BEF', '#ecf0f1')}\n` + 12 | ` ${outputUtil.getString('BTC: 14rbdLr2YXDkguVaqRKnPftTPX52tnv2x2', '#f2a900')}\n`); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/submission.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | 3 | module.exports = class Submission { 4 | constructor(accountId, height, nonce, deadline, secretPhrase = null) { 5 | this._accountId = accountId; 6 | this._height = parseInt(height, 10); 7 | this._nonce = new BigNumber(nonce); 8 | this._deadline = new BigNumber(deadline); 9 | this._secretPhrase = secretPhrase; 10 | } 11 | 12 | isValid() { 13 | return this.accountId !== '' && !isNaN(this.height) && !this.nonce.isNaN() && (!this.deadline.isNaN() || this.secretPhrase); 14 | } 15 | 16 | get accountId() { 17 | return this._accountId; 18 | } 19 | 20 | get height() { 21 | return this._height; 22 | } 23 | 24 | get deadline() { 25 | return this._deadline; 26 | } 27 | 28 | get nonce() { 29 | return this._nonce; 30 | } 31 | 32 | get secretPhrase() { 33 | return this._secretPhrase; 34 | } 35 | 36 | toObject() { 37 | const obj = { 38 | accountId: this.accountId, 39 | height: this.height, 40 | nonce: this.nonce.toString(), 41 | }; 42 | if (this.secretPhrase) { 43 | obj.secretPhrase = this.secretPhrase; 44 | } else { 45 | obj.deadline = this.deadline.toString() 46 | } 47 | 48 | return obj; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /lib/transports/http-multiple-ports.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('koa-bodyparser'); 2 | const chalk = require('chalk'); 3 | const http = require('http'); 4 | const Koa = require('koa'); 5 | const Router = require('koa-router'); 6 | const eventBus = require('../services/event-bus'); 7 | const store = require('../services/store'); 8 | const outputUtil = require('../output-util'); 9 | const HttpTransportMixin = require('./http-transport-mixin'); 10 | 11 | class HttpMultiplePortsTransport extends HttpTransportMixin { 12 | constructor(listenHost, listenPortStart) { 13 | super(); 14 | this.listenHost = listenHost; 15 | this.listenPortStart = listenPortStart; 16 | } 17 | 18 | addProxies(proxies) { 19 | this.proxies = proxies.map(proxy => { 20 | const result = { 21 | proxy, 22 | }; 23 | 24 | const localApp = new Koa(); 25 | localApp.on('error', err => { 26 | eventBus.publish('log/error', `${outputUtil.getName(proxy.proxyConfig)} | Error: ${err.message}`); 27 | }); 28 | const localRouter = new Router(); 29 | localApp.use(bodyParser()); 30 | const endpointWithScanTime = '/:maxScanTime'; 31 | localRouter.get('/burst', (ctx) => HttpTransportMixin.handleGet(ctx, proxy)); 32 | localRouter.post('/burst', (ctx) => HttpTransportMixin.handlePost(ctx, proxy)); 33 | localRouter.get(`${endpointWithScanTime}/burst`, (ctx) => HttpTransportMixin.handleGet(ctx, proxy)); 34 | localRouter.post(`${endpointWithScanTime}/burst`, (ctx) => HttpTransportMixin.handlePost(ctx, proxy)); 35 | localApp.use(localRouter.routes()); 36 | localApp.use(localRouter.allowedMethods()); 37 | const localServer = http.createServer(localApp.callback()); 38 | const listenPort = this.listenPortStart + proxy.proxyConfig.index + 1; 39 | const listenAddr = `${this.listenHost}:${listenPort}`; 40 | localServer.listen(listenPort, this.listenHost); 41 | result.server = localServer; 42 | 43 | const startupLine = `${outputUtil.getName(proxy.proxyConfig)} | Proxy configured and reachable via http://${listenAddr}`; 44 | eventBus.publish('log/info', store.getUseColors() ? chalk.cyan(startupLine) : startupLine); 45 | 46 | return result; 47 | }); 48 | } 49 | } 50 | 51 | module.exports = HttpMultiplePortsTransport; 52 | -------------------------------------------------------------------------------- /lib/transports/http-single-port.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const eventBus = require('../services/event-bus'); 3 | const store = require('../services/store'); 4 | const outputUtil = require('../output-util'); 5 | const HttpTransportMixin = require('./http-transport-mixin'); 6 | 7 | class HttpSinglePortTransport extends HttpTransportMixin { 8 | constructor(router, listenAddr) { 9 | super(); 10 | this.router = router; 11 | this.listenAddr = listenAddr; 12 | } 13 | 14 | addProxies(proxies) { 15 | this.proxies = proxies.map(proxy => { 16 | const result = { 17 | proxy, 18 | }; 19 | 20 | const endpoint = `/${encodeURIComponent(proxy.proxyConfig.name.toLowerCase().replace(/ /g, '-'))}`; 21 | this.addEndpointHandler(endpoint, proxy); 22 | let startupLine = `${outputUtil.getName(proxy.proxyConfig)} | Proxy configured and reachable via http://${this.listenAddr}${endpoint}`; 23 | if (proxies.length === 1) { 24 | // Single proxy configured, add a handler for root as well 25 | this.addEndpointHandler('', proxy); 26 | startupLine += ` and http://${this.listenAddr}`; 27 | } 28 | 29 | eventBus.publish('log/info', store.getUseColors() ? chalk.blueBright(startupLine) : startupLine); 30 | 31 | return result; 32 | }); 33 | } 34 | 35 | addEndpointHandler(endpoint, proxy) { 36 | const endpointWithScanTime = `${endpoint}/:maxScanTime`; 37 | this.router.get(`${endpoint}/burst`, (ctx) => HttpTransportMixin.handleGet(ctx, proxy)); 38 | this.router.post(`${endpoint}/burst`, (ctx) => HttpTransportMixin.handlePost(ctx, proxy)); 39 | this.router.get(`${endpointWithScanTime}/burst`, (ctx) => HttpTransportMixin.handleGet(ctx, proxy)); 40 | this.router.post(`${endpointWithScanTime}/burst`, (ctx) => HttpTransportMixin.handlePost(ctx, proxy)); 41 | } 42 | } 43 | 44 | module.exports = HttpSinglePortTransport; 45 | -------------------------------------------------------------------------------- /lib/transports/http-transport-mixin.js: -------------------------------------------------------------------------------- 1 | const eventBus = require('../services/event-bus'); 2 | const outputUtil = require('../output-util'); 3 | 4 | class HttpTransportMixin { 5 | static handleGet(ctx, proxy) { 6 | const maxScanTime = ctx.params.maxScanTime && parseInt(ctx.params.maxScanTime, 10) || null; 7 | const requestType = ctx.query.requestType; 8 | switch (requestType) { 9 | case 'getMiningInfo': 10 | ctx.body = proxy.getMiningInfo(maxScanTime); 11 | break; 12 | default: 13 | eventBus.publish('log/error', `${outputUtil.getName(proxy.proxyConfig)} | unknown requestType ${requestType} with data: ${JSON.stringify(ctx.params)}. Please message this info to the creator of this software.`); 14 | ctx.status = 400; 15 | ctx.body = { 16 | error: { 17 | message: 'unknown request type', 18 | code: 4, 19 | }, 20 | }; 21 | } 22 | } 23 | 24 | static async handlePost(ctx, proxy) { 25 | const maxScanTime = ctx.params.maxScanTime && parseInt(ctx.params.maxScanTime, 10) || null; 26 | const requestType = ctx.query.requestType; 27 | switch (requestType) { 28 | case 'getMiningInfo': 29 | ctx.body = proxy.getMiningInfo(maxScanTime); 30 | break; 31 | case 'submitNonce': 32 | const options = { 33 | ip: ctx.req.headers['x-forwarded-for'] || ctx.request.ip, 34 | maxScanTime: ctx.params.maxScanTime, 35 | minerName: ctx.req.headers['x-minername'] || ctx.req.headers['x-miner'], 36 | userAgent: ctx.req.headers['user-agent'], 37 | miner: ctx.req.headers['x-miner'], 38 | capacity: ctx.req.headers['x-capacity'], 39 | accountKey: ctx.req.headers['x-account'], 40 | payoutAddress: ctx.req.headers['x-account'], 41 | accountName: ctx.req.headers['x-accountname'] || ctx.req.headers['x-mineralias'] || null, 42 | distributionRatio: ctx.req.headers['x-distributionratio'] || null, 43 | color: ctx.req.headers['x-color'] || null, 44 | }; 45 | if (options.minerName) { 46 | options.minerName = decodeURI(options.minerName); 47 | } 48 | if (options.accountName) { 49 | options.accountName = decodeURI(options.accountName); 50 | } 51 | const submissionObj = { 52 | accountId: ctx.query.accountId, 53 | height: ctx.query.blockheight, 54 | nonce: ctx.query.nonce, 55 | deadline: ctx.query.deadline, 56 | secretPhrase: ctx.query.secretPhrase !== '' ? ctx.query.secretPhrase : null, 57 | }; 58 | ctx.body = await proxy.submitNonce(submissionObj, options); 59 | if (ctx.body.error) { 60 | ctx.status = 400; 61 | } 62 | break; 63 | default: 64 | eventBus.publish('log/error', `${outputUtil.getName(proxy.proxyConfig)} | unknown requestType ${requestType} with data: ${JSON.stringify(ctx.params)}. Please message this info to the creator of this software.`); 65 | ctx.status = 400; 66 | ctx.body = { 67 | error: { 68 | message: 'unknown request type', 69 | code: 4, 70 | }, 71 | }; 72 | } 73 | } 74 | } 75 | 76 | module.exports = HttpTransportMixin; 77 | -------------------------------------------------------------------------------- /lib/transports/index.js: -------------------------------------------------------------------------------- 1 | const HttpSinglePortTransport = require('./http-single-port'); 2 | const HttpMultiplePortsTransport = require('./http-multiple-ports'); 3 | const SocketIoTransport = require('./socketio'); 4 | 5 | module.exports = { 6 | HttpSinglePortTransport, 7 | HttpMultiplePortsTransport, 8 | SocketIoTransport, 9 | }; -------------------------------------------------------------------------------- /lib/transports/socketio.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const eventBus = require('../services/event-bus'); 3 | const store = require('../services/store'); 4 | const outputUtil = require('../output-util'); 5 | const CurrentRoundManager = require('../currentRoundManager'); 6 | 7 | class SocketIoTransport { 8 | constructor(io, listenAddr) { 9 | this.io = io; 10 | this.listenAddr = listenAddr; 11 | } 12 | 13 | addProxies(proxies) { 14 | this.proxies = proxies.map(proxy => { 15 | const result = { 16 | proxy, 17 | }; 18 | 19 | const endpoint = `/${encodeURIComponent(proxy.proxyConfig.name.toLowerCase().replace(/ /g, '-'))}`; 20 | const localIo = this.io.of(endpoint); 21 | localIo.on('connection', (socket) => { 22 | let maxScanTime = proxy.maxScanTime; 23 | const handleNewRound = (miningInfo) => { 24 | socket.emit('miningInfo', miningInfo); 25 | }; 26 | proxy.currentRoundManagers[maxScanTime].eventEmitter.on('new-round', handleNewRound); 27 | 28 | socket.on('setMaxScanTime', newMaxScanTime => { 29 | if (!proxy.currentRoundManagers[newMaxScanTime]) { 30 | proxy.currentRoundManagers[newMaxScanTime] = new CurrentRoundManager(newMaxScanTime, proxy.currentRoundEmitter); 31 | proxy.currentRoundManagers[newMaxScanTime].copyRoundsFromManager(proxy.currentRoundManager); 32 | } 33 | proxy.currentRoundManagers[maxScanTime].eventEmitter.removeListener('new-round', handleNewRound); 34 | maxScanTime = newMaxScanTime; 35 | proxy.currentRoundManagers[maxScanTime].eventEmitter.on('new-round', handleNewRound); 36 | }); 37 | 38 | socket.on('getMiningInfo', async (cb) => { 39 | cb(proxy.getMiningInfo(maxScanTime)); 40 | }); 41 | 42 | socket.on('submitNonce', async (submission, options, cb) => { 43 | options.ip = socket.conn.remoteAddress; 44 | const res = await proxy.submitNonce(submission, options); 45 | cb(res); 46 | }); 47 | 48 | socket.on('disconnect', () => { 49 | proxy.currentRoundManagers[maxScanTime].eventEmitter.removeListener('new-round', handleNewRound); 50 | }); 51 | }); 52 | 53 | const startupLine = `${outputUtil.getName(proxy.proxyConfig)} | Proxy configured and reachable via http://${this.listenAddr}${endpoint} (socket.io)`; 54 | eventBus.publish('log/info', store.getUseColors() ? chalk.cyan(startupLine) : startupLine); 55 | 56 | return result; 57 | }); 58 | } 59 | } 60 | 61 | module.exports = SocketIoTransport; 62 | -------------------------------------------------------------------------------- /lib/upstream/base.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const connectionQualityMixin = require('./mixins/connection-quality-mixin'); 3 | const estimatedCapacityMixin = require('./mixins/estimated-capacity-mixin'); 4 | const statsMixin = require('./mixins/stats-mixin'); 5 | const submitProbabilityMixin = require('./mixins/submit-probability-mixin'); 6 | const configMixin = require('./mixins/config-mixin'); 7 | const cliColorMixin = require('./mixins/cli-color-mixin'); 8 | 9 | module.exports = cliColorMixin(configMixin(submitProbabilityMixin(statsMixin(estimatedCapacityMixin(connectionQualityMixin( 10 | EventEmitter 11 | )))))); -------------------------------------------------------------------------------- /lib/upstream/foxy-pool-multi.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base'); 2 | const foxyPoolGateway = require('../services/foxy-pool-gateway'); 3 | 4 | const { hostname } = require('os'); 5 | const eventBus = require('../services/event-bus'); 6 | const MiningInfo = require('../miningInfo'); 7 | const util = require('./util'); 8 | const version = require('../version'); 9 | const outputUtil = require('../output-util'); 10 | 11 | class FoxyPoolMulti extends Base { 12 | constructor(upstreamConfig, miners, proxyConfig) { 13 | super(); 14 | this.proxyConfig = proxyConfig; 15 | this.fullUpstreamName = `${proxyConfig.name}/${upstreamConfig.name}`; 16 | this.fullUpstreamNameLogs = outputUtil.getFullUpstreamNameLogs(proxyConfig, upstreamConfig); 17 | this.isBHD = upstreamConfig.isBHD; 18 | this.upstreamConfig = upstreamConfig; 19 | this.upstreamConfig.mode = 'pool'; 20 | this.historicalRoundsToKeep = this.upstreamConfig.historicalRoundsToKeep || (this.isBHD ? 288 : 360) * 2; 21 | this.userAgent = `Foxy-Proxy ${version}`; 22 | this.miningInfo = {height: 0, toObject: () => ({height: 0})}; 23 | this.deadlines = {}; 24 | this.miners = miners; 25 | this.roundStart = new Date(); 26 | this.accountName = this.upstreamConfig.accountName || this.upstreamConfig.minerAlias || this.upstreamConfig.accountAlias; 27 | if (this.upstreamConfig.targetDL === undefined) { 28 | this.upstreamConfig.targetDL = 31536000; 29 | } 30 | } 31 | 32 | async init() { 33 | await super.init(); 34 | this.coin = this.upstreamConfig.coin.toUpperCase(); 35 | this.connected = false; 36 | 37 | foxyPoolGateway.onConnectionStateChange(() => { 38 | this.connected = foxyPoolGateway.connected; 39 | }); 40 | foxyPoolGateway.onNewMiningInfo(this.coin, this.onNewMiningInfo.bind(this)); 41 | 42 | const miningInfo = await foxyPoolGateway.getMiningInfo(this.coin); 43 | await this.onNewMiningInfo(miningInfo); 44 | } 45 | 46 | async onNewMiningInfo(para) { 47 | if (this.upstreamConfig.sendTargetDL) { 48 | para.targetDeadline = this.upstreamConfig.sendTargetDL; 49 | } 50 | const miningInfo = new MiningInfo(para.height, para.baseTarget, para.generationSignature, para.targetDeadline, this.upstreamConfig.coin); 51 | if (this.miningInfo && this.miningInfo.height === miningInfo.height && this.miningInfo.baseTarget === miningInfo.baseTarget) { 52 | return; 53 | } 54 | 55 | if (this.useSubmitProbability) { 56 | this.updateDynamicTargetDL(miningInfo); 57 | } 58 | 59 | await this.createOrUpdateRound({ miningInfo }); 60 | const isFork = miningInfo.height === this.miningInfo.height && miningInfo.baseTarget !== this.miningInfo.baseTarget; 61 | const oldMiningInfo = this.miningInfo; 62 | 63 | this.deadlines = {}; 64 | this.roundStart = new Date(); 65 | this.miningInfo = miningInfo; 66 | this.emit('new-round', miningInfo); 67 | eventBus.publish('stats/current-round', this.fullUpstreamName, this.getCurrentRoundStats()); 68 | let newBlockLine = `${this.fullUpstreamNameLogs} | ${outputUtil.getString(`New block ${outputUtil.getString(miningInfo.height, this.newBlockColor)}, baseTarget ${outputUtil.getString(miningInfo.baseTarget, this.newBlockBaseTargetColor)}, netDiff ${outputUtil.getString(miningInfo.modifiedNetDiffFormatted, this.newBlockNetDiffColor)}`, this.newBlockLineColor)}`; 69 | if (miningInfo.targetDeadline) { 70 | newBlockLine += outputUtil.getString(`, targetDeadline: ${outputUtil.getString(miningInfo.targetDeadline, this.newBlockTargetDeadlineColor)}`, this.newBlockLineColor); 71 | } 72 | eventBus.publish('log/info', newBlockLine); 73 | 74 | if (isFork) { 75 | return; 76 | } 77 | 78 | await this.onRoundEnded({ oldMiningInfo }); 79 | eventBus.publish('stats/historical', this.fullUpstreamName, await this.getHistoricalStats()); 80 | } 81 | 82 | async submitNonce(submission, minerSoftware, options) { 83 | let minerSoftwareName = this.userAgent; 84 | if (this.upstreamConfig.sendMiningSoftwareName) { 85 | minerSoftwareName += ` | ${minerSoftware}`; 86 | } 87 | let minerName = this.upstreamConfig.minerName || hostname(); 88 | let capacity = this.totalCapacity; 89 | if (this.upstreamConfig.minerPassthrough) { 90 | if (options.capacity) { 91 | capacity = options.capacity; 92 | } 93 | if (options.minerName) { 94 | minerName = options.minerName; 95 | } 96 | } 97 | const optionsToSubmit = { 98 | minerName, 99 | userAgent: minerSoftwareName, 100 | capacity, 101 | payoutAddress: this.upstreamConfig.payoutAddress || this.upstreamConfig.accountKey, 102 | accountName: this.accountName || options.accountName || null, 103 | distributionRatio: options.distributionRatio || this.upstreamConfig.distributionRatio || null, 104 | }; 105 | const result = await foxyPoolGateway.submitNonce(this.coin, submission.toObject(), optionsToSubmit); 106 | 107 | return { 108 | error: null, 109 | result, 110 | }; 111 | } 112 | 113 | getMiningInfo() { 114 | return this.miningInfo.toObject(); 115 | } 116 | } 117 | 118 | module.exports = FoxyPoolMulti; 119 | -------------------------------------------------------------------------------- /lib/upstream/foxypool.js: -------------------------------------------------------------------------------- 1 | const SocketIo = require('./socketio'); 2 | 3 | class FoxyPool extends SocketIo { 4 | async init() { 5 | if (!this.upstreamConfig.url) { 6 | this.upstreamConfig.url = 'http://miner.bhd.foxypool.io/mining'; 7 | } 8 | if (this.upstreamConfig.url.endsWith('/')) { 9 | this.upstreamConfig.url = this.upstreamConfig.url.slice(0, -1); 10 | } 11 | if (!this.upstreamConfig.url.endsWith('/mining')) { 12 | this.upstreamConfig.url += '/mining'; 13 | } 14 | if (this.upstreamConfig.targetDL === undefined) { 15 | this.upstreamConfig.targetDL = 31536000; 16 | } 17 | this.upstreamConfig.mode = 'pool'; 18 | this.isBHD = this.isBHD === undefined ? true : this.isBHD; 19 | await super.init(); 20 | } 21 | } 22 | 23 | module.exports = FoxyPool; 24 | -------------------------------------------------------------------------------- /lib/upstream/mixins/cli-color-mixin.js: -------------------------------------------------------------------------------- 1 | module.exports = (upstreamClass) => class CliColorMixin extends upstreamClass { 2 | async init() { 3 | this.newBlockLineColor = this.upstreamConfig.newBlockLineColor || 'green'; 4 | this.newBlockColor = this.upstreamConfig.newBlockColor || null; 5 | this.newBlockBaseTargetColor = this.upstreamConfig.newBlockBaseTargetColor || null; 6 | this.newBlockNetDiffColor = this.upstreamConfig.newBlockNetDiffColor || null; 7 | this.newBlockTargetDeadlineColor = this.upstreamConfig.newBlockTargetDeadlineColor || null; 8 | this.minerColor = this.upstreamConfig.minerColor || null; 9 | this.accountColor = this.upstreamConfig.accountColor || null; 10 | this.accountColors = this.upstreamConfig.accountColors || {}; 11 | this.deadlineColor = this.upstreamConfig.deadlineColor || null; 12 | if (super.init) { 13 | await super.init(); 14 | } 15 | } 16 | }; -------------------------------------------------------------------------------- /lib/upstream/mixins/config-mixin.js: -------------------------------------------------------------------------------- 1 | module.exports = (upstreamClass) => class ConfigMixin extends upstreamClass { 2 | getCapacityForAccountId(accountId) { 3 | if (!accountId) { 4 | return this.totalCapacity; 5 | } 6 | 7 | if (this.upstreamConfig.capacityForAccountId && this.upstreamConfig.capacityForAccountId[accountId]) { 8 | return parseInt(this.upstreamConfig.capacityForAccountId[accountId], 10); 9 | } 10 | 11 | return this.totalCapacity; 12 | } 13 | }; -------------------------------------------------------------------------------- /lib/upstream/mixins/connection-quality-mixin.js: -------------------------------------------------------------------------------- 1 | const eventBus = require('../../services/event-bus'); 2 | 3 | module.exports = (upstreamClass) => class ConnectionQualityMixin extends upstreamClass { 4 | constructor() { 5 | super(); 6 | this.connectionQuality = 100; 7 | this.connected = true; 8 | this.smoothedConnectionState = this.connected; 9 | this.prevConnectionState = this.connected; 10 | this.connectionStateCounter = 0; 11 | this.connectionOutageCounterThreshold = 2; 12 | setInterval(this.updateConnectionQuality.bind(this), 1000); 13 | setInterval(this.detectConnectionOutage.bind(this), 1000); 14 | } 15 | 16 | async init() { 17 | const updateMiningInfoInterval = this.upstreamConfig.updateMiningInfoInterval ? this.upstreamConfig.updateMiningInfoInterval : 1000; 18 | this.connectionOutageCounterThreshold = Math.round(updateMiningInfoInterval / 1000) * 2; 19 | if (super.init) { 20 | await super.init(); 21 | } 22 | } 23 | 24 | updateConnectionQuality() { 25 | const prevValue = this.connectionQuality; 26 | if (this.connected) { 27 | this.connectionQuality += 0.01; 28 | } else { 29 | this.connectionQuality -= 0.01; 30 | } 31 | 32 | this.connectionQuality = Math.min(this.connectionQuality, 100); 33 | this.connectionQuality = Math.max(this.connectionQuality, 0); 34 | 35 | if (prevValue === this.connectionQuality) { 36 | return; 37 | } 38 | 39 | eventBus.publish('stats/connection-stats', this.fullUpstreamName, this.getConnectionStats()); 40 | } 41 | 42 | detectConnectionOutage() { 43 | this.prevConnectionState = this.smoothedConnectionState; 44 | 45 | if (this.connected) { 46 | this.smoothedConnectionState = true; 47 | this.connectionStateCounter = 0; 48 | } else if (this.smoothedConnectionState !== this.connected) { 49 | this.connectionStateCounter += 1; 50 | } else { 51 | this.connectionStateCounter = 0; 52 | } 53 | 54 | if (this.connectionStateCounter > this.connectionOutageCounterThreshold) { 55 | this.smoothedConnectionState = this.connected; 56 | this.connectionStateCounter = 0; 57 | } 58 | 59 | if (this.prevConnectionState && !this.smoothedConnectionState) { 60 | eventBus.publish('log/error', `${this.fullUpstreamNameLogs} | Connection outage detected ..`); 61 | } else if (!this.prevConnectionState && this.smoothedConnectionState) { 62 | eventBus.publish('log/error', `${this.fullUpstreamNameLogs} | Connection outage resolved`); 63 | } 64 | } 65 | }; -------------------------------------------------------------------------------- /lib/upstream/mixins/estimated-capacity-mixin.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | const moment = require('moment'); 3 | const database = require('../../../models'); 4 | const coinUtil = require('../../coin-util'); 5 | 6 | module.exports = (upstreamClass) => class EstimatedCapacityMixin extends upstreamClass { 7 | static calculateAlphas(numberOfRounds, minNumberOfRounds) { 8 | return new Array(numberOfRounds).fill().map((x, index) => { 9 | if (index === numberOfRounds - 1) { 10 | return 1; 11 | } 12 | if (index < minNumberOfRounds - 1) { 13 | return 0; 14 | } 15 | const nConf = index + 1; 16 | return 1 - ((numberOfRounds - nConf) / nConf * Math.log(numberOfRounds / (numberOfRounds - nConf))); 17 | }); 18 | } 19 | 20 | async init() { 21 | this.alphas = {}; 22 | this.estimatedCapacityRoundMinimum = 10; 23 | this.estimatedCapacityRoundInterval = this.upstreamConfig.estimatedCapacityRounds || this.upstreamConfig.historicalRoundsToKeep || (this.isBHD ? 288 : 360); 24 | for (let i = this.estimatedCapacityRoundMinimum; i <= this.estimatedCapacityRoundInterval; i += 1) { 25 | this.alphas[i] = EstimatedCapacityMixin.calculateAlphas(i, this.estimatedCapacityRoundMinimum); 26 | } 27 | if (super.init) { 28 | await super.init(); 29 | } 30 | } 31 | 32 | async getEstimatedCapacity(excludeFastBlocks = false) { 33 | const historicalRoundsOriginal = await database().round.findAll({ 34 | where: { 35 | upstream: this.fullUpstreamName, 36 | }, 37 | order: [ 38 | ['blockHeight', 'DESC'], 39 | ], 40 | limit: this.estimatedCapacityRoundInterval, 41 | }); 42 | let historicalRounds = historicalRoundsOriginal; 43 | if (excludeFastBlocks) { 44 | historicalRounds = historicalRoundsOriginal.filter((round, index) => { 45 | if (index === historicalRoundsOriginal.length - 1) { 46 | // last one, skip 47 | return false; 48 | } 49 | const previousRound = historicalRounds[index + 1]; 50 | 51 | return moment(round.createdAt).diff(previousRound.createdAt, 'seconds') > 20; 52 | }); 53 | } 54 | const totalRounds = historicalRoundsOriginal.length; 55 | const roundsWithDLs = historicalRounds.filter(round => round.bestDL !== null); 56 | const nConf = roundsWithDLs.length; 57 | const nConfAlpha = historicalRoundsOriginal.filter(round => round.bestDL !== null).length; 58 | const weightedDLSum = roundsWithDLs 59 | .map(round => BigNumber(round.bestDL).dividedBy(round.netDiff)) 60 | .reduce((acc, curr) => acc.plus(curr), BigNumber(0)); 61 | 62 | if (weightedDLSum.isEqualTo(0)) { 63 | return 0; 64 | } 65 | 66 | const alpha = this.alphas[totalRounds]; 67 | if (!alpha) { 68 | return 0; 69 | } 70 | const pos = nConfAlpha > alpha.length ? alpha.length - 1 : nConfAlpha - 1; 71 | 72 | return alpha[pos] * this.blockTime * (nConf - 1) / weightedDLSum.toNumber(); 73 | } 74 | 75 | get blockTime() { 76 | return coinUtil.blockTime(this.upstreamConfig.coin); 77 | } 78 | }; -------------------------------------------------------------------------------- /lib/upstream/mixins/stats-mixin.js: -------------------------------------------------------------------------------- 1 | const database = require('../../../models'); 2 | const util = require('../util'); 3 | const cache = require('../../services/cache'); 4 | const roundPopulator = require('../../services/round-populator'); 5 | const coinUtil = require('../../coin-util'); 6 | 7 | module.exports = (upstreamClass) => class StatsMixin extends upstreamClass { 8 | constructor() { 9 | super(); 10 | this.totalCapacity = 0; 11 | } 12 | 13 | getBestDL() { 14 | return util.getBestDL(this.deadlines); 15 | } 16 | 17 | getConnectionStats() { 18 | return { 19 | connected: this.connected !== undefined ? this.connected : true, 20 | connectionQuality: this.connectionQuality !== undefined ? this.connectionQuality : 100, 21 | }; 22 | } 23 | 24 | getCurrentRoundStats() { 25 | const bestDL = this.getBestDL(); 26 | 27 | return { 28 | blockNumber: this.miningInfo.height, 29 | netDiff: this.miningInfo.modifiedNetDiff, 30 | roundStart: this.roundStart, 31 | bestDL: bestDL ? coinUtil.modifyDeadline(bestDL.toNumber(), this.upstreamConfig.coin) : null, 32 | }; 33 | } 34 | 35 | async getHistoricalStats() { 36 | const estimatedCapacityInTB = await this.getEstimatedCapacity(); 37 | 38 | const historicalRounds = await this.getHistoricalRounds(); 39 | 40 | return { 41 | estimatedCapacityInTB, 42 | roundsWon: historicalRounds.filter(round => round.roundWon).length, 43 | roundsSubmitted: historicalRounds.filter(round => round.bestDLSubmitted).length, 44 | roundsWithDLs: historicalRounds.filter(round => round.bestDL).length, 45 | totalRounds: historicalRounds.length, 46 | historicalRounds, 47 | }; 48 | } 49 | 50 | async getHistoricalRounds() { 51 | const rounds = await database().round.findAll({ 52 | where: { 53 | upstream: this.fullUpstreamName, 54 | }, 55 | order: [ 56 | ['blockHeight', 'ASC'], 57 | ], 58 | }); 59 | 60 | return rounds.map(round => { 61 | const roundJSON = round.toJSON(); 62 | roundJSON.netDiff = coinUtil.modifyNetDiff(roundJSON.netDiff, this.upstreamConfig.coin); 63 | if (roundJSON.bestDL !== null) { 64 | roundJSON.bestDL = coinUtil.modifyDeadline(parseInt(roundJSON.bestDL, 10), this.upstreamConfig.coin); 65 | } 66 | if (roundJSON.bestDLSubmitted !== null) { 67 | roundJSON.bestDLSubmitted = coinUtil.modifyDeadline(parseInt(roundJSON.bestDLSubmitted, 10), this.upstreamConfig.coin); 68 | } 69 | 70 | return roundJSON; 71 | }); 72 | } 73 | 74 | async getStats() { 75 | const upstreamStats = { 76 | name: this.upstreamConfig.name, 77 | color: this.upstreamConfig.color, 78 | fullName: this.fullUpstreamName, 79 | isBHD: this.isBHD || !!this.upstreamConfig.isBHD, 80 | coin: this.upstreamConfig.coin, 81 | url: this.upstreamConfig.url, 82 | isFoxyPool: this.upstreamConfig.type === 'foxypool', 83 | }; 84 | const connectionStats = this.getConnectionStats(); 85 | const currentRoundStats = this.getCurrentRoundStats(); 86 | const historicalStats = await this.getHistoricalStats(); 87 | 88 | return { 89 | ...upstreamStats, 90 | ...connectionStats, 91 | ...currentRoundStats, 92 | ...historicalStats, 93 | }; 94 | } 95 | 96 | getTotalCapacity() { 97 | if (this.upstreamConfig.capacity !== undefined) { 98 | return parseInt(this.upstreamConfig.capacity, 10); 99 | } 100 | 101 | return util.getTotalMinerCapacity(this.miners); 102 | } 103 | 104 | recalculateTotalCapacity() { 105 | this.totalCapacity = this.getTotalCapacity(); 106 | } 107 | 108 | async createOrUpdateRound({ miningInfo }) { 109 | if (!miningInfo || !miningInfo.height) { 110 | return; 111 | } 112 | await cache.createOrUpdateRound({ 113 | upstream: this.fullUpstreamName, 114 | blockHeight: miningInfo.height, 115 | baseTarget: miningInfo.baseTarget, 116 | netDiff: miningInfo.netDiff, 117 | }); 118 | } 119 | 120 | async onRoundEnded({ oldMiningInfo }) { 121 | if (!oldMiningInfo || !oldMiningInfo.height) { 122 | return; 123 | } 124 | 125 | if (this.canFetchBlockInfo()) { 126 | await new Promise(resolve => setTimeout(resolve, 10 * 1000)); 127 | await roundPopulator.populateUnpopulatedRounds(this, oldMiningInfo.height); 128 | } 129 | 130 | await cache.removeOldCachedRounds(this, oldMiningInfo.height); 131 | await cache.removeOldRounds({ 132 | upstream: this.fullUpstreamName, 133 | roundsToKeep: this.upstreamConfig.historicalRoundsToKeep || 720, 134 | }); 135 | } 136 | 137 | async getActivePlotter(height) { 138 | return database().plotter.findAll({ 139 | where: { 140 | upstream: this.fullUpstreamName, 141 | lastSubmitHeight: { 142 | [database().Op.gte]: height - 100, 143 | }, 144 | }, 145 | }); 146 | } 147 | 148 | get walletUrl() { 149 | let walletUrl = this.upstreamConfig.walletUrl; 150 | if (!walletUrl && this.upstreamConfig.mode === 'solo') { 151 | walletUrl = this.upstreamConfig.url; 152 | } 153 | if (!walletUrl && this.hostedWalletUrl) { 154 | walletUrl = this.hostedWalletUrl; 155 | } 156 | 157 | return walletUrl; 158 | } 159 | 160 | get hostedWalletUrl() { 161 | switch (this.upstreamConfig.coin) { 162 | case 'BHD': return 'https://bhd.wallet.foxypool.io'; 163 | case 'BURST': return 'https://burst.wallet.foxypool.io'; 164 | default: return null; 165 | } 166 | } 167 | 168 | canFetchBlockInfo() { 169 | return !!(this.walletUrl && this.upstreamConfig.coin); 170 | } 171 | 172 | getBlockInfo(height) { 173 | if (!this.canFetchBlockInfo()) { 174 | return null; 175 | } 176 | 177 | switch (this.upstreamConfig.coin) { 178 | case 'LHD': 179 | case 'HDD': 180 | case 'XHD': 181 | case 'DISC': 182 | case 'BHD': return util.getBhdBlockInfo({ url: this.walletUrl, height }); 183 | case 'BURST': return util.getBurstBlockInfo({ url: this.walletUrl, height }); 184 | default: return null; 185 | } 186 | } 187 | }; -------------------------------------------------------------------------------- /lib/upstream/mixins/submit-probability-mixin.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const eventBus = require('../../services/event-bus'); 3 | const coinUtil = require('../../coin-util'); 4 | 5 | module.exports = (upstreamClass) => class SubmitProbabilityMixin extends upstreamClass { 6 | async init() { 7 | this.useSubmitProbability = !!this.upstreamConfig.submitProbability; 8 | this.targetDLFactor = null; 9 | if (this.useSubmitProbability) { 10 | let submitProbability = this.upstreamConfig.submitProbability > 10 ? this.upstreamConfig.submitProbability / 100 : this.upstreamConfig.submitProbability; 11 | if (submitProbability >= 1) { 12 | submitProbability = 0.999999; 13 | } 14 | this.targetDLFactor = -1 * Math.log(1 - submitProbability) * (this.blockTime); 15 | } 16 | if (super.init) { 17 | await super.init(); 18 | } 19 | } 20 | 21 | updateDynamicTargetDL(miningInfo) { 22 | const totalCapacityInTiB = this.getTotalCapacity() / 1024; 23 | if (totalCapacityInTiB === 0) { 24 | this.dynamicTargetDeadline = null; 25 | return; 26 | } 27 | this.dynamicTargetDeadline = Math.round(this.targetDLFactor * miningInfo.netDiff / totalCapacityInTiB); 28 | eventBus.publish('log/debug', `${this.fullUpstreamNameLogs} | Submit Probability | Using targetDL ${this.getFormattedDeadline(this.dynamicTargetDeadline)}`); 29 | } 30 | 31 | getFormattedDeadline(deadline) { 32 | if (!this.proxyConfig.humanizeDeadlines) { 33 | return deadline; 34 | } 35 | 36 | const duration = moment.duration(deadline, 'seconds'); 37 | if (duration.years() > 0) { 38 | return `${duration.years()}y ${duration.months()}m ${duration.days()}d ${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 39 | } else if (duration.months() > 0) { 40 | return `${duration.months()}m ${duration.days()}d ${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 41 | } else if (duration.days() > 0) { 42 | return `${duration.days()}d ${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 43 | } 44 | 45 | return `${duration.hours().toString().padStart(2, '0')}:${duration.minutes().toString().padStart(2, '0')}:${duration.seconds().toString().padStart(2, '0')}`; 46 | } 47 | 48 | get blockTime() { 49 | return coinUtil.blockTime(this.upstreamConfig.coin); 50 | } 51 | }; -------------------------------------------------------------------------------- /lib/upstream/util.js: -------------------------------------------------------------------------------- 1 | const JSONbig = require('json-bigint'); 2 | const superagent = require('superagent'); 3 | 4 | const eventBus = require('../services/event-bus'); 5 | const version = require('../version'); 6 | 7 | function getBestDL(deadlines) { 8 | return Object.keys(deadlines).reduce((acc, accountId) => { 9 | const dl = deadlines[accountId]; 10 | if (!acc) { 11 | return dl; 12 | } 13 | if (acc.isGreaterThan(dl)) { 14 | return dl; 15 | } 16 | 17 | return acc; 18 | }, null); 19 | } 20 | 21 | async function doBitcoinApiCall(url, method, params = []) { 22 | const res = await superagent.post(url).set('User-Agent', `Foxy-Proxy ${version}`).send({ 23 | jsonrpc: '2.0', 24 | id: 0, 25 | method, 26 | params, 27 | }); 28 | 29 | return JSONbig.parse(res.res.text).result; 30 | } 31 | 32 | async function doBurstApiCall(url, method, params = {}, endpoint = 'burst') { 33 | const queryParams = { 34 | requestType: method, 35 | }; 36 | Object.keys(params).forEach(key => { 37 | queryParams[key] = params[key]; 38 | }); 39 | const {text: jsonResult} = await superagent.get(`${url}/${endpoint}`).query(queryParams).set('User-Agent', `Foxy-Proxy ${version}`); 40 | const result = JSON.parse(jsonResult); 41 | 42 | if (result.errorDescription) { 43 | throw new Error(result.errorDescription); 44 | } 45 | 46 | return result; 47 | } 48 | 49 | async function getBhdBlockInfo({url, height}) { 50 | try { 51 | const blockHash = await doBitcoinApiCall(url, 'getblockhash', [height]); 52 | const block = await doBitcoinApiCall(url, 'getblock', [blockHash]); 53 | const plotterId = block.plotterId || block.plotterid; 54 | 55 | return { 56 | height, 57 | hash: block.hash, 58 | plotterId: plotterId.toString(), 59 | }; 60 | } catch (err) { 61 | eventBus.publish('log/error', `Failed retrieving block info for height ${height}: ${err.message}`); 62 | 63 | return null; 64 | } 65 | } 66 | 67 | async function getBurstBlockInfo({url, height}) { 68 | try { 69 | const block = await doBurstApiCall(url, 'getBlock', {height}); 70 | 71 | return { 72 | height, 73 | hash: block.block, 74 | plotterId: block.generator, 75 | }; 76 | } catch (err) { 77 | eventBus.publish('log/error', `Failed retrieving block info for height ${height}: ${err.message}`); 78 | 79 | return null; 80 | } 81 | } 82 | 83 | function getTotalMinerCapacity(minersObj) { 84 | if (!minersObj) { 85 | return 0; 86 | } 87 | const miners = Object.keys(minersObj).map(key => minersObj[key]); 88 | 89 | return miners.reduce((acc, miner) => { 90 | return acc + (miner.capacity || 0); 91 | }, 0); 92 | } 93 | 94 | module.exports = { 95 | getBestDL, 96 | getTotalMinerCapacity, 97 | getBhdBlockInfo, 98 | getBurstBlockInfo, 99 | }; -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mkdirp = require('mkdirp'); 3 | const os = require('os'); 4 | const path = require('path'); 5 | 6 | function ensureFilePathExists(filePath) { 7 | const dirPath = path.dirname(filePath); 8 | if (dirPath === '.') { 9 | return; 10 | } 11 | mkdirp.sync(dirPath); 12 | } 13 | 14 | function detectFilePath(fileName) { 15 | if (fs.existsSync(fileName)) { 16 | return fileName; 17 | } 18 | 19 | return path.join(getDataDirPath(), fileName); 20 | } 21 | 22 | function getDataDirPath() { 23 | return path.join(os.homedir(), '.config/foxy-proxy'); 24 | } 25 | 26 | function getLegacyFilePath(fileName) { 27 | return path.join(os.homedir(), '.config/bhd-burst-proxy', fileName); 28 | } 29 | 30 | module.exports = { 31 | ensureFilePathExists, 32 | detectFilePath, 33 | getLegacyFilePath, 34 | }; 35 | -------------------------------------------------------------------------------- /lib/version.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../package.json').version; 2 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { fork } = require('child_process'); 4 | 5 | class FoxyProxy { 6 | constructor() { 7 | this.start(); 8 | } 9 | 10 | start() { 11 | this.app = fork(`${__dirname}/app.js`, process.argv.slice(2), { 12 | cwd: process.cwd(), 13 | }); 14 | this.app.on('message', this.onMessage.bind(this)); 15 | } 16 | 17 | stop() { 18 | if (!this.app) { 19 | return; 20 | } 21 | this.app.kill(); 22 | this.app = null; 23 | } 24 | 25 | onMessage(message) { 26 | switch(message) { 27 | case 'restart': 28 | this.stop(); 29 | this.start(); 30 | break; 31 | } 32 | } 33 | } 34 | 35 | const foxyProxy = new FoxyProxy(); 36 | -------------------------------------------------------------------------------- /models/config.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define('config', { 2 | id: { 3 | type: DataTypes.INTEGER, 4 | primaryKey: true, 5 | }, 6 | uuid: { 7 | type: DataTypes.STRING, 8 | allowNull: true, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { Sequelize, DataTypes } = require('sequelize'); 3 | 4 | const store = require('../lib/services/store'); 5 | const util = require('../lib/util'); 6 | const configModel = require('./config'); 7 | const roundModel = require('./round'); 8 | const plotterModel = require('./plotter'); 9 | 10 | const db = {}; 11 | 12 | let isInitialized = false; 13 | 14 | function init() { 15 | const sqliteFilePath = store.getDbFilePath(); 16 | migrateDbPath(sqliteFilePath); 17 | migrateLegacyDbPath(sqliteFilePath); 18 | const databaseUrl = process.env.DATABASE_URL || `sqlite:${sqliteFilePath}`; 19 | const isPostgres = databaseUrl.indexOf('postgres') !== -1; 20 | let sequelizeConfig = { 21 | dialect: 'postgres', 22 | protocol: 'postgres', 23 | dialectOptions: { 24 | ssl: { 25 | rejectUnauthorized: false, 26 | }, 27 | }, 28 | logging: false, 29 | }; 30 | if (!isPostgres) { 31 | sequelizeConfig = { 32 | dialect: 'sqlite', 33 | storage: sqliteFilePath, 34 | logging: false, 35 | retry: { 36 | max: 10, 37 | }, 38 | }; 39 | } 40 | const sequelize = new Sequelize(databaseUrl, sequelizeConfig); 41 | 42 | db.config = configModel(sequelize, DataTypes); 43 | db.round = roundModel(sequelize, DataTypes); 44 | db.plotter = plotterModel(sequelize, DataTypes); 45 | 46 | db.sequelize = sequelize; 47 | } 48 | 49 | function migrateDbPath(newPath) { 50 | const oldPath = 'db/db.sqlite'; 51 | if (!fs.existsSync(oldPath)) { 52 | return; 53 | } 54 | 55 | util.ensureFilePathExists(newPath); 56 | fs.renameSync(oldPath, newPath); 57 | fs.rmdirSync('db'); 58 | } 59 | 60 | function migrateLegacyDbPath(newPath) { 61 | const oldPath = util.getLegacyFilePath('db.sqlite'); 62 | if (!fs.existsSync(oldPath)) { 63 | return; 64 | } 65 | 66 | util.ensureFilePathExists(newPath); 67 | fs.renameSync(oldPath, newPath); 68 | } 69 | 70 | db.Sequelize = Sequelize; 71 | db.Op = Sequelize.Op; 72 | 73 | module.exports = () => { 74 | if (!isInitialized) { 75 | init(); 76 | isInitialized = true; 77 | } 78 | 79 | return db; 80 | }; 81 | -------------------------------------------------------------------------------- /models/plotter.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define('plotter', { 2 | id: { 3 | type: DataTypes.INTEGER, 4 | primaryKey: true, 5 | autoIncrement: true, 6 | }, 7 | pid: { 8 | type: DataTypes.STRING, 9 | }, 10 | upstream: { 11 | type: DataTypes.STRING, 12 | }, 13 | lastSubmitHeight: { 14 | type: DataTypes.INTEGER, 15 | }, 16 | }, { 17 | indexes: [ 18 | { 19 | name: 'plotterUpstreamIndex', 20 | unique: false, 21 | fields: ['id', 'upstream'], 22 | }, 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /models/round.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define('round', { 2 | id: { 3 | type: DataTypes.INTEGER, 4 | primaryKey: true, 5 | autoIncrement: true, 6 | }, 7 | upstream: { 8 | type: DataTypes.STRING, 9 | allowNull: false, 10 | validate: { 11 | notEmpty: true, 12 | }, 13 | }, 14 | blockHeight: { 15 | type: DataTypes.INTEGER, 16 | allowNull: false, 17 | }, 18 | blockHash: { 19 | type: DataTypes.STRING, 20 | }, 21 | baseTarget: { 22 | type: DataTypes.INTEGER, 23 | }, 24 | netDiff: { 25 | type: DataTypes.INTEGER, 26 | }, 27 | bestDL: { 28 | type: DataTypes.STRING, 29 | }, 30 | bestDLSubmitted: { 31 | type: DataTypes.STRING, 32 | }, 33 | roundWon: { 34 | type: DataTypes.BOOLEAN, 35 | }, 36 | }, { 37 | indexes: [ 38 | { 39 | name: 'roundUpstreamBlockHeightIndex', 40 | unique: false, 41 | fields: ['upstream', 'blockHeight'], 42 | }, 43 | ], 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foxy-proxy", 3 | "version": "1.40.0", 4 | "description": "A Proof of Capacity proxy which supports solo and pool mining upstreams.", 5 | "keywords": [ 6 | "burst", 7 | "bhd", 8 | "proxy", 9 | "multi-chain", 10 | "collision-free", 11 | "web-ui", 12 | "foxyproxy", 13 | "foxy" 14 | ], 15 | "repository": "https://github.com/felixbrucker/foxy-proxy.git", 16 | "bugs": "https://github.com/felixbrucker/foxy-proxy/issues", 17 | "license": "GPL-3.0", 18 | "dependencies": { 19 | "@sentry/integrations": "^6.9.0", 20 | "@sentry/node": "^6.9.0", 21 | "async": "^3.2.0", 22 | "bignumber.js": "^9.0.1", 23 | "bitmart-api": "^1.2.1", 24 | "chalk": "^4.1.1", 25 | "cli-table3": "^0.6.0", 26 | "commander": "^7.2.0", 27 | "cross-spawn": "^7.0.3", 28 | "js-yaml": "^4.1.0", 29 | "json-bigint": "^1.0.0", 30 | "koa": "^2.13.1", 31 | "koa-bodyparser": "^4.3.0", 32 | "koa-router": "^10.0.0", 33 | "koa-send": "^5.0.1", 34 | "koa-static-cache": "^5.1.4", 35 | "lodash": "^4.17.21", 36 | "log-update": "^4.0.0", 37 | "mkdirp": "^1.0.4", 38 | "moment": "^2.29.1", 39 | "nodemailer": "^6.6.2", 40 | "pg": "^8.6.0", 41 | "rotating-file-stream": "^2.1.5", 42 | "semver": "^7.3.5", 43 | "sequelize": "^6.6.5", 44 | "socket.io": "^4.1.3", 45 | "socket.io-client": "^4.1.3", 46 | "sqlite3": "^5.0.2", 47 | "superagent": "^6.1.0", 48 | "universal-analytics": "^0.4.23", 49 | "uuid": "^8.3.2" 50 | }, 51 | "devDependencies": { 52 | "npm-check-updates": "^11.8.2" 53 | }, 54 | "bin": { 55 | "bhd-burst-proxy": "./main.js", 56 | "foxy-proxy": "./main.js" 57 | }, 58 | "main": "main.js", 59 | "scripts": { 60 | "start": "node main.js", 61 | "install-web": "cd app && npm ci", 62 | "build-web": "cd app && npm run build:prod", 63 | "audit-web": "cd app && npm audit", 64 | "update": "ncu -u" 65 | }, 66 | "engines": { 67 | "node": ">= 10" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /shared/capacity.js: -------------------------------------------------------------------------------- 1 | class Capacity { 2 | constructor(capacityInGiB) { 3 | this.capacityInGiB = capacityInGiB; 4 | } 5 | 6 | static fromGiB(capacityInGiB) { 7 | return new Capacity(capacityInGiB); 8 | } 9 | 10 | static fromTiB(capacityInTiB) { 11 | return new Capacity(capacityInTiB * 1024); 12 | } 13 | 14 | toString(precision = 2, correctUnit = true) { 15 | let capacity = this.capacityInGiB; 16 | let unit = 0; 17 | const units = correctUnit ? ['GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] : ['GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 18 | while (capacity >= 1024) { 19 | capacity /= 1024; 20 | unit += 1; 21 | } 22 | 23 | return `${capacity.toFixed(precision)} ${units[unit]}`; 24 | } 25 | } 26 | 27 | module.exports = Capacity; 28 | --------------------------------------------------------------------------------