import sys import argparse import uuid import asyncio import aiohttp import pkg_resources from aiohttp import web try: import uvloop except ModuleNotFoundError: pass import walletconnect_bridge.keystore from walletconnect_bridge.time import now from walletconnect_bridge.errors import KeystoreWriteError, KeystoreFetchError, WalletConnectPushError, KeystoreTokenExpiredError, KeystorePushTokenError routes = web.RouteTableDef() WC_VERSION = pkg_resources.require("walletconnect-bridge")[0].version REDIS='org.wallet.connect.redis' SESSION='org.wallet.connect.session' SENTINEL='sentinel' SENTINELS='sentinels' HOST='host' SERVICE='service' SESSION_EXPIRATION = 24*60*60 # 24hrs CALL_DATA_EXPIRATION = 60*60 # 1hr def error_message(message): return {'message': message} def get_redis_master(app): if app[REDIS][SENTINEL]: sentinel = app[REDIS][SERVICE] return sentinel.master_for('mymaster') return app[REDIS][SERVICE] @routes.get('/hello') async def hello(request): message = 'Hello World, this is WalletConnect v{}'.format(WC_VERSION) return web.Response(text=message) @routes.get('/info') async def get_info(request): bridge_data = {'name': 'WalletConnect Bridge Server', 'repository': 'py-walletconnect-bridge', 'version': WC_VERSION} return web.json_response(bridge_data) @routes.post('/session/new') async def new_session(request): try: session_id = str(uuid.uuid4()) redis_conn = get_redis_master(request.app) await keystore.add_request_for_session_data(redis_conn, session_id, expiration_in_seconds=SESSION_EXPIRATION) session_data = {'sessionId': session_id} return web.json_response(session_data) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except TypeError: return web.json_response(error_message('Incorrect JSON content type'), status=400) except KeystoreWriteError: return web.json_response(error_message('Error writing to db'), status=500) except: return web.json_response(error_message('Error unknown'), status=500) @routes.put('/session/{sessionId}') async def update_session(request): request_json = await request.json() try: session_id = request.match_info['sessionId'] push_data = request_json.get('push', None) session_data = {'encryptionPayload': request_json['encryptionPayload']} redis_conn = get_redis_master(request.app) if push_data: await keystore.add_push_data(redis_conn, session_id, push_data, expiration_in_seconds=SESSION_EXPIRATION) expires = await keystore.update_session_data(redis_conn, session_id, session_data, expiration_in_seconds=SESSION_EXPIRATION) session_data = {'expires': expires} return web.json_response(session_data) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except TypeError: return web.json_response(error_message('Incorrect JSON content type'), status=400) except KeystoreTokenExpiredError: return web.json_response(error_message('Connection sharing token has expired'), status=500) except: return web.json_response(error_message('Error unknown'), status=500) @routes.get('/session/{sessionId}') async def get_session(request): try: session_id = request.match_info['sessionId'] redis_conn = get_redis_master(request.app) session_data = await keystore.get_session_data(redis_conn, session_id) if session_data: session_data = {'data': session_data} return web.json_response(session_data) else: return web.Response(status=204) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except TypeError: return web.json_response(error_message('Incorrect JSON content type'), status=400) except: return web.json_response(error_message('Error unknown'), status=500) @routes.delete('/session/{sessionId}') async def remove_session(request): try: session_id = request.match_info['sessionId'] redis_conn = get_redis_master(request.app) await keystore.remove_push_data(redis_conn, session_id) await keystore.remove_session_data(redis_conn, session_id) return web.Response(status=200) except: return web.json_response(error_message('Error unknown'), status=500) @routes.post('/session/{sessionId}/call/new') async def new_call(request): try: request_json = await request.json() session_id = request.match_info['sessionId'] call_id = str(uuid.uuid4()) call_data = {'encryptionPayload': request_json['encryptionPayload']} dapp_name = request_json['dappName'] redis_conn = get_redis_master(request.app) await keystore.add_call_data(redis_conn, session_id, call_id, call_data, expiration_in_seconds=CALL_DATA_EXPIRATION) push_data = await keystore.get_push_data(redis_conn, session_id) if push_data: session = request.app[SESSION] await send_push_request(session, push_data, session_id, call_id, dapp_name) data_message = {'callId': call_id} return web.json_response(data_message, status=201) except KeystorePushTokenError: return web.json_response(error_message('Push token for this session is no longer available'), status=500) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except TypeError: return web.json_response(error_message('Incorrect JSON content type'), status=400) except KeystorePushTokenError: return web.json_response(error_message('Error finding Push token for session'), status=500) except WalletConnectPushError: return web.json_response(error_message('Error sending message to walletconnect push webhook'), status=500) except: return web.json_response(error_message('Error unknown'), status=500) @routes.get('/session/{sessionId}/call/{callId}') async def get_call(request): try: session_id = request.match_info['sessionId'] call_id = request.match_info['callId'] redis_conn = get_redis_master(request.app) call_data = await keystore.get_call_data(redis_conn, session_id, call_id) json_response = {'data': call_data} return web.json_response(json_response) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except TypeError: return web.json_response(error_message('Incorrect JSON content type'), status=400) except KeystoreFetchError: return web.json_response(error_message('Error retrieving call data'), status=500) except: return web.json_response(error_message('Error unknown'), status=500) @routes.get('/session/{sessionId}/calls') async def get_all_calls(request): try: session_id = request.match_info['sessionId'] redis_conn = get_redis_master(request.app) all_calls = await keystore.get_all_calls(redis_conn, session_id) json_response = {'data': all_calls} return web.json_response(json_response) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except TypeError: return web.json_response(error_message('Incorrect JSON content type'), status=400) except KeystoreFetchError: return web.json_response(error_message('Error retrieving call data'), status=500) except: return web.json_response(error_message('Error unknown'), status=500) @routes.post('/call-status/{callId}/new') async def new_call_status(request): try: request_json = await request.json() call_id = request.match_info['callId'] call_status_data = {'encryptionPayload': request_json['encryptionPayload']} redis_conn = get_redis_master(request.app) await keystore.update_call_status(redis_conn, call_id, call_status_data) return web.Response(status=201) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except TypeError: return web.json_response(error_message('Incorrect JSON content type'), status=400) except: return web.json_response(error_message('Error unknown'), status=500) @routes.get('/call-status/{callId}') async def get_call_status(request): try: call_id = request.match_info['callId'] redis_conn = get_redis_master(request.app) call_status = await keystore.get_call_status(redis_conn, call_id) if call_status: json_response = {'data': call_status} return web.json_response(json_response) else: return web.Response(status=204) except KeyError: return web.json_response(error_message('Incorrect input parameters'), status=400) except: return web.json_response(error_message('Error unknown'), status=500) async def send_push_request(session, push_data, session_id, call_id, dapp_name): push_type = push_data['type'] push_token = push_data['token'] push_webhook = push_data['webhook'] payload = { 'sessionId': session_id, 'callId': call_id, 'pushType': push_type, 'pushToken': push_token, 'dappName': dapp_name } headers = {'Content-Type': 'application/json'} response = await session.post(push_webhook, json=payload, headers=headers) if response.status != 200: raise WalletConnectPushError async def initialize_client_session(app): app[SESSION] = aiohttp.ClientSession(loop=app.loop) async def initialize_keystore(app): if app[REDIS][SENTINEL]: sentinels = app[REDIS][SENTINELS].split(',') app[REDIS][SERVICE] = await keystore.create_sentinel_connection(event_loop=app.loop, sentinels=sentinels) else: app[REDIS][SERVICE] = await keystore.create_connection(event_loop=app.loop, host=app[REDIS][HOST]) async def close_keystore(app): app[REDIS][SERVICE].close() await app[REDIS][SERVICE].wait_closed() async def close_client_session_connection(app): await app[SESSION].close() def main(): parser = argparse.ArgumentParser() parser.add_argument('--redis-use-sentinel', action='store_true') parser.add_argument('--sentinels', type=str) parser.add_argument('--redis-host', type=str, default='localhost') parser.add_argument('--no-uvloop', action='store_true') parser.add_argument('--host', type=str, default='localhost') parser.add_argument('--port', type=int, default=8080) args = parser.parse_args() app = web.Application() app[REDIS] = { SENTINEL: args.redis_use_sentinel, SENTINELS: args.sentinels, HOST: args.redis_host } app.on_startup.append(initialize_client_session) app.on_startup.append(initialize_keystore) app.on_cleanup.append(close_keystore) app.on_cleanup.append(close_client_session_connection) app.router.add_routes(routes) if not args.no_uvloop: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) web.run_app(app, host=args.host, port=args.port) if __name__ == '__main__': main()