├── .dockerignore ├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── build-image.yml │ ├── build-npm.yml │ ├── bump-version.yml │ └── check.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── creds.json.default ├── demo_clients ├── flask │ ├── Pipfile │ ├── Pipfile.lock │ ├── client.py │ └── templates │ │ └── index.html ├── js │ ├── client.js │ └── index.html ├── python │ ├── Pipfile │ ├── Pipfile.lock │ └── client.py ├── reactjs │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── components │ │ │ ├── qr-code.js │ │ │ └── qrImageStyle.js │ │ ├── index.css │ │ ├── index.js │ │ ├── rpc.js │ │ ├── serviceWorker.js │ │ └── setupTests.js │ └── yarn.lock ├── websocket_js │ ├── client.js │ └── index.html ├── websocket_nodejs │ ├── client.js │ ├── package-lock.json │ └── package.json └── websocket_python │ ├── Pipfile │ ├── Pipfile.lock │ └── client.py ├── docker-compose.yml ├── jest.config.js ├── media ├── NanoRPCPRoxy.png ├── NanoRPCPRoxy_tokens.png ├── NanoRPCProxy_limiter.png ├── NanoRPCProxy_ws.png ├── client_demo_js.png ├── client_demo_python.png ├── client_demo_react_01.png ├── client_demo_react_02.png ├── client_demo_react_03.png ├── client_demo_websocket_js.png ├── client_demo_websocket_nodejs.png ├── client_demo_websocket_python.png ├── demo_curl.png ├── grafana_01.png └── pm2_monitor.png ├── package-lock.json ├── package.json ├── pow_creds.json.default ├── settings.json.default ├── src ├── __test__ │ ├── my_authorizer.test.ts │ ├── pending_response.test.ts │ ├── process_request.test.ts │ ├── proxy.it.ts │ ├── proxy_default.test.ts │ ├── proxy_file.test.ts │ ├── test-commons.ts │ ├── tokens_default.test.ts │ ├── tokens_file.test.ts │ ├── tools.test.ts │ ├── user_settings.test.ts │ └── websockets.test.ts ├── authorize-user.ts ├── common-settings.ts ├── credential-settings.ts ├── http.ts ├── lowdb-schema.ts ├── mynano-api │ └── mynano-api.ts ├── node-api │ ├── node-api.ts │ ├── proxy-api.ts │ ├── token-api.ts │ └── websocket-api.ts ├── pow-settings.ts ├── price-api │ └── price-api.ts ├── prom-client.ts ├── proxy-settings.ts ├── proxy.ts ├── token-settings.ts ├── tokens.ts ├── tools.ts └── user-settings.ts ├── token_settings.json.default ├── tsconfig.json └── user_settings.json.default /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | #NanoNodeProxy Server 107 | db.json 108 | websocket.json 109 | counters.stat 110 | samples.stat 111 | settings.json 112 | token_settings.json 113 | user_settings.json 114 | creds.json 115 | pow_creds.json 116 | request-stat.json 117 | 118 | dist/ 119 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "pip" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feature' 7 | - 'feat' 8 | - 'enhancement' 9 | - title: '🐛 Bug Fixes' 10 | labels: 11 | - 'fix' 12 | - 'bugfix' 13 | - 'bug' 14 | - title: '🧰 Maintenance' 15 | labels: 16 | - 'chore' 17 | - 'refactor' 18 | - 'style' 19 | - 'docs' 20 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 21 | version-resolver: 22 | major: 23 | labels: 24 | - 'major' 25 | minor: 26 | labels: 27 | - 'minor' 28 | patch: 29 | labels: 30 | - 'patch' 31 | default: patch 32 | template: | 33 | ## Changes 34 | 35 | $CHANGES 36 | 37 | ## Docker Container 38 | For equivalent docker version, any of: 39 | ```bash 40 | docker pull public.ecr.aws/y4f6g9v9/nano-rpc-proxy:latest 41 | docker pull public.ecr.aws/y4f6g9v9/nano-rpc-proxy:v$RESOLVED_VERSION 42 | ``` 43 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yml: -------------------------------------------------------------------------------- 1 | name: Build Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*.*.*" 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - Dockerfile 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | permissions: 19 | id-token: write 20 | contents: read 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | 27 | - name: Checkout Code 28 | uses: actions/checkout@v3 29 | 30 | - name: Lint Dockerfile 31 | run: | 32 | curl -L https://github.com/hadolint/hadolint/releases/download/v1.17.6/hadolint-Linux-x86_64 -o hadolint && chmod +x hadolint 33 | ./hadolint Dockerfile 34 | 35 | - name: Configure AWS Credentials 36 | # Run on tag push only. 37 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') 38 | uses: aws-actions/configure-aws-credentials@v2 39 | with: 40 | role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} 41 | aws-region: ${{ vars.AWS_REGION }} 42 | 43 | - name: Login to Amazon ECR Public 44 | # Run on tag push only. 45 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') 46 | id: login-ecr-public 47 | uses: aws-actions/amazon-ecr-login@v1 48 | with: 49 | registry-type: public 50 | 51 | - name: Set docker image name 52 | # Run on tag push only. 53 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') 54 | env: 55 | REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} 56 | REGISTRY_ALIAS: ${{ vars.AWS_REGISTRY_ALIAS }} 57 | REPOSITORY: nano-rpc-proxy 58 | run: | 59 | echo "IMAGE_NAME=$REGISTRY/$REGISTRY_ALIAS/$REPOSITORY" >> $GITHUB_ENV 60 | 61 | - name: Build docker image 62 | env: 63 | IMAGE_NAME: ${{ env.IMAGE_NAME || github.repository }} 64 | run: | 65 | docker build -t $IMAGE_NAME . 66 | 67 | - name: Push docker image with Git tag to Amazon ECR Public 68 | # Run on tag push only. 69 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') 70 | run: | 71 | TAG_VERSION=${GITHUB_REF#refs/tags/} 72 | docker tag $IMAGE_NAME $IMAGE_NAME:$TAG_VERSION 73 | docker push $IMAGE_NAME:$TAG_VERSION 74 | docker push $IMAGE_NAME:latest 75 | -------------------------------------------------------------------------------- /.github/workflows/build-npm.yml: -------------------------------------------------------------------------------- 1 | name: Build NPM 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "v*.*.*" 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | permissions: 13 | id-token: write 14 | contents: write 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v2 22 | 23 | - name: Install modules 24 | run: npm install 25 | 26 | - name: Runs test 27 | run: npm test 28 | 29 | - name: Build typescript 30 | run: npm run build:prod 31 | 32 | - name: Copy settings 33 | run: | 34 | cp ./creds.json.default ./dist/creds.json 35 | cp ./pow_creds.json.default ./dist/pow_creds.json 36 | cp ./settings.json.default ./dist/settings.json 37 | cp ./token_settings.json.default ./dist/token_settings.json 38 | cp ./user_settings.json.default ./dist/user_settings.json 39 | cp ./package.json ./dist/package.json 40 | 41 | - name: Zip Folder 42 | run: zip -r proxy.zip dist/ -x dist/__test__\* 43 | 44 | - name: Store NPM version 45 | id: package-version 46 | uses: martinbeentjes/npm-get-version-action@master 47 | 48 | - name: Release Drafter 49 | # Run on tag push only. 50 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') 51 | id: release_drafter 52 | uses: release-drafter/release-drafter@v5 53 | with: 54 | name: ${{ steps.package-version.outputs.current-version }} 55 | tag: v${{ steps.package-version.outputs.current-version }} 56 | version: ${{ steps.package-version.outputs.current-version }} 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - name: Upload Release Asset 61 | # Run on tag push only. 62 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') 63 | id: upload_release_asset 64 | uses: actions/upload-release-asset@v1 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | with: 68 | upload_url: ${{ steps.release_drafter.outputs.upload_url }} # This pulls ID from the Release Drafter step 69 | asset_path: ./proxy.zip 70 | asset_name: proxy.zip 71 | asset_content_type: application/zip 72 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: Bump Version 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | id-token: write 8 | contents: write 9 | 10 | jobs: 11 | bum-version: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Calculate version 19 | id: tag-version 20 | uses: mathieudutour/github-tag-action@v6.1 21 | with: 22 | dry_run: true 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | # Configure Git 26 | - name: Git configuration 27 | run: | 28 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 29 | git config --global user.name "GitHub Actions" 30 | 31 | - name: Bump npm version 32 | run: npm version --commit-hooks false --git-tag-version false ${{ steps.tag-version.outputs.new_tag }} 33 | 34 | - name: Commit updated npm package.json 35 | uses: stefanzweifel/git-auto-commit-action@v4 36 | with: 37 | commit_message: '[skip ci] bump npm version to ${{ steps.tag-version.outputs.new_tag }}' 38 | branch: main 39 | file_pattern: 'package*.json' 40 | commit_user_name: trust-ci 41 | commit_user_email: "trust-ci@users.noreply.github.com" 42 | 43 | - name: Bump version and push tag 44 | uses: mathieudutour/github-tag-action@v6.1 45 | with: 46 | custom_tag: ${{ steps.tag-version.outputs.new_version }} 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Checkout Code 16 | uses: actions/checkout@v2 17 | 18 | - name: Install modules 19 | run: npm install 20 | 21 | - name: Runs test 22 | run: npm test 23 | 24 | - name: Build typescript 25 | run: npm run build 26 | 27 | - name: Copy settings 28 | run: | 29 | cp ./creds.json.default ./dist/creds.json 30 | cp ./pow_creds.json.default ./dist/pow_creds.json 31 | cp ./settings.json.default ./dist/settings.json 32 | cp ./token_settings.json.default ./dist/token_settings.json 33 | cp ./user_settings.json.default ./dist/user_settings.json 34 | cp ./package.json ./dist/package.json 35 | 36 | - name: Zip Folder 37 | run: zip -r proxy.zip dist/ -x dist/__test__\* 38 | 39 | - name: Upload Artifact 40 | uses: actions/upload-artifact@v2 41 | with: 42 | name: dist 43 | path: proxy.zip 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | #NanoNodeProxy Server 107 | db.json 108 | websocket.json 109 | counters.stat 110 | samples.stat 111 | settings.json 112 | token_settings.json 113 | user_settings.json 114 | creds.json 115 | pow_creds.json 116 | request-stat.json 117 | 118 | dist/ 119 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage # 2 | FROM node:18 AS build 3 | 4 | WORKDIR /usr/src/app 5 | 6 | # Prepare environment 7 | COPY package*.json ./ 8 | COPY tsconfig.json ./ 9 | RUN npm ci 10 | 11 | # Copy source files 12 | COPY ./src ./src 13 | 14 | # Typescript → Javascript 15 | RUN npm run-script build 16 | 17 | # Deploy stage # 18 | FROM node:18 19 | 20 | WORKDIR /app 21 | 22 | # Setup environment variables for docker 23 | ENV CONFIG_CREDS_SETTINGS=/root/creds.json \ 24 | CONFIG_POW_CREDS_SETTINGS=/root/pow_creds.json \ 25 | CONFIG_REQUEST_STAT=/root/request-stat.json \ 26 | CONFIG_SETTINGS=/root/settings.json \ 27 | CONFIG_TOKEN_SETTINGS=/root/token_settings.json \ 28 | CONFIG_USER_SETTINGS=/root/user_settings.json \ 29 | CONFIG_WEBSOCKET_PATH=/root/websocket.json \ 30 | CONFIG_DB_PATH=/root/db.json 31 | 32 | # Install dependencies 33 | COPY package*.json ./ 34 | RUN npm ci --production 35 | 36 | # Copy build files from build stage 37 | COPY --from=build /usr/src/app/dist ./dist 38 | 39 | VOLUME /root 40 | 41 | EXPOSE 9950 42 | 43 | CMD [ "node", "dist/proxy.js" ] 44 | -------------------------------------------------------------------------------- /creds.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "user": "user1", 5 | "password": "user1" 6 | }, 7 | { 8 | "user": "user2", 9 | "password": "user2" 10 | }, 11 | { 12 | "user": "user3", 13 | "password": "user3" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /demo_clients/flask/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | certifi = "==2023.7.22" 8 | chardet = "==3.0.4" 9 | click = "==7.1.1" 10 | Flask = "==1.1.1" 11 | Flask-WTF = "==0.14.3" 12 | idna = "==2.9" 13 | itsdangerous = "==1.1.0" 14 | Jinja2 = "==2.11.3" 15 | MarkupSafe = "==1.1.1" 16 | python-dotenv = "==0.12.0" 17 | requests = "==2.23.0" 18 | urllib3 = "==1.25.9" 19 | Werkzeug = "==1.0.0" 20 | WTForms = "==2.2.1" 21 | 22 | [dev-packages] 23 | 24 | [requires] 25 | python_version = "3.7" 26 | -------------------------------------------------------------------------------- /demo_clients/flask/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "8a01f8dd44cabe2c56ceb123e4deb0a17e6d6b31351a44f4585fd2f97623aed1" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", 22 | "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" 23 | ], 24 | "index": "pypi", 25 | "version": "==2023.7.22" 26 | }, 27 | "chardet": { 28 | "hashes": [ 29 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 30 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.0.4" 34 | }, 35 | "click": { 36 | "hashes": [ 37 | "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", 38 | "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" 39 | ], 40 | "index": "pypi", 41 | "version": "==7.1.1" 42 | }, 43 | "flask": { 44 | "hashes": [ 45 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 46 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 47 | ], 48 | "index": "pypi", 49 | "version": "==1.1.1" 50 | }, 51 | "flask-wtf": { 52 | "hashes": [ 53 | "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2", 54 | "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720" 55 | ], 56 | "index": "pypi", 57 | "version": "==0.14.3" 58 | }, 59 | "idna": { 60 | "hashes": [ 61 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 62 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 63 | ], 64 | "index": "pypi", 65 | "version": "==2.9" 66 | }, 67 | "itsdangerous": { 68 | "hashes": [ 69 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 70 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 71 | ], 72 | "index": "pypi", 73 | "version": "==1.1.0" 74 | }, 75 | "jinja2": { 76 | "hashes": [ 77 | "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", 78 | "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" 79 | ], 80 | "index": "pypi", 81 | "version": "==2.11.3" 82 | }, 83 | "markupsafe": { 84 | "hashes": [ 85 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 86 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 87 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 88 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 89 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 90 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 91 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 92 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 93 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 94 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 95 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 96 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 97 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 98 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 99 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 100 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 101 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 102 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 103 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 104 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 105 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 106 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 107 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 108 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 109 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 110 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 111 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 112 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 113 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 114 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 115 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 116 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 117 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 118 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 119 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 120 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 121 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 122 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 123 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 124 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 125 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 126 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 127 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 128 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 129 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 130 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 131 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 132 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 133 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 134 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 135 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 136 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 137 | ], 138 | "index": "pypi", 139 | "version": "==1.1.1" 140 | }, 141 | "python-dotenv": { 142 | "hashes": [ 143 | "sha256:81822227f771e0cab235a2939f0f265954ac4763cafd806d845801c863bf372f", 144 | "sha256:92b3123fb2d58a284f76cc92bfe4ee6c502c32ded73e8b051c4f6afc8b6751ed" 145 | ], 146 | "index": "pypi", 147 | "version": "==0.12.0" 148 | }, 149 | "requests": { 150 | "hashes": [ 151 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 152 | "sha256:5d2d0ffbb515f39417009a46c14256291061ac01ba8f875b90cad137de83beb4", 153 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 154 | ], 155 | "index": "pypi", 156 | "version": "==2.23.0" 157 | }, 158 | "urllib3": { 159 | "hashes": [ 160 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 161 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 162 | ], 163 | "index": "pypi", 164 | "version": "==1.25.9" 165 | }, 166 | "werkzeug": { 167 | "hashes": [ 168 | "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096", 169 | "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16" 170 | ], 171 | "index": "pypi", 172 | "version": "==1.0.0" 173 | }, 174 | "wtforms": { 175 | "hashes": [ 176 | "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", 177 | "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" 178 | ], 179 | "index": "pypi", 180 | "version": "==2.2.1" 181 | } 182 | }, 183 | "develop": {} 184 | } 185 | -------------------------------------------------------------------------------- /demo_clients/flask/client.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, flash, request 2 | from wtforms import Form 3 | import json 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | 7 | # App config. 8 | DEBUG = False 9 | app = Flask(__name__) 10 | app.config.from_object(__name__) 11 | app.config['SECRET_KEY'] = 'F8756EC47915E4D5CD7700517E22FF2DFE90C1F787EEE42FE07305551DB56AEE' 12 | 13 | commands = [ 14 | '{"action":"block_count"}', 15 | '{"action":"account_info","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}', 16 | '{"action":"account_history","account":"nano_3cpz7oh9qr5b7obbcb5867omqf8esix4sdd5w6mh8kkknamjgbnwrimxsaaf","count":"20"}', 17 | '{"action":"active_difficulty"}', 18 | '{"action":"block_info","json_block":"true","hash":"87434F8041869A01C8F6F263B87972D7BA443A72E0A97D7A3FD0CCC2358FD6F9"}', 19 | '{"action":"receivable","account":"nano_1111111111111111111111111111111111111111111111111117353trpda","count":"5"}', 20 | '{"action":"representatives_online"}', 21 | '{"action":"price"}', 22 | '{"action":"tokens_buy","token_amount":10}', 23 | '{"action":"tokens_buy","token_amount":10,"token_key":"xxx"}', 24 | '{"action":"tokenorder_check","token_key":"xxx"}', 25 | '{"action":"tokens_check","token_key":"xxx"}', 26 | '{"action":"tokenorder_cancel","token_key":"xxx"}', 27 | ] 28 | 29 | username = "user1" 30 | password = "user1" 31 | proxy_server = "http://localhost:9950/proxy" 32 | 33 | # Use html form to collect input data 34 | class ReusableForm(Form): 35 | @app.route("/", methods=['GET', 'POST']) 36 | def GetCommand(): 37 | form = ReusableForm(request.form) 38 | active_command = '' 39 | result_formatted = '' 40 | 41 | if request.method == 'POST': 42 | action = request.form['action'] 43 | try: 44 | # If any input command button with a number 45 | action_number = int(action) 46 | active_command = commands[action_number] 47 | except ValueError: 48 | # Proxy request submit button 49 | active_command = action 50 | result = getRPC(active_command) 51 | # Be able to print both json and error messages 52 | try: 53 | result_formatted = json.dumps(result, indent=2) 54 | except: 55 | result_formatted = result 56 | 57 | # Render the html template 58 | return render_template('index.html', form=form, active_command=active_command, result=result_formatted) 59 | 60 | # Call the proxy server 61 | def getRPC(command): 62 | try: 63 | r = requests.post(proxy_server, json=json.loads(command), verify=False, auth=HTTPBasicAuth(username, password)) 64 | status = r.status_code 65 | #print("Status code: ", status) 66 | if (status == 200): 67 | print("Success!") 68 | try: 69 | #print(r.json()) 70 | return(r.json()) 71 | except: 72 | print(r) 73 | return r 74 | except Exception as e: 75 | print("Fatal error", e) 76 | return "Fatal error", e 77 | 78 | if __name__ == "__main__": 79 | app.run() 80 | -------------------------------------------------------------------------------- /demo_clients/flask/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NanoRPCProxy Client 8 | 9 | 53 | 54 | 55 |

RPC demo client for communicating with NanoRPCProxy

56 |

Send to a live Nano node using RPC json requests
57 | See documentation for more commands
58 |

