├── .editorconfig ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-entrypoint.sh ├── nginx └── defaultConf ├── requirements.txt ├── setup.py ├── ssl ├── .keep └── certbot │ └── .keep ├── systemd └── walletconnect-bridge.service └── walletconnect_bridge ├── __init__.py ├── errors.py ├── keystore.py └── time.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | end_of_line = lf 8 | 9 | [*.py] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.{md,markdown}] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | *.egg-info/ 4 | *.egg 5 | .DS_Store 6 | ssl/*.pem 7 | ssl/certbot/* 8 | !ssl/certbot/.keep 9 | .vscode 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | script: 7 | - make setup URL=test-bridge.mydomain.com 8 | - make build 9 | - make run_daemon_skip_certbot 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | ARG branch=master 3 | RUN apt-get update 4 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y \ 5 | python3-pip \ 6 | git \ 7 | redis-server \ 8 | nginx \ 9 | software-properties-common 10 | RUN add-apt-repository ppa:certbot/certbot 11 | RUN apt-get update 12 | RUN apt-get install -y python-certbot-nginx 13 | ARG revision 14 | RUN git clone https://github.com/WalletConnect/py-walletconnect-bridge 15 | WORKDIR /py-walletconnect-bridge 16 | RUN git checkout ${branch} 17 | RUN pip3 install -r requirements.txt 18 | RUN python3 setup.py install 19 | COPY docker-entrypoint.sh /bin/ 20 | RUN chmod +x /bin/docker-entrypoint.sh 21 | ENTRYPOINT ["/bin/docker-entrypoint.sh"] 22 | EXPOSE 80 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The WalletConnect Association may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the WalletConnect Association. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the WalletConnect Association. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # make targets for WalletConnect/py-walletconnect-bridge 2 | 3 | BRANCH := $(shell git for-each-ref --format='%(objectname) %(refname:short)' refs/heads | awk "/^$$(git rev-parse HEAD)/ {print \$$2}") 4 | HASH := $(shell git rev-parse HEAD) 5 | URL=bridge.mydomain.com 6 | 7 | default: 8 | echo "Available tasks: setup, build, clean, renew, run, run_skip_certbot, run_daemon, run_daemon_skip_certbot, update" 9 | 10 | setup: 11 | sed -i -e 's/bridge.mydomain.com/$(URL)/g' nginx/defaultConf && rm -rf nginx/defaultConf-e 12 | 13 | build: 14 | docker build . -t py-walletconnect-bridge \ 15 | --build-arg branch=$(BRANCH) \ 16 | --build-arg revision=$(shell git ls-remote https://github.com/WalletConnect/py-walletconnect-bridge $(BRANCH) | head -n 1 | cut -f 1) 17 | 18 | clean: 19 | sudo rm -rfv ssl/certbot/* 20 | 21 | renew: 22 | make clean && make run 23 | 24 | run: 25 | docker run -it -v $(shell pwd)/:/source/ -p 443:443 -p 80:80 --name "py-walletconnect-bridge" py-walletconnect-bridge 26 | 27 | run_skip_certbot: 28 | docker run -it -v $(shell pwd)/:/source/ -p 443:443 -p 80:80 --name "py-walletconnect-bridge" py-walletconnect-bridge run --skip-certbot 29 | 30 | run_daemon: 31 | docker run -it -d -v $(shell pwd)/:/source/ -p 443:443 -p 80:80 --name "py-walletconnect-bridge" py-walletconnect-bridge 32 | 33 | run_daemon_skip_certbot: 34 | docker run -it -d -v $(shell pwd)/:/source/ -p 443:443 -p 80:80 --name "py-walletconnect-bridge" py-walletconnect-bridge run_daemon --skip-certbot 35 | 36 | update: 37 | # build a new image 38 | make build 39 | 40 | # save current state of DB and copy it to local machine 41 | docker exec py-walletconnect-bridge redis-cli SAVE 42 | docker cp py-walletconnect-bridge:/py-walletconnect-bridge/dump.rdb dump.rdb 43 | 44 | # stop existing container instance 45 | docker container rm -f py-walletconnect-bridge 46 | 47 | # start the container with `-d` to run in background 48 | make run_daemon 49 | 50 | # stop the redis server, copy the previous state and restart the server 51 | docker exec py-walletconnect-bridge redis-cli SHUTDOWN 52 | docker cp dump.rdb py-walletconnect-bridge:/py-walletconnect-bridge/dump.rdb 53 | docker exec py-walletconnect-bridge chown redis: /py-walletconnect-bridge/dump.rdb 54 | docker exec -d py-walletconnect-bridge redis-server 55 | rm dump.rdb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WalletConnect Bridge Python Implementation 2 | 3 | ![travis](https://travis-ci.org/WalletConnect/py-walletconnect-bridge.svg?branch=master) 4 | 5 | A full introduction is described in our docs: https://docs.walletconnect.org/technical-specification 6 | 7 | ## Pre-requirements 8 | 9 | 1. Python 10 | 2. Docker (for Docker setup) 11 | 3. Make (for Make commands) 12 | 13 | ## Docker setup 14 | 15 | **Step 0.** Point DNS record to your box (required for SSL) 16 | 17 | ```bash 18 | A 192.168.1.1 19 | ``` 20 | 21 | **Step 1.** Setup the bridge URL to match your DNS record 22 | 23 | ```bash 24 | $ make setup URL= 25 | 26 | # OR 27 | 28 | $ sed -i -e 's/bridge.mydomain.com//g' nginx/defaultConf && rm -rf nginx/defaultConf-e 29 | ``` 30 | 31 | **Step 2.** Run the following command to build the Docker image 32 | 33 | ```bash 34 | $ make build 35 | 36 | # OR 37 | 38 | $ docker build . -t py-walletconnect-bridge 39 | ``` 40 | 41 | **Step 3.** Finally run the following command to run the Docker container 42 | 43 | ```bash 44 | $ make run 45 | 46 | # OR 47 | 48 | $ docker run -it -v $(pwd)/:/source/ -p 443:443 -p 80:80 py-walletconnect-bridge 49 | ``` 50 | 51 | You can test it at https:///hello 52 | 53 | ### Choose Branch 54 | 55 | This setup defaults to the active branch in your current directory in order to build a Docker image from another branch, run the following command: 56 | 57 | ```bash 58 | $ make build BRANCH=v0.7.x 59 | 60 | # OR 61 | 62 | $ docker build . -t py-walletconnect-bridge --build-arg branch=v0.7.x 63 | ``` 64 | 65 | For this sample configuration file, the bridge will be available at https:/// . After specifying to 0.0.0.0 in /etc/hosts, 66 | 67 | ### Update Bridge 68 | 69 | To update the bridge, just run the following and it will maintain the existing state of the existing bridge sessions and quickly swap containers to the new version 70 | 71 | ```bash 72 | $ make update 73 | 74 | # Optional (choose branch) 75 | 76 | $ make update BRANCH=develop 77 | ``` 78 | 79 | ### Skip Cerbot 80 | 81 | This approach uses [Certbot](https://certbot.eff.org/) to generate real SSL certificates for your configured nginx hosts. If you would prefer to use the self signed certificates, you can pass the `--skip-certbot` flag to `docker run` as follows: 82 | 83 | ```bash 84 | $ make run_no_certbot 85 | 86 | # OR 87 | 88 | $ docker run -it -v $(pwd)/:/source/ -p 443:443 -p 80:80 py-walletconnect-bridge --skip-certbot 89 | ``` 90 | 91 | Certbot certificates expire after 90 days. To renew, shut down the docker process and run `make renew`. You should back up your old certs before doing this, as they will be deleted. 92 | 93 | ## Manual setup 94 | 95 | If you'd like to keep a separate Python environment for this project's installs, set up virtualenv 96 | 97 | ```bash 98 | $ pip install virtualenv virtualenvwrapper 99 | ``` 100 | 101 | Add the following to your ~/.bashrc 102 | 103 | ```bash 104 | export WORKON_HOME=$HOME/.virtualenvs~ 105 | export PROJECT_HOME=$HOME/Devel 106 | export VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3 107 | source /usr/local/bin/virtualenvwrapper.sh 108 | ``` 109 | 110 | From the project directory, run these commands to install the walletconnect-bridge package in a virtualenv called "walletconnect-bridge" 111 | 112 | ```bash 113 | $ mkvirtualenv walletconnect-bridge 114 | $ pip install -r requirements.txt 115 | $ python setup.py develop 116 | ``` 117 | 118 | In another terminal, start local Redis instance 119 | 120 | ```bash 121 | $ redis-server 122 | ``` 123 | 124 | Run the project locally 125 | 126 | ```bash 127 | $ walletconnect-bridge --redis-local 128 | ``` 129 | 130 | Test your Bridge is working 131 | 132 | ```bash 133 | $ curl https:///hello 134 | ``` 135 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | #linking 5 | rm -rf /etc/nginx/sites-enabled 6 | ln -s /source/nginx /etc/nginx/sites-enabled 7 | ln -s /source/ssl /keys 8 | 9 | #starting local instance of redis server and starting walletconnect bridge connected to local redis 10 | redis-server & 11 | echo "started redis server" 12 | sleep 5 13 | walletconnect-bridge --port 8080 --host 0.0.0.0 & 14 | echo "started walletconnect server" 15 | 16 | #key generation 17 | FILE="/keys/key.pem" 18 | if [ ! -f $FILE ]; then 19 | echo "generating self signed keys" 20 | #make the self signed key so the initial nginx load works 21 | openssl req -x509 \ 22 | -newkey rsa:4096 \ 23 | -keyout $FILE \ 24 | -out /keys/cert.pem \ 25 | -days 365 \ 26 | -nodes \ 27 | -subj "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN=bridge.mydomain.com" 28 | fi 29 | 30 | if [ `ls /source/ssl/certbot` ]; then 31 | #copy keys from local 32 | echo "copying previously generated keys" 33 | mkdir -p /etc/letsencrypt/live 34 | cp -rf /source/ssl/certbot/* /etc/letsencrypt/live/ 35 | else 36 | if [ "$1" != "--skip-certbot" ]; then 37 | echo "generating certbot keys" 38 | #create certificate with certbot 39 | certbot --nginx 40 | #copy keys to local for rehydrating 41 | cp -rfL /etc/letsencrypt/live/* /source/ssl/certbot/ 42 | else 43 | echo "skipping certbot" 44 | fi 45 | fi 46 | echo "generated keys" 47 | 48 | #finish up 49 | service nginx start 50 | echo "started nginx service" 51 | #now sleeping infinitely 52 | tail -f /dev/null 53 | -------------------------------------------------------------------------------- /nginx/defaultConf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name bridge.mydomain.com; 4 | 5 | return 301 https://$host$request_uri; 6 | } 7 | 8 | server { 9 | listen 443 ssl; 10 | server_name bridge.mydomain.com; 11 | 12 | ssl_certificate /keys/cert.pem; 13 | ssl_certificate_key /keys/key.pem; 14 | ssl_session_timeout 1d; 15 | ssl_session_cache shared:SSL:50m; 16 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 17 | ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; 18 | ssl_prefer_server_ciphers on; 19 | 20 | 21 | location / { 22 | proxy_set_header Host $host; 23 | proxy_set_header X-Real-IP $remote_addr; 24 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 25 | proxy_set_header X-Forwarded-Proto $scheme; 26 | proxy_pass http://0.0.0.0:8080; 27 | proxy_read_timeout 90; 28 | proxy_redirect http://0.0.0.0:8080 http://bridge.mydomain.com; 29 | 30 | # Simple requests 31 | if ($request_method ~* "(GET|POST)") { 32 | add_header "Access-Control-Allow-Origin" *; 33 | } 34 | 35 | # Preflighted requests 36 | if ($request_method = OPTIONS ) { 37 | add_header "Access-Control-Allow-Origin" *; 38 | add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD"; 39 | add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept"; 40 | return 200; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.4.4 2 | aioredis==1.2.0 3 | uvloop==0.11.3 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='walletconnect-bridge', 5 | version='0.7.11', 6 | install_requires=[ 7 | 'aiohttp', 8 | 'aioredis', 9 | 'uvloop', 10 | ], 11 | packages=find_packages(), 12 | entry_points={ 13 | 'console_scripts': ['walletconnect-bridge=walletconnect_bridge:main',] 14 | }, 15 | ) 16 | -------------------------------------------------------------------------------- /ssl/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/py-walletconnect-bridge/05e170c0fb01a423ee9011c3efe59f63855cd22b/ssl/.keep -------------------------------------------------------------------------------- /ssl/certbot/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/py-walletconnect-bridge/05e170c0fb01a423ee9011c3efe59f63855cd22b/ssl/certbot/.keep -------------------------------------------------------------------------------- /systemd/walletconnect-bridge.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WalletConnect Bridge daemon 3 | 4 | [Service] 5 | Type=simple 6 | User=wallet-connect 7 | Group=wallet-connect 8 | ExecStart=/usr/local/bin/walletconnect-bridge 9 | ExecStop=/bin/kill `/bin/ps aux | /bin/grep 'walletconnect-bridge$' | /bin/grep -v grep | /usr/bin/awk '{ print $2 }'` 10 | Restart=always 11 | RestartSec=1 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /walletconnect_bridge/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | import uuid 4 | import asyncio 5 | import aiohttp 6 | import pkg_resources 7 | from aiohttp import web 8 | try: 9 | import uvloop 10 | except ModuleNotFoundError: 11 | pass 12 | 13 | import walletconnect_bridge.keystore 14 | from walletconnect_bridge.time import now 15 | from walletconnect_bridge.errors import KeystoreWriteError, KeystoreFetchError, WalletConnectPushError, KeystoreTokenExpiredError, KeystorePushTokenError 16 | 17 | routes = web.RouteTableDef() 18 | 19 | WC_VERSION = pkg_resources.require("walletconnect-bridge")[0].version 20 | REDIS='org.wallet.connect.redis' 21 | SESSION='org.wallet.connect.session' 22 | SENTINEL='sentinel' 23 | SENTINELS='sentinels' 24 | HOST='host' 25 | SERVICE='service' 26 | SESSION_EXPIRATION = 24*60*60 # 24hrs 27 | CALL_DATA_EXPIRATION = 60*60 # 1hr 28 | 29 | def error_message(message): 30 | return {'message': message} 31 | 32 | 33 | def get_redis_master(app): 34 | if app[REDIS][SENTINEL]: 35 | sentinel = app[REDIS][SERVICE] 36 | return sentinel.master_for('mymaster') 37 | return app[REDIS][SERVICE] 38 | 39 | @routes.get('/hello') 40 | async def hello(request): 41 | message = 'Hello World, this is WalletConnect v{}'.format(WC_VERSION) 42 | return web.Response(text=message) 43 | 44 | @routes.get('/info') 45 | async def get_info(request): 46 | bridge_data = {'name': 'WalletConnect Bridge Server', 'repository': 'py-walletconnect-bridge', 'version': WC_VERSION} 47 | return web.json_response(bridge_data) 48 | 49 | @routes.post('/session/new') 50 | async def new_session(request): 51 | try: 52 | session_id = str(uuid.uuid4()) 53 | redis_conn = get_redis_master(request.app) 54 | await keystore.add_request_for_session_data(redis_conn, session_id, expiration_in_seconds=SESSION_EXPIRATION) 55 | session_data = {'sessionId': session_id} 56 | return web.json_response(session_data) 57 | except KeyError: 58 | return web.json_response(error_message('Incorrect input parameters'), status=400) 59 | except TypeError: 60 | return web.json_response(error_message('Incorrect JSON content type'), status=400) 61 | except KeystoreWriteError: 62 | return web.json_response(error_message('Error writing to db'), status=500) 63 | except: 64 | return web.json_response(error_message('Error unknown'), status=500) 65 | 66 | 67 | @routes.put('/session/{sessionId}') 68 | async def update_session(request): 69 | request_json = await request.json() 70 | try: 71 | session_id = request.match_info['sessionId'] 72 | push_data = request_json.get('push', None) 73 | session_data = {'encryptionPayload': request_json['encryptionPayload']} 74 | redis_conn = get_redis_master(request.app) 75 | if push_data: 76 | await keystore.add_push_data(redis_conn, session_id, push_data, expiration_in_seconds=SESSION_EXPIRATION) 77 | expires = await keystore.update_session_data(redis_conn, session_id, session_data, expiration_in_seconds=SESSION_EXPIRATION) 78 | session_data = {'expires': expires} 79 | return web.json_response(session_data) 80 | except KeyError: 81 | return web.json_response(error_message('Incorrect input parameters'), status=400) 82 | except TypeError: 83 | return web.json_response(error_message('Incorrect JSON content type'), status=400) 84 | except KeystoreTokenExpiredError: 85 | return web.json_response(error_message('Connection sharing token has expired'), status=500) 86 | except: 87 | return web.json_response(error_message('Error unknown'), status=500) 88 | 89 | 90 | @routes.get('/session/{sessionId}') 91 | async def get_session(request): 92 | try: 93 | session_id = request.match_info['sessionId'] 94 | redis_conn = get_redis_master(request.app) 95 | session_data = await keystore.get_session_data(redis_conn, session_id) 96 | if session_data: 97 | session_data = {'data': session_data} 98 | return web.json_response(session_data) 99 | else: 100 | return web.Response(status=204) 101 | except KeyError: 102 | return web.json_response(error_message('Incorrect input parameters'), status=400) 103 | except TypeError: 104 | return web.json_response(error_message('Incorrect JSON content type'), status=400) 105 | except: 106 | return web.json_response(error_message('Error unknown'), status=500) 107 | 108 | 109 | @routes.delete('/session/{sessionId}') 110 | async def remove_session(request): 111 | try: 112 | session_id = request.match_info['sessionId'] 113 | redis_conn = get_redis_master(request.app) 114 | await keystore.remove_push_data(redis_conn, session_id) 115 | await keystore.remove_session_data(redis_conn, session_id) 116 | return web.Response(status=200) 117 | except: 118 | return web.json_response(error_message('Error unknown'), status=500) 119 | 120 | 121 | @routes.post('/session/{sessionId}/call/new') 122 | async def new_call(request): 123 | try: 124 | request_json = await request.json() 125 | session_id = request.match_info['sessionId'] 126 | call_id = str(uuid.uuid4()) 127 | call_data = {'encryptionPayload': request_json['encryptionPayload']} 128 | dapp_name = request_json['dappName'] 129 | redis_conn = get_redis_master(request.app) 130 | await keystore.add_call_data(redis_conn, session_id, call_id, call_data, expiration_in_seconds=CALL_DATA_EXPIRATION) 131 | push_data = await keystore.get_push_data(redis_conn, session_id) 132 | if push_data: 133 | session = request.app[SESSION] 134 | await send_push_request(session, push_data, session_id, call_id, dapp_name) 135 | data_message = {'callId': call_id} 136 | return web.json_response(data_message, status=201) 137 | except KeystorePushTokenError: 138 | return web.json_response(error_message('Push token for this session is no longer available'), status=500) 139 | except KeyError: 140 | return web.json_response(error_message('Incorrect input parameters'), status=400) 141 | except TypeError: 142 | return web.json_response(error_message('Incorrect JSON content type'), status=400) 143 | except KeystorePushTokenError: 144 | return web.json_response(error_message('Error finding Push token for session'), status=500) 145 | except WalletConnectPushError: 146 | return web.json_response(error_message('Error sending message to walletconnect push webhook'), status=500) 147 | except: 148 | return web.json_response(error_message('Error unknown'), status=500) 149 | 150 | 151 | @routes.get('/session/{sessionId}/call/{callId}') 152 | async def get_call(request): 153 | try: 154 | session_id = request.match_info['sessionId'] 155 | call_id = request.match_info['callId'] 156 | redis_conn = get_redis_master(request.app) 157 | call_data = await keystore.get_call_data(redis_conn, session_id, call_id) 158 | json_response = {'data': call_data} 159 | return web.json_response(json_response) 160 | except KeyError: 161 | return web.json_response(error_message('Incorrect input parameters'), status=400) 162 | except TypeError: 163 | return web.json_response(error_message('Incorrect JSON content type'), status=400) 164 | except KeystoreFetchError: 165 | return web.json_response(error_message('Error retrieving call data'), status=500) 166 | except: 167 | return web.json_response(error_message('Error unknown'), status=500) 168 | 169 | 170 | @routes.get('/session/{sessionId}/calls') 171 | async def get_all_calls(request): 172 | try: 173 | session_id = request.match_info['sessionId'] 174 | redis_conn = get_redis_master(request.app) 175 | all_calls = await keystore.get_all_calls(redis_conn, session_id) 176 | json_response = {'data': all_calls} 177 | return web.json_response(json_response) 178 | except KeyError: 179 | return web.json_response(error_message('Incorrect input parameters'), status=400) 180 | except TypeError: 181 | return web.json_response(error_message('Incorrect JSON content type'), status=400) 182 | except KeystoreFetchError: 183 | return web.json_response(error_message('Error retrieving call data'), status=500) 184 | except: 185 | return web.json_response(error_message('Error unknown'), status=500) 186 | 187 | 188 | @routes.post('/call-status/{callId}/new') 189 | async def new_call_status(request): 190 | try: 191 | request_json = await request.json() 192 | call_id = request.match_info['callId'] 193 | call_status_data = {'encryptionPayload': request_json['encryptionPayload']} 194 | redis_conn = get_redis_master(request.app) 195 | await keystore.update_call_status(redis_conn, call_id, call_status_data) 196 | return web.Response(status=201) 197 | except KeyError: 198 | return web.json_response(error_message('Incorrect input parameters'), status=400) 199 | except TypeError: 200 | return web.json_response(error_message('Incorrect JSON content type'), status=400) 201 | except: 202 | return web.json_response(error_message('Error unknown'), status=500) 203 | 204 | 205 | @routes.get('/call-status/{callId}') 206 | async def get_call_status(request): 207 | try: 208 | call_id = request.match_info['callId'] 209 | redis_conn = get_redis_master(request.app) 210 | call_status = await keystore.get_call_status(redis_conn, call_id) 211 | if call_status: 212 | json_response = {'data': call_status} 213 | return web.json_response(json_response) 214 | else: 215 | return web.Response(status=204) 216 | except KeyError: 217 | return web.json_response(error_message('Incorrect input parameters'), status=400) 218 | except: 219 | return web.json_response(error_message('Error unknown'), status=500) 220 | 221 | 222 | async def send_push_request(session, push_data, session_id, call_id, dapp_name): 223 | push_type = push_data['type'] 224 | push_token = push_data['token'] 225 | push_webhook = push_data['webhook'] 226 | payload = { 227 | 'sessionId': session_id, 228 | 'callId': call_id, 229 | 'pushType': push_type, 230 | 'pushToken': push_token, 231 | 'dappName': dapp_name 232 | } 233 | headers = {'Content-Type': 'application/json'} 234 | response = await session.post(push_webhook, json=payload, headers=headers) 235 | if response.status != 200: 236 | raise WalletConnectPushError 237 | 238 | 239 | async def initialize_client_session(app): 240 | app[SESSION] = aiohttp.ClientSession(loop=app.loop) 241 | 242 | 243 | async def initialize_keystore(app): 244 | if app[REDIS][SENTINEL]: 245 | sentinels = app[REDIS][SENTINELS].split(',') 246 | app[REDIS][SERVICE] = await keystore.create_sentinel_connection(event_loop=app.loop, 247 | sentinels=sentinels) 248 | else: 249 | app[REDIS][SERVICE] = await keystore.create_connection(event_loop=app.loop, 250 | host=app[REDIS][HOST]) 251 | 252 | 253 | async def close_keystore(app): 254 | app[REDIS][SERVICE].close() 255 | await app[REDIS][SERVICE].wait_closed() 256 | 257 | 258 | async def close_client_session_connection(app): 259 | await app[SESSION].close() 260 | 261 | 262 | def main(): 263 | parser = argparse.ArgumentParser() 264 | parser.add_argument('--redis-use-sentinel', action='store_true') 265 | parser.add_argument('--sentinels', type=str) 266 | parser.add_argument('--redis-host', type=str, default='localhost') 267 | parser.add_argument('--no-uvloop', action='store_true') 268 | parser.add_argument('--host', type=str, default='localhost') 269 | parser.add_argument('--port', type=int, default=8080) 270 | args = parser.parse_args() 271 | 272 | app = web.Application() 273 | app[REDIS] = { 274 | SENTINEL: args.redis_use_sentinel, 275 | SENTINELS: args.sentinels, 276 | HOST: args.redis_host 277 | } 278 | app.on_startup.append(initialize_client_session) 279 | app.on_startup.append(initialize_keystore) 280 | app.on_cleanup.append(close_keystore) 281 | app.on_cleanup.append(close_client_session_connection) 282 | app.router.add_routes(routes) 283 | if not args.no_uvloop: 284 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 285 | web.run_app(app, host=args.host, port=args.port) 286 | 287 | 288 | if __name__ == '__main__': 289 | main() 290 | -------------------------------------------------------------------------------- /walletconnect_bridge/errors.py: -------------------------------------------------------------------------------- 1 | class WalletConnectPushError(Exception): 2 | pass 3 | 4 | 5 | class KeystoreWriteError(Exception): 6 | pass 7 | 8 | 9 | class KeystoreTokenExpiredError(Exception): 10 | pass 11 | 12 | 13 | class KeystorePushTokenError(Exception): 14 | pass 15 | 16 | 17 | class KeystoreFetchError(Exception): 18 | pass 19 | -------------------------------------------------------------------------------- /walletconnect_bridge/keystore.py: -------------------------------------------------------------------------------- 1 | import aioredis 2 | import json 3 | 4 | from walletconnect_bridge.time import get_expiration_time 5 | from walletconnect_bridge.errors import KeystoreWriteError, KeystoreFetchError, KeystoreTokenExpiredError, KeystorePushTokenError 6 | 7 | async def create_connection(event_loop, host='localhost', port=6379, db=0): 8 | redis_uri = 'redis://{}:{}/{}'.format(host, port, db) 9 | return await aioredis.create_redis(address=redis_uri, db=db, 10 | encoding='utf-8', loop=event_loop) 11 | 12 | 13 | async def create_sentinel_connection(event_loop, sentinels): 14 | default_port = 26379 15 | sentinel_ports = [(x, default_port) for x in sentinels] 16 | sentinel = await aioredis.create_sentinel(sentinel_ports, 17 | encoding='utf-8', 18 | loop=event_loop) 19 | return sentinel 20 | 21 | 22 | async def add_request_for_session_data(conn, session_id, expiration_in_seconds): 23 | key = session_key(session_id) 24 | success = await write(conn, key, '', expiration_in_seconds) 25 | if not success: 26 | raise KeystoreWriteError('Error adding request for data') 27 | 28 | 29 | 30 | async def update_session_data(conn, session_id, session_data, expiration_in_seconds): 31 | key = session_key(session_id) 32 | data = json.dumps(session_data) 33 | success = await write(conn, key, data, expiration_in_seconds, write_only_if_exists=True) 34 | expires = get_expiration_time(ttl_in_seconds=expiration_in_seconds) 35 | if not success: 36 | raise KeystoreTokenExpiredError 37 | return expires 38 | 39 | 40 | 41 | async def get_session_data(conn, session_id): 42 | key = session_key(session_id) 43 | data = await conn.get(key) 44 | if data: 45 | ttl_in_seconds = await conn.ttl(key) 46 | expires = get_expiration_time(ttl_in_seconds) 47 | session_data = json.loads(data) 48 | session_data['expires'] = expires 49 | return session_data 50 | else: 51 | return None 52 | 53 | 54 | async def remove_session_data(conn, session_id): 55 | key = session_key(session_id) 56 | await conn.delete(key) 57 | 58 | 59 | async def add_push_data(conn, session_id, push_data, expiration_in_seconds): 60 | key = push_session_key(session_id) 61 | data = json.dumps(push_data) 62 | success = await write(conn, key, data, expiration_in_seconds) 63 | expires = get_expiration_time(ttl_in_seconds=expiration_in_seconds) 64 | if not success: 65 | raise KeystoreWriteError('Could not write session Push data') 66 | return expires 67 | 68 | 69 | async def get_push_data(conn, session_id): 70 | session_key = push_session_key(session_id) 71 | data = await conn.get(session_key) 72 | if not data: 73 | return None 74 | return json.loads(data) 75 | 76 | 77 | async def remove_push_data(conn, session_id): 78 | session_key = push_session_key(session_id) 79 | await conn.delete(session_key) 80 | 81 | 82 | async def add_call_data(conn, session_id, call_id, call_data, expiration_in_seconds): 83 | key = call_key(session_id, call_id) 84 | data = json.dumps(call_data) 85 | success = await write(conn, key, data, expiration_in_seconds) 86 | if not success: 87 | raise KeystoreWriteError('Error adding call data') 88 | 89 | 90 | async def get_call_data(conn, session_id, call_id): 91 | key = call_key(session_id, call_id) 92 | data = await conn.get(key) 93 | if not data: 94 | raise KeystoreFetchError('Error getting call data') 95 | else: 96 | await conn.delete(key) 97 | return json.loads(data) 98 | 99 | 100 | async def get_all_calls(conn, session_id): 101 | key = call_key(session_id, '*') 102 | all_keys = [] 103 | cur = b'0' # set initial cursor to 0 104 | while cur: 105 | cur, keys = await conn.scan(cur, match=key) 106 | all_keys.extend(keys); 107 | if not all_keys: 108 | return {} 109 | data = await conn.mget(*all_keys) 110 | call_ids = map(lambda x: x.split(':')[2], all_keys) 111 | zipped_results = dict(zip(call_ids, data)) 112 | filtered_results = {k: json.loads(v) for k, v in zipped_results.items() if v} 113 | await conn.delete(*all_keys) 114 | return filtered_results 115 | 116 | 117 | async def update_call_status(conn, call_id, call_status): 118 | key = call_status_key(call_id) 119 | data = json.dumps(call_status) 120 | success = await write(conn, key, data) 121 | if not success: 122 | raise KeystoreWriteError('Error adding call status') 123 | 124 | 125 | async def get_call_status(conn, call_id): 126 | key = call_status_key(call_id) 127 | data = await conn.get(key) 128 | if data: 129 | await conn.delete(key) 130 | return json.loads(data) 131 | else: 132 | return None 133 | 134 | 135 | def session_key(session_id): 136 | return 'session:{}'.format(session_id) 137 | 138 | 139 | def push_session_key(session_id): 140 | return 'pushsession:{}'.format(session_id) 141 | 142 | 143 | def call_key(session_id, call_id): 144 | return 'call:{}:{}'.format(session_id, call_id) 145 | 146 | 147 | def call_status_key(call_id): 148 | return 'callstatus:{}'.format(call_id) 149 | 150 | 151 | async def write(conn, key, value='', expiration_in_seconds=60*60, write_only_if_exists=False): 152 | exist = 'SET_IF_EXIST' if write_only_if_exists else None 153 | success = await conn.set(key, value, expire=expiration_in_seconds, exist=exist) 154 | return success 155 | -------------------------------------------------------------------------------- /walletconnect_bridge/time.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def now(): 4 | now = time.time() 5 | return now 6 | 7 | def get_expiration_time(ttl_in_seconds): 8 | expires = int(now() + ttl_in_seconds) * 1000 9 | return expires 10 | --------------------------------------------------------------------------------