class DataHandler(object): def __init__(self, data_directory): self.data_directory = data_directory try: with open(os.path.join(self.data_directory, 'settings.json')) as f: self.settings = rlk_jsonloads(f.read()) except JSONDecodeError as e: logger.critical('settings.json file could not be decoded and is corrupt: {}'.format(e)) self.settings = empty_settings except FileNotFoundError: self.settings = empty_settings self.db = None self.eth_tokens = [] dir_path = os.path.dirname(os.path.realpath(__file__)) with open(os.path.join(dir_path, 'data', 'eth_tokens.json'), 'r') as f: self.eth_tokens = rlk_jsonloads(f.read()) def unlock(self, username, password, create_new): user_data_dir = os.path.join(self.data_directory, username) if create_new: if os.path.exists(user_data_dir): raise AuthenticationError('User {} already exists'.format(username)) else: os.mkdir(user_data_dir) else: if not os.path.exists(user_data_dir): raise AuthenticationError('User {} does not exist'.format(username)) if not os.path.exists(os.path.join(user_data_dir, 'rotkehlchen.db')): # This is bad. User directory exists but database is missing. # Make a backup of the directory that user should probably remove # on his own. At the same time delete the directory so that a new # user account can be created shutil.move( user_data_dir, os.path.join(self.data_directory, 'backup_%s' % username) ) raise AuthenticationError( 'User {} exists but DB is missing. Somehow must have been manually ' 'deleted or is corrupt. Please recreate the user account.'.format(username)) self.db = DBHandler(user_data_dir, username, password) self.user_data_dir = user_data_dir return user_data_dir def main_currency(self): return self.settings['main_currency'] def historical_start_date(self): return self.settings.get('historical_data_start_date', DEFAULT_START_DATE) def save_balances_data(self, data): self.db.write_balances_data(data) def write_owned_eth_tokens(self, tokens): self.db.write_owned_tokens(tokens) def add_blockchain_account(self, blockchain, account): self.db.add_blockchain_account(blockchain, account) def remove_blockchain_account(self, blockchain, account): self.db.remove_blockchain_account(blockchain, account) def set_premium_credentials(self, api_key, api_secret): self.db.set_rotkehlchen_premium(api_key, api_secret) def set_main_currency(self, currency, accountant): self.settings['main_currency'] = currency accountant.set_main_currency(currency) with open(os.path.join(self.data_directory, 'settings.json'), 'w') as f: f.write(rlk_jsondumps(self.settings)) def set_ui_floating_precision(self, val): self.settings['ui_floating_precision'] = val with open(os.path.join(self.data_directory, 'settings.json'), 'w') as f: f.write(rlk_jsondumps(self.settings)) def set_settings(self, settings, accountant): self.settings = settings accountant.set_main_currency(settings['main_currency']) with open(os.path.join(self.data_directory, 'settings.json'), 'w') as f: f.write(rlk_jsondumps(self.settings)) def get_eth_accounts(self): blockchain_accounts = self.db.get_blockchain_accounts() return blockchain_accounts['ETH'] if 'ETH' in blockchain_accounts else [] def set_fiat_balance(self, currency, balance): if currency not in FIAT_CURRENCIES: return False, 'Provided currency {} is unknown' if balance == 0 or balance == '': self.db.remove_fiat_balance(currency) else: try: balance = FVal(balance) except ValueError: return False, 'Provided amount is not a number' self.db.add_fiat_balance(currency, str(balance)) return True, '' def get_fiat_balances(self): return self.db.get_fiat_balances() def get_external_trades(self): return get_external_trades(self.data_directory) def add_external_trade(self, data): timestamp, message = check_otctrade_data_valid(data) if not timestamp: return False, message rate = float(data['otc_rate']) amount = float(data['otc_amount']) cost = rate * amount pair = data['otc_pair'] external_trades = get_external_trades(self.data_directory) external_trades.append({ 'timestamp': timestamp, 'pair': pair, 'type': data['otc_type'], 'rate': rate, 'cost': cost, # for now cost/fee currency is always second. # TODO: Make it configurable 'cost_currency': get_pair_position(pair, 'second'), 'fee_currency': get_pair_position(pair, 'second'), 'fee': data['otc_fee'], 'amount': amount, 'location': 'external', 'link': data['otc_link'], 'notes': data['otc_notes'], }) with open(os.path.join(self.data_directory, EXTERNAL_TRADES_FILE), 'w') as f: f.write(rlk_jsondumps(external_trades)) return True, '' def edit_external_trade(self, data): timestamp, message = check_otctrade_data_valid(data) if not timestamp: return False, message rate = float(data['otc_rate']) amount = float(data['otc_amount']) cost = rate * amount pair = data['otc_pair'] external_trades = get_external_trades(self.data_directory) # TODO: When we switch to sql, editing should be done with the primary key found = False for idx, trade in enumerate(external_trades): if timestamp == trade['timestamp']: external_trades[idx] = { 'timestamp': timestamp, 'pair': pair, 'type': data['otc_type'], 'rate': rate, 'cost': cost, # for now cost/fee currency is always second. # TODO: Make it configurable 'cost_currency': get_pair_position(pair, 'second'), 'fee_currency': get_pair_position(pair, 'second'), 'fee': data['otc_fee'], 'amount': amount, 'location': 'external', 'link': data['otc_link'], 'notes': data['otc_notes'], } found = True break if not found: return False, 'Could not find the requested trade for editing' with open(os.path.join(self.data_directory, EXTERNAL_TRADES_FILE), 'w') as f: f.write(rlk_jsondumps(external_trades)) return True, '' def delete_external_trade(self, data): external_trades = get_external_trades(self.data_directory) # TODO: When using sql just use primary key as id found_idx = -1 for idx, trade in enumerate(external_trades): if trade['timestamp'] == data['timestamp']: found_idx = idx break if found_idx == -1: return False, 'Could not find the requested trade for deletion' del external_trades[found_idx] with open(os.path.join(self.data_directory, EXTERNAL_TRADES_FILE), 'w') as f: f.write(rlk_jsondumps(external_trades)) return True, '' def compress_and_encrypt_db(self, password): """ Decrypt the DB, dump in temporary plaintextdb, compress it, and then re-encrypt it Returns a b64 encoded binary blob""" with tempfile.TemporaryDirectory() as tmpdirname: tempdb = os.path.join(tmpdirname, 'temp.db') self.db.export_unencrypted(tempdb) with open(tempdb, 'rb') as f: data_blob = f.read() original_data_hash = base64.b64encode( hashlib.sha256(data_blob).digest() ).decode() compressed_data = zlib.compress(data_blob, level=9) encrypted_data = encrypt(password.encode(), compressed_data) print('COMPRESSED-ENCRYPTED LENGTH: {}'.format(len(encrypted_data))) return encrypted_data.encode(), original_data_hash def decompress_and_decrypt_db(self, password, encrypted_data): """ Decrypt and decompress the encrypted data we receive from the server If succesfull then replace our local Database""" decrypted_data = decrypt(password.encode(), encrypted_data) decompressed_data = zlib.decompress(decrypted_data) self.db.import_unencrypted(decompressed_data, password)