Skip to content

Instantly share code, notes, and snippets.

@chris-belcher
Last active November 7, 2021 20:42
Show Gist options
  • Save chris-belcher/61b1abf9f60063aac495a2546876b3e2 to your computer and use it in GitHub Desktop.
Save chris-belcher/61b1abf9f60063aac495a2546876b3e2 to your computer and use it in GitHub Desktop.
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 <[email protected]> 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.libera.chat', 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 = '')
#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*20
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, reward=None):
subsidy = 6.25
halvening_height = 840000
if height >= halvening_height:
subsidy = 3.125
if reward is None:
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]])
block_template = rpc.call("getblocktemplate", [{"rules": ["segwit"]}])
message += " NEXT BLOCK"
fees, reward, subsidy, halvening_height = get_miner_fee_stats(block_template["height"], None, block_template["coinbasevalue"]/1e8)
message += ' fees=' + str(fees) + 'btc(' + ('%.2f%%)' % (100.0*fees / reward))
message += " txes=" + str(len(block_template["transactions"]))
non_cpfp_template_txes = filter(lambda tx: len(tx["depends"]) == 0, block_template["transactions"])
non_cpfp_template_txes = sorted(non_cpfp_template_txes, key=lambda x : -x["fee"]/x["weight"])
message += " cpfp=" + str(len(block_template["transactions"]) - len(non_cpfp_template_txes))
if len(non_cpfp_template_txes) > 0:
message += " feerates(sat/vbyte):"
first_tx = non_cpfp_template_txes[0]
message += " top=%.2f" % (first_tx["fee"] * 4.0 / first_tx["weight"],)
median_tx = non_cpfp_template_txes[len(non_cpfp_template_txes) // 2]
message += " median=%.2f" % (median_tx["fee"] * 4.0 / median_tx["weight"],)
last_tx = non_cpfp_template_txes[-1]
message += " last=%.2f" % (last_tx["fee"] * 4.0 / last_tx["weight"],)
print("fee rates = " + str([int(tx["fee"]*4 / tx["weight"]) for tx in non_cpfp_template_txes]))
#TODO maybe also cost per input/cost per output
et = time.time()
print 'mempool time taken = ' + 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)
#message += ' 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)) + ')')
#bit2set = ((head["version"] >> 2) & 1) == 1
#message2 += " bit2=" + str(bit2set)
until_halvening = halvening_height - bci["blocks"]
#message2 += " halvening=" + str(until_halvening) + "blocks(" + str(until_halvening/144) + "days) subsidy=" + str(subsidy) + "btc"
TAPROOT_ACTIVATION_HEIGHT = 709632
until_taproot_activation = TAPROOT_ACTIVATION_HEIGHT - bci["blocks"]
message2 += " TaprootActivation=" + str(until_taproot_activation) + "blocks(" + str(until_taproot_activation/144) + "days) active=" + ("no" if until_taproot_activation > 0 else "YES!")
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
if line.startswith(':NickServ') and 'registered' in line and 'identify' in line:
print 'sending nickserv password'
time.sleep(4) #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')
elif chunks[1] == "404" or chunks[1] == "437":
#:hitchcock.freenode.net 404 Guest58403 #bitcoin-blocks :Cannot send to nick/channel
#:tolkien.freenode.net 437 * Alectryon :Nick/channel is temporarily unavailable
print 'releasing nickserv id'
sock.sendall('PRIVMSG NickServ :release ' + nick + ' ' + nickserv_password + '\r\n')
time.sleep(5)
sock.sendall('NICK ' + nick + '\r\n')
time.sleep(5)
sock.sendall('PRIVMSG NickServ :identify ' + nickserv_password + '\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