59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 |
82 |
83 | 84 |
85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /demo_clients/js/client.js: -------------------------------------------------------------------------------- 1 | const RPC_TIMEOUT = 10000 // 10sec timeout for calling RPC proxy 2 | const RPC_LIMIT = 'You have done too many RPC requests. Try again later.' 3 | const SAMPLE_COMMANDS = [ 4 | '{"action":"block_count"}', 5 | '{"action":"account_info","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}', 6 | '{"action":"account_history", "account":"nano_3cpz7oh9qr5b7obbcb5867omqf8esix4sdd5w6mh8kkknamjgbnwrimxsaaf", "count":"20"}', 7 | '{"action":"active_difficulty"}', 8 | '{"action":"block_info","json_block":"true","hash":"87434F8041869A01C8F6F263B87972D7BA443A72E0A97D7A3FD0CCC2358FD6F9"}', 9 | '{"action":"receivable","account":"nano_1111111111111111111111111111111111111111111111111117353trpda","count": "5"}', 10 | '{"action":"representatives_online"}', 11 | '{"action":"price"}', 12 | '{"action":"tokens_buy","token_amount":10}', 13 | '{"action":"tokens_buy","token_amount":10,"token_key":"xxx"}', 14 | '{"action":"tokenorder_check","token_key":"xxx"}', 15 | '{"action":"tokens_check","token_key":"xxx"}', 16 | '{"action":"tokenorder_cancel","token_key":"xxx"}', 17 | ] 18 | const NODE_SERVER = 'http://localhost:9950/proxy' 19 | const CREDS = 'user1:user1' 20 | 21 | // Custom error class 22 | class RPCError extends Error { 23 | constructor(code, ...params) { 24 | super(...params) 25 | 26 | // Maintains proper stack trace for where our error was thrown (only available on V8) 27 | if (Error.captureStackTrace) { 28 | Error.captureStackTrace(this, RPCError) 29 | } 30 | this.name = 'RPCError' 31 | // Custom debugging information 32 | this.code = code 33 | } 34 | } 35 | 36 | function fillCommand(c) { 37 | document.getElementById("myInput").value = SAMPLE_COMMANDS[c] 38 | } 39 | 40 | function callRPC() { 41 | try { 42 | var command = JSON.parse(document.getElementById("myInput").value) 43 | } 44 | catch(e) { 45 | console.log("Could not parse json string") 46 | return 47 | } 48 | //postDataSimple(command) 49 | postData(command) 50 | .then((data) => { 51 | console.log(data) 52 | document.getElementById("myTextarea").value = JSON.stringify(data, null, 2) 53 | }) 54 | .catch(function(error) { 55 | handleRPCError(error) 56 | }) 57 | } 58 | 59 | function handleRPCError(error) { 60 | if (error.code) { 61 | console.log("RPC request failed: "+error.message) 62 | document.getElementById("myTextarea").value = "RPC request failed: "+error.message 63 | } 64 | else { 65 | console.log("RPC request failed: "+error) 66 | document.getElementById("myTextarea").value = "RPC request failed: "+error 67 | } 68 | } 69 | 70 | // Post RPC data with timeout and catch errors 71 | async function postData(data = {}, server=NODE_SERVER) { 72 | let didTimeOut = false; 73 | 74 | return new Promise(function(resolve, reject) { 75 | const timeout = setTimeout(function() { 76 | didTimeOut = true; 77 | reject(new Error('Request timed out')); 78 | }, RPC_TIMEOUT); 79 | 80 | fetch(server, { 81 | method: 'POST', // *GET, POST, PUT, DELETE, etc. 82 | mode: 'cors', // no-cors, *cors, same-origin 83 | cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached 84 | credentials: 'same-origin', // include, *same-origin, omit 85 | headers: { 86 | 'Authorization': 'Basic ' + Base64.encode(CREDS) 87 | }, 88 | redirect: 'follow', // manual, *follow, error 89 | referrerPolicy: 'no-referrer', // no-referrer, *client 90 | body: JSON.stringify(data) // body data type must match "Content-Type" header 91 | }) 92 | .then(async function(response) { 93 | // Clear the timeout as cleanup 94 | clearTimeout(timeout); 95 | if(!didTimeOut) { 96 | if(response.status === 200) { 97 | resolve(await response.json()) 98 | } 99 | // catch blocked (to display on the site) 100 | else if(response.status === 429) { 101 | resolve({"error":await response.text()}) 102 | } 103 | // catch unauthorized (to display on the site) 104 | else if(response.status === 401) { 105 | resolve({"error": "unauthorized"}) 106 | } 107 | else if(response.status === 500) { 108 | resolve(await response.json()) 109 | } 110 | else { 111 | throw new RPCError(response.status, resolve(response)) 112 | } 113 | } 114 | }) 115 | .catch(function(err) { 116 | // Rejection already happened with setTimeout 117 | if(didTimeOut) return; 118 | // Reject with error 119 | reject(err); 120 | }); 121 | }) 122 | .then(async function(result) { 123 | // Request success and no timeout 124 | return await result 125 | }) 126 | } 127 | 128 | async function postDataSimple(data = {}, server=NODE_SERVER) { 129 | // Default options are marked with * 130 | const response = await fetch(server, { 131 | method: 'POST', // *GET, POST, PUT, DELETE, etc. 132 | cache: 'no-cache', 133 | headers: { 134 | 'Authorization': 'Basic ' + Base64.encode(CREDS) 135 | }, 136 | body: JSON.stringify(data) // body data type must match "Content-Type" header 137 | }) 138 | return await response.json(); // parses JSON response into native JavaScript objects 139 | } 140 | -------------------------------------------------------------------------------- /demo_clients/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NanoRPCProxy Client 8 | 9 | 10 | 11 | 51 | 52 | 53 |

RPC demo client for communicating with NanoRPCProxy

54 |

Send to a live Nano node using RPC json requests
55 | See documentation for more commands
56 |

