├── .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 |
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 |
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
;
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 APIBad 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 |
--------------------------------------------------------------------------------