bitcoin-blockchain-feed-bot
#jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket/blob/master/joinmarket/jsonrpc.py | |
#copyright # Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu> and phelix / blockchained.com | |
import base64 | |
import httplib | |
import json | |
class JsonRpcError(Exception): | |
def __init__(self, obj): | |
self.message = obj | |
class JsonRpcConnectionError(JsonRpcError): pass | |
class JsonRpc(object): | |
def __init__(self, host, port, user, password): | |
self.host = host | |
self.port = port | |
self.authstr = "%s:%s" % (user, password) | |
self.queryId = 1 | |
def queryHTTP(self, obj): | |
headers = {"User-Agent": "joinmarket", | |
"Content-Type": "application/json", | |
"Accept": "application/json"} | |
headers["Authorization"] = "Basic %s" % base64.b64encode(self.authstr) | |
body = json.dumps(obj) | |
try: | |
conn = httplib.HTTPConnection(self.host, self.port) | |
conn.request("POST", "", body, headers) | |
response = conn.getresponse() | |
if response.status == 401: | |
conn.close() | |
raise JsonRpcConnectionError( | |
"authentication for JSON-RPC failed") | |
# All of the codes below are 'fine' from a JSON-RPC point of view. | |
if response.status not in [200, 404, 500]: | |
conn.close() | |
raise JsonRpcConnectionError("unknown error in JSON-RPC") | |
data = response.read() | |
conn.close() | |
return json.loads(data) | |
except JsonRpcConnectionError as exc: | |
raise exc | |
except Exception as exc: | |
raise JsonRpcConnectionError("JSON-RPC connection failed. Err:" + | |
repr(exc)) | |
def call(self, method, params): | |
currentId = self.queryId | |
self.queryId += 1 | |
request = {"method": method, "params": params, "id": currentId} | |
response = self.queryHTTP(request) | |
if response["id"] != currentId: | |
raise JsonRpcConnectionError("invalid id returned by query") | |
if response["error"] is not None: | |
raise JsonRpcError(response["error"]) | |
return response["result"] | |
#irc bot code starts here | |
##single file, no dependencies | |
#by belcher | |
#Ares punished Alectryon by turning him into a rooster which never forgets | |
#to announce the arrival of the sun in the morning by its crowing. | |
import socket, time | |
from datetime import datetime | |
from getpass import getpass | |
#configuration stuff | |
nick = 'Alectryon' | |
hostport = ('irc.freenode.net', 6667) | |
nickserv_password = getpass('enter nickserv password for ' + nick + ': ') | |
channel = '#bitcoin-blocks' | |
#TODO :orwell.freenode.net 437 * Alectryon :Nick/channel is temporarily unavailable, then send /ns release <nick> <password> then wait then NICK <nick> | |
rpc = JsonRpc(host = 'localhost', | |
port = 8332, | |
user = 'bitcoinrpc', | |
password = 'password') | |
#TODO :Alectryon!~Alectryon@host NICK :Guest11162, for when you fuck up and dont identify in time | |
#TODO :barjavel.freenode.net 404 Guest878 #bitcoin-blocks :Cannot send to channel | |
check_for_new_block_interval = 1 | |
fee_estimate_output_interval = 60*30 | |
ping_interval_seconds = 120 | |
ping_timeout_seconds = 60*5 | |
old_bci = [rpc.call('getblockchaininfo', [])] | |
print 'bestblock = ' + old_bci[0]['bestblockhash'] | |
old_head = [rpc.call('getblockheader', [old_bci[0]['bestblockhash']])] | |
print("generating block times list") | |
block_times = [] | |
head = old_head[0] | |
for c in range(2016): | |
block_times.append(head['time']) | |
head = rpc.call('getblockheader', [head['previousblockhash']]) | |
block_times.reverse() | |
def get_miner_fee_stats(height, coinbase_tx): | |
subsidy = 6.25 | |
halvening_height = 840000 | |
if height >= halvening_height: | |
subsidy = 3.125 | |
reward = sum([tx_out['value'] for tx_out in coinbase_tx['vout']]) | |
fees = reward - subsidy | |
return fees, reward, subsidy, halvening_height | |
print("generating miner fee data list") | |
st = time.time() | |
head = old_head[0] | |
miner_fee_data = [] | |
#''' | |
for c in range(2016): | |
coinbase_txid = rpc.call("getblock", [head["hash"]])["tx"][0] | |
coinbase_tx = rpc.call("getrawtransaction", [coinbase_txid, True, head["hash"]]) | |
fees, reward, subsidy, halvening_height = get_miner_fee_stats(head["height"], coinbase_tx) | |
head = rpc.call("getblockheader", [head["previousblockhash"]]) | |
miner_fee_data.append((fees, reward)) | |
miner_fee_data.reverse() | |
#''' | |
print("miner fee data generated in " + str(time.time() - st) + "sec") | |
last_fee_estimate = [datetime.now()] | |
def strtimediff(s): | |
return "%02d:%02d" % (s/60, s%60) | |
def strfeerate(con, econ): | |
if con == econ: | |
return str(con*1e5) | |
else: | |
return str(con*1e5) + ', ' + str(econ*1e5) | |
def create_fee_rate_stats(): | |
st = time.time() | |
mempoolinfo = rpc.call("getmempoolinfo", []) | |
message = '\x0304MEMPOOL\x03' | |
message += " txes=" + str(mempoolinfo["size"]) | |
message += " size=" + ('%.3f' % (mempoolinfo["bytes"] / 1000000.0)) + " vMB" | |
message += " minfee=" + str(round(mempoolinfo["mempoolminfee"]*1e5, 4)) + "sat/vb" | |
message += " " | |
blocks = [(2, 'asap'), (6, 'hour'), (12, '2h'), (24, '4h'), (36, '6h'), (72, '12h'), (144, 'day'), (432, '3day'), (1008, 'week')] | |
data = [] | |
for b, t in blocks: | |
con = rpc.call('estimatesmartfee', [b, 'CONSERVATIVE']) | |
econ = rpc.call('estimatesmartfee', [b, 'ECONOMICAL']) | |
data.append((b, t, con['feerate'], econ['feerate'])) | |
message += 'FEE ESTIMATION' | |
message += ' fee rates: target-->(conservative, economical)sat/vbyte ' + ' '.join( | |
[t + '-->(' + strfeerate(con, econ) + ')' for b, t, con, econ in data]) | |
#blocks_index = [0, 6, 8] #asap, day, week | |
##typical tx 74a0b9a414f59dfe846473474b24c5d1d9d9c5110b0f54c8a22811f9eaa7a137 | |
#TYPICAL_VBYTES_KB = 168 | |
#message += ' typical tx(' + str(TYPICAL_VBYTES_KB) + ' vbytes): ' + ' '.join( | |
# [t + '-->' + '%.5f'%(con*TYPICAL_VBYTES_KB) + 'mbtc' for b, t, con, econ in [data[i] for i in blocks_index]]) | |
#TODO maybe also cost per input/cost per output | |
et = time.time() | |
print 'fees = ' + str(et - st) + 'sec' | |
return message | |
def check_fee_estimate(sock): | |
#return ######comment this to stop fee estimate outputs | |
try: | |
if (datetime.now() - last_fee_estimate[0]).total_seconds() < fee_estimate_output_interval: | |
return | |
last_fee_estimate[0] = datetime.now() | |
message = create_fee_rate_stats() | |
print message | |
sock.sendall('PRIVMSG ' + channel + ' :' + message + '\r\n') | |
except JsonRpcError as e: | |
print repr(e) | |
def create_block_stats(blockhash): | |
st = time.time() | |
#TODO average fee rate, median fee rate, minimum fee rate | |
bci = rpc.call('getblockchaininfo', []) | |
head = rpc.call('getblockheader', [blockhash]) | |
MAX_WEIGHT = 4000000 #TODO lower this | |
block = rpc.call('getblock', [blockhash, 2]) | |
coinbase_tx = block['tx'][0] | |
fees, reward, subsidy, halvening_height = get_miner_fee_stats(block["height"], coinbase_tx) | |
if block_times[-1] != head["time"]: | |
print("updating block time and miner fee list") | |
block_times.pop(0) #remove oldest block | |
block_times.append(head['time']) #add new block | |
miner_fee_data.pop(0) | |
miner_fee_data.append((fees, reward)) | |
else: | |
print("not updating block time and miner fee list") | |
coinbase_str = coinbase_tx['vin'][0]['coinbase'] | |
readable_coinbase = ''.join([i for i in coinbase_str.decode('hex') | |
if i < '\x7f' and i > '\x19']) | |
windows = [6, 36, 144, 432, 1008, 2016] | |
#note the off-by-one error avoided here, 6 blocks have 5 intervals between them | |
intervals = [(block_times[-1] - block_times[-w])/(w-1) for w in windows] | |
fees_list, rewards_list = zip(*miner_fee_data) | |
until_retarget = -(bci['blocks'] % -2016) | |
utxos_produced = 0 | |
utxos_consumed = 0 | |
for tx in block['tx']: | |
utxos_produced += len(tx['vout']) | |
utxos_consumed += len(tx['vin']) | |
message = '\x0303BLOCK\x03' | |
message += ' hash=' + blockhash | |
#message += ' prevhash ' + head['previousblockhash'] | |
message += ' height=' + str(bci['blocks']) | |
message += (' ts=' + | |
datetime.fromtimestamp(head['time']).strftime("%Y-%m-%d %H:%M:%S")) | |
message += ' tx=' + str(len(block['tx'])) | |
message += ' outs=' + str(utxos_produced) + ' ins=' + str(utxos_consumed) + ' out-in=' + '%+d'%(utxos_produced - utxos_consumed) | |
message += ' outs/tx=' + ('%.3f' % (1.0*utxos_produced / len(block['tx']))) + ' ins/tx=' + ('%.3f' % (1.0*utxos_consumed / len(block['tx']))) | |
message += ' fees=' + str(fees) + 'btc(' + ('%.2f%%)' % (100.0*fees / reward)) | |
#message += ' size=' + ('%.0f' % (block['size']/1000.0)) + 'kB' | |
message += ' weight=' + str(block['weight']) + '(%.0f%%)' % (100.0 * block['weight'] / MAX_WEIGHT) | |
message += ' interval=' + str(strtimediff(head['time'] - old_head[0]['time'])) | |
message += ' minermsg=' + readable_coinbase | |
message2 = 'median=' + datetime.fromtimestamp(bci['mediantime']).strftime("%Y-%m-%d %H:%M:%S") | |
#message2 += ' average-intervals(' + ', '.join([str(w) for w in windows]) + ')blocks=(' + ', '.join([str(strtimediff(d)) for d in intervals]) + ')' | |
message2 += ' avg (blocks, intervals, fee%) = (' + ', '.join([str(w) for w in windows]) + '), (' + ', '.join([str(strtimediff(d)) for d in intervals]) + '), (' + ', '.join(["%.2f%%" % (100.0*sum(fees_list[-w:])/sum(rewards_list[-w:])) for w in windows]) + ')' | |
message2 += ' retarget=' + str(until_retarget) + 'blocks(' + str(until_retarget/144) + 'days)' | |
#print 'diff=' + str(bci['difficulty']) + ' olddiff=' + str(old_bci[0]['difficulty']) | |
if bci['difficulty'] != old_bci[0]['difficulty']: | |
message2 += (' RETARGET! new difficulty=' + str(bci['difficulty']) + '(' + ('%+.1f%%' | |
% ((bci['difficulty'] - old_bci[0]['difficulty']) / | |
old_bci[0]['difficulty'] * 100)) + ')') | |
until_halvening = halvening_height - bci["blocks"] | |
#message2 += " halvening=" + str(until_halvening) + "blocks(" + str(until_halvening/144) + "days) subsidy=" + str(subsidy) + "btc" | |
old_bci[0] = bci | |
old_head[0] = head | |
et = time.time() | |
print 'block stats = ' + str(et - st) + 'sec, msglength=' + str(len(message)) | |
return message, message2 | |
def check_new_block(sock): | |
try: | |
blockhash = rpc.call('getbestblockhash', []) | |
if blockhash == old_bci[0]['bestblockhash']: | |
return | |
last_fee_estimate[0] = datetime.fromtimestamp(0) | |
message, message2 = create_block_stats(blockhash) | |
print message | |
print message2 | |
sock.sendall('PRIVMSG ' + channel + ' :' + message + '\r\nPRIVMSG ' + | |
channel + ' :' + message2 + '\r\n') | |
check_fee_estimate(sock) | |
except JsonRpcError as e: | |
print repr(e) | |
def handle_irc_line(sock, line, chunks): | |
if len(chunks) < 2: | |
return | |
# sock.sendall('JOIN ' + channel + '\r\n') | |
#nuh_chunks = chunks[0].split('!') | |
#if nuh_chunks[0] == ':NickServ' and 'registered' in line and 'identify' in line: | |
if chunks[1] == '376': ##end of modt | |
print 'sending nickserv password' | |
time.sleep(5) #sleep because technically you need to send the password after nickserv asks for it | |
line = 'PRIVMSG NickServ :identify ' + nickserv_password + '\r\n' | |
#print(line) | |
sock.sendall(line) | |
if chunks[1] == '396': | |
#:sinisalo.freenode.net 396 beIcher unaffiliated/belcher :is now your hidden host (set by services.) | |
print 'joining channel' | |
sock.sendall('JOIN ' + channel + '\r\n') | |
print create_block_stats(old_bci[0]['bestblockhash']) | |
print create_fee_rate_stats() | |
time.sleep(3) | |
''' | |
import sys | |
sys.exit(0) | |
''' | |
while True: | |
try: | |
print 'connecting' | |
sock = socket.socket() | |
sock.connect(hostport) | |
print 'connected' | |
sock.settimeout(check_for_new_block_interval) | |
sock.sendall('USER ' + nick + ' b c :' + nick + '\r\n') | |
sock.sendall('NICK ' + nick + '\r\n') | |
recv_buffer = "" | |
last_ping = datetime.now() | |
waiting_for_pong = False | |
while True: | |
try: | |
#print 'reading' | |
recv_data = sock.recv(4096) | |
if not recv_data or len(recv_data) == 0: | |
raise EOFError() | |
recv_buffer += recv_data | |
lb = recv_data.find('\n') | |
if lb == -1: | |
continue | |
while lb != -1: | |
line = recv_buffer[:lb].rstrip() | |
recv_buffer = recv_buffer[lb + 1:] | |
lb = recv_buffer.find('\n') | |
#print str(datetime.now()) + ' ' + line | |
#print line | |
chunks = line.split(' ') | |
if chunks[0] == 'PING': | |
sock.sendall(line.replace('PING', 'PONG') + '\r\n') | |
elif len(chunks) > 1 and chunks[1] == 'PONG': | |
#print 'server replied to ping' | |
last_ping = datetime.now() | |
waiting_for_pong = False | |
else: | |
print(line) | |
handle_irc_line(sock, line, chunks) | |
except socket.timeout: | |
#print 'timed out' | |
check_new_block(sock) | |
check_fee_estimate(sock) | |
if waiting_for_pong: | |
if (datetime.now() - last_ping).total_seconds() < ping_timeout_seconds: | |
continue | |
print 'server ping timed out' | |
sock.close() | |
else: | |
if (datetime.now() - last_ping).total_seconds() < ping_interval_seconds: | |
continue | |
last_ping = datetime.now() | |
#print 'sending ping to server' | |
waiting_for_pong = True | |
sock.sendall('PING :hello world\r\n') | |
except (IOError, EOFError) as e: | |
print repr(e) | |
time.sleep(5) | |
finally: | |
try: | |
sock.close() | |
except IOError: | |
pass |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment