diff --git a/.gitignore b/.gitignore index 67045665..666b01d3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* +/xdcchatpay + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..8001d1a5 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn app:app \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..ae5ddc79 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +## Inspiration +Although crytpto and blockchain has been on an increase in developing nations, there has been slow uptake due to low literacy levels, trust, device constraints and the lack of cash on/off ramps. + +We are looking at building and launching XDC ChatPay as a simple yet effective remittance solution that addresses the challenges above. Being simple yet effective, our solution will drive users to use the remittance platform which has been built on the XDC network in order to increase adoption. + +## What it does + +XDC ChatPay allows users to easily on/off ramp by voucher top-ups or withdrawals directly to and from accounts using the [1Voucher](https://1foryou.com/) service, users are also able to send and receive funds in seconds. We are looking into adding additional functionalities like purchasing of utility vouchers, airtime, and allowing for scanning of QR codes for easy payments + +## How we built it + +The MVP was built using: + +**Twilio for the Whatsapp messaging functionality** +**Python Flask app for the backend** +**Tatum.io for the XDC chain account integration** +**Heroku for hosting** + +## Challenges we ran into + +When ran into challenges when conneting the XinFin api endpoints on Tatum for account creation, however I was able to overcome this and get it to work. Also, doing the user research proved to be challenging because of time constraints. The business approval time on the 1Voucher platform is lengthy so I was not able to include the live voucher authentication to the demo + +## Accomplishments that we're proud of + +We managed to build a working MVP using the mainnet endpoints, + +## What we learned + +We learned a lot about the XinFin chain and also about designing simple yet effective systems. + +## What's next for XDC ChatPay + +Implement live voucher integration +Improve on application and user security +Add additional functionality to application + +## TO run the application locally + +- Create an account on Twilio +- Install ngrok +- Create a Python virtual environment +- PIP install the requirements.txt file +- Run the flask application +- Connect the ngrok endpoint to your Twilio account and start sending XDC ChatPay messages. \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 00000000..cbaa8ca7 --- /dev/null +++ b/app.py @@ -0,0 +1,125 @@ +import click +from flask import Flask, render_template, request, redirect, url_for, flash, jsonify +from twilio.twiml.messaging_response import MessagingResponse +from messages import Messages + +from registered_users import RegisteredUsers +from user import User +from wallet_handler import WalletHandler + +app = Flask(__name__) + +registered_users = RegisteredUsers() + +wallet_handler = WalletHandler() + +message_template = Messages() + +@app.route('/') +def index(): + return 'XDC ChatPay' + +@app.route('/chat', methods=['POST']) +def chat(): + + resp = MessagingResponse() + + print(request.form) + incoming_msg = request.values.get('Body', '').lower() + + responded = False + + if incoming_msg == 'hello xdc': + resp.message(message_template.home_menu()) + responded = True + + elif incoming_msg == '1': + resp.message(message_template.register_menu()) + responded = True + + elif incoming_msg == '2': + incoming_msg = request.values.get('WaId', '').lower() + user = registered_users.get_user(incoming_msg) + if user is None: + resp.message(message_template.wallet_not_found()) + responded = True + else: + wallet = user.get_wallet() + resp.message(message_template.view_wallet(wallet_address=wallet)) + responded = True + + elif incoming_msg == '3': + incoming_msg = request.values.get('WaId', '').lower() + user = registered_users.get_user(incoming_msg) + if user is None: + resp.message(message_template.wallet_not_found()) + responded = True + else: + wallet = user.get_wallet() + balance = wallet_handler.get_balance(wallet) + resp.message(message_template.wallet_balance(balance=balance)) + responded = True + + + elif incoming_msg == '4': + resp.message(message_template.new_payment()) + responded = True + + elif incoming_msg == '5': + resp.message(message_template.top_up_menu()) + responded = True + + elif incoming_msg == '6': + incoming_msg = request.values.get('WaId', '').lower() + user = registered_users.get_user(incoming_msg) + if user is None: + resp.message(message_template.wallet_not_found()) + responded = True + else: + wallet = user.get_wallet() + transactions = wallet_handler.get_transactions(wallet) + resp.message(message_template.transaction_history(transactions=transactions)) + responded = True + + + elif incoming_msg == '7': + resp.message(message_template.withdraw_menu()) + responded = True + + elif incoming_msg == '8': + resp.message(message_template.exit_menu()) + responded = True + + elif len(incoming_msg.split(" ")) == 2 and incoming_msg.split()[0] == 'register': + name = incoming_msg.split()[1] + account_number, accountId = wallet_handler.create_wallet(name) + key = request.values.get('WaId', '').lower() + user = registered_users.register(User(name,account_number,accountId, key)) + resp.message(message_template.wallet_created(accountId)) + responded = True + + elif len(incoming_msg.split(" ")) == 2 and incoming_msg.split()[0] == 'topup': + voucher = incoming_msg.split()[1] + user_number = request.values.get('WaId', '').lower() + user = registered_users.get_user(user_number) + topup_response = wallet_handler.buy_xdc(user.get_accountId()) + resp.message(message_template.top_up_success()) + responded = True + + elif len(incoming_msg.split(" ")) == 3 and incoming_msg.split()[0] == 'pay': + wallet_address = incoming_msg.split()[1] + amount = incoming_msg.split()[2] + user_number = request.values.get('WaId', '').lower() + user = registered_users.get_user(user_number) + payment_response = wallet_handler.send_xdc(wallet_address, amount) + resp.message(message_template.payment_sent(amount, wallet_address)) + responded = True + + if not responded: + resp.message("Sorry, I don't understand.") + return str(resp) + +if __name__ == '__main__': + app.run(debug=True) + # app.run(host='localhost', port=8080, debug=True) + diff --git a/messages.py b/messages.py new file mode 100644 index 00000000..8485cfc8 --- /dev/null +++ b/messages.py @@ -0,0 +1,123 @@ +import random + + +class Messages: + def __init__(self): + pass + + def home_menu(self): + return """ + 1. Register a wallet +2. View wallet +3. View balance +4. Send XDC +5. Buy XDC +6. Transactions +7. Withdraw XDC with 1Voucher +7. Exit +""" + + def register_menu(self): + return """ + Please enter your username in one word: +example: register JohnSmith123 + """ + + def wallet_created(self, wallet_address): + return """ + Thank you for registering your wallet. +Your wallet address is: {0} +""".format(wallet_address) + + def wallet_not_created(self): + return """ + Sorry, your wallet could not be created. +Please try again. +""" + + def wallet_not_found(self): + return """ + Sorry, your wallet could not be found. +Please try again. +""" + + def view_wallet(self, wallet_address): + return """ + Your wallet address is: {0} + """.format(wallet_address) + + def wallet_balance(self, balance): + return """ + Your wallet balance is: {0} + """.format(balance) + + def new_payment(self): + return """ + Enter; pay + wallet/number + an amount. +example: pay 123456789 100 +""" + + def payment_sent(self, amount, address): + return """ + You have sent {0} XDC to {1} + """.format(amount, address) + + def payment_not_sent(self): + return """ + Sorry, your payment could not be sent. +Please try again. +""" + + def payment_received(self, amount, address): + return """ + You have received {0} XDC from {1} + """.format(amount, address) + + def transaction_history(self, transactions): + return """ + Your transaction history is: + {0} + """.format(transactions) + + def top_up_menu(self): + return """ + Please enter topup + your 1Voucher reference number: +example: topup 123456789 +""" + + def top_up_success(self): + return """ + You have successfully toped up your wallet with 50 XDC. + """ + + def top_up_failure(self): + return """ + Sorry, your top up could not be completed. +Please try again. +""" + + def withdraw_menu(self): + return """ + Please withdraw + the amount you wish to withdraw: +example: withdraw 100 +""" + + def withdraw_success(self, amount): + return """ + You have successfully withdrawn {0} XDC. +Your 1Voucher reference is: {1} +""".format(amount, self.generate_reference()) + + def withdraw_failure(self): + return """ + Sorry, your withdrawal could not be completed. +Please try again. +""" + + def exit_menu(self): + return """ + Thank you for using XDC ChatPay. + """ + + def generate_reference(self): + return str(random.randint(1000000, 9999999)) \ No newline at end of file diff --git a/registered_users.py b/registered_users.py new file mode 100644 index 00000000..69f02441 --- /dev/null +++ b/registered_users.py @@ -0,0 +1,27 @@ +class RegisteredUsers(): + + def __init__(self): + self.registered_users = {} + + def register(self, user): + if user.number in self.registered_users: + return False + else: + self.registered_users[user.number] = user + return True + + def get_user(self, number): + if number in self.registered_users: + return self.registered_users[number] + else: + return None + + def get_all_users(self): + return self.registered_users + + def delete_user(self, number): + if number in self.registered_users: + del self.registered_users[number] + return True + else: + return False \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..99e4765a Binary files /dev/null and b/requirements.txt differ diff --git a/user.py b/user.py new file mode 100644 index 00000000..94804efd --- /dev/null +++ b/user.py @@ -0,0 +1,20 @@ +class User: + + def __init__(self, username, account_number, wallet, number): + self.username = username + self.account_number = account_number + self.accountId = wallet + self.number = number + self.verification_code = None + + def set_wallet(self, wallet): + self.wallet = wallet + + def get_accountId(self): + return self.accountId + + def get_account_number(self): + return self.wallet + + def set_verification_code(self, verification_code): + self.verification_code = verification_code \ No newline at end of file diff --git a/wallet_handler.py b/wallet_handler.py new file mode 100644 index 00000000..55277631 --- /dev/null +++ b/wallet_handler.py @@ -0,0 +1,145 @@ +from random import randint, random +import time +import requests + +class WalletHandler: + + def __init__(self, wallet_api_key=""): + self.wallet_url = 'https://api-eu1.tatum.io' + self.wallet_api_key = 'd46a119e-d436-4955-9712-3a5e5cf48b06_100' + self.xpub = "" + self.mneumonic = "" + self.accountId = "" + self.headers = {'x-api-key': self.wallet_api_key, 'Content-Type': "application/json"} + self.main_account_setup() + self.account_number = 0000000000 + + def main_account_setup(self): + + #setup main account + response_object = requests.get(self.wallet_url + '/v3/xdc/wallet?mnemonic=string', headers=self.headers) + self.xpub = response_object.json()['xpub'] + self.mneumonic = response_object.json()['mnemonic'] + print(response_object.json()) + + #setup virtual xdc currency + body = { + "name":"VC_XDC", + "supply":"10000000000", + "basePair":"XDC", + "baseRate":1, + "customer":{ + "accountingCurrency":"USD", + "customerCountry":"SA", + "externalId":"123654", + "providerCountry":"SA"}, + "description":"XDC ChatPay", + "accountCode":"Main_Account", + "accountNumber":"1234567890", + "accountingCurrency":"USD"} + response_object = requests.post(self.wallet_url + '/v3/ledger/virtualCurrency', json=body, headers=self.headers) + if response_object.status_code == 200: + print("Virtual Currency Created") + else: + print("Virtual Currency Failed to Create") + print(response_object.json()) + self.get_virtual_xdc() + print(response_object.json()) + + def get_virtual_xdc(self): + response = requests.get(self.wallet_url + '/v3/ledger/virtualCurrency' + '/VC_XDC', headers=self.headers) + print(response.json()) + self.accountId = response.json()['accountId'] + return response.json() + + def get_balance(self, address): + response = requests.get(self.wallet_url + f'/v3/ledger/account/{address}/balance', headers=self.headers) + print(response.json()) + return response.json()['availableBalance'] + + def send_xdc(self, sender_address, receiver_address, amount): + body = { + "senderAccountId": sender_address, + "recipientAccountId": receiver_address, + "amount": amount, + "anonymous": False, + "compliant": False, + "transactionCode": "1_01_EXTERNAL_CODE", + "paymentId": randint(1, 1000000000), + "recipientNote": "xdc chatpay", + "baseRate": 1, + "senderNote": "xdc chatpay", + } + response = requests.post(self.wallet_url + '/v3/ledger/transaction', json=body, headers=self.headers) + try: + print(response.json()) + return response.json()['reference'] + except KeyError as error: + print(response.json()) + print(error) + return None + + def buy_xdc(self, address): + body = { + "senderAccountId": self.accountId, + "recipientAccountId": address, + "amount": '50', + "anonymous": False, + "compliant": False, + "transactionCode": "1_01_EXTERNAL_CODE", + "paymentId": randint(1, 1000000000), + "recipientNote": "xdc chatpay", + "baseRate": 1, + "senderNote": "xdc chatpay", + } + response = requests.post(self.wallet_url + '/v3/ledger/transaction', json=body, headers=self.headers) + try: + print(response.json()) + return response.json()['reference'] + except KeyError as error: + print(response.json()) + print(error) + return None + + def get_transactions(self, address): + body = { + "id" : address + } + response = requests.post(self.wallet_url + '/v3/ledger/transaction/account?pageSize=50&offset=0&count=false', json=body, headers=self.headers) + print(response.json()) + return self.parse_transactions(response) + + def create_wallet(self, name): + self.account_number += 1 + body = {"currency":"VC_XDC", + "customer": + { + "accountingCurrency":"USD", + "customerCountry":"SA", + "externalId": name, + "providerCountry":"US" + }, + "compliant":False, + "accountCode":"TRANSACTIONAL_ACCOUNT", + "accountingCurrency":"USD", + "accountNumber": str(self.account_number)} + response = requests.post(self.wallet_url + '/v3/ledger/account', json=body, headers=self.headers) + print(response.json()) + return response.json()['accountNumber'], response.json()['id'] + + def parse_transactions(self, transaction_list): + parsed_transactions = [] + for transaction in transaction_list: + parsed_transaction = {} + parsed_transaction['counterAccount'] = transaction['counterAccountId'] + parsed_transaction['amount'] = transaction['amount'] + transaction_date = self.convert_epoch_time(str(transaction['created'])) + parsed_transaction['date'] = transaction_date + parsed_transaction['reference'] = transaction['reference'] + parsed_transactions.append(parsed_transaction) + return parsed_transactions + + def convert_epoch_time(self, epoch_time): + epoch_str_to_epoch_milli_sec = epoch_time[:-3]+"."+epoch_time[-3:] + epoch = float(epoch_str_to_epoch_milli_sec) + return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(epoch)) \ No newline at end of file