├── .babelrc ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE.md ├── Makefile ├── OLD-README.md ├── README.md ├── babel-polyfill.js ├── ops ├── cloudflare-secret.sh ├── deploy.sh ├── docker-compose.dev.yml ├── docker-compose.monitor.yml ├── docker-compose.prod.yml ├── docker-compose.yml ├── grafana │ ├── dashboards │ │ ├── cadvisor.json │ │ ├── cadvisor.yml │ │ ├── dockermetrics.json │ │ ├── dockermetrics.yml │ │ ├── hoststats.json │ │ ├── hoststats.yml │ │ ├── nginxstats.json │ │ ├── nginxstats.yml │ │ ├── redisstats.json │ │ └── redisstats.yml │ └── datasources │ │ └── datasource.yml ├── nginx │ ├── dhparams.pem │ ├── entry.sh │ ├── exporter.conf │ ├── letsencrypt.conf │ ├── nginx.Dockerfile │ └── nginx.conf ├── node.Dockerfile └── prometheus.yml ├── package-lock.json ├── package.json ├── src ├── config │ └── index.ts ├── index.ts ├── keystore.ts ├── notification.ts ├── pubsub.ts └── types.ts ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "corejs": 3, 7 | "useBuiltIns": "usage" 8 | } 9 | ], 10 | "@babel/typescript" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | LICENSE.md 4 | README.md 5 | Makefile 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Dependency directories 28 | node_modules 29 | jspm_packages 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional eslint cache 35 | .eslintcache 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # Output of 'npm pack' 41 | *.tgz 42 | 43 | dist 44 | build 45 | 46 | .DS_Store 47 | 48 | .makeFlags* 49 | config 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2018 WalletConnect Association. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | This version of the GNU Lesser General Public License incorporates 9 | the terms and conditions of version 3 of the GNU General Public 10 | License, supplemented by the additional permissions listed below. 11 | 12 | 0. Additional Definitions. 13 | 14 | As used herein, "this License" refers to version 3 of the GNU Lesser 15 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 16 | General Public License. 17 | 18 | "The Library" refers to a covered work governed by this License, 19 | other than an Application or a Combined Work as defined below. 20 | 21 | An "Application" is any work that makes use of an interface provided 22 | by the Library, but which is not otherwise based on the Library. 23 | Defining a subclass of a class defined by the Library is deemed a mode 24 | of using an interface provided by the Library. 25 | 26 | A "Combined Work" is a work produced by combining or linking an 27 | Application with the Library. The particular version of the Library 28 | with which the Combined Work was made is also called the "Linked 29 | Version". 30 | 31 | The "Minimal Corresponding Source" for a Combined Work means the 32 | Corresponding Source for the Combined Work, excluding any source code 33 | for portions of the Combined Work that, considered in isolation, are 34 | based on the Application, and not on the Linked Version. 35 | 36 | The "Corresponding Application Code" for a Combined Work means the 37 | object code and/or source code for the Application, including any data 38 | and utility programs needed for reproducing the Combined Work from the 39 | Application, but excluding the System Libraries of the Combined Work. 40 | 41 | 1. Exception to Section 3 of the GNU GPL. 42 | 43 | You may convey a covered work under sections 3 and 4 of this License 44 | without being bound by section 3 of the GNU GPL. 45 | 46 | 2. Conveying Modified Versions. 47 | 48 | If you modify a copy of the Library, and, in your modifications, a 49 | facility refers to a function or data to be supplied by an Application 50 | that uses the facility (other than as an argument passed when the 51 | facility is invoked), then you may convey a copy of the modified 52 | version: 53 | 54 | a) under this License, provided that you make a good faith effort to 55 | ensure that, in the event an Application does not supply the 56 | function or data, the facility still operates, and performs 57 | whatever part of its purpose remains meaningful, or 58 | 59 | b) under the GNU GPL, with none of the additional permissions of 60 | this License applicable to that copy. 61 | 62 | 3. Object Code Incorporating Material from Library Header Files. 63 | 64 | The object code form of an Application may incorporate material from 65 | a header file that is part of the Library. You may convey such object 66 | code under terms of your choice, provided that, if the incorporated 67 | material is not limited to numerical parameters, data structure 68 | layouts and accessors, or small macros, inline functions and templates 69 | (ten or fewer lines in length), you do both of the following: 70 | 71 | a) Give prominent notice with each copy of the object code that the 72 | Library is used in it and that the Library and its use are 73 | covered by this License. 74 | 75 | b) Accompany the object code with a copy of the GNU GPL and this license 76 | document. 77 | 78 | 4. Combined Works. 79 | 80 | You may convey a Combined Work under terms of your choice that, 81 | taken together, effectively do not restrict modification of the 82 | portions of the Library contained in the Combined Work and reverse 83 | engineering for debugging such modifications, if you also do each of 84 | the following: 85 | 86 | a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | 90 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 91 | document. 92 | 93 | c) For a Combined Work that displays copyright notices during 94 | execution, include the copyright notice for the Library among 95 | these notices, as well as a reference directing the user to the 96 | copies of the GNU GPL and this license document. 97 | 98 | d) Do one of the following: 99 | 100 | 0) Convey the Minimal Corresponding Source under the terms of this 101 | License, and the Corresponding Application Code in a form 102 | suitable for, and under terms that permit, the user to 103 | recombine or relink the Application with a modified version of 104 | the Linked Version to produce a modified Combined Work, in the 105 | manner specified by section 6 of the GNU GPL for conveying 106 | Corresponding Source. 107 | 108 | 1) Use a suitable shared library mechanism for linking with the 109 | Library. A suitable mechanism is one that (a) uses at run time 110 | a copy of the Library already present on the user's computer 111 | system, and (b) will operate properly with a modified version 112 | of the Library that is interface-compatible with the Linked 113 | Version. 114 | 115 | e) Provide Installation Information, but only if you would otherwise 116 | be required to provide such information under section 6 of the 117 | GNU GPL, and only to the extent that such information is 118 | necessary to install and execute a modified version of the 119 | Combined Work produced by recombining or relinking the 120 | Application with a modified version of the Linked Version. (If 121 | you use option 4d0, the Installation Information must accompany 122 | the Minimal Corresponding Source and Corresponding Application 123 | Code. If you use option 4d1, you must provide the Installation 124 | Information in the manner specified by section 6 of the GNU GPL 125 | for conveying Corresponding Source.) 126 | 127 | 5. Combined Libraries. 128 | 129 | You may place library facilities that are a work based on the 130 | Library side by side in a single library together with other library 131 | facilities that are not Applications and are not covered by this 132 | License, and convey such a combined library under terms of your 133 | choice, if you do both of the following: 134 | 135 | a) Accompany the combined library with a copy of the same work based 136 | on the Library, uncombined with any other library facilities, 137 | conveyed under the terms of this License. 138 | 139 | b) Give prominent notice with the combined library that part of it 140 | is a work based on the Library, and explaining where to find the 141 | accompanying uncombined form of the same work. 142 | 143 | 6. Revised Versions of the GNU Lesser General Public License. 144 | 145 | The WalletConnect Association may publish revised and/or new versions 146 | of the GNU Lesser General Public License from time to time. Such new 147 | versions will be similar in spirit to the present version, but may 148 | differ in detail to address new problems or concerns. 149 | 150 | Each version is given a distinguishing version number. If the 151 | Library as you received it specifies that a certain numbered version 152 | of the GNU Lesser General Public License "or any later version" 153 | applies to it, you have the option of following the terms and 154 | conditions either of that published version or of any later version 155 | published by the WalletConnect Association. If the Library as you 156 | received it does not specify a version number of the GNU Lesser 157 | General Public License, you may choose any version of the GNU Lesser 158 | General Public License ever published by the WalletConnect Association. 159 | 160 | If the Library as you received it specifies that a proxy can decide 161 | whether future versions of the GNU Lesser General Public License shall 162 | apply, that proxy's public statement of acceptance of any version is 163 | permanent authorization for you to choose that version for the 164 | Library. 165 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ### Deploy configs 2 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 3 | REMOTE="https://github.com/WalletConnect/node-walletconnect-bridge" 4 | REMOTE_HASH=$(shell git ls-remote $(REMOTE) $(BRANCH) | head -n1 | cut -f1) 5 | project=walletconnect 6 | redisImage='redis:5-alpine' 7 | nginxImage='$(project)/nginx:$(BRANCH)' 8 | walletConnectImage='$(project)/bridge:$(BRANCH)' 9 | 10 | ### Makefile internal coordination 11 | flags=.makeFlags 12 | VPATH=$(flags) 13 | 14 | $(shell mkdir -p $(flags)) 15 | 16 | .PHONY: all clean default 17 | define DEFAULT_TEXT 18 | Available make rules: 19 | 20 | pull:\tdownloads docker images 21 | 22 | setup:\tconfigures domain an certbot email 23 | 24 | build:\tbuilds docker images 25 | 26 | dev:\truns local docker stack with open ports 27 | 28 | deploy:\tdeploys to production 29 | 30 | deploy-monitoring: 31 | \tdeploys to production with grafana 32 | 33 | cloudflare: asks for a cloudflare DNS api and creates a docker secret 34 | 35 | stop:\tstops all walletconnect docker stacks 36 | 37 | upgrade: 38 | \tpulls from remote git. Builds the containers and updates each individual 39 | \tcontainer currently running with the new version that was just built. 40 | 41 | clean:\tcleans current docker build 42 | 43 | reset:\treset local config 44 | endef 45 | 46 | ### Rules 47 | export DEFAULT_TEXT 48 | default: 49 | @echo -e "$$DEFAULT_TEXT" 50 | 51 | pull: 52 | docker pull $(redisImage) 53 | @touch $(flags)/$@ 54 | @echo "MAKE: Done with $@" 55 | @echo 56 | 57 | setup: 58 | @read -p 'Bridge URL domain: ' bridge; \ 59 | echo "BRIDGE_URL="$$bridge > config 60 | @read -p 'Email for SSL certificate (default noreply@gmail.com): ' email; \ 61 | echo "CERTBOT_EMAIL="$$email >> config 62 | @read -p 'Is your DNS configured with cloudflare proxy? [y/N]: ' cf; \ 63 | echo "CLOUDFLARE="$${cf:-false} >> config 64 | @touch $(flags)/$@ 65 | @echo "MAKE: Done with $@" 66 | @echo 67 | 68 | build-node: pull 69 | docker build \ 70 | -t $(walletConnectImage) \ 71 | --build-arg BRANCH=$(BRANCH) \ 72 | --build-arg REMOTE_HASH=$(REMOTE_HASH) \ 73 | -f ops/node.Dockerfile . 74 | @touch $(flags)/$@ 75 | @echo "MAKE: Done with $@" 76 | @echo 77 | 78 | build-nginx: pull 79 | docker build \ 80 | -t $(nginxImage) \ 81 | --build-arg BRANCH=$(BRANCH) \ 82 | --build-arg REMOTE_HASH=$(REMOTE_HASH) \ 83 | -f ops/nginx/nginx.Dockerfile ./ops/nginx 84 | @touch $(flags)/$@ 85 | @echo "MAKE: Done with $@" 86 | @echo 87 | 88 | build: pull build-node build-nginx 89 | @touch $(flags)/$@ 90 | @echo "MAKE: Done with $@" 91 | @echo 92 | 93 | dev: pull build 94 | BRIDGE_IMAGE=$(walletConnectImage) \ 95 | NGINX_IMAGE=$(nginxImage) \ 96 | docker stack deploy \ 97 | -c ops/docker-compose.yml \ 98 | -c ops/docker-compose.dev.yml \ 99 | dev_$(project) 100 | @echo "MAKE: Done with $@" 101 | @echo 102 | 103 | dev-monitoring: pull build 104 | BRIDGE_IMAGE=$(walletConnectImage) \ 105 | NGINX_IMAGE=$(nginxImage) \ 106 | docker stack deploy \ 107 | -c ops/docker-compose.yml \ 108 | -c ops/docker-compose.dev.yml \ 109 | -c ops/docker-compose.monitor.yml \ 110 | dev_$(project) 111 | @echo "MAKE: Done with $@" 112 | @echo 113 | 114 | redeploy: 115 | $(MAKE) clean 116 | $(MAKE) down 117 | $(MAKE) dev-monitoring 118 | 119 | cloudflare: setup 120 | bash ops/cloudflare-secret.sh $(project) 121 | @touch $(flags)/$@ 122 | @echo "MAKE: Done with $@" 123 | @echo 124 | 125 | deploy: setup build cloudflare 126 | BRIDGE_IMAGE=$(walletConnectImage) \ 127 | NGINX_IMAGE=$(nginxImage) \ 128 | PROJECT=$(project) \ 129 | bash ops/deploy.sh 130 | @echo "MAKE: Done with $@" 131 | @echo 132 | 133 | deploy-monitoring: setup build cloudflare 134 | BRIDGE_IMAGE=$(walletConnectImage) \ 135 | NGINX_IMAGE=$(nginxImage) \ 136 | PROJECT=$(project) \ 137 | MONITORING=true \ 138 | bash ops/deploy.sh 139 | @echo "MAKE: Done with $@" 140 | @echo 141 | 142 | down: stop 143 | 144 | stop: 145 | docker stack rm $(project) 146 | docker stack rm dev_$(project) 147 | while [ -n "`docker network ls --quiet --filter label=com.docker.stack.namespace=$(project)`" ]; do echo -n '.' && sleep 1; done 148 | @echo 149 | while [ -n "`docker network ls --quiet --filter label=com.docker.stack.namespace=dev_$(project)`" ]; do echo -n '.' && sleep 1; done 150 | @echo "MAKE: Done with $@" 151 | @echo 152 | 153 | upgrade: setup 154 | rm -f $(flags)/build* 155 | $(MAKE) build 156 | @echo "MAKE: Done with $@" 157 | @echo 158 | git fetch origin $(BRANCH) 159 | git merge origin/$(BRANCH) 160 | docker service update --force $(project)_bridge0 161 | docker service update --force $(project)_bridge1 162 | docker service update --force $(project)_nginx 163 | docker service update --force $(project)_redis 164 | 165 | reset: 166 | $(MAKE) clean-all 167 | rm -f config 168 | @echo "MAKE: Done with $@" 169 | @echo 170 | 171 | clean: 172 | rm -rf .makeFlags/build* 173 | @echo "MAKE: Done with $@" 174 | @echo 175 | 176 | clean-all: 177 | rm -rf .makeFlags 178 | @echo "MAKE: Done with $@" 179 | @echo 180 | -------------------------------------------------------------------------------- /OLD-README.md: -------------------------------------------------------------------------------- 1 | # WalletConnect Bridge Server ⏮️🖥️⏭️ 2 | 3 | Bridge Server for relaying WalletConnect connections 4 | 5 | ## Development 🧪 6 | 7 | Local dev work is using local self signed certificates within the docker environment. 8 | 9 | Your Walletconnect enabled app needs to be on the same local network. 10 | 11 | ``` 12 | make dev # ports 80, 443, 5001, 6379 will be exposed locally 13 | ``` 14 | 15 | ## Production 🗜️ 16 | 17 | #### Setting up docker 🎚️ 18 | 19 | Dependencies: 20 | - git 21 | - docker 22 | - make 23 | 24 | You will need to have docker swarm enabled: 25 | 26 | ```bash 27 | docker swarm init 28 | # If you get the following error: `could not chose an IP address to advertise...`. You can do the following: 29 | docker swarm init --advertise-addr `curl -s ipecho.net/plain` 30 | ``` 31 | 32 | ### Deploying 🚀 33 | 34 | Run the following command and fill in the prompts: 35 | 36 | ```bash 37 | git clone https://github.com/WalletConnect/node-walletconnect-bridge 38 | cd node-walletconnect-bridge 39 | make deploy 40 | Bridge URL domain: 41 | Email for SSL certificate (default noreply@gmail.com): 42 | ``` 43 | 44 | #### Additional Monitoring with Grafana 45 | 46 | If you want a grafana dashboard you can use the following commands: 47 | 48 | ```bash 49 | git clone https://github.com/WalletConnect/node-walletconnect-bridge 50 | cd node-walletconnect-bridge 51 | make deploy-monitoring 52 | Bridge URL domain: 53 | Email for SSL certificate (default noreply@gmail.com): 54 | ``` 55 | 56 | For this to work you must point grafana.`` to the same ip as ``. 57 | 58 | #### Cloudflare Support 59 | 60 | The config step of the Makefile will ask you whether you are using cloudflare as a DNS proxy for your bridge domain. If you answer yes then the certbot will need a Cloudflare API token that can be obtained from: https://dash.cloudflare.com/profile/api-tokens. The type of token you need is a `Edit zone DNS` with access to the bridge domain. 61 | 62 | The API token will be safeguarded with a `docker secret`. 63 | 64 | ### Upgrading ⏫ 65 | 66 | This will upgrade your current bridge with minimal downtime. 67 | 68 | ⚠️ ATTENTION: This will run `git fetch && git merge origin/master` in your repo ⚠️ 69 | 70 | ```bash 71 | make upgrade 72 | ``` 73 | 74 | ### Monitoring 📜 75 | 76 | This stack deploys 3 containers one of redis, nginx and node.js. You can follow the logs of the nginx container by running the following command: 77 | 78 | ``` 79 | docker service logs --raw -f walletconnect_nginx 80 | ``` 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WalletConnect Bridge Server 2 | 3 | **[DEPRECATED]** Please refer to [walletconnect-monorepo](https://github.com/walletconnect/walletconnect-monorepo) under servers/relay 4 | 5 | You can read the old documentation [here](./OLD-README.md) 6 | -------------------------------------------------------------------------------- /babel-polyfill.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | extensions: ['.ts', '.js', '.tsx', '.jsx'] 3 | }) 4 | -------------------------------------------------------------------------------- /ops/cloudflare-secret.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin 2 | project=${1} 3 | secretName="${project}_cloudflare" 4 | 5 | cloudflare=$(grep CLOUDFLARE config | cut -f2 -d=) 6 | case $cloudflare in 7 | false | "N" | "NO" | "No" | "no" | "n" ) 8 | cloudflare=false 9 | ;; 10 | * ) 11 | cloudflare=true 12 | ;; 13 | esac 14 | 15 | if [[ $cloudflare == false ]];then 16 | sed -i 's/^CLOUDFLARE=.$/CLOUDFLARE=false/g' config 17 | else 18 | sed -i 's/^CLOUDFLARE=.$/CLOUDFLARE=true/g' config 19 | read -p "Please paste your cloudflare dns api token: " token 20 | docker secret rm $secretName 21 | printf $token | docker secret create $secretName - 22 | 23 | cat - > /tmp/${project}.secrets.yml< /run/secrets/cloudflare.ini 43 | certbot certonly --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare.ini -d $fullDomain -m $email --agree-tos --no-eff-email -n 44 | fi 45 | 46 | if [[ ! $? -eq 0 ]] 47 | then 48 | echo "ERROR" 49 | echo "Sleeping to not piss off certbot" 50 | sleep 9999 # FREEZE! Don't pester eff & get throttled 51 | fi 52 | fi 53 | } 54 | 55 | function waitForContainerToBeUp () { 56 | count=0 57 | while true; do 58 | ping -c 1 $1 59 | if [ $1 ]; then 60 | break 61 | fi 62 | if [[ $count -gt 20 ]]; then 63 | echo "Container $1 is not live! Exiting" 64 | exit 1 65 | fi 66 | count=$((1 + $count)) 67 | done 68 | } 69 | 70 | function configSubDomain () { 71 | subDomain=$1 72 | dockerPort=$2 73 | rootDomain=$3 74 | fullDomain=$subDomain.$rootDomain 75 | echo "Configuring Subdomain: $fullDomain" 76 | certDirectory=$LETSENCRYPT/$fullDomain 77 | mkdir -vp $certDirectory 78 | makeCert "$fullDomain" $certDirectory 79 | cat - > "$SERVERS/$fullDomain.conf" <> $configPath<> $configPath 123 | else 124 | echo "server $dockerContainerName$i:$port backup;" >> $configPath 125 | fi 126 | done 127 | echo "}" >> $configPath 128 | } 129 | 130 | function configRootDomain () { 131 | domain=$1 132 | printf "\nConfiguring root domain: $domain\n" 133 | certDirectory=$LETSENCRYPT/$domain 134 | mkdir -vp $certDirectory 135 | makeCert $domain $certDirectory 136 | configPath="$SERVERS/$domain.conf" 137 | cat - > $configPath <", 13 | "homepage": "https://walletconnect.org", 14 | "license": "LGPL-3.0", 15 | "dependencies": { 16 | "axios": "^0.19.2", 17 | "bluebird": "^3.7.2", 18 | "fastify": "^2.15.3", 19 | "fastify-helmet": "^3.0.2", 20 | "pino-pretty": "^4.2.1", 21 | "redis": "^2.8.0", 22 | "ws": "^7.3.1" 23 | }, 24 | "scripts": { 25 | "build": "babel ./src --extensions '.ts' --out-dir ./build", 26 | "test": "mocha --require ./babel-polyfill.js test/**/*.spec.ts", 27 | "start": "node ./build" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/walletconnect/node-walletconnect-bridge.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/walletconnect/node-walletconnect-bridge/issues" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.11.6", 38 | "@babel/core": "^7.11.6", 39 | "@babel/node": "^7.10.5", 40 | "@babel/polyfill": "^7.11.5", 41 | "@babel/preset-env": "^7.11.5", 42 | "@babel/preset-typescript": "^7.10.4", 43 | "@babel/register": "^7.11.5", 44 | "@types/axios": "^0.14.0", 45 | "@types/bluebird": "^3.5.32", 46 | "@types/core-js": "^2.5.3", 47 | "@types/node": "^12.12.55", 48 | "@types/redis": "^2.8.27", 49 | "@types/ws": "^6.0.4", 50 | "core-js": "^3.6.5", 51 | "eslint": "^5.16.0", 52 | "eslint-config-standard": "^12.0.0", 53 | "eslint-plugin-import": "^2.22.0", 54 | "eslint-plugin-node": "^9.1.0", 55 | "eslint-plugin-promise": "^4.0.1", 56 | "eslint-plugin-standard": "^4.0.0", 57 | "tslint": "^5.20.1", 58 | "tslint-config-standard": "^8.0.1", 59 | "typescript": "^3.9.7" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV || 'development' 2 | const debug = env !== 'production' 3 | const port = process.env.PORT || (env === 'production' ? 5000 : 5001) 4 | const host = process.env.HOST || `0.0.0.0:${port}` 5 | 6 | const redis = { 7 | url: process.env.REDIS_URL || 'redis://localhost:6379/0', 8 | prefix: process.env.REDIS_PREFIX || 'walletconnect-bridge' 9 | } 10 | 11 | export default { 12 | env: env, 13 | debug: debug, 14 | port, 15 | host, 16 | redis 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import Helmet from 'fastify-helmet' 3 | import WebSocket from 'ws' 4 | import config from './config' 5 | import pubsub from './pubsub' 6 | import { setNotification } from './keystore' 7 | import { IWebSocket } from './types' 8 | import pkg from '../package.json' 9 | 10 | const app = fastify({ 11 | logger: { prettyPrint: { forceColor: true } } 12 | }) 13 | 14 | app.register(Helmet) 15 | 16 | app.get('/health', (_, res) => { 17 | res.status(204).send() 18 | }) 19 | 20 | app.get('/hello', (req, res) => { 21 | res.status(200).send(`Hello World, this is WalletConnect v${pkg.version}`) 22 | }) 23 | 24 | app.get('/info', (req, res) => { 25 | res.status(200).send({ 26 | name: pkg.name, 27 | description: pkg.description, 28 | version: pkg.version 29 | }) 30 | }) 31 | 32 | app.post('/subscribe', async (req, res) => { 33 | if (!req.body || typeof req.body !== 'object') { 34 | res.status(400).send({ 35 | message: 'Error: missing or invalid request body' 36 | }) 37 | } 38 | 39 | const { topic, webhook } = req.body 40 | 41 | if (!topic || typeof topic !== 'string') { 42 | res.status(400).send({ 43 | message: 'Error: missing or invalid topic field' 44 | }) 45 | } 46 | 47 | if (!webhook || typeof webhook !== 'string') { 48 | res.status(400).send({ 49 | message: 'Error: missing or invalid webhook field' 50 | }) 51 | } 52 | 53 | await setNotification({ topic, webhook }) 54 | 55 | res.status(200).send({ 56 | success: true 57 | }) 58 | }) 59 | 60 | const wsServer = new WebSocket.Server({ server: app.server }) 61 | 62 | app.ready(() => { 63 | wsServer.on('connection', (socket: IWebSocket) => { 64 | socket.on('message', async data => { 65 | pubsub(socket, data, app.log) 66 | }) 67 | 68 | socket.on('pong', () => { 69 | socket.isAlive = true 70 | }) 71 | 72 | socket.on("error", (e: Error) => { 73 | if (!e.message.includes("Invalid WebSocket frame")) { 74 | throw e 75 | } 76 | app.log.warn({type: e.name, message: e.message}) 77 | }) 78 | }) 79 | 80 | setInterval( 81 | () => { 82 | const sockets: any = wsServer.clients 83 | sockets.forEach((socket: IWebSocket) => { 84 | if (socket.isAlive === false) { 85 | return socket.terminate() 86 | } 87 | 88 | function noop () {} 89 | 90 | socket.isAlive = false 91 | socket.ping(noop) 92 | }) 93 | }, 94 | 10000 // 10 seconds 95 | ) 96 | }) 97 | 98 | const [host, port] = config.host.split(':') 99 | app.listen(+port, host, (err, address) => { 100 | if (err) throw err 101 | app.log.info(`Server listening on ${address}`) 102 | }) 103 | -------------------------------------------------------------------------------- /src/keystore.ts: -------------------------------------------------------------------------------- 1 | import redis from 'redis' 2 | import { ISocketMessage, ISocketSub, INotification } from './types' 3 | import bluebird from 'bluebird' 4 | import config from './config' 5 | 6 | bluebird.promisifyAll(redis.RedisClient.prototype) 7 | bluebird.promisifyAll(redis.Multi.prototype) 8 | 9 | const redisClient: any = redis.createClient(config.redis) 10 | 11 | const subs: ISocketSub[] = [] 12 | 13 | export const setSub = (subscriber: ISocketSub) => subs.push(subscriber) 14 | export const getSub = (topic: string) => 15 | subs.filter( 16 | subscriber => 17 | subscriber.topic === topic && subscriber.socket.readyState === 1 18 | ) 19 | 20 | export const setPub = (socketMessage: ISocketMessage) => 21 | redisClient.lpushAsync( 22 | `socketMessage:${socketMessage.topic}`, 23 | JSON.stringify(socketMessage) 24 | ) 25 | 26 | export const getPub = (topic: string): ISocketMessage[] => { 27 | return redisClient 28 | .lrangeAsync(`socketMessage:${topic}`, 0, -1) 29 | .then((data: any) => { 30 | if (data) { 31 | let localData: ISocketMessage[] = data.map((item: string) => 32 | JSON.parse(item) 33 | ) 34 | redisClient.del(`socketMessage:${topic}`) 35 | return localData 36 | } 37 | }) 38 | } 39 | 40 | export const setNotification = (notification: INotification) => 41 | redisClient.lpushAsync( 42 | `notification:${notification.topic}`, 43 | JSON.stringify(notification) 44 | ) 45 | 46 | export const getNotification = (topic: string) => { 47 | return redisClient 48 | .lrangeAsync(`notification:${topic}`, 0, -1) 49 | .then((data: any) => { 50 | if (data) { 51 | return data.map((item: string) => JSON.parse(item)) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/notification.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { INotification } from './types' 3 | import { getNotification } from './keystore' 4 | 5 | export const pushNotification = async (topic: string) => { 6 | const notifications = await getNotification(topic) 7 | 8 | if (notifications && notifications.length) { 9 | notifications.forEach((notification: INotification) => 10 | axios.post(notification.webhook, { topic }) 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pubsub.ts: -------------------------------------------------------------------------------- 1 | import { ISocketMessage, ISocketSub, IWebSocket, WebSocketData,Logger } from './types' 2 | import { pushNotification } from './notification' 3 | import { setSub, getSub, setPub, getPub } from './keystore' 4 | 5 | async function socketSend (socket: IWebSocket, socketMessage: ISocketMessage , logger: Logger) { 6 | if (socket.readyState === 1) { 7 | const message = JSON.stringify(socketMessage) 8 | socket.send(message) 9 | logger.info({ type: 'outgoing', message }) 10 | } else { 11 | await setPub(socketMessage) 12 | } 13 | } 14 | 15 | async function SubController ( 16 | socket: IWebSocket, 17 | socketMessage: ISocketMessage, 18 | logger: Logger 19 | ) { 20 | const topic = socketMessage.topic 21 | 22 | const subscriber = { topic, socket } 23 | 24 | await setSub(subscriber) 25 | 26 | const pending = await getPub(topic) 27 | 28 | if (pending && pending.length) { 29 | await Promise.all( 30 | pending.map((pendingMessage: ISocketMessage) => 31 | socketSend(socket, pendingMessage, logger) 32 | ) 33 | ) 34 | } 35 | } 36 | 37 | async function PubController (socketMessage: ISocketMessage, logger: Logger) { 38 | const subscribers = await getSub(socketMessage.topic) 39 | 40 | if (!socketMessage.silent) { 41 | await pushNotification(socketMessage.topic) 42 | } 43 | 44 | if (subscribers.length) { 45 | await Promise.all( 46 | subscribers.map((subscriber: ISocketSub) => 47 | socketSend(subscriber.socket, socketMessage, logger) 48 | ) 49 | ) 50 | } else { 51 | await setPub(socketMessage) 52 | } 53 | } 54 | 55 | export default async (socket: IWebSocket, data: WebSocketData, logger: Logger) => { 56 | const message: string = String(data) 57 | 58 | if (!message || !message.trim()) { 59 | return 60 | } 61 | 62 | logger.info({ type: 'incoming', message }) 63 | 64 | try { 65 | let socketMessage: ISocketMessage | null = null 66 | 67 | try { 68 | socketMessage = JSON.parse(message) 69 | } catch (e) { 70 | // do nothing 71 | } 72 | 73 | if (!socketMessage) { 74 | return 75 | } 76 | 77 | switch (socketMessage.type) { 78 | case 'sub': 79 | await SubController(socket, socketMessage, logger) 80 | break 81 | case 'pub': 82 | await PubController(socketMessage, logger) 83 | break 84 | default: 85 | break 86 | } 87 | } catch (e) { 88 | console.error(e) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | 3 | export { Logger } from 'fastify' 4 | 5 | export type WebSocketData = WebSocket.Data 6 | 7 | export interface IWebSocket extends WebSocket { 8 | isAlive: boolean 9 | } 10 | 11 | export interface ISocketMessage { 12 | topic: string 13 | type: string 14 | payload: string 15 | silent: boolean 16 | } 17 | 18 | export interface ISocketSub { 19 | topic: string 20 | socket: IWebSocket 21 | } 22 | 23 | export interface INotification { 24 | topic: string 25 | webhook: string 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017", "es2018"], 6 | "strict": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard"], 3 | "rules": { 4 | "no-empty": false 5 | } 6 | } 7 | --------------------------------------------------------------------------------