diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f36b2..3bec770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [2.1.0] - 2022-08-24 + +**BREAKING** +Date times need to be updated do datetime aware objects in the database. + +In postgres, you can run the following migration: + +``` +alter table blocks alter column created_at type timestamptz; +alter table accounts alter column created_at type timestamptz; +alter table adhoc_accounts alter column created_at type timestamptz; +alter table wallets alter column created_at type timestamptz; +``` + +- Update Tortoise ORM +- Improve connection handling + ## [2.0.2] - 2022-08-15 - Support blockAward in BoomPoW v2 diff --git a/kubernetes/banano/deployment.yaml b/kubernetes/banano/deployment.yaml index b4cd462..910f553 100644 --- a/kubernetes/banano/deployment.yaml +++ b/kubernetes/banano/deployment.yaml @@ -15,7 +15,8 @@ spec: spec: containers: - name: pippin-banano - image: bananocoin/pippin:2.0.1 + image: bananocoin/pippin:2.1.0 + imagePullPolicy: Always resources: requests: cpu: 100m diff --git a/kubernetes/nano/deployment.yaml b/kubernetes/nano/deployment.yaml index 9fb3c89..34fbc1a 100644 --- a/kubernetes/nano/deployment.yaml +++ b/kubernetes/nano/deployment.yaml @@ -15,7 +15,8 @@ spec: spec: containers: - name: pippin-nano - image: bananocoin/pippin:2.0.1 + image: bananocoin/pippin:2.1.0 + imagePullPolicy: Always resources: requests: cpu: 500m diff --git a/kubernetes/wban/deployment.yaml b/kubernetes/wban/deployment.yaml index 15a6096..2ee5f3f 100644 --- a/kubernetes/wban/deployment.yaml +++ b/kubernetes/wban/deployment.yaml @@ -15,7 +15,8 @@ spec: spec: containers: - name: pippin-banano - image: bananocoin/pippin:2.0.1 + image: bananocoin/pippin:2.1.0 + imagePullPolicy: Always resources: requests: cpu: 100m diff --git a/pippin/db/models/block.py b/pippin/db/models/block.py index efb7cbc..9a7b8d7 100644 --- a/pippin/db/models/block.py +++ b/pippin/db/models/block.py @@ -15,4 +15,7 @@ class Block(Model): class Meta: table = 'blocks' - unique_together = ('account', 'send_id') \ No newline at end of file + unique_together = ('account', 'send_id') + + + diff --git a/pippin/db/tortoise_config.py b/pippin/db/tortoise_config.py index a26ae45..75ca339 100644 --- a/pippin/db/tortoise_config.py +++ b/pippin/db/tortoise_config.py @@ -3,11 +3,14 @@ from tortoise import Tortoise from pippin.util.utils import Utils import pathlib +from tortoise.contrib.aiohttp import register_tortoise + class DBConfig(object): - def __init__(self, mock = False): + def __init__(self, mock=False): self.logger = logging.getLogger() - self.modules = {'db': ['pippin.db.models.wallet', 'pippin.db.models.account', 'pippin.db.models.adhoc_account', 'pippin.db.models.block']} + self.modules = {'db': ['pippin.db.models.wallet', 'pippin.db.models.account', + 'pippin.db.models.adhoc_account', 'pippin.db.models.block']} self.mock = mock if self.mock: self.use_postgres = False @@ -23,7 +26,8 @@ def __init__(self, mock = False): if self.postgres_db is not None and self.postgres_user is not None and self.postgres_password is not None: self.use_postgres = True elif self.postgres_db is not None or self.postgres_user is not None or self.postgres_password is not None: - raise Exception("ERROR: Postgres is not properly configured. POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD environment variables are all required.") + raise Exception( + "ERROR: Postgres is not properly configured. POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD environment variables are all required.") # MySQL self.use_mysql = False if not self.use_postgres: @@ -35,7 +39,13 @@ def __init__(self, mock = False): if self.mysql_db is not None and self.mysql_user is not None and self.mysql_password is not None: self.use_mysql = True elif self.mysql_db is not None or self.mysql_user is not None or self.mysql_password is not None: - raise Exception("ERROR: Postgres is not properly configured. MYSQL_DB, MYSQL_USER, and MYSQL_PASSWORD environment variables are all required.") + raise Exception( + "ERROR: Postgres is not properly configured. MYSQL_DB, MYSQL_USER, and MYSQL_PASSWORD environment variables are all required.") + + def init_db_aiohttp(self, app): + register_tortoise(app, db_url=f'postgres://{self.postgres_user}:{self.postgres_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}', + modules=self.modules, + generate_schemas=True) async def init_db(self): if self.use_postgres: @@ -52,10 +62,11 @@ async def init_db(self): ) else: self.logger.info(f"Using SQLite database pippin.db") - dbpath = Utils.get_project_root().joinpath(pathlib.PurePath('pippin.db')) if not self.mock else Utils.get_project_root().joinpath(pathlib.PurePath('mock.db')) + dbpath = Utils.get_project_root().joinpath(pathlib.PurePath('pippin.db') + ) if not self.mock else Utils.get_project_root().joinpath(pathlib.PurePath('mock.db')) await Tortoise.init( db_url=f'sqlite://{dbpath}', modules=self.modules ) # Create tables - await Tortoise.generate_schemas(safe=True) \ No newline at end of file + await Tortoise.generate_schemas(safe=True) diff --git a/pippin/main.py b/pippin/main.py index bcfad4a..e3dbe50 100644 --- a/pippin/main.py +++ b/pippin/main.py @@ -18,7 +18,6 @@ from aiohttp import web, log from pippin.db.redis import RedisDB from tortoise import Tortoise -from pippin.db.tortoise_config import DBConfig from pippin.config import Config from logging.handlers import TimedRotatingFileHandler, WatchedFileHandler from pippin.network.rpc_client import RPCClient @@ -75,7 +74,6 @@ def main(): try: # Initialize database first log.server_logger.info("Initializing database") - loop.run_until_complete(DBConfig().init_db()) if os.getenv('BPOW_KEY', None) is not None: log.server_logger.info( "💥 Using BoomPOW For Work Generation") @@ -112,7 +110,6 @@ def main(): RedisDB.close(), WorkClient.close(), NanoUtil.close(), - Tortoise.close_connections() ] loop.run_until_complete(asyncio.wait(tasks)) loop.close() diff --git a/pippin/pippin_cli.py b/pippin/pippin_cli.py index aee5b4e..4a7ee8a 100755 --- a/pippin/pippin_cli.py +++ b/pippin/pippin_cli.py @@ -1,27 +1,26 @@ +import nanopy +import os +from pippin.config import Config +from pippin.version import __version__ +from pippin.util.validators import Validators +from pippin.util.random import RandomUtil +from pippin.util.crypt import AESCrypt, DecryptionError +from tortoise.transactions import in_transaction +from tortoise import Tortoise, run_async +from pippin.db.tortoise_config import DBConfig +from pippin.db.models.wallet import Wallet, WalletLocked, WalletNotFound +import getpass +import asyncio +import argparse +from pippin.util.utils import Utils import pathlib from dotenv import load_dotenv load_dotenv() -from pippin.util.utils import Utils -load_dotenv(dotenv_path=Utils.get_project_root().joinpath(pathlib.PurePath('.env'))) - -import argparse -import asyncio -import getpass - -from pippin.db.models.wallet import Wallet, WalletLocked, WalletNotFound -from pippin.db.tortoise_config import DBConfig -from tortoise import Tortoise -from tortoise.transactions import in_transaction -from pippin.util.crypt import AESCrypt, DecryptionError -from pippin.util.random import RandomUtil -from pippin.util.validators import Validators -from pippin.version import __version__ +load_dotenv(dotenv_path=Utils.get_project_root().joinpath( + pathlib.PurePath('.env'))) -from pippin.config import Config -import os # Set and patch nanopy -import nanopy nanopy.account_prefix = 'ban_' if Config.instance().banano else 'nano_' if Config.instance().banano: nanopy.standard_exponent = 29 @@ -33,36 +32,52 @@ wallet_parser = subparsers.add_parser('wallet_list') wallet_create_parser = subparsers.add_parser('wallet_create') -wallet_create_parser.add_argument('--seed', type=str, help='Seed for wallet (optional)', required=False) +wallet_create_parser.add_argument( + '--seed', type=str, help='Seed for wallet (optional)', required=False) wallet_change_seed_parser = subparsers.add_parser('wallet_change_seed') -wallet_change_seed_parser.add_argument('--wallet', type=str, help='ID of wallet to change seed for', required=True) -wallet_change_seed_parser.add_argument('--seed', type=str, help='New seed for wallet (optional)', required=False) -wallet_change_seed_parser.add_argument('--encrypt', action='store_true', help='If specified, will get prompted for a password to encrypt the wallet', default=False) +wallet_change_seed_parser.add_argument( + '--wallet', type=str, help='ID of wallet to change seed for', required=True) +wallet_change_seed_parser.add_argument( + '--seed', type=str, help='New seed for wallet (optional)', required=False) +wallet_change_seed_parser.add_argument( + '--encrypt', action='store_true', help='If specified, will get prompted for a password to encrypt the wallet', default=False) wallet_view_seed_parser = subparsers.add_parser('wallet_view_seed') -wallet_view_seed_parser.add_argument('--wallet', type=str, help='Wallet ID', required=True) -wallet_view_seed_parser.add_argument('--password', type=str, help='Password needed to decrypt wallet (if encrypted)', required=False) -wallet_view_seed_parser.add_argument('--all-keys', action='store_true', help='Also show all of the wallet address and keys', default=False) +wallet_view_seed_parser.add_argument( + '--wallet', type=str, help='Wallet ID', required=True) +wallet_view_seed_parser.add_argument( + '--password', type=str, help='Password needed to decrypt wallet (if encrypted)', required=False) +wallet_view_seed_parser.add_argument( + '--all-keys', action='store_true', help='Also show all of the wallet address and keys', default=False) account_create_parser = subparsers.add_parser('account_create') -account_create_parser.add_argument('--wallet', type=str, help='Wallet ID', required=True) -account_create_parser.add_argument('--key', type=str, help='AdHoc Account Key', required=False) -account_create_parser.add_argument('--count', type=int, help='Number of accounts to create (min: 1)', required=False) +account_create_parser.add_argument( + '--wallet', type=str, help='Wallet ID', required=True) +account_create_parser.add_argument( + '--key', type=str, help='AdHoc Account Key', required=False) +account_create_parser.add_argument( + '--count', type=int, help='Number of accounts to create (min: 1)', required=False) wallet_destroy_parser = subparsers.add_parser('wallet_destroy') -wallet_destroy_parser.add_argument('--wallet', type=str, help='Wallet ID', required=True) +wallet_destroy_parser.add_argument( + '--wallet', type=str, help='Wallet ID', required=True) repget_parser = subparsers.add_parser('wallet_representative_get') -repget_parser.add_argument('--wallet', type=str, help='Wallet ID', required=True) +repget_parser.add_argument( + '--wallet', type=str, help='Wallet ID', required=True) repset_parser = subparsers.add_parser('wallet_representative_set') -repset_parser.add_argument('--wallet', type=str, help='Wallet ID', required=True) -repset_parser.add_argument('--representative', type=str, help='New Wallet Representative', required=True) -repset_parser.add_argument('--update-existing', action='store_true', help='Update existing accounts', default=False) +repset_parser.add_argument( + '--wallet', type=str, help='Wallet ID', required=True) +repset_parser.add_argument( + '--representative', type=str, help='New Wallet Representative', required=True) +repset_parser.add_argument('--update-existing', action='store_true', + help='Update existing accounts', default=False) options = parser.parse_args() + async def wallet_list(): wallets = await Wallet.all().prefetch_related('accounts', 'adhoc_accounts') if len(wallets) == 0: @@ -75,6 +90,7 @@ async def wallet_list(): for a in w.accounts: print(a.address) + async def wallet_create(seed): async with in_transaction() as conn: wallet = Wallet( @@ -84,6 +100,7 @@ async def wallet_create(seed): new_acct = await wallet.account_create(using_db=conn) print(f"Wallet created, ID: {wallet.id}\nFirst account: {new_acct}") + async def wallet_change_seed(wallet_id: str, seed: str, password: str) -> str: encrypt = False old_password = None @@ -129,7 +146,9 @@ async def wallet_change_seed(wallet_id: str, seed: str, password: str) -> str: # Get newest account newest = await wallet.get_newest_account() - print(f"Seed changed for wallet {wallet.id}\nFirst account: {newest.address}") + print( + f"Seed changed for wallet {wallet.id}\nFirst account: {newest.address}") + async def wallet_view_seed(wallet_id: str, password: str, all_keys: bool) -> str: # Retrieve wallet @@ -167,19 +186,22 @@ async def wallet_view_seed(wallet_id: str, password: str, all_keys: bool) -> str print(f"Seed: {wallet.seed}") if all_keys: for a in await wallet.accounts.all(): - print(f"Addr: {a.address} PrivKey: {nanopy.deterministic_key(wallet.seed, index=a.account_index)[0].upper()}") + print( + f"Addr: {a.address} PrivKey: {nanopy.deterministic_key(wallet.seed, index=a.account_index)[0].upper()}") else: print(f"AdHoc accounts:") for a in await wallet.adhoc_accounts.all(): if not wallet.encrypted: print(f"Addr: {a.address} PrivKey: {a.private_key.upper()}") else: - print(f"Addr: {a.address} PrivKey: {crypt.decrypt(a.private_key)}") + print( + f"Addr: {a.address} PrivKey: {crypt.decrypt(a.private_key)}") + async def account_create(wallet_id: str, key: str, count: int = 1) -> str: # Retrieve wallet crypt = None - password=None + password = None if count is None: count = 1 try: @@ -192,13 +214,14 @@ async def account_create(wallet_id: str, key: str, count: int = 1) -> str: if key is not None: while True: try: - npass = getpass.getpass(prompt='Enter current password to encrypt ad-hoc key:') + npass = getpass.getpass( + prompt='Enter current password to encrypt ad-hoc key:') crypt = AESCrypt(npass) try: decrypted = crypt.decrypt(wl.wallet.seed) wallet = wl.wallet wallet.seed = decrypted - password=npass + password = npass except DecryptionError: print("**Invalid password**") except KeyboardInterrupt: @@ -218,6 +241,7 @@ async def account_create(wallet_id: str, key: str, count: int = 1) -> str: a = await wallet.adhoc_account_create(key, password=password) print(f"account: {a}") + async def wallet_destroy(wallet_id: str): # Retrieve wallet try: @@ -231,6 +255,7 @@ async def wallet_destroy(wallet_id: str): await wallet.delete() print("Wallet destroyed") + async def wallet_representative_get(wallet_id: str): # Retrieve wallet try: @@ -246,11 +271,12 @@ async def wallet_representative_get(wallet_id: str): else: print(f"Wallet representative: {wallet.representative}") + async def wallet_representative_set(wallet_id: str, rep: str, update_existing: bool = False): # Retrieve wallet # Retrieve wallet crypt = None - password=None + password = None if not Validators.is_valid_address(rep): print("Invalid representative") exit(1) @@ -264,13 +290,14 @@ async def wallet_representative_set(wallet_id: str, rep: str, update_existing: b if update_existing: while True: try: - npass = getpass.getpass(prompt='Enter current password to decrypt wallet:') + npass = getpass.getpass( + prompt='Enter current password to decrypt wallet:') crypt = AESCrypt(npass) try: decrypted = crypt.decrypt(wl.wallet.seed) wallet = wl.wallet wallet.seed = decrypted - password=npass + password = npass except DecryptionError: print("**Invalid password**") except KeyboardInterrupt: @@ -282,6 +309,7 @@ async def wallet_representative_set(wallet_id: str, rep: str, update_existing: b await wallet.bulk_representative_update(rep) print(f"Representative changed") + def main(): loop = asyncio.new_event_loop() try: @@ -302,7 +330,8 @@ def main(): else: while True: try: - options.seed = getpass.getpass(prompt='Enter new wallet seed:') + options.seed = getpass.getpass( + prompt='Enter new wallet seed:') if Validators.is_valid_block_hash(options.seed): break print("**Invalid seed**, should be a 64-character hex string") @@ -313,16 +342,19 @@ def main(): if options.encrypt: while True: try: - password = getpass.getpass(prompt='Enter password to encrypt wallet:') + password = getpass.getpass( + prompt='Enter password to encrypt wallet:') if password.strip() == '': print("**Bad password** - cannot be blanke") break except KeyboardInterrupt: break exit(0) - loop.run_until_complete(wallet_change_seed(options.wallet, options.seed, password)) + loop.run_until_complete(wallet_change_seed( + options.wallet, options.seed, password)) elif options.command == 'wallet_view_seed': - loop.run_until_complete(wallet_view_seed(options.wallet, options.password, options.all_keys)) + loop.run_until_complete(wallet_view_seed( + options.wallet, options.password, options.all_keys)) elif options.command == 'account_create': if options.key is not None: if not Validators.is_valid_block_hash(options.key): @@ -334,21 +366,23 @@ def main(): elif options.count is not None: if options.count < 1: print("Count needs to be at least 1...") - loop.run_until_complete(account_create(options.wallet, options.key, options.count)) + loop.run_until_complete(account_create( + options.wallet, options.key, options.count)) elif options.command == 'wallet_destroy': loop.run_until_complete(wallet_destroy(options.wallet)) elif options.command == 'wallet_representative_get': loop.run_until_complete(wallet_representative_get(options.wallet)) elif options.command == 'wallet_representative_set': - loop.run_until_complete(wallet_representative_set(options.wallet, options.representative, update_existing=options.update_existing)) + loop.run_until_complete(wallet_representative_set( + options.wallet, options.representative, update_existing=options.update_existing)) else: parser.print_help() except Exception as e: print(str(e)) raise e finally: - loop.run_until_complete(Tortoise.close_connections()) loop.close() + if __name__ == "__main__": - main() + run_async(main()) diff --git a/pippin/server/pippin_server.py b/pippin/server/pippin_server.py index 44bc4fe..f46be74 100644 --- a/pippin/server/pippin_server.py +++ b/pippin/server/pippin_server.py @@ -22,6 +22,7 @@ from pippin.util.validators import Validators from pippin.util.wallet import (InsufficientBalance, ProcessFailed, WalletUtil, WorkFailed) +from pippin.db.tortoise_config import DBConfig class PippinServer(object): @@ -30,6 +31,7 @@ class PippinServer(object): def __init__(self, host: str, port: int): self.app = web.Application( middlewares=[web.normalize_path_middleware()]) + DBConfig().init_db_aiohttp(self.app) self.app.add_routes([ web.post('/', self.gateway) ]) @@ -969,7 +971,7 @@ async def work_generate(self, request: web.Request, request_json: dict): if 'block_award' in request_json: block_award = request_json['block_award'] else: - block_award = False + block_award = True # Generate work work = await WorkClient.instance().work_generate(request_json['hash'], difficulty, blockAward=block_award) diff --git a/pippin/version.py b/pippin/version.py index 0309ae2..9aa3f90 100644 --- a/pippin/version.py +++ b/pippin/version.py @@ -1 +1 @@ -__version__ = "2.0.2" +__version__ = "2.1.0" diff --git a/requirements.txt b/requirements.txt index 484047b..168854d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -tortoise-orm>=0.15.24,<0.16 +tortoise-orm pypika<0.37.0 aiosqlite>=0.10.0 asyncpg>=0.20.0 diff --git a/setup.py b/setup.py index e59c1db..5238669 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ def requirements() -> list: return ret except FileNotFoundError: ret = [ - 'tortoise-orm>=0.15.24,<0.16', + 'tortoise-orm', 'aiosqlite>=0.10.0', 'asyncpg>=0.20.0', 'pypika<0.37.0',