├── .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 | [](LICENSE)
7 | [](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 |
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 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 |
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 |
12 |
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 |
17 |
18 |
19 |
22 |
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 |
13 |
14 |
15 |
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 |
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 |
13 |
14 |
15 | Submitted
16 | {{getSubmitPercent()}}% {{roundsSubmitted}}/{{totalRounds}}
17 |
18 | Won
19 | {{roundsWon}}
20 |
21 |
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 |
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 |
0">
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 | = 99.4 ? true: null"
7 | [attr.intermediary]="!connected && connectionQuality < 99.4 && connectionQuality >= 94 ? true: null"
8 | [attr.negative]="!connected && connectionQuality < 94 ? true: null">
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 |
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 |
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 |
--------------------------------------------------------------------------------