57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /demo_clients/python/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "==2.20.0" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /demo_clients/python/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ef7ed3c35be60a0a8e9353e9aa1f651cf3549865129c0b81413e8e3d7c413b16" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", 22 | "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" 23 | ], 24 | "index": "pypi", 25 | "version": "==2023.7.22" 26 | }, 27 | "chardet": { 28 | "hashes": [ 29 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 30 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 31 | ], 32 | "version": "==3.0.4" 33 | }, 34 | "idna": { 35 | "hashes": [ 36 | "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", 37 | "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" 38 | ], 39 | "version": "==2.7" 40 | }, 41 | "requests": { 42 | "hashes": [ 43 | "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", 44 | "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" 45 | ], 46 | "index": "pypi", 47 | "version": "==2.20.0" 48 | }, 49 | "urllib3": { 50 | "hashes": [ 51 | "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", 52 | "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" 53 | ], 54 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", 55 | "version": "==1.24.3" 56 | } 57 | }, 58 | "develop": {} 59 | } 60 | -------------------------------------------------------------------------------- /demo_clients/python/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import argparse 4 | from requests.auth import HTTPBasicAuth 5 | 6 | # Make sure the proxy server is running and call this with where the number represent one of the commands below 7 | # The example is for MAIN NET and will show bad results on BETA NET 8 | 9 | commands = [ 10 | {"action":"block_count"}, 11 | {"action":"account_info","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}, 12 | {"action":"account_history", "account":"nano_3cpz7oh9qr5b7obbcb5867omqf8esix4sdd5w6mh8kkknamjgbnwrimxsaaf", "count":"20"}, 13 | {"action":"active_difficulty"}, 14 | {"action":"block_info","json_block":"true","hash":"87434F8041869A01C8F6F263B87972D7BA443A72E0A97D7A3FD0CCC2358FD6F9"}, 15 | {"action":"receivable","account":"nano_1111111111111111111111111111111111111111111111111117353trpda","count": "5"}, 16 | {"action":"representatives_online"}, 17 | {"action":"price"}, 18 | ] 19 | 20 | username = "user1" 21 | password = "user1" 22 | 23 | # Parse argument --c [int] to call different commands 24 | parser = argparse.ArgumentParser(description="Call proxy server") 25 | parser.add_argument("--c", default=1, type=int, choices=[1, 2, 3, 4, 5, 6, 7, 8], required=True, help="The action to call") 26 | parser.add_argument("--a", action="store_true", help="Use authentication") 27 | args = parser.parse_args() 28 | command = commands[int(args.c)-1] 29 | 30 | try: 31 | # If using auth or not 32 | if args.a: 33 | print("Authorizing with " + username + " | " + password) 34 | r = requests.post('http://localhost:9950/proxy', json=command, verify=False, auth=HTTPBasicAuth(username, password)) 35 | #r = requests.get('http://localhost:9950/proxy?action=block_count', auth=HTTPBasicAuth(username, password)) 36 | else: 37 | r = requests.post("http://localhost:9950/proxy", json=command) 38 | status = r.status_code 39 | print("Status code: ", status) 40 | if (status == 200): 41 | print("Success!") 42 | 43 | try: 44 | print(r.json()) 45 | except: 46 | print(r) 47 | 48 | r.raise_for_status() 49 | 50 | except requests.exceptions.HTTPError as errh: 51 | print ("Http Error:",errh) 52 | except requests.exceptions.ConnectionError as errc: 53 | print ("Error Connecting:",errc) 54 | except requests.exceptions.Timeout as errt: 55 | print ("Timeout Error:",errt) 56 | except requests.exceptions.RequestException as err: 57 | print ("Oops: Something Else",err) 58 | except Exception as e: 59 | print("Fatal error", e) 60 | -------------------------------------------------------------------------------- /demo_clients/reactjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | #/build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /demo_clients/reactjs/README.md: -------------------------------------------------------------------------------- 1 | ## Demo client for interacting with NanoRPCProxy 2 | Support for server authentication via src/rpc.js and token purchases. 3 | 4 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 5 | 6 | ## Available Scripts 7 | 8 | In the project directory, you can run: 9 | 10 | ### `yarn start` 11 | 12 | Runs the app in the development mode.
13 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 14 | 15 | The page will reload if you make edits.
16 | You will also see any lint errors in the console. 17 | 18 | ### `yarn test` 19 | 20 | Launches the test runner in the interactive watch mode.
21 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 22 | 23 | ### `yarn build` 24 | 25 | Builds the app for production to the `build` folder.
26 | It correctly bundles React in production mode and optimizes the build for the best performance. 27 | 28 | The build is minified and the filenames include the hashes.
29 | Your app is ready to be deployed! 30 | 31 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 32 | -------------------------------------------------------------------------------- /demo_clients/reactjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NanoRPCProxyClient", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": ".", 6 | "license": "GPL-3.0", 7 | "author": "Joohansson", 8 | "dependencies": { 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.3.2", 11 | "@testing-library/user-event": "^7.1.2", 12 | "bootstrap": "^4.4.1", 13 | "html-react-parser": "^0.10.3", 14 | "jquery": "^3.5.0", 15 | "js-base64": "^2.5.2", 16 | "nanocurrency": "^2.4.0", 17 | "popper.js": "^1.16.1", 18 | "react": "^16.13.1", 19 | "react-bootstrap": "^1.0.0", 20 | "react-dom": "^16.13.1", 21 | "react-scripts": "5.0.1", 22 | "typescript": "^3.8.3" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo_clients/reactjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/demo_clients/reactjs/public/favicon.ico -------------------------------------------------------------------------------- /demo_clients/reactjs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | NanoRPCProxy Client 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo_clients/reactjs/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/demo_clients/reactjs/public/logo192.png -------------------------------------------------------------------------------- /demo_clients/reactjs/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/demo_clients/reactjs/public/logo512.png -------------------------------------------------------------------------------- /demo_clients/reactjs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /demo_clients/reactjs/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/App.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | } 5 | 6 | body { 7 | background-color: #000034; 8 | } 9 | 10 | .App { 11 | /*text-align: center;*/ 12 | color: #EEE; 13 | font-size: calc(10px + 0.5vmin); 14 | line-height: calc(10px + 1.2vmin); 15 | height: 100%; 16 | max-height: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | } 20 | 21 | .App-header { 22 | width: 80%; 23 | margin: 0 auto; 24 | margin-top: 1vh; 25 | } 26 | 27 | p { 28 | font-size: calc(10px + 0.7vmin); 29 | line-height: calc(10px + 1.0vmin); 30 | margin-bottom: 0.5rem; 31 | } 32 | 33 | a { 34 | color: #4a90e2; 35 | text-decoration: none; 36 | } 37 | 38 | a:hover, a:focus { 39 | color: #377ccc; 40 | text-decoration: underline; 41 | cursor: pointer; 42 | } 43 | 44 | #input, #output-area { 45 | font-size: calc(10px + 0.3vmin); 46 | } 47 | 48 | .line { 49 | height:1px; 50 | background-color: #4a90e2; 51 | margin-bottom: 15px; 52 | } 53 | 54 | .hidden { 55 | display: none; 56 | } 57 | 58 | .btn-primary { 59 | color: #fff; 60 | background-color: #4a90e2; 61 | border-color: #4a90e2; 62 | margin-right: 20px; 63 | } 64 | 65 | .btn-primary:focus { 66 | box-shadow: none; 67 | background-color: #4a90e2; 68 | border-color: #4a90e2; 69 | } 70 | 71 | .btn-outline-secondary:focus { 72 | box-shadow: none; 73 | } 74 | 75 | .form-control:focus { 76 | box-shadow: none; 77 | } 78 | 79 | /* Special margin for double text box */ 80 | .edit-short { 81 | max-width: 200px; 82 | } 83 | 84 | .btn-active { 85 | background-color: #6C757D; 86 | color: #FFF; 87 | } 88 | 89 | /* QR style */ 90 | .QR-container { 91 | width: 148px; 92 | height: 148px; 93 | padding: 10px; 94 | background-color: #EEE; 95 | text-align: center; 96 | margin-top: 20px; 97 | margin-bottom: 10px; 98 | } 99 | 100 | .QR-img { 101 | width: 128px; 102 | height: 128px; 103 | cursor: pointer; 104 | } 105 | 106 | .QR-container-2x { 107 | width: 276px; 108 | height: 276px; 109 | padding: 10px; 110 | background-color: #EEE; 111 | margin-top: 20px; 112 | margin-bottom: 10px; 113 | text-align: center; 114 | } 115 | 116 | .QR-img-2x { 117 | width: 256px; 118 | height: 256px; 119 | cursor: pointer; 120 | } 121 | 122 | .QR-container-4x { 123 | width: 532px; 124 | height: 532px; 125 | padding: 10px; 126 | background-color: #EEE; 127 | margin-top: 20px; 128 | margin-bottom: 10px; 129 | text-align: center; 130 | } 131 | 132 | .QR-img-4x { 133 | width: 512px; 134 | height: 512px; 135 | cursor: pointer; 136 | } 137 | 138 | /* Medium width button */ 139 | .btn-medium { 140 | width: 160px; 141 | margin-bottom: 0; 142 | } 143 | 144 | /* Token output text */ 145 | .token-text { 146 | border: 1px #eee solid; 147 | padding: 5px; 148 | margin-bottom: 10px; 149 | overflow-wrap: break-word; 150 | } 151 | .token-text p { 152 | font-size: calc(10px + 0.5vmin); 153 | line-height: calc(10px + 0.8vmin); 154 | margin-bottom: 0.5vh; 155 | } 156 | 157 | /* The left thing of input boxes */ 158 | .input-group-prepend, .input-group-text { 159 | width: 140px; 160 | } 161 | 162 | .command-dropdown { 163 | margin-top: 2vh; 164 | margin-bottom: 15px; 165 | } 166 | 167 | .command-dropdown .dropdown-toggle { 168 | font-size: calc(10px + 0.8vmin); 169 | width: 280px; 170 | text-align: left; 171 | } 172 | 173 | /* Striped dropdown menu */ 174 | .dropdown-menu > a:nth-child(odd) { 175 | background: #EEE; 176 | } 177 | 178 | /* Change color on dropdown menu hover */ 179 | .dropdown-item:hover { 180 | background: #4a90e2 !important; 181 | } 182 | 183 | /* RADIO BUTTONS */ 184 | .auth-title { 185 | width: 100px; 186 | } 187 | 188 | @media screen and (max-width: 800px) { 189 | h3{font-size: 1.5rem;} 190 | .App { 191 | font-size: calc(10px + 0.4vmin); 192 | line-height: calc(10px + 1.0vmin); 193 | } 194 | .App-header { 195 | width: 95%; 196 | margin: 0 auto; 197 | padding: 0 0; 198 | } 199 | 200 | .dropdown-toggle.btn-primary { 201 | width: 100%; 202 | } 203 | 204 | .btn-medium { 205 | width: 160px; 206 | margin-bottom: 1vh; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import * as rpc from './rpc' //rpc creds not shared on github 4 | import { Base64 } from 'js-base64'; 5 | import { Dropdown, DropdownButton, InputGroup, FormControl, Button} from 'react-bootstrap' 6 | import QrImageStyle from './components/qrImageStyle' 7 | import Parser from 'html-react-parser' 8 | import * as Nano from 'nanocurrency' 9 | import $ from 'jquery' 10 | 11 | const RPC_TIMEOUT = 10000 // 10sec timeout for calling RPC proxy 12 | 13 | //CONSTANTS 14 | export const constants = { 15 | // These are taken from the creds file 16 | RPC_SERVER: rpc.RPC_SERVER, 17 | RPC_CREDS: rpc.RPC_CREDS, 18 | 19 | // Nano sample commands 20 | SAMPLE_COMMANDS: [ 21 | '{"action":"account_history", "account":"nano_3cpz7oh9qr5b7obbcb5867omqf8esix4sdd5w6mh8kkknamjgbnwrimxsaaf","count":"20"}', 22 | '{"action":"account_info","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}', 23 | '{"action":"account_balance","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}', 24 | '{"action":"account_key","account":"nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx"}', 25 | '{"action":"account_representative","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}', 26 | '{"action":"account_weight","account":"nano_1iuz18n4g4wfp9gf7p1s8qkygxw7wx9qfjq6a9aq68uyrdnningdcjontgar"}', 27 | '{"action":"active_difficulty"}', 28 | '{"action":"available_supply"}', 29 | '{"action":"block_info","json_block":"true","hash":"87434F8041869A01C8F6F263B87972D7BA443A72E0A97D7A3FD0CCC2358FD6F9"}', 30 | '{"action":"block_account","hash":"F94A33B1CDC646A5A1F51AB576590EBD1A65171FFC31EF5C608B71C94BA24695"}', 31 | '{"action":"block_count"}', 32 | '{"action":"chain","block":"87434F8041869A01C8F6F263B87972D7BA443A72E0A97D7A3FD0CCC2358FD6F9","count":"20"}', 33 | '{"action":"confirmation_quorum"}', 34 | '{"action":"delegators","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}', 35 | '{"action":"delegators_count","account":"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3"}', 36 | '{"action":"frontiers", "account":"nano_3cpz7oh9qr5b7obbcb5867omqf8esix4sdd5w6mh8kkknamjgbnwrimxsaaf","count":"20"}', 37 | '{"action":"key_create"}', 38 | '{"action": "process","json_block": "true","subtype": "send","block": {"type": "state","account": "nano_1qato4k7z3spc8gq1zyd8xeqfbzsoxwo36a45ozbrxcatut7up8ohyardu1z","previous": "6CDDA48608C7843A0AC1122BDD46D9E20E21190986B19EAC23E7F33F2E6A6766","representative": "nano_3pczxuorp48td8645bs3m6c3xotxd3idskrenmi65rbrga5zmkemzhwkaznh","balance": "40200000001000000000000000000000000","link": "87434F8041869A01C8F6F263B87972D7BA443A72E0A97D7A3FD0CCC2358FD6F9","link_as_account":"nano_33t5by1653nt196hfwm5q3wq7oxtaix97r7bhox5zn8eratrzoqsny49ftsd","signature": "A5DB164F6B81648F914E49CAB533900C389FAAD64FBB24F6902F9261312B29F730D07E9BCCD21D918301419B4E05B181637CF8419ED4DCBF8EF2539EB2467F07","work": "000bc55b014e807d"}}', 39 | '{"action":"receivable","account":"nano_1111111111111111111111111111111111111111111111111117353trpda","count":"5"}', 40 | '{"action":"receivable_exists","hash":"4563ADCE3495BF1EA6F3B7A84E94AC866039F57706C2A28FAE27EE3F1767FD7D"}', 41 | '{"action":"representatives_online"}', 42 | '{"action":"successors","block":"991CF190094C00F0B68E2E5F75F6BEE95A2E0BD93CEAA4A6734DB9F19B728948","count": "5"}', 43 | '{"action":"mnano_to_raw","amount":"1"}', 44 | '{"action":"mnano_from_raw","amount":"1000000000000000000000000000000"}', 45 | '{"action":"validate_account_number","account": "nano_1111111111111111111111111111111111111111111111111117353trpda"}', 46 | '{"action":"version"}', 47 | '{"action":"verified_accounts"}', 48 | '{"action":"price"}', 49 | 50 | ], 51 | // For dropdown titles 52 | SAMPLE_COMMAND_NAMES: [ 53 | "account_history", 54 | "account_info", 55 | "account_balance", 56 | "account_key", 57 | "account_representative", 58 | "account_weight", 59 | "active_difficulty", 60 | "available_supply", 61 | "block_info", 62 | "block_account", 63 | "block_count", 64 | "chain", 65 | "confirmation_quorum", 66 | "delegators", 67 | "delegators_count", 68 | "frontiers", 69 | "key_create", 70 | "process", 71 | "representatives_online", 72 | "receivable", 73 | "receivable_exists", 74 | "successors", 75 | "mnano_to_raw", 76 | "mnano_from_raw", 77 | "validate_account_number", 78 | "version", 79 | "verified_accounts", 80 | "Nano price" 81 | ] 82 | } 83 | 84 | // Custom error class 85 | class RPCError extends Error { 86 | constructor(code, ...params) { 87 | super(...params) 88 | 89 | // Maintains proper stack trace for where our error was thrown (only available on V8) 90 | if (Error.captureStackTrace) { 91 | Error.captureStackTrace(this, RPCError) 92 | } 93 | this.name = 'RPCError' 94 | // Custom debugging information 95 | this.code = code 96 | } 97 | } 98 | 99 | class App extends Component { 100 | constructor(props) { 101 | super(props) 102 | 103 | // QR css 104 | this.qrClassesContainer = ["QR-container", "QR-container-2x", "QR-container-4x"] 105 | this.qrClassesImg = ["QR-img", "QR-img-2x", "QR-img-4x"] 106 | 107 | this.state = { 108 | command: '', 109 | key: '', 110 | amount: 10, 111 | nanoAmount: 0, 112 | output: '', 113 | validKey: false, 114 | fetchingRPC: false, 115 | paymentActive: false, 116 | activeCommandId: 0, 117 | activeCommandName: 'Select a sample', 118 | useAuth: true, 119 | tokenText1: "", 120 | tokenText2: "", 121 | tokenText3: "", 122 | qrContent: '', 123 | qrSize: 512, 124 | qrState: 0, //qr size 125 | qrHidden: true, 126 | payinfoHidden: true, 127 | apiText: "", 128 | tokenPrice: 0.0001, //temp price, real price grabbed from server 129 | } 130 | 131 | this.getRPC = this.getRPC.bind(this) 132 | this.buyTokens = this.buyTokens.bind(this) 133 | this.checkTokens = this.checkTokens.bind(this) 134 | this.cancelOrder = this.cancelOrder.bind(this) 135 | this.prepareForPayment = this.prepareForPayment.bind(this) 136 | this.handleCommandChange = this.handleCommandChange.bind(this) 137 | this.handleKeyChange = this.handleKeyChange.bind(this) 138 | this.handleAmountChange = this.handleAmountChange.bind(this) 139 | this.handleNanoChange = this.handleNanoChange.bind(this) 140 | this.handleRPCError = this.handleRPCError.bind(this) 141 | this.selectCommand = this.selectCommand.bind(this) 142 | this.postData = this.postData.bind(this) 143 | this.handleOptionChange = this.handleOptionChange.bind(this) 144 | this.updateQR = this.updateQR.bind(this) 145 | this.double = this.double.bind(this) 146 | } 147 | 148 | // Init component 149 | componentDidMount() { 150 | // try update the price 151 | var command = { 152 | action: "tokenprice_check", 153 | } 154 | this.postData(command) 155 | .then((data) => { 156 | if ("token_price" in data) { 157 | let nano = this.state.amount * parseFloat(data.token_price) 158 | this.setState({ 159 | nanoAmount: nano 160 | }) 161 | } 162 | }) 163 | .catch(function(error) { 164 | this.handleRPCError(error) 165 | }.bind(this)) 166 | 167 | // calculate nano cost regardless of if server respond or not 168 | let nano = this.state.amount * this.state.tokenPrice 169 | this.setState({ 170 | nanoAmount: nano 171 | }) 172 | } 173 | 174 | handleCommandChange(event) { 175 | let command = event.target.value 176 | try { 177 | let query = $.param(JSON.parse(command)) //convert json to query string 178 | this.setState({ 179 | apiText: query 180 | }) 181 | } 182 | catch { 183 | this.setState({ 184 | apiText: "Bad json format" 185 | }) 186 | } 187 | 188 | this.setState({ 189 | command: command 190 | }) 191 | } 192 | 193 | handleKeyChange(event) { 194 | let key = event.target.value 195 | if (key.length === 64) { 196 | this.setState({ 197 | validKey: true 198 | }) 199 | } 200 | else { 201 | this.setState({ 202 | validKey: false 203 | }) 204 | } 205 | this.setState({ 206 | key: key 207 | }) 208 | } 209 | 210 | handleAmountChange(event) { 211 | if (event.target.value !== "") { 212 | let amount = parseInt(event.target.value) 213 | if (Number.isSafeInteger(amount)) { 214 | // calculate nano cost 215 | let nano = amount * this.state.tokenPrice 216 | this.setState({ 217 | amount: amount, 218 | nanoAmount: nano 219 | }) 220 | } 221 | } 222 | else { 223 | this.setState({ 224 | amount: event.target.value 225 | }) 226 | } 227 | } 228 | 229 | handleNanoChange(event) { 230 | if (event.target.value !== "") { 231 | let amount = event.target.value 232 | if (Number.isSafeInteger(parseInt(amount)) || this.isFloat(parseFloat(amount))) { 233 | // calculate tokens 234 | let tokens = Math.round(parseFloat(amount) / this.state.tokenPrice) 235 | this.setState({ 236 | nanoAmount: event.target.value, 237 | amount: tokens 238 | }) 239 | } 240 | } 241 | else { 242 | this.setState({ 243 | nanoAmount: event.target.value 244 | }) 245 | } 246 | } 247 | 248 | // Select Auth 249 | handleOptionChange = changeEvent => { 250 | this.setState({ 251 | useAuth: !this.state.useAuth 252 | }) 253 | } 254 | 255 | updateQR(address, amount=0) { 256 | let raw = this.MnanoToRaw(amount.toString()) 257 | this.setState({ 258 | qrContent: "nano:"+address+"?amount="+raw+"&message=RPC Proxy Tokens", 259 | }) 260 | if (address === "") { 261 | this.setState({ 262 | qrHidden: true, 263 | }) 264 | } 265 | else { 266 | this.setState({ 267 | qrHidden: false, 268 | }) 269 | } 270 | } 271 | 272 | // loop qr state 1x, 2x, 4x 273 | double() { 274 | var state = this.state.qrState 275 | state = state + 1 276 | if (state >= this.qrClassesContainer.length) { 277 | state = 0 278 | } 279 | this.setState({ 280 | qrState: state 281 | }) 282 | } 283 | 284 | MnanoToRaw(input) { 285 | return this.isNumeric(input) ? Nano.convert(input, {from: Nano.Unit.NANO, to: Nano.Unit.raw}) : 'N/A' 286 | } 287 | 288 | // Check if numeric string 289 | isNumeric(val) { 290 | //numerics and last character is not a dot and number of dots is 0 or 1 291 | let isnum = /^-?\d*\.?\d*$/.test(val) 292 | if (isnum && String(val).slice(-1) !== '.') { 293 | return true 294 | } 295 | else { 296 | return false 297 | } 298 | } 299 | 300 | // Check if float string 301 | isFloat(x) { 302 | return !!(x % 1) 303 | } 304 | 305 | // Change tool to view on main page 306 | selectCommand(eventKey) { 307 | let command = constants.SAMPLE_COMMANDS[eventKey] 308 | let query = $.param(JSON.parse(command)) //convert json to query string 309 | this.setState({ 310 | command: constants.SAMPLE_COMMANDS[eventKey], 311 | activeCommandId: eventKey, 312 | activeCommandName: constants.SAMPLE_COMMAND_NAMES[eventKey], 313 | apiText: query 314 | }) 315 | } 316 | 317 | handleRPCError(error) { 318 | this.setState({fetchingRPC: false}) 319 | if (error.code) { 320 | console.log("RPC request failed: "+error.message) 321 | this.writeOutput({error:"RPC request failed: "+error.message}) 322 | } 323 | else { 324 | console.log("RPC request failed: "+error) 325 | this.writeOutput({error:"RPC request failed: "+error}) 326 | } 327 | } 328 | 329 | buyTokens(event) { 330 | this.setState({payinfoHidden: false}) 331 | 332 | let amount = parseInt(this.state.amount) 333 | if (Number.isInteger(amount) && amount > 0) { 334 | var command = { 335 | action: "tokens_buy", 336 | token_amount: this.state.amount 337 | } 338 | if (this.state.key.length === 64) { 339 | command.token_key = this.state.key 340 | } 341 | this.getRPC(null, command) 342 | } 343 | } 344 | 345 | checkTokens(event) { 346 | this.setState({payinfoHidden: true}) 347 | var command = { 348 | action: "tokens_check", 349 | } 350 | if (this.state.key.length === 64) { 351 | command.token_key = this.state.key 352 | this.getRPC(null, command) 353 | } 354 | } 355 | 356 | cancelOrder(event) { 357 | this.setState({payinfoHidden: true}) 358 | var command = { 359 | action: "tokenorder_cancel", 360 | } 361 | if (this.state.key.length === 64) { 362 | command.token_key = this.state.key 363 | this.getRPC(null, command) 364 | } 365 | } 366 | 367 | // Make RPC call 368 | getRPC(event, command="") { 369 | this.updateQR("") 370 | 371 | // Read command from text box if not provided from other function 372 | if (command === "") { 373 | this.setState({payinfoHidden: true}) 374 | try { 375 | command = JSON.parse(this.state.command) 376 | if (this.state.key.length === 64) { 377 | command.token_key = this.state.key 378 | } 379 | } 380 | catch(e) { 381 | console.log("Could not parse json string") 382 | return 383 | } 384 | } 385 | 386 | if (Object.keys(command).length > 0) { 387 | this.setState({fetchingRPC: true}) 388 | this.postData(command) 389 | .then((data) => { 390 | this.setState({fetchingRPC: false}) 391 | this.writeOutput(data) 392 | }) 393 | .catch(function(error) { 394 | this.handleRPCError(error) 395 | }.bind(this)) 396 | } 397 | } 398 | 399 | // Inform user how to pay and check status 400 | prepareForPayment(json) { 401 | this.setState({ 402 | tokenText1: 'Pay ' + json.payment_amount + 'Nano: ' + json.address + '', 403 | tokenText2: "Request key (save): " + json.token_key, 404 | paymentActive: true 405 | }) 406 | this.updateQR(json.address, json.payment_amount) 407 | 408 | let command = {action:"tokenorder_check",token_key:json.token_key} 409 | 410 | // Check status every second until payment completed or timed out 411 | var timer = setInterval(() => { 412 | this.postData(command) 413 | .then((data) => { 414 | if ("order_time_left" in data) { 415 | if (parseInt(data.order_time_left) > 0) { 416 | this.setState({tokenText3: "You have " + data.order_time_left + "sec to pay"}) 417 | } 418 | } 419 | else if ("tokens_total" in data && "tokens_ordered" in data) { 420 | this.writeOutput(data) 421 | this.setState({ 422 | tokenText1: "Payment completed for " + data.tokens_ordered + " tokens! You now have " + data.tokens_total + " tokens to use", 423 | tokenText2: "Request key: " + json.token_key, 424 | tokenText3: "", 425 | paymentActive: false, 426 | }) 427 | clearInterval(timer) 428 | this.updateQR("") 429 | } 430 | else if ("error" in data) { 431 | this.writeOutput(data) 432 | this.setState({ 433 | tokenText1: data.error, 434 | tokenText2: "", 435 | tokenText3: "", 436 | paymentActive: false, 437 | }) 438 | clearInterval(timer) 439 | this.updateQR("") 440 | } 441 | else { 442 | this.writeOutput(data) 443 | this.setState({ 444 | tokenText1: "Unknown error occured", 445 | tokenText2: "", 446 | tokenText3: "", 447 | paymentActive: false, 448 | }) 449 | clearInterval(timer) 450 | this.updateQR("") 451 | } 452 | }) 453 | .catch(function(error) { 454 | this.setState({ 455 | tokenText1: "", 456 | tokenText2: "", 457 | tokenText3: "", 458 | paymentActive: false, 459 | }) 460 | clearInterval(timer) 461 | this.updateQR("") 462 | this.handleRPCError(error) 463 | }.bind(this)) 464 | },1000) 465 | } 466 | 467 | // Write result in output area 468 | writeOutput(json) { 469 | if ('address' in json) { 470 | this.prepareForPayment(json) 471 | } 472 | try { 473 | this.setState({ 474 | output: JSON.stringify(json, null, 2) 475 | }) 476 | } 477 | catch(error) { 478 | console.log("Bad JSON: "+error) 479 | } 480 | } 481 | 482 | // Post RPC data with timeout and catch errors 483 | async postData(data = {}, server=constants.RPC_SERVER) { 484 | let didTimeOut = false; 485 | var headers = {} 486 | if (this.state.useAuth) { 487 | headers.Authorization = 'Basic ' + Base64.encode(constants.RPC_CREDS) 488 | } 489 | 490 | return new Promise(function(resolve, reject) { 491 | const timeout = setTimeout(function() { 492 | didTimeOut = true; 493 | reject(new Error('Request timed out')) 494 | }, RPC_TIMEOUT); 495 | 496 | fetch(server, { 497 | method: 'POST', // *GET, POST, PUT, DELETE, etc. 498 | mode: 'cors', // no-cors, *cors, same-origin 499 | cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached 500 | credentials: 'same-origin', // include, *same-origin, omit 501 | headers: headers, 502 | redirect: 'follow', // manual, *follow, error 503 | referrerPolicy: 'no-referrer', // no-referrer, *client 504 | body: JSON.stringify(data) // body data type must match "Content-Type" header 505 | }) 506 | .then(async function(response) { 507 | // Clear the timeout as cleanup 508 | clearTimeout(timeout) 509 | if(!didTimeOut) { 510 | if(response.status === 200) { 511 | resolve(await response.json()) 512 | } 513 | // catch blocked (to display on the site) 514 | else if(response.status === 429) { 515 | resolve({"error":await response.text()}) 516 | } 517 | // catch unauthorized (to display on the site) 518 | else if(response.status === 401) { 519 | resolve({"error": "unauthorized"}) 520 | } 521 | else if(response.status === 500) { 522 | resolve(await response.json()) 523 | } 524 | else { 525 | throw new RPCError(response.status, resolve(response)) 526 | } 527 | } 528 | }) 529 | .catch(function(err) { 530 | // Rejection already happened with setTimeout 531 | if(didTimeOut) return 532 | // Reject with error 533 | reject(err) 534 | }) 535 | }) 536 | .then(async function(result) { 537 | // Request success and no timeout 538 | return result 539 | }) 540 | } 541 | 542 | render() { 543 | return ( 544 |
545 |
546 |

Demo client for communicating with NanoRPCProxy

547 |

Send to a live Nano node using RPC json requests

548 |
    549 |
  • Everyone are allowed x requests/day (shown in the response). Purchase optional tokens if you need more.
  • 550 |
  • Tokens can be refilled/extended using the same Request Key. The order is done when said Nano (or more) is registered.
  • 551 |
  • If you send nano but order fail you can claim the private key. The old deposit account will be destroyed/replaced.
  • 552 |
553 | 558 | {constants.SAMPLE_COMMAND_NAMES.map(function(command, index){ 559 | return {command}; 560 | }.bind(this))} 561 | 562 | 563 | 564 | 565 | 566 | RPC Command 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | Request Key 576 | 577 | 578 | 579 | 580 | 581 |
582 | GET Query Equivalent (need basic auth headers if using server auth):
{constants.RPC_SERVER+"/?"+this.state.apiText} 583 |
584 | 585 | 586 |
Use Auth:
587 |
588 | 589 |
590 |
591 | 592 | 593 | 594 | 595 | 596 |
597 |
598 | 599 | 600 | 601 | 602 | Token Amount 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | Nano Amount 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 |
624 |

{Parser(this.state.tokenText1)}

625 |

{this.state.tokenText2}

626 |

{this.state.tokenText3}

627 |
628 | 629 |
630 |
631 | 632 |
633 |
634 | 635 | 636 | 637 | 638 |
639 |
640 | ) 641 | } 642 | } 643 | 644 | export default App; 645 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/components/qrImageStyle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import QrCode from './qr-code.js'; 3 | 4 | export default class QrImageStyle extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | image: null, 9 | prevContent: this.props.qrContent 10 | }; 11 | } 12 | 13 | static getDerivedStateFromProps(props, state) { 14 | if (props.content !== state.prevContent) { 15 | const qr = document.createElement('canvas'); 16 | QrCode.render({ 17 | text: props.content, 18 | radius: 0.5, // 0.0 to 0.5 19 | ecLevel: 'Q', // L, M, Q, H 20 | fill: { 21 | type: 'radial-gradient', // or 'linear-gradient' 22 | position: [ 0.5,0.5,0, 0.5,0.5,0.75 ], //xPos,yPos,radius of inner and outer circle where position is 0-1 of full dimension 23 | colorStops: [ 24 | [ 0, '#376ab4' ], //from 0 to 100% (0-1) 25 | [ 1, '#000034' ], 26 | ] 27 | }, // foreground color 28 | background: null, // color or null for transparent 29 | size: props.size // in pixels 30 | }, qr); 31 | 32 | return { 33 | prevContent: props.content, 34 | image: qr.toDataURL('image/png') 35 | }; 36 | } 37 | return null; 38 | } 39 | 40 | render() { 41 | return QR; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import 'bootstrap/dist/css/bootstrap.css'; 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/rpc.js: -------------------------------------------------------------------------------- 1 | // If sharing source code publically like on github, this file can be git ignored. The creds will be hidden inside the react app 2 | export const RPC_SERVER = 'http://localhost:9950/proxy' 3 | export const RPC_CREDS = 'user1:user1' 4 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /demo_clients/reactjs/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /demo_clients/websocket_js/client.js: -------------------------------------------------------------------------------- 1 | const RPC_TIMEOUT = 10000 // 10sec timeout for calling RPC proxy 2 | const WS_SERVER = 'ws://localhost:9952' 3 | 4 | function sleep_simple(ms) { 5 | return new Promise(resolve => setTimeout(resolve, ms)) 6 | } 7 | 8 | var socket_nano 9 | 10 | function callWebsocketSubscribe() { 11 | callWebsocket('subscribe') 12 | } 13 | 14 | function callWebsocketUpdate() { 15 | callWebsocket('update') 16 | } 17 | 18 | function callWebsocket(action) { 19 | var nanoWebsocketOffline = false 20 | let tracked_accounts = document.getElementById("myInput").value.replace(" ", "").split(',') 21 | 22 | // Websocket for NANO with automatic reconnect 23 | async function socket_sleep(sleep=5000) { 24 | await sleep_simple(sleep) 25 | socket_nano = new WebSocket(WS_SERVER) 26 | socket_nano.addEventListener('open', socketOpenListener) 27 | socket_nano.addEventListener('error', socketErrorListener) 28 | socket_nano.addEventListener('message', socketMessageListener) 29 | socket_nano.addEventListener('close', socketCloseListener) 30 | } 31 | 32 | const socketMessageListener = (event) => { 33 | let res = JSON.parse(event.data) 34 | var output = document.getElementById("myTextarea").value 35 | document.getElementById("myTextarea").value = output + JSON.stringify(res, null, 2) + '\n-----------------\n' 36 | } 37 | 38 | const socketOpenListener = (event) => { 39 | console.log("NANO socket opened") 40 | nanoWebsocketOffline = false 41 | //Subscribe 42 | let msg = { 43 | "action": action, 44 | "topic": "confirmation", 45 | "id": "1", 46 | "ack": true, 47 | "options": { 48 | "accounts": tracked_accounts.length > 0 ? tracked_accounts : [] 49 | } 50 | } 51 | socket_nano.send(JSON.stringify(msg)) 52 | } 53 | 54 | const socketErrorListener = (event) => { 55 | console.error("Websocket looks offline. Please try again later.") 56 | nanoWebsocketOffline = true 57 | } 58 | 59 | const socketCloseListener = (event) => { 60 | if (socket_nano) { 61 | console.error('NANO socket disconnected due to inactivity.') 62 | // if socket offline, try again in 5min 63 | if (nanoWebsocketOffline) { 64 | socket_sleep(300000) 65 | } 66 | // or in one second 67 | else { 68 | socket_sleep(1000) 69 | } 70 | } 71 | else { 72 | socket_sleep(1000) 73 | } 74 | } 75 | 76 | // Start the websocket client 77 | socketCloseListener() 78 | } 79 | -------------------------------------------------------------------------------- /demo_clients/websocket_js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NanoRPCProxy Client 8 | 9 | 10 | 11 | 87 | 88 | 89 |
90 |

Demo client for subscribing to NanoRPCProxy websocket

91 |

Enter Nano accounts to track (comma separated)
92 | See documentation for more info about websockets
93 |

94 |

95 | Message for block confirmations (ws://localhost:9952) 96 |

97 |
98 |
{
 99 |     "action": "subscribe",
100 |     "topic": "confirmation",
101 |     "options": {
102 |       "accounts": ["account1","account2"]
103 |     }
104 | }
105 |       
106 |
107 |

108 | Try subscribing to nano-faucet: nano_34prihdxwz3u4ps8qjnn14p7ujyewkoxkwyxm3u665it8rg5rdqw84qrypzk 109 |

110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /demo_clients/websocket_nodejs/client.js: -------------------------------------------------------------------------------- 1 | const ReconnectingWebSocket = require('reconnecting-websocket') 2 | const WS = require('ws') 3 | 4 | // Connect to this host 5 | let ws_host = 'ws://localhost:9952' 6 | // Subscribe to these accounts 7 | let tracked_accounts = ['nano_3jsonxwips1auuub94kd3osfg98s6f4x35ksshbotninrc1duswrcauidnue', 'nano_3g81wxaobotd7ocqpqto4exxeei7mazufaq9xzfogtaqosy9orcnxuybnyjo'] 8 | 9 | console.log("Requesting to subscribe to accounts:\n", tracked_accounts) 10 | 11 | // Create a websocket and reconnect if broken 12 | ws = new ReconnectingWebSocket(ws_host, [], { 13 | WebSocket: WS, 14 | connectionTimeout: 1000, 15 | maxRetries: Infinity, 16 | maxReconnectionDelay: 8000, 17 | minReconnectionDelay: 3000 18 | }) 19 | 20 | // A tracked account was detected 21 | ws.onmessage = msg => { 22 | if (typeof msg.data === 'string') { 23 | console.log(msg.data) 24 | } 25 | } 26 | 27 | // As soon as we connect, subscribe to confirmations 28 | ws.onopen = () => { 29 | console.log('WebSocket Client Connected') 30 | if (ws.readyState === ws.OPEN) { 31 | let msg = { 32 | "action": "subscribe", 33 | "topic": "confirmation", 34 | "options": { 35 | "accounts": tracked_accounts 36 | } 37 | } 38 | ws.send(JSON.stringify(msg)) 39 | } 40 | } 41 | ws.onclose = () => { 42 | console.log("WebSocket Client Closed") 43 | } 44 | ws.onerror = (e) => { 45 | console.log("Websocket: " + e.error) 46 | } 47 | -------------------------------------------------------------------------------- /demo_clients/websocket_nodejs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NanoRPCProxy", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "reconnecting-websocket": { 8 | "version": "4.4.0", 9 | "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", 10 | "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==" 11 | }, 12 | "ws": { 13 | "version": "7.4.6", 14 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 15 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo_clients/websocket_nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NanoRPCProxy", 3 | "version": "1.0.0", 4 | "description": "Websocket demo", 5 | "main": "client.js", 6 | "license": "GPL-3.0", 7 | "repository": "https://github.com/Joohansson/NanoRPCProxy", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "start": "node -r esm client.js" 11 | }, 12 | "homepage": "https://github.com/joohansson/NanoRPCProxy", 13 | "dependencies": { 14 | "reconnecting-websocket": "^4.4.0", 15 | "ws": "^7.4.6" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo_clients/websocket_python/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | websockets = "==9.1" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /demo_clients/websocket_python/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "acf64ac72e7f234b00f108693503210baa62f0a458c04c8e555360f5079b518d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "websockets": { 20 | "hashes": [ 21 | "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc", 22 | "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e", 23 | "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135", 24 | "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02", 25 | "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3", 26 | "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf", 27 | "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b", 28 | "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2", 29 | "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af", 30 | "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d", 31 | "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880", 32 | "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077", 33 | "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f", 34 | "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec", 35 | "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25", 36 | "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0", 37 | "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe", 38 | "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a", 39 | "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb", 40 | "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d", 41 | "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857", 42 | "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c", 43 | "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0", 44 | "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40", 45 | "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4", 46 | "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20", 47 | "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314", 48 | "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da", 49 | "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58", 50 | "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2", 51 | "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd", 52 | "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a", 53 | "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd" 54 | ], 55 | "index": "pypi", 56 | "version": "==9.1" 57 | } 58 | }, 59 | "develop": {} 60 | } 61 | -------------------------------------------------------------------------------- /demo_clients/websocket_python/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | import json 4 | import argparse 5 | 6 | # Connect to this host 7 | ws_host = 'ws://localhost:9952' 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('--a', nargs='+', dest='accounts', required=True, help="The accounts to track (space separated)") 11 | args = parser.parse_args() 12 | 13 | print ("Requesting to subscribe to accounts:\n", args.accounts) 14 | 15 | def pretty(message): 16 | return json.dumps(message, indent=2) 17 | 18 | async def main(): 19 | # Predefined subscription message 20 | msg = { 21 | "action": "subscribe", 22 | "topic": "confirmation", 23 | "options": { 24 | "accounts": args.accounts 25 | } 26 | } 27 | try: 28 | async with websockets.connect(ws_host) as websocket: 29 | await websocket.send(json.dumps(msg)) 30 | while 1: 31 | rec = json.loads(await websocket.recv()) 32 | print(pretty(rec)) 33 | except: 34 | print("Websocket connection error") 35 | # wait 5sec and reconnect 36 | await asyncio.sleep(5) 37 | await main() 38 | 39 | try: 40 | asyncio.get_event_loop().run_until_complete(main()) 41 | except KeyboardInterrupt: 42 | pass 43 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | nano-rpc-proxy: 4 | build: . 5 | environment: 6 | - CONFIG_CREDS_SETTINGS=/etc/nano-rpc-proxy/creds.json 7 | - CONFIG_POW_CREDS_SETTINGS=/etc/nano-rpc-proxy/pow_creds.json 8 | - CONFIG_SETTINGS=/etc/nano-rpc-proxy/settings.json 9 | - CONFIG_TOKEN_SETTINGS=/etc/nano-rpc-proxy/token_settings.json 10 | - CONFIG_USER_SETTINGS=/etc/nano-rpc-proxy/user_settings.json 11 | - CONFIG_REQUEST_STAT=/var/lib/nano-rpc-proxy/request-stat.json 12 | - CONFIG_WEBSOCKET_PATH=/var/lib/nano-rpc-proxy/websocket.json 13 | - CONFIG_DB_PATH=/var/lib/nano-rpc-proxy/db.json 14 | ports: 15 | - "9950:9950" 16 | - "9952:9952" 17 | volumes: 18 | - ./settings.json:/etc/nano-rpc-proxy/settings.json 19 | - ./creds.json:/etc/nano-rpc-proxy/creds.json 20 | - ./user_settings.json:/etc/nano-rpc-proxy/user_settings.json 21 | - ./token_settings.json:/etc/nano-rpc-proxy/token_settings.json 22 | - ./db.json:/var/lib/nano-rpc-proxy/db.json 23 | - ./request-stat.json:/var/lib/nano-rpc-proxy/request-stat.json 24 | - ./websocket.json:/var/lib/nano-rpc-proxy/websocket.json 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "testMatch": [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/?(*.)+(spec|test|it).+(ts|tsx|js)" 8 | ], 9 | "transform": { 10 | "^.+\\.(ts|tsx)$": "ts-jest" 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /media/NanoRPCPRoxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/NanoRPCPRoxy.png -------------------------------------------------------------------------------- /media/NanoRPCPRoxy_tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/NanoRPCPRoxy_tokens.png -------------------------------------------------------------------------------- /media/NanoRPCProxy_limiter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/NanoRPCProxy_limiter.png -------------------------------------------------------------------------------- /media/NanoRPCProxy_ws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/NanoRPCProxy_ws.png -------------------------------------------------------------------------------- /media/client_demo_js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_js.png -------------------------------------------------------------------------------- /media/client_demo_python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_python.png -------------------------------------------------------------------------------- /media/client_demo_react_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_react_01.png -------------------------------------------------------------------------------- /media/client_demo_react_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_react_02.png -------------------------------------------------------------------------------- /media/client_demo_react_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_react_03.png -------------------------------------------------------------------------------- /media/client_demo_websocket_js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_websocket_js.png -------------------------------------------------------------------------------- /media/client_demo_websocket_nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_websocket_nodejs.png -------------------------------------------------------------------------------- /media/client_demo_websocket_python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/client_demo_websocket_python.png -------------------------------------------------------------------------------- /media/demo_curl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/demo_curl.png -------------------------------------------------------------------------------- /media/grafana_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/grafana_01.png -------------------------------------------------------------------------------- /media/pm2_monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustwallet/nano-rpc-proxy/672d9f2a99eb0e6f5f805aa64bde728c09fd18d8/media/pm2_monitor.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nano-rpc-proxy", 3 | "version": "1.0.8", 4 | "description": "Relay, firewall, rate limiter, ddos protection and cache system between a client and a Nano node RPC interface", 5 | "main": "src/proxy.js", 6 | "license": "GPL-3.0", 7 | "repository": "https://github.com/trustwallet/nano-rpc-proxy", 8 | "scripts": { 9 | "test": "jest --runInBand", 10 | "start": "npm run-script build && node -r esm dist/proxy.js", 11 | "build": "npx tsc", 12 | "build:prod": "npx tsc --sourceMap false" 13 | }, 14 | "homepage": "https://trustwallet.com", 15 | "dependencies": { 16 | "big-integer": "^1.6.48", 17 | "bigdecimal": "^0.6.1", 18 | "console-stamp": "^3.1.1", 19 | "cors": "^2.8.5", 20 | "dotenv": "^16.0.3", 21 | "esm": "^3.2.25", 22 | "express": "^4.19.2", 23 | "express-basic-auth": "^1.2.0", 24 | "express-ipfilter": "^1.2.0", 25 | "express-slow-down": "^1.6.0", 26 | "fs": "0.0.1-security", 27 | "helmet": "^4.1.1", 28 | "http": "0.0.0", 29 | "https": "^1.0.0", 30 | "ip-range-check": "^0.2.0", 31 | "lowdb": "^1.0.0", 32 | "nanocurrency": "^2.5.0", 33 | "nanocurrency-web": "^1.4.3", 34 | "node-cache": "^5.1.2", 35 | "node-fetch": "^2.6.7", 36 | "node-schedule": "^2.1.1", 37 | "prom-client": "^14.2.0", 38 | "rate-limiter-flexible": "^2.2.1", 39 | "reconnecting-websocket": "^4.4.0", 40 | "remove-trailing-zeros": "^1.0.3", 41 | "tweetnacl": "^1.0.1", 42 | "websocket": "^1.0.31", 43 | "ws": "^7.4.6" 44 | }, 45 | "devDependencies": { 46 | "@types/console-stamp": "^3.0.0", 47 | "@types/cors": "^2.8.9", 48 | "@types/express": "^4.17.17", 49 | "@types/express-slow-down": "^1.3.2", 50 | "@types/jest": "^26.0.19", 51 | "@types/lowdb": "^1.0.9", 52 | "@types/node": "^14.14.14", 53 | "@types/node-fetch": "^2.5.7", 54 | "@types/node-schedule": "^2.1.0", 55 | "@types/supertest": "^2.0.10", 56 | "@types/websocket": "^1.0.1", 57 | "@types/ws": "^7.4.0", 58 | "jest": "^26.6.3", 59 | "supertest": "^6.3.3", 60 | "ts-jest": "^26.4.4", 61 | "typescript": "^4.1.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pow_creds.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "dpow": { 3 | "user": "user", 4 | "key": "key" 5 | }, 6 | "bpow": { 7 | "user": "user", 8 | "key": "key" 9 | }, 10 | "work_server": { 11 | "url": "http://127.0.0.1", 12 | "port": "55555" 13 | } 14 | } -------------------------------------------------------------------------------- /settings.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "node_url": "http://[::1]:7076", 3 | "node_headers": { 4 | "Content-Type": "application/json" 5 | }, 6 | "node_ws_url": "ws://127.0.0.1:7078", 7 | "http_port": 9950, 8 | "https_port": 9951, 9 | "request_path": "/proxy", 10 | "websocket_http_port": 9952, 11 | "websocket_https_port": 9953, 12 | "use_auth": false, 13 | "use_slow_down": true, 14 | "use_rate_limiter": true, 15 | "use_cache": true, 16 | "use_http": true, 17 | "use_https": false, 18 | "https_cert": "/etc/letsencrypt/live/example.com/fullchain.pem", 19 | "https_key": "/etc/letsencrypt/live/example.com/privkey.pem", 20 | "use_output_limiter": true, 21 | "use_ip_blacklist": true, 22 | "use_tokens": false, 23 | "use_websocket": false, 24 | "allow_websocket_all": false, 25 | "use_cors": true, 26 | "use_dpow": false, 27 | "use_bpow": false, 28 | "use_work_server": false, 29 | "use_work_peers": false, 30 | "enable_prometheus_for_ips": [], 31 | "allowed_commands": [ 32 | "account_history", 33 | "account_info", 34 | "account_balance", 35 | "accounts_balances", 36 | "account_key", 37 | "account_representative", 38 | "account_weight", 39 | "accounts_frontiers", 40 | "accounts_receivable", 41 | "active_difficulty", 42 | "available_supply", 43 | "block_account", 44 | "block_info", 45 | "block_count", 46 | "block_create", 47 | "block_confirm", 48 | "blocks_info", 49 | "chain", 50 | "confirmation_quorum", 51 | "delegators_count", 52 | "frontiers", 53 | "key_create", 54 | "process", 55 | "receivable", 56 | "receivable_exists", 57 | "representatives", 58 | "representatives_online", 59 | "sign", 60 | "successors", 61 | "price", 62 | "mnano_to_raw", 63 | "mnano_from_raw", 64 | "work_validate", 65 | "validate_account_number", 66 | "version", 67 | "verified_accounts" 68 | ], 69 | "cached_commands": { 70 | "block_count": 30, 71 | "available_supply": 3600, 72 | "active_difficulty": 30, 73 | "representatives_online": 300 74 | }, 75 | "limited_commands": { 76 | "account_history": 500, 77 | "accounts_frontiers": 500, 78 | "accounts_balances": 500, 79 | "accounts_receivable": 50, 80 | "chain": 500, 81 | "frontiers": 500, 82 | "receivable": 500 83 | }, 84 | "ip_blacklist": [ 85 | "8.8.8.8" 86 | ], 87 | "slow_down": { 88 | "time_window": 600000, 89 | "request_limit": 400, 90 | "delay_increment": 100, 91 | "max_delay": 2000 92 | }, 93 | "rate_limiter": { 94 | "time_window": 86400000, 95 | "request_limit": 5000 96 | }, 97 | "ddos_protection": { 98 | "time_window": 10000, 99 | "request_limit": 100 100 | }, 101 | "proxy_hops": 0, 102 | "websocket_max_accounts": 100, 103 | "cors_whitelist": [], 104 | "log_level": "info" 105 | } 106 | -------------------------------------------------------------------------------- /src/__test__/my_authorizer.test.ts: -------------------------------------------------------------------------------- 1 | import {createProxyAuthorizer} from "../authorize-user"; 2 | import {readUserSettings, UserSettings} from "../user-settings"; 3 | import {readCredentials} from "../credential-settings"; 4 | 5 | const defaultUserSettings: UserSettings = { 6 | allowed_commands: [], 7 | cached_commands: {}, 8 | limited_commands: {}, 9 | use_cache: false, 10 | use_output_limiter: false 11 | }; 12 | 13 | const authorizer = createProxyAuthorizer( 14 | defaultUserSettings, 15 | readUserSettings('user_settings.json.default'), 16 | readCredentials('creds.json.default') 17 | ) 18 | 19 | test('myAuthorizer should authorize existing user', () => { 20 | const authorized = authorizer.myAuthorizer('user1', 'user1') 21 | expect(authorized).toBeTruthy() 22 | }) 23 | 24 | test('myAuthorizer should deny user with wrong password', () => { 25 | const authorized = authorizer.myAuthorizer('user2', 'wrong_password') 26 | expect(authorized).toBeFalsy() 27 | }) 28 | 29 | test('myAuthorizer should override with custom settings', () => { 30 | const userSettings = authorizer.getUserSettings('user2', 'user2') 31 | expect(userSettings?.allowed_commands).toStrictEqual([ 'account_history', 'account_info', 'block_info', 'block_count' ]) 32 | expect(userSettings?.use_cache).toStrictEqual(true) 33 | }) 34 | -------------------------------------------------------------------------------- /src/__test__/pending_response.test.ts: -------------------------------------------------------------------------------- 1 | const expectedResponse = ` 2 | { 3 | "blocks" : { 4 | "000D1BAEC8EC208142C99059B393051BAC8380F9B5A2E6B2489A277D81789F3F": { 5 | "amount": "6000000000000000000000000000000", 6 | "source": "nano_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr" 7 | } 8 | } 9 | } 10 | ` 11 | 12 | test('receivable response should deserialize to Record', async () => { 13 | const receivable: ReceivableResponse = JSON.parse(expectedResponse) 14 | const receivableBlock = receivable.blocks['000D1BAEC8EC208142C99059B393051BAC8380F9B5A2E6B2489A277D81789F3F'] 15 | expect(receivableBlock.amount).toStrictEqual('6000000000000000000000000000000') 16 | expect(receivableBlock.source).toStrictEqual('nano_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr') 17 | }) 18 | -------------------------------------------------------------------------------- /src/__test__/process_request.test.ts: -------------------------------------------------------------------------------- 1 | import {copyConfigFiles, deleteConfigFiles} from "./test-commons"; 2 | 3 | class MockResponse { 4 | statusCode: number | null = null 5 | jsonResponse: any | null = null 6 | 7 | status(statusCode: number): MockResponse { 8 | this.statusCode = statusCode 9 | return this; 10 | } 11 | json(json: any): MockResponse { 12 | this.jsonResponse = json 13 | return this; 14 | } 15 | } 16 | 17 | const filePaths = ['settings.json']; 18 | 19 | beforeAll(() => { 20 | process.env.OVERRIDE_USE_HTTP = 'false' 21 | process.env.CONFIG_SETTINGS = 'src/__test__/settings.json' 22 | copyConfigFiles(filePaths) 23 | }) 24 | 25 | test('processRequest should fail at unreachable node', async () => { 26 | const proxy = require('../proxy') 27 | 28 | let body = { 29 | action: 'block_info' 30 | } 31 | let request: any = { 32 | get: (name: string) => undefined 33 | } 34 | let mockResponse: MockResponse = new MockResponse() 35 | 36 | await proxy.processRequest(body, request, mockResponse) 37 | expect(mockResponse.statusCode).toBe(500) 38 | expect(mockResponse.jsonResponse.error).toContain('Error: Connection error: FetchError') 39 | }) 40 | 41 | test('processRequest should fail on invalid command', async () => { 42 | const proxy = require('../proxy') 43 | 44 | let body = { 45 | action: 'not_supported_command' 46 | } 47 | let request: any = {} 48 | let mockResponse: MockResponse = new MockResponse() 49 | 50 | await proxy.processRequest(body, request, mockResponse) 51 | expect(mockResponse.statusCode).toBe(500) 52 | expect(mockResponse.jsonResponse).toStrictEqual({ 53 | error: 'Action not_supported_command not allowed' 54 | }) 55 | }) 56 | 57 | afterAll(() => deleteConfigFiles(filePaths)) 58 | -------------------------------------------------------------------------------- /src/__test__/proxy.it.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import {copyConfigFiles, deleteConfigFiles} from "./test-commons"; 3 | import * as Fs from "fs"; 4 | 5 | const settingsFilePath = 'settings.json'; 6 | const configSettings = 'src/__test__/settings.json'; 7 | 8 | beforeAll(() => { 9 | process.env.OVERRIDE_USE_HTTP = 'false' 10 | process.env.CONFIG_SETTINGS = configSettings 11 | copyConfigFiles([settingsFilePath]) 12 | let settings = JSON.parse(Fs.readFileSync(configSettings, 'utf-8')) 13 | settings.enable_prometheus_for_ips = ['0.0.0.0/0'] 14 | Fs.writeFileSync(configSettings, JSON.stringify(settings), 'utf-8') 15 | }) 16 | 17 | describe('root request', () => { 18 | it("GET / - success", async () => { 19 | const app = require('../proxy').app 20 | const res = await request(app).get("/") 21 | expect(res.text).toStrictEqual('RPCProxy API

Bad API path

') 22 | }) 23 | }) 24 | 25 | describe('/proxy with default config responds to GET/POST requests', () => { 26 | it("GET /proxy - success", async () => { 27 | const app = require('../proxy').app 28 | const res = await request(app).get("/proxy?action=account_history"); 29 | expect(res.status).toStrictEqual(500) 30 | expect(res.text).toContain('Error: Connection error: FetchError') 31 | }) 32 | it("POST /proxy - success", async () => { 33 | const app = require('../proxy').app 34 | const res = await request(app).post("/proxy").send({action: 'account_history'}); 35 | expect(res.status).toStrictEqual(500) 36 | expect(res.text).toContain('Error: Connection error: FetchError') 37 | }) 38 | }) 39 | 40 | describe('/prometheus', () => { 41 | it("GET /prometheus - success", async () => { 42 | const app = require('../proxy').app 43 | const res = await request(app).get("/prometheus"); 44 | expect(res.text.split('\n').length).toBeGreaterThan(100) 45 | expect(res.status).toStrictEqual(200) 46 | }) 47 | }) 48 | 49 | afterAll(() => deleteConfigFiles([settingsFilePath])) 50 | 51 | -------------------------------------------------------------------------------- /src/__test__/proxy_default.test.ts: -------------------------------------------------------------------------------- 1 | import ProxySettings, {proxyLogSettings, readProxySettings} from "../proxy-settings"; 2 | 3 | const expectedDefaultSettings = [ 4 | 'PROXY SETTINGS:\n-----------', 5 | 'Node url: http://[::1]:7076', 6 | 'Websocket url: ws://127.0.0.1:7078', 7 | 'Http port: 9950', 8 | 'Https port: 9951', 9 | 'Request path: /proxy', 10 | 'Use authentication: false', 11 | 'Use slow down: false', 12 | 'Use rate limiter: false', 13 | 'Use cached requests: false', 14 | 'Use output limiter: false', 15 | 'Use IP blacklist: false', 16 | 'Use token system: false', 17 | 'Use websocket system: false', 18 | 'Use dPoW: false', 19 | 'Use bPoW: false', 20 | 'Use work server: false', 21 | 'Use work peers: false', 22 | 'Listen on http: true', 23 | 'Listen on https: false', 24 | 'Allowed commands:\n-----------\n\n', 25 | 'DDOS protection settings:\n\n', 26 | 'Use cors. Any ORIGIN allowed', 27 | 'Main log level: none' 28 | ] 29 | 30 | test('log proxy settings with no config', () => { 31 | let settings: string[] = [] 32 | const readSettings: ProxySettings = readProxySettings('path-does-not-exist') 33 | proxyLogSettings((setting: string) => settings.push(setting), readSettings) 34 | expect(settings.length).toBe(24); 35 | expect(settings).toStrictEqual(expectedDefaultSettings) 36 | }); 37 | -------------------------------------------------------------------------------- /src/__test__/proxy_file.test.ts: -------------------------------------------------------------------------------- 1 | import {copyConfigFiles, deleteConfigFiles, getTestPath} from "./test-commons"; 2 | import {proxyLogSettings, readProxySettings} from "../proxy-settings"; 3 | 4 | const expectedSettingsWithFile = [ 5 | 'PROXY SETTINGS:\n-----------', 6 | 'Node url: http://[::1]:7076', 7 | 'Node headers:\n\nContent-Type : application/json\n', 8 | 'Websocket url: ws://127.0.0.1:7078', 9 | 'Http port: 9950', 10 | 'Https port: 9951', 11 | 'Request path: /proxy', 12 | 'Use authentication: false', 13 | 'Use slow down: true', 14 | 'Use rate limiter: true', 15 | 'Use cached requests: true', 16 | 'Use output limiter: true', 17 | 'Use IP blacklist: true', 18 | 'Use token system: false', 19 | 'Use websocket system: false', 20 | 'Use dPoW: false', 21 | 'Use bPoW: false', 22 | 'Use work server: false', 23 | 'Use work peers: false', 24 | 'Listen on http: true', 25 | 'Listen on https: false', 26 | 'Allowed commands:\n' + 27 | '-----------\n' + 28 | '\n' + 29 | '0 : account_history\n' + 30 | '1 : account_info\n' + 31 | '2 : account_balance\n' + 32 | '3 : accounts_balances\n' + 33 | '4 : account_key\n' + 34 | '5 : account_representative\n' + 35 | '6 : account_weight\n' + 36 | '7 : accounts_frontiers\n' + 37 | '8 : accounts_receivable\n' + 38 | '9 : active_difficulty\n' + 39 | '10 : available_supply\n' + 40 | '11 : block_account\n' + 41 | '12 : block_info\n' + 42 | '13 : block_count\n' + 43 | '14 : block_create\n' + 44 | '15 : block_confirm\n' + 45 | '16 : blocks_info\n' + 46 | '17 : chain\n' + 47 | '18 : confirmation_quorum\n' + 48 | '19 : delegators_count\n' + 49 | '20 : frontiers\n' + 50 | '21 : key_create\n' + 51 | '22 : process\n' + 52 | '23 : receivable\n' + 53 | '24 : receivable_exists\n' + 54 | '25 : representatives\n' + 55 | '26 : representatives_online\n' + 56 | '27 : sign\n' + 57 | '28 : successors\n' + 58 | '29 : price\n' + 59 | '30 : mnano_to_raw\n' + 60 | '31 : mnano_from_raw\n' + 61 | '32 : work_validate\n' + 62 | '33 : validate_account_number\n' + 63 | '34 : version\n' + 64 | '35 : verified_accounts\n', 65 | 'Cached commands:\n' + 66 | '\n' + 67 | 'block_count : 30\n' + 68 | 'available_supply : 3600\n' + 69 | 'active_difficulty : 30\n' + 70 | 'representatives_online : 300\n', 71 | 'Limited commands:\n' + 72 | '\n' + 73 | 'account_history : 500\n' + 74 | 'accounts_frontiers : 500\n' + 75 | 'accounts_balances : 500\n' + 76 | 'accounts_receivable : 50\n' + 77 | 'chain : 500\n' + 78 | 'frontiers : 500\n' + 79 | 'receivable : 500\n', 80 | 'Slow down settings:\n' + 81 | '\n' + 82 | 'time_window : 600000\n' + 83 | 'request_limit : 400\n' + 84 | 'delay_increment : 100\n' + 85 | 'max_delay : 2000\n', 86 | 'Rate limiter settings:\n\ntime_window : 86400000\nrequest_limit : 5000\n', 87 | 'DDOS protection settings:\n\ntime_window : 10000\nrequest_limit : 100\n', 88 | 'IPs blacklisted:\n\n0 : 8.8.8.8\n', 89 | 'Use cors. Any ORIGIN allowed', 90 | 'Main log level: info' 91 | ] 92 | 93 | const settingsFilePath = 'settings.json'; 94 | 95 | beforeAll(() => copyConfigFiles([settingsFilePath])) 96 | 97 | test('log proxy settings with default config from file', () => { 98 | let settings: string[] = [] 99 | const readSettings = readProxySettings(getTestPath(settingsFilePath)) 100 | proxyLogSettings((setting: string) => settings.push(setting), readSettings) 101 | expect(settings.length).toBe(30); 102 | expect(settings).toStrictEqual(expectedSettingsWithFile) 103 | }) 104 | 105 | afterAll(() => deleteConfigFiles([settingsFilePath])) 106 | -------------------------------------------------------------------------------- /src/__test__/test-commons.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const testFolder = 'src/__test__/'; 4 | 5 | export function getTestPath(filename: String) { 6 | return `${testFolder}${filename}` 7 | } 8 | 9 | export function copyConfigFiles(filePaths: string[]) { 10 | filePaths.forEach(filePath => { 11 | fs.copyFileSync(`${filePath}.default`, getTestPath(filePath), ) 12 | }) 13 | } 14 | export function deleteConfigFiles(filePaths: string[]) { 15 | filePaths.forEach(filePath => { 16 | try { 17 | let path = getTestPath(filePath); 18 | fs.unlinkSync(path) 19 | } catch (e) { 20 | // file might not be found which is OK 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/__test__/tokens_default.test.ts: -------------------------------------------------------------------------------- 1 | import {readTokenSettings, tokenLogSettings} from "../token-settings"; 2 | 3 | export {} 4 | 5 | const expectedDefaultSettings = [ 6 | 'TOKEN SETTINGS:\n-----------', 7 | 'Work Server: http://[::1]:7076', 8 | 'Token Price: 0.0001 Nano/token', 9 | 'Payment Timeout: 180', 10 | 'Receivable Interval: 2', 11 | 'Receivable Threshold: 100000000000000000000000', 12 | 'Receivable Max Count: 10', 13 | 'Difficulty Multiplier: 1.0', 14 | 'Min allowed tokens to purchase: 1', 15 | 'Max allowed tokens to purchase: 10000000', 16 | 'Token system log level: info' 17 | ] 18 | 19 | test('log token settings with no config', () => { 20 | let settings: string[] = [] 21 | const readSettings = readTokenSettings('path-does-not-exist.json') 22 | tokenLogSettings((setting: string) => settings.push(setting), readSettings) 23 | expect(settings.length).toBe(11); 24 | expect(settings).toStrictEqual(expectedDefaultSettings) 25 | }); 26 | -------------------------------------------------------------------------------- /src/__test__/tokens_file.test.ts: -------------------------------------------------------------------------------- 1 | import {copyConfigFiles, deleteConfigFiles} from "./test-commons"; 2 | import {readTokenSettings, tokenLogSettings} from "../token-settings"; 3 | 4 | const expectedDefaultSettings = [ 5 | 'TOKEN SETTINGS:\n-----------', 6 | 'Work Server: http://[::1]:7076', 7 | 'Token Price: 0.0001 Nano/token', 8 | 'Payment Timeout: 180', 9 | 'Receivable Interval: 2', 10 | 'Receivable Threshold: 100000000000000000000000', 11 | 'Receivable Max Count: 10', 12 | 'Difficulty Multiplier: 1.0', 13 | 'Min allowed tokens to purchase: 1', 14 | 'Max allowed tokens to purchase: 10000000', 15 | 'Token system log level: info' 16 | ] 17 | 18 | const filePaths = ['token_settings.json']; 19 | 20 | beforeAll(() => { 21 | process.env.CONFIG_TOKEN_SETTINGS = 'src/__test__/token_settings.json' 22 | copyConfigFiles(filePaths) 23 | }) 24 | 25 | test('log tokens settings with default config from file', () => { 26 | let settings: string[] = [] 27 | const readSettings = readTokenSettings(filePaths[0]) 28 | tokenLogSettings((setting: string) => settings.push(setting), readSettings) 29 | expect(settings.length).toBe(11); 30 | expect(settings).toStrictEqual(expectedDefaultSettings) 31 | }); 32 | 33 | afterAll(() => deleteConfigFiles(filePaths)) 34 | -------------------------------------------------------------------------------- /src/__test__/tools.test.ts: -------------------------------------------------------------------------------- 1 | import {bigAdd, MnanoToRaw, multiplierFromDifficulty, rawToMnano} from "../tools"; 2 | 3 | 4 | describe('rawToMnano', () => { 5 | test('given raw converts to mnano', () => { 6 | expect(rawToMnano('1')).toStrictEqual('0.000000000000000000000000000001') 7 | }) 8 | test('given string should handle gracefully', () => { 9 | expect(rawToMnano('adsf')).toStrictEqual('N/A') 10 | }) 11 | }) 12 | 13 | describe('MnanoToRaw', () => { 14 | test('given mnano converts to raw', () => { 15 | expect(MnanoToRaw('0.1')).toStrictEqual('100000000000000000000000000000') 16 | }) 17 | test('given string should handle gracefully', () => { 18 | expect(rawToMnano('adsf')).toStrictEqual('N/A') 19 | }) 20 | }) 21 | 22 | describe('bigAdd', () => { 23 | test('add two large numbers', () => { 24 | expect(bigAdd('100000000000000000000000', '100000000000000000000000')).toStrictEqual('200000000000000000000000') 25 | }) 26 | }) 27 | 28 | describe('multiplierFromDifficulty', () => { 29 | test('calculates multiplier given base and difficulty', () => { 30 | expect(multiplierFromDifficulty('fffffff800000000', 'fffffe0000000000')).toStrictEqual('64') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/__test__/user_settings.test.ts: -------------------------------------------------------------------------------- 1 | import {readUserSettings} from "../user-settings"; 2 | import {copyConfigFiles, deleteConfigFiles} from "./test-commons"; 3 | 4 | const filePaths = ['user_settings.json']; 5 | 6 | beforeAll(() => { 7 | copyConfigFiles(filePaths) 8 | }) 9 | 10 | test('parseUserSettings should parse to Map', async () => { 11 | const settings = readUserSettings('src/__test__/user_settings.json') 12 | expect(Object.entries(settings).length).toBe(2) 13 | const userSettings = settings['user2'] 14 | expect(userSettings).toBeDefined() 15 | expect(userSettings.allowed_commands.length).toBe(4) 16 | expect(Object.entries(userSettings.cached_commands).length).toBe(1) 17 | expect(Object.entries(userSettings.limited_commands).length).toBe(7) 18 | }) 19 | 20 | afterAll(() => deleteConfigFiles(filePaths)) 21 | -------------------------------------------------------------------------------- /src/__test__/websockets.test.ts: -------------------------------------------------------------------------------- 1 | import {deleteConfigFiles} from "./test-commons"; 2 | import {User} from "../lowdb-schema"; 3 | 4 | let proxy: any; 5 | 6 | describe('trackAccount', () => { 7 | 8 | beforeEach(() => { 9 | // deleteConfigFiles([websocketPath]) 10 | process.env.CONFIG_WEBSOCKET_PATH = 'src/__test__/websocket.json' 11 | process.env.OVERRIDE_USE_HTTP = 'false' 12 | proxy = require('../proxy') 13 | deleteConfigFiles(['websocket.json']) 14 | }) 15 | 16 | test('given invalid nano address should return false', () => { 17 | const res = proxy.trackAccount('192.16.1.1', 'invalid-address') 18 | expect(res).toBeFalsy() 19 | }); 20 | 21 | test('given valid nano address should return true and write to cache', () => { 22 | Date.now = jest.fn(() => 1609595113) 23 | const res = proxy.trackAccount('192.168.1.1', 'nano_3m497b1ghppe316aiu4o5eednfyueemzjf7a8wye3gi5rjrkpk1p59okghwb','1','0') 24 | expect(res).toBeTruthy() 25 | const expectedUser: User = { 26 | ip: '192.168.1.1', 27 | tracked_accounts: { 28 | nano_3m497b1ghppe316aiu4o5eednfyueemzjf7a8wye3gi5rjrkpk1p59okghwb: { 29 | "timestamp": 1609595, 30 | } 31 | }, 32 | rpcId: '1', 33 | clientId: '0' 34 | } 35 | expect(proxy.tracking_db.get('users').value()).toStrictEqual([expectedUser]) 36 | }) 37 | 38 | test('given same ip and different address, should append new address', () => { 39 | Date.now = jest.fn(() => 1609595113) 40 | proxy.trackAccount('192.168.1.1', 'nano_3m497b1ghppe316aiu4o5eednfyueemzjf7a8wye3gi5rjrkpk1p59okghwb','1','0') 41 | proxy.trackAccount('192.168.1.1', 'nano_3jsonxwips1auuub94kd3osfg98s6f4x35ksshbotninrc1duswrcauidnue','1','0') 42 | const expectedUser: User = { 43 | ip: '192.168.1.1', 44 | tracked_accounts: { 45 | nano_3m497b1ghppe316aiu4o5eednfyueemzjf7a8wye3gi5rjrkpk1p59okghwb: { 46 | "timestamp": 1609595, 47 | }, 48 | nano_3jsonxwips1auuub94kd3osfg98s6f4x35ksshbotninrc1duswrcauidnue: { 49 | "timestamp": 1609595, 50 | } 51 | }, 52 | rpcId: '1', 53 | clientId: '0' 54 | } 55 | expect(proxy.tracking_db.get('users').value()).toStrictEqual([expectedUser]) 56 | }) 57 | 58 | afterEach(() => deleteConfigFiles(['websocket.json'])) 59 | }) 60 | -------------------------------------------------------------------------------- /src/authorize-user.ts: -------------------------------------------------------------------------------- 1 | import {UserSettings, UserSettingsConfig} from "./user-settings"; 2 | import {Credentials} from "./credential-settings"; 3 | import BasicAuth from 'express-basic-auth' 4 | import {PromClient} from "./prom-client"; 5 | 6 | export interface ProxyAuthorizer { 7 | getUserSettings: (username: string, password: string) => UserSettings | undefined 8 | myAuthorizer: (username: string, password: string) => boolean 9 | } 10 | 11 | function validUser({user, password}: Credentials, suppliedUsername: string, suppliedPassword: string): boolean { 12 | return BasicAuth.safeCompare(suppliedUsername, user) && BasicAuth.safeCompare(suppliedPassword, password) 13 | } 14 | 15 | export function createProxyAuthorizer( 16 | defaultSettings: UserSettings, 17 | userSettings: UserSettingsConfig, 18 | users: Credentials[], 19 | promClient?: PromClient 20 | ): ProxyAuthorizer { 21 | 22 | const findValidUserSettings = (username: string, password: string): UserSettings | undefined => { 23 | return users 24 | .filter(user => validUser(user, username, password)) 25 | .map(validUser => { 26 | const settings: UserSettings | undefined = userSettings[validUser.user] 27 | return { 28 | ...defaultSettings, 29 | ...settings 30 | } 31 | })[0] 32 | } 33 | 34 | return { 35 | myAuthorizer: (username: string, password: string) => { 36 | const authorized = findValidUserSettings(username, password) !== undefined 37 | promClient?.incAuthorizeAttempt(username, authorized) 38 | return authorized 39 | }, 40 | getUserSettings: (username: string, password: string) => findValidUserSettings(username, password), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/common-settings.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = 'none' | 'warning' | 'info' 2 | 3 | interface LogLevels { 4 | none: LogLevel 5 | info: LogLevel 6 | warning: LogLevel 7 | } 8 | export const log_levels: LogLevels = { 9 | none: 'none', 10 | info: 'info', 11 | warning: 'warning' 12 | } 13 | 14 | export type Command = string 15 | export type LimitedCommands = Record 16 | export type CachedCommands = Record 17 | 18 | export interface LogData { 19 | date: string; 20 | count: number; 21 | } 22 | 23 | type ConfigPath = string 24 | export interface ConfigPaths { 25 | request_stat: ConfigPath 26 | token_settings: ConfigPath 27 | creds: ConfigPath 28 | pow_creds: ConfigPath 29 | settings: ConfigPath 30 | user_settings: ConfigPath 31 | websocket_path: ConfigPath 32 | db_path: ConfigPath 33 | } 34 | 35 | export function readConfigPathsFromENV(): ConfigPaths { 36 | return { 37 | creds: process.env.CONFIG_CREDS_SETTINGS || 'creds.json', 38 | pow_creds: process.env.CONFIG_POW_CREDS_SETTINGS || 'pow_creds.json', 39 | request_stat: process.env.CONFIG_REQUEST_STAT || 'request-stat.json', 40 | settings: process.env.CONFIG_SETTINGS || 'settings.json', 41 | token_settings: process.env.CONFIG_TOKEN_SETTINGS || 'token_settings.json', 42 | user_settings: process.env.CONFIG_USER_SETTINGS || 'user_settings.json', 43 | websocket_path: process.env.CONFIG_WEBSOCKET_PATH || 'websocket.json', 44 | db_path: process.env.CONFIG_DB_PATH || 'db.json', 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/credential-settings.ts: -------------------------------------------------------------------------------- 1 | import * as Fs from "fs"; 2 | 3 | export interface Credentials { 4 | user: string 5 | password: string 6 | } 7 | 8 | export interface CredentialSettings { 9 | users: Credentials[] 10 | } 11 | 12 | export function readCredentials(path: string): Credentials[] { 13 | try { 14 | const credentials: CredentialSettings = JSON.parse(Fs.readFileSync(path, 'utf-8')) 15 | return credentials.users 16 | } 17 | catch(e) { 18 | console.log("Could not read creds.json", e) 19 | return [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/http.ts: -------------------------------------------------------------------------------- 1 | import ProxySettings from "./proxy-settings"; 2 | import * as Fs from "fs"; 3 | import * as http from "http"; 4 | import * as https from "https"; 5 | 6 | const listeningListener = (type: string, port: number) => () => { 7 | console.log(`${type} started on port: ${port}`) 8 | } 9 | 10 | export function createHttpServer(requestListener: http.RequestListener, httpPort: number, type: string = "Http"): http.Server { 11 | return http.createServer(requestListener).listen(httpPort, listeningListener(type, httpPort)) 12 | } 13 | 14 | export function readHttpsOptions(settings: ProxySettings): https.ServerOptions | undefined { 15 | try { 16 | return { 17 | cert: Fs.readFileSync(settings.https_cert), 18 | key: Fs.readFileSync(settings.https_key) 19 | } 20 | } catch(err) { 21 | console.error("Problem reading https cert/key file. Will not be able to create HTTPS server.") 22 | console.error(err) 23 | return undefined 24 | } 25 | } 26 | 27 | export function createHttpsServer(requestListener: http.RequestListener, httpsPort: number, httpOptions: https.ServerOptions, type: string = "Https"): https.Server { 28 | return https.createServer(httpOptions, requestListener).listen(httpsPort, listeningListener(type, httpsPort)) 29 | } 30 | 31 | export function websocketListener(request: http.IncomingMessage, response: http.ServerResponse): void { 32 | response.writeHead(404) 33 | response.end() 34 | } 35 | -------------------------------------------------------------------------------- /src/lowdb-schema.ts: -------------------------------------------------------------------------------- 1 | import lowdb from "lowdb"; 2 | 3 | export interface Order { 4 | address: string; 5 | token_key: string; 6 | priv_key: string; 7 | tokens: number; 8 | order_waiting: boolean; 9 | nano_amount: number; 10 | token_amount: number; 11 | order_time_left: number; 12 | processing: boolean; 13 | timestamp: number; 14 | previous: any; 15 | hashes: any[] 16 | } 17 | export interface OrderSchema { 18 | orders: Order[] 19 | } 20 | 21 | export type OrderDB = lowdb.LowdbSync 22 | 23 | export interface TrackedAccount { 24 | timestamp: number 25 | } 26 | 27 | export interface User { 28 | ip: string 29 | rpcId: string 30 | clientId: string 31 | tracked_accounts: Record 32 | } 33 | 34 | export interface UserSchema { 35 | users: User[] 36 | } 37 | export type UserDB = lowdb.LowdbSync 38 | -------------------------------------------------------------------------------- /src/mynano-api/mynano-api.ts: -------------------------------------------------------------------------------- 1 | import {VerifiedAccount} from "../node-api/proxy-api"; 2 | 3 | /** @see https://mynano.ninja/api/accounts/verified */ 4 | interface MynanoVerifiedAccount { 5 | votingweight: number 6 | delegators: number 7 | uptime: number 8 | score: number 9 | account: string 10 | alias: string 11 | } 12 | 13 | export type MynanoVerifiedAccountsResponse = MynanoVerifiedAccount[] 14 | 15 | export const mynanoToVerifiedAccount: (a: MynanoVerifiedAccount, tokensTotal?: number) => VerifiedAccount = (verifiedAccount, tokensTotal) => { 16 | return { 17 | votingweight: verifiedAccount.votingweight, 18 | delegators: verifiedAccount.delegators, 19 | uptime: verifiedAccount.uptime, 20 | score: verifiedAccount.score, 21 | account: verifiedAccount.account, 22 | alias: verifiedAccount.alias, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/node-api/node-api.ts: -------------------------------------------------------------------------------- 1 | /** @see https://docs.nano.org/commands/rpc-protocol/#active_difficulty */ 2 | 3 | interface ProcessDataResponse { 4 | difficulty: any 5 | multiplier: string 6 | tokens_total: number 7 | error: string | null 8 | work: string | null 9 | } 10 | 11 | interface ReceivableBlock { 12 | amount: string 13 | source: string 14 | } 15 | 16 | /** @see https://docs.nano.org/commands/rpc-protocol/#receivable */ 17 | interface ReceivableResponse { 18 | blocks: Record 19 | error: string | null 20 | } 21 | 22 | /** @see https://docs.nano.org/commands/rpc-protocol/#account_info */ 23 | interface AccountInfoResponse { 24 | frontier: string 25 | balance: string 26 | representative: string 27 | error: string | null 28 | } 29 | 30 | /** @see https://docs.nano.org/commands/rpc-protocol/#work_generate */ 31 | interface WorkGenerateResponse { 32 | work: string 33 | difficulty: string 34 | multiplier: string 35 | hash: string 36 | } 37 | 38 | /** @see https://docs.nano.org/commands/rpc-protocol/#process */ 39 | interface ProcessResponse { 40 | hash: string 41 | error: string | null 42 | } 43 | -------------------------------------------------------------------------------- /src/node-api/proxy-api.ts: -------------------------------------------------------------------------------- 1 | import {TokenAPIActions} from "./token-api"; 2 | 3 | export type RPCAction = TokenAPIActions | 'mnano_to_raw' | 'mnano_from_raw' | 'process' | 'work_generate' | 'price' | 'verified_accounts' | 'accounts_frontiers' | 'accounts_balances' | 'accounts_receivable' | 'accounts_pending' | 'receivable' | 'pending' | 'receivable_exists' | 'pending_exists' 4 | 5 | export interface ProxyRPCRequest { 6 | action: RPCAction 7 | token_amount: number 8 | token_key: string 9 | amount: string 10 | difficulty: string | undefined 11 | use_peers: string | undefined 12 | user: string | undefined 13 | api_key: string | undefined 14 | timeout: number 15 | count: number 16 | account_filter?: string[] 17 | hash: string 18 | accounts: string[] 19 | account: string 20 | } 21 | 22 | export interface VerifiedAccount { 23 | votingweight: number 24 | delegators: number 25 | uptime: number 26 | score: number 27 | account: string 28 | alias: string 29 | } 30 | -------------------------------------------------------------------------------- /src/node-api/token-api.ts: -------------------------------------------------------------------------------- 1 | import {RPCAction} from "./proxy-api"; 2 | 3 | export interface TokenRPCError { 4 | error: string 5 | } 6 | 7 | export interface TokenInfo { 8 | address: string 9 | token_key: string 10 | payment_amount: number 11 | } 12 | 13 | export interface TokenResponse { 14 | token_key: string 15 | tokens_ordered: number 16 | tokens_total: number 17 | } 18 | 19 | export interface WaitingTokenOrder { 20 | token_key: string 21 | order_time_left: number 22 | } 23 | 24 | export interface CancelOrder { 25 | priv_key: string 26 | status: string 27 | } 28 | 29 | export interface TokenStatusResponse { 30 | tokens_total: number 31 | status: string 32 | } 33 | export interface TokenPriceResponse { 34 | token_price: number 35 | } 36 | 37 | export interface StatusCallback { 38 | amount: number 39 | hashes?: string[] 40 | } 41 | 42 | export type TokenAPIResponses = TokenResponse | TokenInfo | WaitingTokenOrder | CancelOrder | TokenStatusResponse | TokenPriceResponse | TokenRPCError 43 | 44 | export type TokenAPIActions = 'tokens_buy' | 'tokenorder_check' | 'tokenorder_cancel' | 'tokens_check' | 'tokenprice_check' 45 | 46 | export function isTokensRequest(action: RPCAction): boolean { 47 | return action === 'tokens_buy' || action === 'tokenorder_check' || action === 'tokenorder_cancel' || action === 'tokens_check' || action === 'tokenprice_check' 48 | } 49 | -------------------------------------------------------------------------------- /src/node-api/websocket-api.ts: -------------------------------------------------------------------------------- 1 | type WSTopic = 'confirmation' 2 | type WSAction = 'subscribe' | 'update' | 'unsubscribe' | 'ping' | 'pong' 3 | 4 | interface WSNodeSubscribe { 5 | action: WSAction 6 | topic: WSTopic 7 | ack: boolean 8 | id: string 9 | options: { 10 | all_local_accounts: boolean 11 | accounts: string[] 12 | } 13 | } 14 | 15 | interface WSNodeSubscribeAll { 16 | action: WSAction 17 | topic: WSTopic 18 | ack: boolean 19 | id: string 20 | } 21 | 22 | interface WSNodeReceive { 23 | topic: WSTopic 24 | message: { 25 | account: string 26 | block: { 27 | link_as_account: string 28 | } 29 | } 30 | ack: WSAction 31 | id: string 32 | } 33 | 34 | interface WSMessage { 35 | topic: WSTopic 36 | action: WSAction 37 | options: { 38 | accounts: string[] 39 | } 40 | id: string 41 | } 42 | 43 | interface WSError { 44 | error: string 45 | } 46 | 47 | interface WSSubscribe { 48 | ack: WSAction 49 | id: string 50 | } 51 | 52 | interface WSPong { 53 | ack: WSAction 54 | time: string 55 | id: string 56 | } 57 | -------------------------------------------------------------------------------- /src/pow-settings.ts: -------------------------------------------------------------------------------- 1 | import ProxySettings from "./proxy-settings"; 2 | import * as Fs from "fs"; 3 | 4 | interface UserKeyPair { 5 | user: string; 6 | key: string; 7 | } 8 | 9 | interface ServerPair { 10 | url: string; 11 | port: string; 12 | } 13 | 14 | export interface PowSettings { 15 | dpow?: UserKeyPair 16 | bpow?: UserKeyPair 17 | work_server?: ServerPair 18 | } 19 | 20 | /** Reads proof-of-work settings from file */ 21 | export function readPowSettings(path: string, settings: ProxySettings): PowSettings { 22 | try { 23 | const readSettings: PowSettings = JSON.parse(Fs.readFileSync(path, 'utf-8')) 24 | return { 25 | dpow: settings.use_dpow ? readSettings.dpow : undefined, 26 | bpow: settings.use_bpow ? readSettings.bpow : undefined, 27 | work_server: settings.use_work_server ? readSettings.work_server : undefined, 28 | } 29 | } 30 | catch(e) { 31 | console.log("Could not read pow_creds.json", e) 32 | return { 33 | dpow: undefined, 34 | bpow: undefined, 35 | work_server: undefined, 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/price-api/price-api.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface PriceResponse { 3 | tokens_total: number 4 | } 5 | -------------------------------------------------------------------------------- /src/prom-client.ts: -------------------------------------------------------------------------------- 1 | import client, {LabelValues} from "prom-client"; 2 | import {RPCAction} from "./node-api/proxy-api"; 3 | import {LogLevel} from "./common-settings"; 4 | 5 | export type MaybeTimedCall = ((labels?: LabelValues) => number) | undefined 6 | 7 | export interface PromClient { 8 | metrics(): Promise 9 | incRequest: (action: RPCAction, ip: string, token_used: boolean) => void 10 | incLogging: (logLevel: LogLevel) => void 11 | incRateLimited: (ip: string) => void, 12 | incSlowDown: (ip: string) => void, 13 | incDDOS: (ip: string) => void, 14 | incWebsocketSubscription: (ip: string) => void, 15 | incWebsocketMessage: (ip: string) => void, 16 | incWebsocketMessageAll: (ip: string) => void, 17 | incAuthorizeAttempt: (username: string, wasAuthorized: boolean) => void 18 | timeNodeRpc: (action: RPCAction) => MaybeTimedCall, 19 | timePrice: () => MaybeTimedCall, 20 | timeVerifiedAccounts: () => MaybeTimedCall, 21 | path: string 22 | } 23 | 24 | export function createPrometheusClient(): PromClient { 25 | const collectDefaultMetrics = client.collectDefaultMetrics; 26 | const Registry = client.Registry; 27 | const register = new Registry(); 28 | collectDefaultMetrics({ register }); 29 | 30 | let processRequestCounter = new client.Counter({ 31 | registers: [register], 32 | name: "process_request", 33 | help: "Counts processRequest per IP address and action", 34 | labelNames: ["action", "ip", "token_used"] 35 | }) 36 | 37 | let logCounter = new client.Counter({ 38 | registers: [register], 39 | name: "log", 40 | help: "Counts number of logged events", 41 | labelNames: ["log_level"] 42 | }) 43 | 44 | let countRateLimited = new client.Counter({ 45 | registers: [register], 46 | name: "user_rate_limited", 47 | help: "Counts number of times an IP address is rate limited", 48 | labelNames: ["ip"] 49 | }) 50 | 51 | let countSlowDown = new client.Counter({ 52 | registers: [register], 53 | name: "user_slow_down", 54 | help: "Counts number of times an IP address is rate limited with slow down", 55 | labelNames: ["ip"] 56 | }) 57 | 58 | let countDDOS = new client.Counter({ 59 | registers: [register], 60 | name: "user_ddos", 61 | help: "Counts number of times an IP address is rate limited from DDOS", 62 | labelNames: ["ip"] 63 | }) 64 | 65 | let countWebsocketSubscription = new client.Counter({ 66 | registers: [register], 67 | name: "websocket_subscription", 68 | help: "Counts number of times an IP has subscribed to websocket", 69 | labelNames: ["ip"] 70 | }) 71 | 72 | let countWebsocketMessage = new client.Counter({ 73 | registers: [register], 74 | name: "websocket_message", 75 | help: "Counts number of times an IP has received a websocket message", 76 | labelNames: ["ip"] 77 | }) 78 | 79 | let countWebsocketMessageAll = new client.Counter({ 80 | registers: [register], 81 | name: "websocket_message_all", 82 | help: "Counts number of times an IP has received a websocket message when subscribed to all", 83 | labelNames: ["ip"] 84 | }) 85 | 86 | let countAuthorizedAttempts = new client.Counter({ 87 | registers: [register], 88 | name: "authorized_attempts", 89 | help: "Counts basic auth attempts for a given user", 90 | labelNames: ["username", "access"] 91 | }) 92 | 93 | let rpcHistogram = new client.Histogram({ 94 | registers: [register], 95 | name: "time_rpc_call", 96 | help: "Times RPC calls to the Nano backend", 97 | labelNames: ["action"] 98 | }) 99 | 100 | let priceHistogram = new client.Histogram({ 101 | registers: [register], 102 | name: "time_price_call", 103 | help: "Times external call to get price information" 104 | }) 105 | 106 | let verifiedAccountsHistogram = new client.Histogram({ 107 | registers: [register], 108 | name: "time_verified_call", 109 | help: "Times external call to get verified accounts" 110 | }) 111 | 112 | return { 113 | metrics: async () => register.metrics(), 114 | incRequest: (action: RPCAction, ip: string, token_used: boolean) => processRequestCounter.labels(action, ip, token_used?"1":"0").inc(), 115 | incLogging: (logLevel: LogLevel) => logCounter.labels(logLevel).inc(), 116 | incRateLimited: (ip: string) => countRateLimited.labels(ip).inc(), 117 | incSlowDown: (ip: string) => countSlowDown.labels(ip).inc(), 118 | incDDOS: (ip: string) => countDDOS.labels(ip).inc(), 119 | incWebsocketSubscription: (ip: string) => countWebsocketSubscription.labels(ip).inc(), 120 | incWebsocketMessage: (ip: string) => countWebsocketMessage.labels(ip).inc(), 121 | incWebsocketMessageAll: (ip: string) => countWebsocketMessageAll.labels(ip).inc(), 122 | incAuthorizeAttempt: (username, wasAuthorized) => countAuthorizedAttempts.labels(username, wasAuthorized ? 'authorized' : 'denied').inc(), 123 | timeNodeRpc: (action: RPCAction) => rpcHistogram.startTimer({action: action}), 124 | timePrice: () => priceHistogram.startTimer(), 125 | timeVerifiedAccounts: () => verifiedAccountsHistogram.startTimer(), 126 | path: '/prometheus' 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/proxy-settings.ts: -------------------------------------------------------------------------------- 1 | import {CachedCommands, LimitedCommands, log_levels, LogLevel} from "./common-settings"; 2 | import * as Fs from "fs"; 3 | 4 | /** Contains the settings for the slow down filter */ 5 | export interface SlowDown { 6 | // rolling time window in ms 7 | time_window: number 8 | // allow x requests per time window, then start slowing down 9 | request_limit: number 10 | // begin adding X ms of delay per request when delayAfter has been reached 11 | delay_increment: number 12 | // max delay in ms to slow down 13 | max_delay: number 14 | } 15 | 16 | /** Contains the settings for the rate limiter */ 17 | export interface RateLimiter { 18 | // Limit each IP to x requests per duration 19 | request_limit: number 20 | // Rolling time window in sec 21 | time_window: number 22 | } 23 | 24 | /** Contains the settings for the ddos protection */ 25 | export interface DdosProtection { 26 | // Limit each IP to x requests per duration 27 | request_limit: number 28 | // Rolling time window in sec 29 | time_window: number 30 | } 31 | 32 | /** Base config for the Proxy */ 33 | export default interface ProxySettings { 34 | // nano node RPC url (default for beta network is 'http://[::1]:55000') 35 | node_url: string; 36 | // nano node RPC headers 37 | node_headers?: Record | undefined; 38 | // node websocket server (only used if activated with use_websocket) 39 | node_ws_url: string; 40 | // port to listen on for http (enabled default with use_http) 41 | http_port: number; 42 | // port to listen on for https (disabled default with use_https) 43 | https_port: number; 44 | // port to listen on for http websocket connection (only used if activated with use_websocket) 45 | websocket_http_port: number; 46 | // port to listen on for https websocket connection (only used if activated with use_websocket) 47 | websocket_https_port: number; 48 | // Prefix in the request path, e.g. '/proxy' for 'https://server:port/proxy' 49 | request_path: string; 50 | // if require username and password when connecting to proxy 51 | use_auth: boolean; 52 | // if slowing down requests for IPs doing above set limit (defined in slow_down) 53 | use_slow_down: boolean; 54 | // if blocking IPs for a certain amount of time when they request above set limit (defined in rate_limiter) 55 | use_rate_limiter: boolean; 56 | // if caching certain commands set in cached_commands 57 | use_cache: boolean; 58 | // listen on http (active by default) 59 | use_http: boolean; 60 | // listen on https (inactive by default) (a valid cert and key file is needed via https_cert and https_key) 61 | use_https: boolean; 62 | // if limiting number of response objects, like receivable transactions, to a certain max amount set in limited_commands. Only supported for RPC actions that have a "count" key 63 | use_output_limiter: boolean; 64 | // if blocking access to IPs listed in ip_blacklist 65 | use_ip_blacklist: boolean; 66 | // if activating the token system for purchase via Nano 67 | use_tokens: boolean; 68 | // if enable subscriptions on the node websocket (protected by the proxy) 69 | use_websocket: boolean; 70 | // if handling cors policy here, if not taken care of in upstream proxy (cors_whitelist=[] means allow ANY ORIGIN) 71 | allow_websocket_all: boolean; 72 | // If allowing users to subscribe to ALL accounts (more traffic) 73 | use_cors: boolean; 74 | // if allow work_generate to be done by dPoW instead of local node. Work will consume 10 token points. If "difficulty" is not provided with the work_generate request the "default send difficulty" will be used. (The priority order is bpow > dpow > work server. If all three are set to false, it will use the node to generate work) (requires work_generate in allowed_commands and credentials to be set in pow_creds.json) 75 | use_dpow: boolean; 76 | // if allow work_generate to be done by BoomPoW intead of local node. Work will consume 10 token points. If "difficulty" is not provided with the work_generate request the "default send difficulty" will be used. (The priority order is bpow > dpow > work server. If all three are set to false, it will use the node to generate work) (requires work_generate in allowed_commands and credentials to be set in pow_creds.json) 77 | use_bpow: boolean; 78 | // if allow work_generate to be done by external work server instead of local node. Work will consume 10 token points. If "difficulty" is not provided with the work_generate request the "default send difficulty" will be used. (The priority order is bpow > dpow > work server. If all three are set to false, it will use the node to generate work) (requires work_generate in allowed_commands) 79 | use_work_server: boolean; 80 | // if allow work_generate implicitly add "use_peers": "true" to the request to use work_peers configured in the nano node. 81 | use_work_peers: boolean; 82 | // file path for pub cert file 83 | https_cert: string; 84 | // file path for private key file 85 | https_key: string; 86 | // only allow RPC actions in this list 87 | allowed_commands: string[]; 88 | // a list of commands [key] that will be cached for corresponding duration in seconds as [value] 89 | cached_commands: CachedCommands; 90 | // a list of commands [key] to limit the output response for with max count as [value] 91 | limited_commands: LimitedCommands; 92 | slow_down: SlowDown | any; 93 | rate_limiter: RateLimiter | any; 94 | ddos_protection: DdosProtection | any; 95 | // a list of IPs to deny always 96 | ip_blacklist: string[]; 97 | // if the NanoRPCProxy is behind other proxies such as apache or cloudflare the source IP will be wrongly detected and the filters will not work as intended. Enter the number of additional proxies here. 98 | proxy_hops: number; 99 | // // maximum number of accounts allowed to subscribe to for block confirmations 100 | websocket_max_accounts: number; 101 | // whitelist requester ORIGIN for example https://mywallet.com or http://localhost:8080 (require use_cors) [list of hostnames] 102 | cors_whitelist: any[]; 103 | // the log level to use (startup info is always logged): none=zero active logging, warning=only errors/warnings, info=both errors/warnings and info 104 | log_level: LogLevel; 105 | // IP addresses to enable prometheus for. Typically '127.0.0.1', or '::ffff:127.0.0.1' for IPv6 106 | enable_prometheus_for_ips: string[]; 107 | } 108 | 109 | function logObjectEntries(logger: (...data: any[]) => void, title: string, object: any) { 110 | let log_string = title + "\n" 111 | for (const [key, value] of Object.entries(object)) { 112 | if(key) { 113 | log_string = log_string + key + " : " + value + "\n" 114 | } else { 115 | log_string = log_string + " " + value + "\n" 116 | } 117 | } 118 | logger(log_string) 119 | } 120 | 121 | export function proxyLogSettings(logger: (...data: any[]) => void, settings: ProxySettings) { 122 | logger("PROXY SETTINGS:\n-----------") 123 | logger("Node url: " + settings.node_url) 124 | if (settings.node_headers) { 125 | logObjectEntries(logger, "Node headers:\n", settings.node_headers) 126 | } 127 | logger("Websocket url: " + settings.node_ws_url) 128 | logger("Http port: " + String(settings.http_port)) 129 | logger("Https port: " + String(settings.https_port)) 130 | logger("Request path: " + settings.request_path) 131 | if (settings.use_websocket) { 132 | logger("Websocket http port: " + String(settings.websocket_http_port)) 133 | logger("Websocket https port: " + String(settings.websocket_https_port)) 134 | logger("Websocket nax accounts: " + String(settings.websocket_max_accounts)) 135 | logger("Allow websocket subscribe all: " + settings.allow_websocket_all) 136 | } 137 | logger("Use authentication: " + settings.use_auth) 138 | logger("Use slow down: " + settings.use_slow_down) 139 | logger("Use rate limiter: " + settings.use_rate_limiter) 140 | logger("Use cached requests: " + settings.use_cache) 141 | logger("Use output limiter: " + settings.use_output_limiter) 142 | logger("Use IP blacklist: " + settings.use_ip_blacklist) 143 | logger("Use token system: " + settings.use_tokens) 144 | logger("Use websocket system: " + settings.use_websocket) 145 | logger("Use dPoW: " + settings.use_dpow) 146 | logger("Use bPoW: " + settings.use_bpow) 147 | logger("Use work server: " + settings.use_work_server) 148 | logger("Use work peers: " + settings.use_work_peers) 149 | logger("Listen on http: " + settings.use_http) 150 | logger("Listen on https: " + settings.use_https) 151 | 152 | logObjectEntries(logger, "Allowed commands:\n-----------\n", settings.allowed_commands) 153 | if(settings.use_cache) { 154 | logObjectEntries(logger, "Cached commands:\n", settings.cached_commands) 155 | } 156 | if (settings.use_output_limiter) { 157 | logObjectEntries(logger, "Limited commands:\n", settings.limited_commands) 158 | } 159 | if(settings.use_slow_down) { 160 | logObjectEntries(logger, "Slow down settings:\n", settings.slow_down) 161 | } 162 | if (settings.use_rate_limiter) { 163 | logObjectEntries(logger, "Rate limiter settings:\n", settings.rate_limiter) 164 | } 165 | logObjectEntries(logger, "DDOS protection settings:\n", settings.ddos_protection) 166 | 167 | if (settings.use_ip_blacklist) { 168 | logObjectEntries(logger, "IPs blacklisted:\n", settings.ip_blacklist) 169 | } 170 | if(settings.enable_prometheus_for_ips.length > 0) { 171 | logObjectEntries(logger, "Prometheus enabled for the following addresses:\n", settings.enable_prometheus_for_ips) 172 | } 173 | 174 | if (settings.proxy_hops > 0) { 175 | logger("Additional proxy servers: " + settings.proxy_hops) 176 | } 177 | if (settings.use_cors) { 178 | if (settings.cors_whitelist.length == 0) { 179 | logger("Use cors. Any ORIGIN allowed") 180 | } 181 | else { 182 | logObjectEntries(logger, "Use cors. Whitelisted ORIGINs or IPs:\n", settings.cors_whitelist) 183 | } 184 | } 185 | logger("Main log level: " + settings.log_level) 186 | } 187 | 188 | export function readProxySettings(settingsPath: string): ProxySettings { 189 | const defaultSettings: ProxySettings = { 190 | node_url: "http://[::1]:7076", 191 | node_headers: undefined, 192 | node_ws_url: "ws://127.0.0.1:7078", 193 | http_port: 9950, 194 | https_port: 9951, 195 | websocket_http_port: 9952, 196 | websocket_https_port: 9953, 197 | request_path: '/proxy', 198 | use_auth: false, 199 | use_slow_down: false, 200 | use_rate_limiter: false, 201 | use_cache: false, 202 | use_http: true, 203 | use_https: false, 204 | use_output_limiter: false, 205 | use_ip_blacklist: false, 206 | use_tokens: false, 207 | use_websocket: false, 208 | allow_websocket_all: false, 209 | use_cors: true, 210 | use_dpow: false, 211 | use_bpow: false, 212 | use_work_server: false, 213 | use_work_peers: false, 214 | https_cert: '', 215 | https_key: '', 216 | allowed_commands: [], 217 | cached_commands: {}, 218 | limited_commands: {}, 219 | slow_down: {}, 220 | rate_limiter: {}, 221 | ddos_protection: {}, 222 | ip_blacklist: [], 223 | proxy_hops: 0, 224 | websocket_max_accounts: 100, 225 | cors_whitelist: [], 226 | log_level: log_levels.none, 227 | enable_prometheus_for_ips: [], 228 | } 229 | try { 230 | const settings: ProxySettings = JSON.parse(Fs.readFileSync(settingsPath, 'utf-8')) 231 | const requestPath = settings.request_path || defaultSettings.request_path 232 | const normalizedRequestPath = requestPath.startsWith('/') ? requestPath : '/' + requestPath 233 | return {...defaultSettings, ...settings, request_path: normalizedRequestPath } 234 | } catch(e) { 235 | console.log("Could not read settings.json", e) 236 | return defaultSettings; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/token-settings.ts: -------------------------------------------------------------------------------- 1 | import {LogLevel} from "./common-settings"; 2 | import Fs from "fs"; 3 | 4 | export interface TokenSettings { 5 | // the work server for doing PoW (the node can be used as well, for example http://127.0.0.1:7076, but enable_control is needed in the node config) 6 | // To use bpow or dpow, just point the server to itself such as http://127.0.0.1:9950/proxy (requires bpow/dpow to be configured and work_generate as allowed command) 7 | work_server: string 8 | // Nano per token 9 | token_price: number 10 | // timeout after 120sec 11 | payment_timeout: number 12 | // time to wait for each check for receivable Nano 13 | receivable_interval: number 14 | // only allow receivable tx above this raw value 15 | receivable_threshold: string 16 | // max number of receivable to process per account for each order (normally only 1 should be needed) 17 | receivable_count: number 18 | // Multipliers used when using the node for PoW 19 | difficulty_multiplier: string 20 | // where to send the payment 21 | payment_receive_account: string 22 | // min allowed tokens to be purchased 23 | min_token_amount: number 24 | // max allowed tokens to be purchased 25 | max_token_amount: number 26 | // the log level to use (startup info is always logged): none=zero active logging, warning=only errors/warnings, info=both errors/warnings and info 27 | log_level: LogLevel 28 | } 29 | 30 | /** Try reading TokenSettings from file and merge with default settings. Fall back to default settings if no file found */ 31 | export function readTokenSettings(settingsPath: string): TokenSettings { 32 | const defaultSettings: TokenSettings = { 33 | work_server: "http://[::1]:7076", 34 | token_price: 0.0001, 35 | payment_timeout: 180, 36 | receivable_interval: 2, 37 | receivable_threshold: "100000000000000000000000", 38 | receivable_count: 10, 39 | difficulty_multiplier: "1.0", 40 | payment_receive_account: "nano_1gur37mt5cawjg5844bmpg8upo4hbgnbbuwcerdobqoeny4ewoqshowfakfo", 41 | min_token_amount: 1, 42 | max_token_amount: 10000000, 43 | log_level: "info", 44 | } 45 | try { 46 | const readSettings: TokenSettings = JSON.parse(Fs.readFileSync(settingsPath, 'utf-8')) 47 | return {...defaultSettings, ...readSettings} 48 | } 49 | catch(e) { 50 | console.log("Could not read token_settings.json, returns default settings", e) 51 | return defaultSettings 52 | } 53 | } 54 | 55 | export function tokenLogSettings(logger: (...data: any[]) => void, settings: TokenSettings) { 56 | logger("TOKEN SETTINGS:\n-----------") 57 | logger("Work Server: " + settings.work_server) 58 | logger("Token Price: " + settings.token_price + " Nano/token") 59 | logger("Payment Timeout: " + settings.payment_timeout) 60 | logger("Receivable Interval: " + settings.receivable_interval) 61 | logger("Receivable Threshold: " + settings.receivable_threshold) 62 | logger("Receivable Max Count: " + settings.receivable_count) 63 | logger("Difficulty Multiplier: " + settings.difficulty_multiplier) 64 | logger("Min allowed tokens to purchase: " + settings.min_token_amount) 65 | logger("Max allowed tokens to purchase: " + settings.max_token_amount) 66 | logger("Token system log level: " + settings.log_level) 67 | } 68 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import {readTokenSettings, tokenLogSettings, TokenSettings} from "./token-settings"; 2 | import {log_levels, LogLevel, readConfigPathsFromENV} from "./common-settings"; 3 | import {Order, OrderDB} from "./lowdb-schema"; 4 | import { wallet } from "nanocurrency-web"; 5 | import * as Nano from 'nanocurrency' 6 | import * as Tools from './tools' 7 | import Nacl from 'tweetnacl/nacl' 8 | import { 9 | CancelOrder, 10 | StatusCallback, 11 | TokenInfo, TokenPriceResponse, 12 | TokenResponse, 13 | TokenStatusResponse, 14 | WaitingTokenOrder, 15 | TokenRPCError, 16 | } from "./node-api/token-api"; 17 | 18 | const API_TIMEOUT = 10000 // 10sec timeout for calling http APIs 19 | const tokenSettings = readConfigPathsFromENV().token_settings 20 | const settings: TokenSettings = readTokenSettings(tokenSettings) 21 | tokenLogSettings(console.log, settings) 22 | 23 | // --- 24 | const sleep = (milliseconds: number) => { 25 | return new Promise(resolve => setTimeout(resolve, milliseconds)) 26 | } 27 | 28 | let node_url = "" // will be set by main script 29 | let node_headers: Record | undefined 30 | 31 | // Functions to be required from another file 32 | // Generates and provides a payment address while checking for receivable tx and collect them 33 | export async function requestTokenPayment(token_amount: number, token_key: string, order_db: OrderDB, url: string, headers: Record | undefined): Promise { 34 | // Block request if amount is not within interval 35 | if (token_amount < settings.min_token_amount) { 36 | return {error: "Token amount must be larger than " + settings.min_token_amount} 37 | } 38 | if (token_amount > settings.max_token_amount) { 39 | return {error: "Token amount must be smaller than " + settings.max_token_amount} 40 | } 41 | 42 | node_url = url 43 | node_headers = headers 44 | let priv_key = "" 45 | let address = "" 46 | let nano_amount = token_amount*settings.token_price // the Nano to be received 47 | 48 | // If token_key was passed it means refill tokens and update db order 49 | // first check if key exist in DB and the order is not currently processing 50 | if (token_key != "" && order_db.get('orders').find({token_key: token_key}).value()) { 51 | if (!order_db.get('orders').find({token_key: token_key}).value().order_waiting) { 52 | order_db.get('orders').find({token_key: token_key}).assign({"order_waiting":true, "nano_amount":nano_amount, "token_amount":0, "order_time_left":settings.payment_timeout, "processing":false, "timestamp":Math.floor(Date.now()/1000)}).write() 53 | address = order_db.get('orders').find({token_key: token_key}).value().address //reuse old address 54 | } 55 | else { 56 | return {error:"This order is already processing or was interrupted. Please try again later or request a new key."} 57 | } 58 | } 59 | // Store new order in db 60 | else { 61 | token_key = genSecureKey() 62 | let seed = genSecureKey().toUpperCase() 63 | let nanowallet = wallet.generate(seed) 64 | let accounts = wallet.accounts(nanowallet.seed, 0, 0) 65 | priv_key = accounts[0].privateKey 66 | let pub_key: string = Nano.derivePublicKey(priv_key) 67 | address = Nano.deriveAddress(pub_key, {useNanoPrefix: true}) 68 | 69 | const order: Order = {"address":address, "token_key":token_key, "priv_key":priv_key, "tokens":0, "order_waiting":true, "nano_amount":nano_amount, "token_amount":0, "order_time_left":settings.payment_timeout, "processing":false, "timestamp":Math.floor(Date.now()/1000), "previous": null, "hashes": []} 70 | order_db.get("orders").push(order).write() 71 | } 72 | 73 | // Start checking for receivable and cancel order if taking too long 74 | logThis("Start checking receivable tx every " + settings.receivable_interval + "sec for a total of " + nano_amount + " Nano...", log_levels.info) 75 | checkReceivable(address, order_db) 76 | 77 | // Return payment request 78 | return { address: address, token_key:token_key, payment_amount:nano_amount } 79 | } 80 | 81 | export async function checkOrder(token_key: string, order_db: OrderDB): Promise { 82 | // Get the right order based on token_key 83 | const order: Order | undefined = order_db.get('orders').find({token_key: token_key}).value() 84 | if (order) { 85 | if (!order.order_waiting && order.order_time_left > 0) { 86 | return { token_key: token_key, tokens_ordered: order.token_amount, tokens_total:order.tokens } 87 | } 88 | else if (order.order_time_left > 0){ 89 | return {token_key:token_key, order_time_left: order.order_time_left} 90 | } 91 | else { 92 | return {error: "Order timed out for key: " + token_key} 93 | } 94 | } 95 | else { 96 | return {error: "Order not found for key: " + token_key} 97 | } 98 | } 99 | export async function cancelOrder(token_key: string, order_db: OrderDB): Promise { 100 | // Get the right order based on token_key 101 | const order: Order | undefined = order_db.get('orders').find({token_key: token_key}).value() 102 | if (order) { 103 | let previous_priv_key = order.priv_key 104 | let seed = genSecureKey().toUpperCase() 105 | let nanowallet = wallet.generate(seed) 106 | let accounts = wallet.accounts(nanowallet.seed, 0, 0) 107 | let priv_key = accounts[0].privateKey 108 | let pub_key: string = Nano.derivePublicKey(priv_key) 109 | let address: string = Nano.deriveAddress(pub_key, {useNanoPrefix: true}) 110 | 111 | // Replace the address and private key and reset status 112 | if (!order.processing) { 113 | order_db.get('orders').find({token_key: token_key}).assign({"address":address, "priv_key":priv_key, "order_waiting":false, "nano_amount":0, "order_time_left":settings.payment_timeout, "processing":false, "timestamp":Math.floor(Date.now()/1000)}).write() 114 | logThis("Order was cancelled for " + token_key + ". Previous private key was " + previous_priv_key, log_levels.info) 115 | return {priv_key: previous_priv_key, status: "Order canceled and account replaced. You can use the private key to claim any leftover funds."} 116 | } 117 | else { 118 | logThis("Order tried to cancel but still in process: " + token_key, log_levels.info) 119 | return {priv_key: "",status: "Order is currently processing, please try again later."} 120 | } 121 | 122 | } 123 | else { 124 | return {error: "Order not found"} 125 | } 126 | } 127 | export async function checkTokens(token_key: string, order_db: OrderDB): Promise { 128 | // Get the right order based on token_key 129 | const order = order_db.get('orders').find({token_key: token_key}).value() 130 | if (order) { 131 | if (order.order_waiting === false && order.order_time_left > 0) { 132 | return {tokens_total:order.tokens, status:"OK"} 133 | } 134 | else if (order.order_time_left > 0){ 135 | return {tokens_total:order.tokens, status:'Something went wrong with the last order. You can try the buy command again with the same key to see if it register the receivable or you can cancel it and claim private key with "action":"tokenorder_cancel"'} 136 | } 137 | else { 138 | return {tokens_total:order.tokens, status:'The last order timed out. If you sent Nano you can try the buy command again with the same key to see if it register the receivable or you can cancel it and claim private key with "action":"tokenorder_cancel"'} 139 | } 140 | } 141 | else { 142 | return {error: "Tokens not found for that key"} 143 | } 144 | } 145 | 146 | export async function checkTokenPrice(): Promise { 147 | return {token_price: settings.token_price} 148 | } 149 | 150 | export async function repairOrder(address: string, order_db: OrderDB, url: string, headers: Record | undefined): Promise { 151 | node_url = url 152 | node_headers = headers 153 | checkReceivable(address, order_db, false) 154 | } 155 | 156 | // Check if order payment has arrived as a receivable block, continue check at intervals until time is up. If continue is set to false it will only check one time 157 | async function checkReceivable(address: string, order_db: OrderDB, moveOn: boolean = true, total_received = 0): Promise { 158 | // Check receivable and claim 159 | let priv_key = order_db.get('orders').find({address: address}).value().priv_key 160 | let nano_amount = order_db.get('orders').find({address: address}).value().nano_amount 161 | order_db.get('orders').find({address: address}).assign({"processing":true}).write() // set processing status (to avoid stealing of the private key via orderCancel before receivable has been retrieved) 162 | try { 163 | let receivable_result: any = await processAccount(priv_key, order_db) 164 | order_db.get('orders').find({address: address}).assign({"processing":false}).write() // reset processing status 165 | 166 | // Payment is OK when combined receivable is equal or larger than was ordered (to make sure spammed receivable is not counted as an order) 167 | if('amount' in receivable_result && receivable_result.amount > 0) { 168 | total_received = total_received + receivable_result.amount 169 | // Get the right order based on address 170 | const order = order_db.get('orders').find({address: address}).value() 171 | if(total_received >= nano_amount-0.000001) { // add little margin here because of floating number precision deviation when adding many tx together 172 | let tokens_purchased = Math.round(total_received / settings.token_price) 173 | 174 | if (order) { 175 | // Save previous hashes to be appended with new discovered hashes 176 | let prev_hashes = [] 177 | if ('hashes' in order && Array.isArray(order.hashes)) { 178 | prev_hashes = order.hashes 179 | } 180 | 181 | // Update the total tokens count, actual nano paid and receivable hashes that was processed 182 | logThis("Enough receivable amount detected: Order successfully updated! Continuing processing receivable internally", log_levels.info) 183 | order_db.get('orders').find({address: address}).assign({tokens: order.tokens + tokens_purchased, nano_amount: total_received, token_amount:order.token_amount + tokens_purchased, order_waiting: false, hashes:prev_hashes.concat(receivable_result.hashes)}).write() 184 | return 185 | } 186 | logThis("Address paid was not found in the DB", log_levels.warning) 187 | return 188 | } 189 | else { 190 | logThis("Still need " + (nano_amount - total_received) + " Nano to finilize the order", log_levels.info) 191 | if (order) { 192 | // Save previous hashes to be appended with new discovered hashes 193 | let prev_hashes = [] 194 | if ('hashes' in order && Array.isArray(order.hashes)) { 195 | prev_hashes = order.hashes 196 | } 197 | 198 | // Update the receivable hashes 199 | order_db.get('orders').find({address: address}).assign({hashes:prev_hashes.concat(receivable_result.hashes)}).write() 200 | } 201 | } 202 | } 203 | else if (!receivable_result?.amount) { 204 | logThis("Awaiting amount", log_levels.warning) 205 | } 206 | } 207 | catch(err) { 208 | logThis(err.toString(), log_levels.warning) 209 | } 210 | 211 | // If repairing accounts, only check one time and stop here 212 | if (!moveOn) { 213 | return 214 | } 215 | // pause x sec and check again 216 | await sleep(settings.receivable_interval * 1000) 217 | 218 | // Find the order and update the timeout key 219 | const order = order_db.get('orders').find({address: address}).value() 220 | if (order) { 221 | // Update the order time left 222 | let new_time = order.order_time_left - settings.receivable_interval 223 | if (new_time < 0) { 224 | new_time = 0 225 | } 226 | order_db.get('orders').find({address: address}).assign({order_time_left: new_time}).write() 227 | 228 | // continue checking as long as the db order has time left 229 | if (order.order_time_left > 0) { 230 | checkReceivable(address, order_db, true, total_received) // check again 231 | } 232 | else { 233 | order_db.get('orders').find({address: address}).assign({order_waiting: false}).write() 234 | logThis("Payment timed out for " + address, log_levels.info) 235 | } 236 | return 237 | } 238 | logThis("Address paid was not found in the DB", log_levels.warning) 239 | return 240 | } 241 | 242 | 243 | // Generate secure random 64 char hex 244 | function genSecureKey(): string { 245 | const rand = Nacl.randomBytes(32) 246 | return rand.reduce((hex: string, idx: number) => hex + (`0${idx.toString(16)}`).slice(-2), '') 247 | } 248 | 249 | // Process an account 250 | async function processAccount(privKey: string, order_db: OrderDB): Promise { 251 | let promise = new Promise(async (resolve: (value: StatusCallback) => void, reject: (reason?: any) => void) => { 252 | let pubKey: string = Nano.derivePublicKey(privKey) 253 | let address: string = Nano.deriveAddress(pubKey, {useNanoPrefix: true}) 254 | 255 | // get account info required to build the block 256 | let command: any = {} 257 | command.action = 'account_info' 258 | command.account = address 259 | command.representative = true 260 | 261 | let balance: string = "0" // balance will be 0 if open block 262 | let adjustedBalance: string = balance.toString() 263 | let previous: string | null = null // previous is null if we create open block 264 | order_db.get('orders').find({priv_key: privKey}).assign({previous: previous}).write() 265 | let representative = 'nano_1iuz18n4g4wfp9gf7p1s8qkygxw7wx9qfjq6a9aq68uyrdnningdcjontgar' 266 | let subType = 'open' 267 | 268 | // retrive from RPC 269 | try { 270 | let data: AccountInfoResponse = await Tools.postData(command, node_url, node_headers, API_TIMEOUT) 271 | let validResponse = false 272 | // if frontier is returned it means the account has been opened and we create a receive block 273 | if (data.frontier) { 274 | balance = data.balance 275 | adjustedBalance = balance 276 | previous = data.frontier 277 | order_db.get('orders').find({priv_key: privKey}).assign({previous: previous}).write() 278 | representative = data.representative 279 | subType = 'receive' 280 | validResponse = true 281 | } 282 | else if (data.error === "Account not found") { 283 | validResponse = true 284 | adjustedBalance = "0" 285 | } 286 | if (validResponse) { 287 | // create and publish all receivable 288 | createReceivableBlocks(order_db, privKey, address, balance, adjustedBalance, previous, subType, representative, pubKey, function(previous: string | null, newAdjustedBalance: string) { 289 | // the previous is the last received block and will be used to create the final send block 290 | if (parseInt(newAdjustedBalance) > 0) { 291 | processSend(order_db, privKey, previous, representative, () => { 292 | logThis("Done processing final send", log_levels.info) 293 | }) 294 | } 295 | else { 296 | logThis("Balance is 0", log_levels.warning) 297 | resolve({'amount':0}) 298 | } 299 | }, 300 | // callback for status (accountCallback) 301 | (status: StatusCallback) => resolve(status)) 302 | } 303 | else { 304 | logThis("Bad RPC response", log_levels.warning) 305 | reject(new Error('Bad RPC response')) 306 | } 307 | } 308 | catch (err) { 309 | logThis(err.toString(), log_levels.warning) 310 | reject(new Error('Connection error: ' + err)) 311 | } 312 | }) 313 | return await promise 314 | } 315 | 316 | // Create receivable blocks based on current balance and previous block (or start with an open block) 317 | async function createReceivableBlocks(order_db: OrderDB, privKey: string, address: string, balance: string, adjustedBalance: string, previous: string | null, subType: string, representative: string, pubKey: string, callback: (previous: string | null, newAdjustedBalance: string) => any, accountCallback: (status: StatusCallback) => any): Promise { 318 | // check for receivable first 319 | // Solving this with websocket subscription instead of checking receivable x times for each order would be nice but since we must check for previous receivable that was done before the order initated, it makes it very complicated without rewriting the whole thing.. 320 | let command: any = {} 321 | command.action = 'receivable' 322 | command.account = address 323 | command.count = 10 324 | command.source = 'true' 325 | command.sorting = 'true' //largest amount first 326 | command.include_only_confirmed = 'true' 327 | command.threshold = settings.receivable_threshold 328 | 329 | // retrive from RPC 330 | try { 331 | let data: ReceivableResponse = await Tools.postData(command, node_url, node_headers, API_TIMEOUT) 332 | // if there are any receivable, process them 333 | if (data.blocks) { 334 | // sum all raw amounts and create receive blocks for all receivable 335 | let raw = '0' 336 | let keys: string[] = [] 337 | let blocks: any = {} 338 | const order = order_db.get('orders').find({address: address}).value() 339 | Object.keys(data.blocks).forEach(function(key) { 340 | let found = false 341 | // Check if the receivable hashes have not already been processed 342 | if (order && 'hashes' in order) { 343 | order.hashes.forEach(function(hash) { 344 | if (key === hash) { 345 | found = true 346 | } 347 | }) 348 | } 349 | if (!found) { 350 | raw = Tools.bigAdd(raw,data.blocks[key].amount) 351 | keys.push(key) 352 | blocks[key] = data.blocks[key] // copy the original dictionary key and value to new dictionary 353 | } 354 | }) 355 | // if no new receivable found, continue checking for receivable 356 | if (keys.length == 0) { 357 | accountCallback({'amount':0}) 358 | } 359 | else { 360 | let nanoAmount = Tools.rawToMnano(raw) 361 | let row = "Found " + keys.length + " new receivable containing total " + nanoAmount + " NANO" 362 | logThis(row,log_levels.info) 363 | 364 | accountCallback({amount:parseFloat(nanoAmount), hashes: keys}) 365 | 366 | // use previous from db instead for full compatability with multiple receivables 367 | previous = order.previous 368 | // If there is a previous in db it means there already has been an open block thus next block must be a receive 369 | if (previous != null) { 370 | subType = 'receive' 371 | } 372 | processReceivable(order_db, blocks, keys, 0, privKey, previous, subType, representative, pubKey, adjustedBalance, callback) 373 | } 374 | } 375 | else if (data.error) { 376 | logThis(data.error, log_levels.warning) 377 | accountCallback({ amount:0 }) 378 | } 379 | // no receivable, create final block directly 380 | else { 381 | if (parseInt(adjustedBalance) > 0) { 382 | processSend(order_db, privKey, previous, representative, () => { 383 | accountCallback({amount: 0}) 384 | }) 385 | } 386 | else { 387 | accountCallback({amount: 0}) 388 | } 389 | } 390 | } 391 | catch(err) { 392 | logThis(err, log_levels.warning) 393 | } 394 | } 395 | 396 | // For each receivable block: Create block, generate work and process 397 | async function processReceivable(order_db: OrderDB, blocks: any, keys: any, keyCount: any, privKey: string, previous: string | null, subType: string, representative: string, pubKey: string, adjustedBalance: string, receivableCallback: (previous: string | null, newAdjustedBalance: string) => any): Promise { 398 | let key = keys[keyCount] 399 | 400 | // generate local work 401 | try { 402 | let newAdjustedBalance: string = Tools.bigAdd(adjustedBalance,blocks[key].amount) 403 | logThis("Started generating PoW...", log_levels.info) 404 | 405 | // determine input work hash depending if open block or receive block 406 | let workInputHash = previous 407 | if (subType === 'open') { 408 | // input hash is the opening address public key 409 | workInputHash = pubKey 410 | } 411 | 412 | let command: any = {} 413 | command.action = "work_generate" 414 | command.hash = workInputHash 415 | command.multiplier = settings.difficulty_multiplier 416 | command.use_peers = "true" 417 | 418 | // retrive from RPC 419 | try { 420 | // NOTE: post data to work_server doesn't support custom headers 421 | let data: WorkGenerateResponse = await Tools.postData(command, settings.work_server, undefined, API_TIMEOUT) 422 | if (data.work) { 423 | let work = data.work 424 | // create the block with the work found 425 | let block: Nano.Block = Nano.createBlock(privKey,{balance:newAdjustedBalance, representative:representative, 426 | work:work, link:key, previous:previous}) 427 | // replace xrb with nano (old library) 428 | block.block.account = block.block.account.replace('xrb', 'nano') 429 | block.block.link_as_account = block.block.link_as_account.replace('xrb', 'nano') 430 | // new previous 431 | previous = block.hash 432 | 433 | // publish block for each iteration 434 | let jsonBlock = {action: "process", json_block: "true", subtype:subType, block: block.block} 435 | subType = 'receive' // only the first block can be an open block, reset for next loop 436 | 437 | try { 438 | let data: ProcessResponse = await Tools.postData(jsonBlock, node_url, node_headers, API_TIMEOUT) 439 | if (data.hash) { 440 | logThis("Processed receivable: " + data.hash, log_levels.info) 441 | 442 | // update db with latest previous (must use this if final block was sent before the next receivable could be processed in the same account, in the rare event of multiple receivable) 443 | order_db.get('orders').find({priv_key: privKey}).assign({previous: previous}).write() 444 | 445 | // continue with the next receivable 446 | keyCount += 1 447 | if (keyCount < keys.length) { 448 | processReceivable(order_db, blocks, keys, keyCount, privKey, previous, subType, representative, pubKey, newAdjustedBalance, receivableCallback) 449 | } 450 | // all receivable done, now we process the final send block 451 | else { 452 | logThis("All receivable processed!", log_levels.info) 453 | receivableCallback(previous, newAdjustedBalance) 454 | } 455 | } 456 | else { 457 | logThis("Failed processing block: " + data.error, log_levels.warning) 458 | } 459 | } 460 | catch(err) { 461 | logThis(err, log_levels.warning) 462 | } 463 | } 464 | else { 465 | logThis("Bad PoW result", log_levels.warning) 466 | } 467 | } 468 | catch(err) { 469 | logThis(err, log_levels.warning) 470 | } 471 | } 472 | catch(error) { 473 | if(error.message === 'invalid_hash') { 474 | logThis("Block hash must be 64 character hex string", log_levels.warning) 475 | } 476 | else { 477 | logThis("An unknown error occurred while generating PoW" + error, log_levels.warning) 478 | } 479 | return 480 | } 481 | } 482 | 483 | // Process final send block to payment destination 484 | async function processSend(order_db: OrderDB, privKey: string, previous: string | null, representative: string, sendCallback: () => void): Promise { 485 | let pubKey = Nano.derivePublicKey(privKey) 486 | let address = Nano.deriveAddress(pubKey, {useNanoPrefix: true}) 487 | 488 | logThis("Final transfer started for: " + address, log_levels.info) 489 | let command: any = {} 490 | command.action = 'work_generate' 491 | command.hash = previous 492 | command.multiplier = settings.difficulty_multiplier 493 | command.use_peers = "true" 494 | 495 | // retrive from RPC 496 | try { 497 | let data: WorkGenerateResponse = await Tools.postData(command, settings.work_server, undefined, API_TIMEOUT) 498 | if (data.work) { 499 | let work = data.work 500 | // create the block with the work found 501 | let block = Nano.createBlock(privKey, {balance:'0', representative:representative, 502 | work:work, link:settings.payment_receive_account, previous:previous}) 503 | // replace xrb with nano (old library) 504 | block.block.account = block.block.account.replace('xrb', 'nano') 505 | block.block.link_as_account = block.block.link_as_account.replace('xrb', 'nano') 506 | 507 | // publish block for each iteration 508 | let jsonBlock = {action: "process", json_block: "true", subtype:"send", block: block.block} 509 | try { 510 | let data: ProcessResponse = await Tools.postData(jsonBlock, node_url, node_headers, API_TIMEOUT) 511 | if (data.hash) { 512 | logThis("Funds transferred at block: " + data.hash + " to " + settings.payment_receive_account, log_levels.info) 513 | // update the db with latest hash to be used if processing receivable for the same account 514 | order_db.get('orders').find({priv_key: privKey}).assign({previous: data.hash}).write() 515 | } 516 | else { 517 | logThis("Failed processing block: " + data.error, log_levels.warning) 518 | } 519 | sendCallback() 520 | } 521 | catch(err) { 522 | logThis(err, log_levels.warning) 523 | } 524 | } 525 | else { 526 | logThis("Bad PoW result", log_levels.warning) 527 | } 528 | } 529 | catch(err) { 530 | logThis(err, log_levels.warning) 531 | sendCallback() 532 | } 533 | } 534 | 535 | // Log function 536 | function logThis(message: any, level: LogLevel) { 537 | if (settings.log_level == log_levels.info || level == settings.log_level) { 538 | if (level == log_levels.info) { 539 | console.info(message) 540 | } 541 | else { 542 | console.warn(message) 543 | } 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- 1 | import Fetch, {Response} from 'node-fetch' 2 | import BigInt from 'big-integer' 3 | import * as Nano from 'nanocurrency' 4 | const Dec = require('bigdecimal') //https://github.com/iriscouch/bigdecimal.js 5 | const RemoveTrailingZeros = require('remove-trailing-zeros') 6 | 7 | // Custom error class 8 | class APIError extends Error { 9 | 10 | private code: any 11 | 12 | constructor(code: string, ...params: any[]) { 13 | super(...params) 14 | 15 | // Maintains proper stack trace for where our error was thrown (only available on V8) 16 | if (Error.captureStackTrace) { 17 | Error.captureStackTrace(this, APIError) 18 | } 19 | this.name = 'APIError' 20 | // Custom debugging information 21 | this.code = code 22 | } 23 | } 24 | 25 | // Get data from URL. let data = await getData("url", TIMEOUT) 26 | export async function getData(server: string, timeout: number): Promise { 27 | let options: any = { 28 | method: 'get', 29 | timeout: timeout, 30 | } 31 | 32 | let promise = new Promise(async (resolve: (value: data) => void, reject) => { 33 | // https://www.npmjs.com/package/node-fetch 34 | Fetch(server, options) 35 | .then(checkStatus) 36 | .then(res => res.json()) 37 | .then(json => resolve(json)) 38 | .catch(err => reject(new Error('Connection error: ' + err))) 39 | }) 40 | return await promise // return promise result when finished instead of returning the promise itself, to avoid nested ".then" 41 | } 42 | 43 | // Post data, for example to RPC node. let data = await postData({"action":"block_count"}, "url", TIMEOUT) 44 | export async function postData(data: any, server: string, headers: Record | undefined, timeout: number): Promise { 45 | if (!headers) { 46 | headers = { 'Content-Type': 'application/json' } 47 | } 48 | let options: any = { 49 | method: 'post', 50 | body: JSON.stringify(data), 51 | headers: headers, 52 | timeout: timeout, 53 | } 54 | 55 | let promise = new Promise(async (resolve: (value: ResponseData) => void, reject) => { 56 | // https://www.npmjs.com/package/node-fetch 57 | Fetch(server, options) 58 | .then(checkStatus) 59 | .then(res => res.json()) 60 | .then(json => resolve(json)) 61 | .catch(err => reject(new Error('Connection error: ' + err))) 62 | }) 63 | return await promise // return promise result when finished instead of returning the promise itself, to avoid nested ".then" 64 | } 65 | // Check if a string is a valid JSON 66 | export function isValidJson(obj: any) { 67 | if (obj != null) { 68 | try { 69 | JSON.parse(JSON.stringify(obj)) 70 | return true 71 | } catch (e) { 72 | return false 73 | } 74 | } 75 | else { 76 | return false 77 | } 78 | } 79 | // Add two big integers 80 | export function bigAdd(input: string, value: string): string { 81 | let insert = BigInt(input) 82 | let val = BigInt(value) 83 | return insert.add(val).toString() 84 | } 85 | export function rawToMnano(input: string) { 86 | return isNumeric(input) ? Nano.convert(input, {from: Nano.Unit.raw, to: Nano.Unit.NANO}) : 'N/A' 87 | } 88 | export function MnanoToRaw(input: string) { 89 | return isNumeric(input) ? Nano.convert(input, {from: Nano.Unit.NANO, to: Nano.Unit.raw}) : 'N/A' 90 | } 91 | // Validate nano address, both format and checksum 92 | export function validateAddress(address: string) { 93 | return Nano.checkAddress(address) 94 | } 95 | 96 | function checkStatus(res: Response) { 97 | if (res.ok) { // res.status >= 200 && res.status < 300 98 | return res 99 | } else { 100 | throw new APIError(res.statusText) 101 | } 102 | } 103 | 104 | // Check if numeric string 105 | function isNumeric(val: string): boolean { 106 | //numerics and last character is not a dot and number of dots is 0 or 1 107 | let isnum = /^-?\d*\.?\d*$/.test(val) 108 | if (isnum && String(val).slice(-1) !== '.') { 109 | return true 110 | } 111 | else { 112 | return false 113 | } 114 | } 115 | 116 | // Determine new multiplier from base difficulty (hexadecimal string) and target difficulty (hexadecimal string). Returns float 117 | export function multiplierFromDifficulty(difficulty: string, base_difficulty: string): string { 118 | let big64 = Dec.BigDecimal(2).pow(64) 119 | let big_diff = Dec.BigDecimal(Dec.BigInteger(difficulty,16)) 120 | let big_base = Dec.BigDecimal(Dec.BigInteger(base_difficulty,16)) 121 | let mode = Dec.RoundingMode.HALF_DOWN() 122 | return RemoveTrailingZeros(big64.subtract(big_base).divide(big64.subtract(big_diff),32,mode).toPlainString()) 123 | } 124 | -------------------------------------------------------------------------------- /src/user-settings.ts: -------------------------------------------------------------------------------- 1 | import {CachedCommands, LimitedCommands} from "./common-settings"; 2 | import * as Fs from "fs"; 3 | import ProxySettings from "./proxy-settings"; 4 | 5 | export interface UserSettings { 6 | use_cache: boolean; 7 | use_output_limiter: boolean; 8 | allowed_commands: string[]; 9 | cached_commands: CachedCommands; 10 | limited_commands: LimitedCommands; 11 | } 12 | 13 | export type UserSettingsConfig = Record 14 | 15 | /** Clone default settings for custom user specific vars, to be used if no auth */ 16 | export function loadDefaultUserSettings(settings: ProxySettings): UserSettings { 17 | return { 18 | allowed_commands: settings.allowed_commands || [], 19 | cached_commands: settings.cached_commands || {}, 20 | limited_commands: settings.limited_commands || {}, 21 | use_cache: settings.use_cache || false, 22 | use_output_limiter: settings.use_output_limiter || false 23 | } 24 | } 25 | 26 | /** Read user settings from file, override default settings if they exist for specific users */ 27 | export function readUserSettings(settingsPath: string): UserSettingsConfig { 28 | try { 29 | return JSON.parse(Fs.readFileSync(settingsPath, 'utf-8')) 30 | } catch (e) { 31 | console.log("Could not read user_settings.json, returns empty settings.", e) 32 | return {}; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /token_settings.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "work_server": "http://[::1]:7076", 3 | "token_price": 0.0001, 4 | "payment_timeout": 180, 5 | "receivable_interval": 2, 6 | "receivable_threshold": "100000000000000000000000", 7 | "receivable_count": 10, 8 | "difficulty_multiplier": "1.0", 9 | "payment_receive_account": "nano_1gur37mt5cawjg5844bmpg8upo4hbgnbbuwcerdobqoeny4ewoqshowfakfo", 10 | "min_token_amount": "1", 11 | "max_token_amount": "10000000", 12 | "log_level": "info" 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "include": ["./src/**/*"] 71 | } 72 | -------------------------------------------------------------------------------- /user_settings.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "user2": { 3 | "use_cache": true, 4 | "use_output_limiter": true, 5 | "allowed_commands": [ 6 | "account_history", 7 | "account_info", 8 | "block_info", 9 | "block_count" 10 | ], 11 | "cached_commands": { 12 | "block_count": 60 13 | }, 14 | "limited_commands": { 15 | "account_history": 50, 16 | "accounts_frontiers": 50, 17 | "accounts_balances": 500, 18 | "accounts_receivable": 50, 19 | "chain": 50, 20 | "frontiers": 50, 21 | "receivable": 50 22 | } 23 | }, 24 | "user3": { 25 | "use_cache": true, 26 | "use_output_limiter": true 27 | } 28 | } 29 | --------------------------------------------------------------------------------