#!/usr/bin/env python3
# addrwatch - A Bitcoin Cash address watcher and notifier
# Author: Simon Volpert <simon@simonvolpert.com>
# Project page: https://simonvolpert.com/addrwatch/
# This program is free software, released under the Apache License, Version 2.0. See the LICENSE file for more information
# Consult the README file for usage instructions and other helpful hints
import os
import sys
import subprocess
import time
import bch # Local library file
import sendmail # Local library file
usage = '''Usage: addrwatch [CONFIG_FILE]
See the README file for more information.'''
email_from = 'addrwatch <noreply@localhost.localdomain>'
rate_cache_time = 3600
text_body = [
'Total address balance: %s',
'Total address balance (%s): %s',
'Unconfirmed balance: %s',
'Unconfirmed balance (%s): %s',
'Confirmed balance: %s',
'Confirmed balance (%s): %s'
]
# Look for a configuration file
config_file_locations = [
os.path.join(os.path.expanduser('~'), '.addrwatch.cfg'),
os.path.join(sys.path[0], 'addrwatch.cfg'),
'addrwatch.cfg'
]
if len(sys.argv) > 1:
if sys.argv[1] == '-h' or sys.argv[1] == '--help':
print(usage)
sys.exit(0)
else:
config_file_locations.insert(0, sys.argv[1])
# Try to open the configuration file
cfg_file = None
for file_name in config_file_locations:
try:
f = open(file_name, 'r')
cfg_file = file_name
except:
continue
else:
print('Using %s as config file' % cfg_file)
break
if cfg_file is None:
print('''Could not open the configuration file in any of the checked locations.
Please copy addrwatch.cfg.sample to addrwatch.cfg or $HOME/.addrwatch.cfg and
edit it to your liking, or run addrwatch with a file name as the only argument.''')
sys.exit(1)
# Read the configuration file
lines = f.readlines()
f.close()
# Parse config file
config = {}
addresses = {}
for line in lines:
# Skip blank lines and comments
if line.strip() == '' or line.startswith('#'):
continue
# Split to key and value pairs
words = line.strip().split('=')
key = words[0].strip()
value = '='.join(words[1:]).strip()
if key == 'address':
value = value.split(',')
if len(value) == 0:
continue
elif len(value) == 1:
value.append(0)
else:
try:
value[1] = float(value[1])
except ValueError:
value[1] = 0
addresses[value[0]] = value[1]
else:
config[key] = value
# Sanitize configuration
try:
config['currency'] = config['currency'].upper()
except:
config['currency'] = ''
try:
config['frequency'] = int(config['frequency'])
if config['frequency'] < 0:
raise ValueError
except:
print('Invalid or missing frequency setting, using defaults')
config['frequency'] = 60
try:
if config['notification'].lower() == 'yes':
config['notification'] = True
else:
raise ValueError
except:
config['notification'] = False
try:
if config['unconfirmed'].lower() == 'yes':
config['unconfirmed'] = True
else:
raise ValueError
except:
config['unconfirmed'] = False
if 'email' not in config:
config['email'] = ''
if 'email_from' not in config.keys() or config['email_from'] == '':
config['email_from'] = email_from
while True:
# Delay after the first pass
try:
balance
except NameError:
balance = 0
loop_time = rate_cache_time
else:
loop_time += config['frequency']
try:
time.sleep(config['frequency'])
except KeyboardInterrupt:
sys.exit()
# Pull exchange rate if needed
if config['currency'] == '':
rate = 1
elif loop_time >= rate_cache_time:
try:
rate = bch.get_price(config['currency'])
loop_time = 0
except KeyboardInterrupt:
sys.exit()
except:
print('Could not load conversion rate: %s' % sys.exc_info()[1])
if config['frequency'] == 0:
sys.exit(0)
continue
# Pull address balances from block explorer
for addr in addresses.keys():
try:
balance, unconfirmed = bch.get_balance(addr)
except KeyboardInterrupt:
sys.exit()
except:
exception = sys.exc_info()[1]
try:
print('Could not load address balance: %s' % exception.reason)
except AttributeError:
print('Could not load address balance: %s' % exception)
if config['frequency'] == 0:
sys.exit(0)
break
# Calculate balance change
if config['unconfirmed']:
balance += unconfirmed
report = ''
if balance != addresses[addr]:
if config['currency'] != '':
diff = bch.fiat((balance - addresses[addr]) * rate)
else:
diff = bch.btc(balance - addresses[addr])
if diff.startswith('-'):
report = 'Payment sent: %s %s from address %s' % (diff, config['currency'], addr)
else:
report = 'Payment received: %s %s to address %s' % (diff, config['currency'], addr)
# Report balance change
if report != '':
# Desktop notification
if config['notification'] and 'DISPLAY' in os.environ:
subprocess.call(['notify-send', report])
# Email message
if config['email'] != '':
# Format the message body
text_body[0] = text_body[0] % bch.btc(balance)
text_body[2] = text_body[2] % bch.btc(unconfirmed)
text_body[4] = text_body[4] % bch.btc(balance - unconfirmed)
if config['currency'] == '':
del(text_body[5])
del(text_body[3])
del(text_body[1])
else:
text_body[1] = text_body[1] % (config['currency'], bch.fiat(balance * rate))
text_body[3] = text_body[3] % (config['currency'], bch.fiat(unconfirmed * rate))
text_body[5] = text_body[5] % (config['currency'], bch.fiat((balance - unconfirmed) * rate))
# Send the email
sendmail.send(config, config['email'], report, '\n'.join(text_body))
print(report)
# Update balances
addresses[addr] = balance
for line in lines:
if line.startswith('address=%s' % addr):
lines[lines.index(line)] = 'address=%s,%s\n' % (addr, bch.btc(balance))
break
# Write updates to the config file
with open(cfg_file, 'w') as f:
f.writelines(lines)
if config['frequency'] == 0:
sys.exit(0)