├── .gitignore ├── LICENSE ├── README.md ├── config ├── __init__.py └── settings.py ├── main.py ├── requirements.txt └── utils ├── __init__.py ├── discord.py ├── network.py └── thenewboston.py /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .DS_Store 3 | .idea 4 | .vscode 5 | 6 | # Log files 7 | *.log 8 | logs/* 9 | 10 | # pytest 11 | .pytest_cache 12 | 13 | # Environments 14 | .env 15 | .venv 16 | ENV/ 17 | env.bak/ 18 | env/ 19 | venv 20 | venv.bak/ 21 | venv/ 22 | 23 | # Git 24 | !.gitkeep 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bucky Roberts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Setup 2 | 3 | Follow the steps below to set up the project on your environment. 4 | 5 | ## Mac Setup 6 | 7 | Homebrew requires the Xcode command-line tools from Apple's Xcode. Install the Xcode command-line tools by running the 8 | following command in your macOS Terminal: 9 | ``` 10 | xcode-select --install 11 | ``` 12 | 13 | Install brew using the official [Homebrew installation instructions](https://brew.sh/#install). 14 | 15 | Install MongoDB by running the following commands in your macOS Terminal: 16 | ``` 17 | brew tap mongodb/brew 18 | brew install mongodb-community@5.0 19 | ``` 20 | 21 | Use the following commands to run and stop MongoDB (i.e. the mongod process) as a macOS service: 22 | ``` 23 | brew services start mongodb-community@5.0 24 | brew services stop mongodb-community@5.0 25 | ``` 26 | 27 | Create the initial structure in MongoDB: 28 | - Database: `discord-db` 29 | - Collection: `users` 30 | 31 | ## Local Development 32 | 33 | Create a virtual environment with Python 3.7 or higher. 34 | 35 | Install required packages: 36 | ``` 37 | pip3 install -r requirements.txt 38 | ``` 39 | 40 | ## Environment Variables 41 | 42 | Create a `.env` file in the projects root directory with the following variables set: 43 | ``` 44 | DISCORD_TOKEN=ODg0MTU1MDg1NDUyNjExNjQ2.YTUXlw.LrDNHvwYo3VeHdpgRzN0Jq8DzXg 45 | ``` 46 | 47 | ## Community 48 | 49 | Join the community to stay updated on the most recent developments. 50 | 51 | - [thenewboston.com](https://thenewboston.com/) 52 | - [Discord](https://discord.gg/thenewboston) 53 | - [Facebook](https://www.facebook.com/TheNewBoston-464114846956315/) 54 | - [Instagram](https://www.instagram.com/thenewboston_official/) 55 | - [LinkedIn](https://www.linkedin.com/company/thenewboston-developers/) 56 | - [Reddit](https://www.reddit.com/r/thenewboston/) 57 | - [Twitch](https://www.twitch.tv/thenewboston/videos) 58 | - [Twitter](https://twitter.com/thenewboston_og) 59 | - [YouTube](https://www.youtube.com/user/thenewboston) 60 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckyroberts/Discord-Python-Framework/8731e9a01c2e2f83af9882838ad809696fe89e22/config/__init__.py -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | # Application 8 | MAXIMUM_CONFIRMATION_CHECKS = 20 9 | 10 | # thenewboston 11 | BANK_IP = '54.183.16.194' 12 | BANK_PROTOCOL = 'http' 13 | BOT_ACCOUNT_NUMBER = '598428d3a9df5423aab3e593b5d1b5f056b9fa353607fccb1aa76385cf233851' 14 | 15 | # Discord 16 | DISCORD_TOKEN = os.getenv('DISCORD_TOKEN') 17 | 18 | # Mongo 19 | MONGO_DB_NAME = 'discord-db' 20 | MONGO_HOST = 'localhost' 21 | MONGO_PORT = 27017 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands, tasks 2 | from pymongo import MongoClient 3 | from pymongo.errors import DuplicateKeyError 4 | 5 | from config.settings import ( 6 | BANK_IP, 7 | BANK_PROTOCOL, 8 | BOT_ACCOUNT_NUMBER, 9 | DISCORD_TOKEN, 10 | MAXIMUM_CONFIRMATION_CHECKS, 11 | MONGO_DB_NAME, 12 | MONGO_HOST, 13 | MONGO_PORT 14 | ) 15 | from utils.discord import generate_verification_code, send_embed, send_verification_message 16 | from utils.network import fetch 17 | from utils.thenewboston import is_valid_account_number 18 | 19 | bot = commands.Bot(command_prefix='>') 20 | 21 | mongo = MongoClient(MONGO_HOST, MONGO_PORT) 22 | database = mongo[MONGO_DB_NAME] 23 | 24 | DEPOSITS = database['deposits'] 25 | REGISTRATIONS = database['registrations'] 26 | USERS = database['users'] 27 | 28 | """ 29 | DEPOSIT 30 | _id: "7ca8d42a-80fd-4d8b-987a-470a3725d098" 31 | amount: 1 32 | block_id: "a24f8d90-0502-4b16-84c4-a123962989e9" 33 | confirmation_checks: 1 34 | is_confirmed: true 35 | memo: "7OJYUJ9K" 36 | sender: "a37e2836805975f334108b55523634c995bd2a4db610062f404510617e83126f" 37 | 38 | REGISTRATION 39 | _id: 310922051613491868 40 | account_number: "a37e2836805975f334108b55523634c995bd2a4db610062f404510617e83126f" 41 | verification_code: "7OJYUJ9K" 42 | 43 | USER 44 | _id: 310922051613491868 45 | account_number: "a37e2836805975f334108b55523634c995bd2a4db610062f404510617e83126f" 46 | balance: 0 47 | """ 48 | 49 | 50 | def check_confirmations(): 51 | """ 52 | Query unconfirmed deposits from database 53 | Check bank for confirmation status 54 | """ 55 | 56 | unconfirmed_deposits = DEPOSITS.find({ 57 | 'confirmation_checks': {'$lt': MAXIMUM_CONFIRMATION_CHECKS}, 58 | 'is_confirmed': False 59 | }) 60 | 61 | for deposit in unconfirmed_deposits: 62 | block_id = deposit['block_id'] 63 | url = ( 64 | f'{BANK_PROTOCOL}://{BANK_IP}/confirmation_blocks' 65 | f'?block={block_id}' 66 | ) 67 | 68 | try: 69 | data = fetch(url=url, headers={}) 70 | confirmations = data['count'] 71 | 72 | if confirmations: 73 | handle_deposit_confirmation(deposit=deposit) 74 | 75 | except Exception: 76 | pass 77 | 78 | increment_confirmation_checks(deposit=deposit) 79 | 80 | 81 | def check_deposits(): 82 | """ 83 | Fetch bank transactions from bank 84 | Insert new deposits into database 85 | """ 86 | 87 | next_url = ( 88 | f'{BANK_PROTOCOL}://{BANK_IP}/bank_transactions' 89 | f'?recipient={BOT_ACCOUNT_NUMBER}' 90 | f'&ordering=-block__created_date' 91 | ) 92 | 93 | while next_url: 94 | data = fetch(url=next_url, headers={}) 95 | bank_transactions = data['results'] 96 | next_url = data['next'] 97 | 98 | for bank_transaction in bank_transactions: 99 | 100 | try: 101 | DEPOSITS.insert_one({ 102 | '_id': bank_transaction['id'], 103 | 'amount': bank_transaction['amount'], 104 | 'block_id': bank_transaction['block']['id'], 105 | 'confirmation_checks': 0, 106 | 'is_confirmed': False, 107 | 'memo': bank_transaction['memo'], 108 | 'sender': bank_transaction['block']['sender'] 109 | }) 110 | except DuplicateKeyError: 111 | break 112 | 113 | 114 | def handle_deposit_confirmation(*, deposit): 115 | """ 116 | Update confirmation status of deposit 117 | Increase users balance or create new user if they don't already exist 118 | """ 119 | 120 | DEPOSITS.update_one( 121 | {'_id': deposit['_id']}, 122 | { 123 | '$set': { 124 | 'is_confirmed': True 125 | } 126 | } 127 | ) 128 | 129 | registration = REGISTRATIONS.find_one({ 130 | 'account_number': deposit['sender'], 131 | 'verification_code': deposit['memo'] 132 | }) 133 | 134 | if registration: 135 | handle_registration(registration=registration) 136 | else: 137 | USERS.update_one( 138 | {'account_number': deposit['sender']}, 139 | { 140 | '$inc': { 141 | 'balance': deposit['amount'] 142 | } 143 | } 144 | ) 145 | 146 | 147 | def handle_registration(*, registration): 148 | """ 149 | Ensure account number is not already registered 150 | Create a new users or update account number of existing user 151 | """ 152 | 153 | discord_user_id = registration['_id'] 154 | account_number_registered = bool(USERS.find_one({'account_number': registration['account_number']})) 155 | 156 | if not account_number_registered: 157 | existing_user = USERS.find_one({'_id': discord_user_id}) 158 | 159 | if existing_user: 160 | USERS.update_one( 161 | {'_id': discord_user_id}, 162 | { 163 | '$set': { 164 | 'account_number': registration['account_number'] 165 | } 166 | } 167 | ) 168 | else: 169 | USERS.insert_one({ 170 | '_id': discord_user_id, 171 | 'account_number': registration['account_number'], 172 | 'balance': 0 173 | }) 174 | 175 | REGISTRATIONS.delete_one({'_id': discord_user_id}) 176 | 177 | 178 | def increment_confirmation_checks(*, deposit): 179 | """ 180 | Increment the number of confirmation checks for the given deposit 181 | """ 182 | 183 | DEPOSITS.update_one( 184 | {'_id': deposit['_id']}, 185 | { 186 | '$inc': { 187 | 'confirmation_checks': 1 188 | } 189 | } 190 | ) 191 | 192 | 193 | @tasks.loop(seconds=5.0) 194 | async def poll_blockchain(): 195 | """ 196 | Poll blockchain for new transactions/deposits sent to the bot account 197 | Only accept confirmed transactions 198 | """ 199 | 200 | print('Polling blockchain...') 201 | check_deposits() 202 | check_confirmations() 203 | 204 | 205 | @bot.event 206 | async def on_ready(): 207 | """ 208 | Start polling blockchain 209 | """ 210 | 211 | print('Ready') 212 | poll_blockchain.start() 213 | 214 | 215 | @bot.command() 216 | async def register(ctx, account_number): 217 | """ 218 | >register a37e2836805975f334108b55523634c995bd2a4db610062f404510617e83126f 219 | """ 220 | 221 | if not is_valid_account_number(account_number): 222 | await send_embed( 223 | ctx=ctx, 224 | title='Invalid', 225 | description='Invalid account number.' 226 | ) 227 | return 228 | 229 | user = USERS.find_one({'account_number': account_number}) 230 | 231 | if user: 232 | await send_embed( 233 | ctx=ctx, 234 | title='Already Registered', 235 | description=f'The account {account_number} is already registered.' 236 | ) 237 | return 238 | 239 | discord_user_id = ctx.author.id 240 | verification_code = generate_verification_code() 241 | 242 | results = REGISTRATIONS.update_one( 243 | {'_id': discord_user_id}, 244 | { 245 | '$set': { 246 | 'account_number': account_number, 247 | 'verification_code': verification_code 248 | } 249 | }, 250 | upsert=True 251 | ) 252 | 253 | if results.modified_count: 254 | await send_embed( 255 | ctx=ctx, 256 | title='Registration Updated', 257 | description=( 258 | 'Your registration has been updated. ' 259 | 'To complete registration, follow the instructions sent via DM.' 260 | ) 261 | ) 262 | else: 263 | await send_embed( 264 | ctx=ctx, 265 | title='Registration Created', 266 | description=( 267 | 'Registration created. ' 268 | 'To complete registration, follow the instructions sent via DM.' 269 | ) 270 | ) 271 | 272 | await send_verification_message( 273 | ctx=ctx, 274 | registration_account_number=account_number, 275 | registration_verification_code=verification_code 276 | ) 277 | 278 | 279 | if __name__ == '__main__': 280 | bot.run(DISCORD_TOKEN) 281 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | discord.py==1.7.3 2 | pymongo==3.12.0 3 | python-dotenv~=0.19.0 4 | requests~=2.26.0 5 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckyroberts/Discord-Python-Framework/8731e9a01c2e2f83af9882838ad809696fe89e22/utils/__init__.py -------------------------------------------------------------------------------- /utils/discord.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | import discord 5 | 6 | from config.settings import BOT_ACCOUNT_NUMBER 7 | 8 | 9 | def generate_verification_code(): 10 | """ 11 | Generate random verification code 12 | """ 13 | 14 | chars = string.ascii_uppercase + string.digits 15 | return ''.join(random.choice(chars) for _ in range(8)) 16 | 17 | 18 | async def send_embed(*, ctx, title, description): 19 | """ 20 | Send a simple embed with a title and a description 21 | """ 22 | 23 | embed = discord.Embed( 24 | title=title, 25 | description=description, 26 | color=discord.Colour.red() 27 | ) 28 | 29 | await ctx.send(embed=embed) 30 | 31 | 32 | async def send_verification_message(*, ctx, registration_account_number, registration_verification_code): 33 | """ 34 | Send verification message DM 35 | """ 36 | 37 | embed = discord.Embed( 38 | title='Account Registration', 39 | description='To complete registration please send the following transaction.', 40 | color=discord.Colour.red() 41 | ) 42 | embed.add_field( 43 | name='From', 44 | value=registration_account_number, 45 | inline=False 46 | ) 47 | embed.add_field( 48 | name='To', 49 | value=BOT_ACCOUNT_NUMBER, 50 | inline=False 51 | ) 52 | embed.add_field( 53 | name='Memo', 54 | value=registration_verification_code, 55 | inline=False 56 | ) 57 | embed.add_field( 58 | name='Amount', 59 | value='1', 60 | inline=False 61 | ) 62 | 63 | await ctx.author.send(embed=embed) 64 | -------------------------------------------------------------------------------- /utils/network.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def fetch(*, url, headers): 5 | """ 6 | Send a GET request and return response as Python object 7 | """ 8 | 9 | response = requests.get(url, headers=headers) 10 | return response.json() 11 | -------------------------------------------------------------------------------- /utils/thenewboston.py: -------------------------------------------------------------------------------- 1 | def is_valid_account_number(account_number): 2 | """ 3 | Checks if the given account number is valid 4 | """ 5 | 6 | if len(str(account_number)) != 64: 7 | return False 8 | 9 | try: 10 | bytes.fromhex(account_number) 11 | except Exception: 12 | return False 13 | 14 | return True 15 | --------------------------------------------------------------------------------