├── README.md ├── LICENSE ├── feeloop.py └── authproxy.py /README.md: -------------------------------------------------------------------------------- 1 | # CoreFeeHelper 2 | Muh Twitter feebot @CoreFeeHelper 3 | 4 | How to install: 5 | ``` 6 | git clone git@github.com:instagibbs/CoreFeeHelper.git 7 | cd CoreFeeHelper 8 | pip install tweepy # Or install your own favorite way 9 | ``` 10 | 11 | 12 | ``` 13 | ./feeloop.py 14 | ``` 15 | 16 | Profit. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gregory Sanders 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /feeloop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | from authproxy import AuthServiceProxy 5 | import tweepy 6 | import random 7 | import urllib.request 8 | import json 9 | import re 10 | import sys 11 | 12 | # Requires running Core RPC server on standard mainnet RPC port 13 | if len(sys.argv) < 7: 14 | raise Exception('feeloop.py ') 15 | 16 | while True: 17 | bitcoin_req = "http://"+sys.argv[1]+":"+sys.argv[2]+"@127.0.0.1:8332" 18 | bitcoin = AuthServiceProxy(bitcoin_req) 19 | 20 | def get_rounded_feerate(result): 21 | rate = str(int(result*1000000)/10.0)+" sat/byte " 22 | if len(re.split("\.", rate)[0]) == 1: 23 | rate = " "+rate 24 | return rate 25 | 26 | try: 27 | mempool_info = bitcoin.getmempoolinfo() 28 | nextblock = ["Next: ", bitcoin.estimatesmartfee(1, "ECONOMICAL")["feerate"]] 29 | hour = ["1h: ", bitcoin.estimatesmartfee(6, "ECONOMICAL")["feerate"]] 30 | six_hours = ["6h: ", bitcoin.estimatesmartfee(6*6, "ECONOMICAL")["feerate"]] 31 | twelve_hours = ["12h: ", bitcoin.estimatesmartfee(6*12, "ECONOMICAL")["feerate"]] 32 | day = ["1d: ", bitcoin.estimatesmartfee(144, "ECONOMICAL")["feerate"]] 33 | half_week = ["3d: ", bitcoin.estimatesmartfee(int(144*3.5), "ECONOMICAL")["feerate"]] 34 | week = ["1wk: ", bitcoin.estimatesmartfee(144*7, "ECONOMICAL")["feerate"]] 35 | mem_min = ["Min: ", mempool_info["mempoolminfee"]] 36 | 37 | bitstampprice = urllib.request.urlopen("https://www.bitstamp.net/api/v2/ticker/btcusd/").read() 38 | latest_price = float(json.loads(bitstampprice)["last"]) 39 | price_for_250 = latest_price*(211/1000) # Price for 2-input-2-output taproot rx (211 vbytes) 40 | 41 | tweet = "" 42 | for estimate in [nextblock, hour, six_hours, twelve_hours, day, half_week, week, mem_min]: 43 | tweet += estimate[0]+get_rounded_feerate(estimate[1]) + " ${:0.2f}".format(round(price_for_250*float(estimate[1]),2))+"\n" 44 | 45 | count_str = f"{bitcoin.getblockcount():,d}" 46 | tweet += "Block height: "+ count_str+"\n" 47 | tweet += "Mempool depth: "+str(int(mempool_info["bytes"]/1000/1000)) 48 | 49 | except Exception as e: 50 | print("Couldn't estimate. Sleeping: {}".format(str(e))) 51 | time.sleep(3600) 52 | continue 53 | 54 | try: 55 | client = tweepy.Client( 56 | consumer_key=sys.argv[3], 57 | consumer_secret=sys.argv[4], 58 | access_token=sys.argv[5], 59 | access_token_secret=sys.argv[6], 60 | ) 61 | 62 | # Post the tweet 63 | response = client.create_tweet(text=tweet) 64 | 65 | # Check response 66 | if response.data: 67 | print("Tweet successfully posted!") 68 | print("Tweet ID:", response.data['id']) 69 | else: 70 | print("Failed to post tweet. Response:", response) 71 | 72 | except Exception as err: 73 | print("Error: "+str(err)) 74 | print(tweet) 75 | print("------------------") 76 | pass 77 | 78 | 79 | time.sleep(3600) 80 | -------------------------------------------------------------------------------- /authproxy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Jeff Garzik 2 | # 3 | # Previous copyright, from python-jsonrpc/jsonrpc/proxy.py: 4 | # 5 | # Copyright (c) 2007 Jan-Klaas Kollhof 6 | # 7 | # This file is part of jsonrpc. 8 | # 9 | # jsonrpc is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU Lesser General Public License as published by 11 | # the Free Software Foundation; either version 2.1 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This software is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Lesser General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Lesser General Public License 20 | # along with this software; if not, write to the Free Software 21 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | """HTTP proxy for opening RPC connection to bitcoind. 23 | 24 | AuthServiceProxy has the following improvements over python-jsonrpc's 25 | ServiceProxy class: 26 | 27 | - HTTP connections persist for the life of the AuthServiceProxy object 28 | (if server supports HTTP/1.1) 29 | - sends protocol 'version', per JSON-RPC 1.1 30 | - sends proper, incrementing 'id' 31 | - sends Basic HTTP authentication headers 32 | - parses all JSON numbers that look like floats as Decimal 33 | - uses standard Python json lib 34 | """ 35 | 36 | import base64 37 | import decimal 38 | from http import HTTPStatus 39 | import http.client 40 | import json 41 | import logging 42 | import os 43 | import socket 44 | import time 45 | import urllib.parse 46 | 47 | HTTP_TIMEOUT = 30 48 | USER_AGENT = "AuthServiceProxy/0.1" 49 | 50 | log = logging.getLogger("BitcoinRPC") 51 | 52 | class JSONRPCException(Exception): 53 | def __init__(self, rpc_error, http_status=None): 54 | try: 55 | errmsg = '%(message)s (%(code)i)' % rpc_error 56 | except (KeyError, TypeError): 57 | errmsg = '' 58 | super().__init__(errmsg) 59 | self.error = rpc_error 60 | self.http_status = http_status 61 | 62 | 63 | def EncodeDecimal(o): 64 | if isinstance(o, decimal.Decimal): 65 | return str(o) 66 | raise TypeError(repr(o) + " is not JSON serializable") 67 | 68 | class AuthServiceProxy(): 69 | __id_count = 0 70 | 71 | # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps 72 | def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True): 73 | self.__service_url = service_url 74 | self._service_name = service_name 75 | self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests 76 | self.__url = urllib.parse.urlparse(service_url) 77 | user = None if self.__url.username is None else self.__url.username.encode('utf8') 78 | passwd = None if self.__url.password is None else self.__url.password.encode('utf8') 79 | authpair = user + b':' + passwd 80 | self.__auth_header = b'Basic ' + base64.b64encode(authpair) 81 | self.timeout = timeout 82 | self._set_conn(connection) 83 | 84 | def __getattr__(self, name): 85 | if name.startswith('__') and name.endswith('__'): 86 | # Python internal stuff 87 | raise AttributeError 88 | if self._service_name is not None: 89 | name = "%s.%s" % (self._service_name, name) 90 | return AuthServiceProxy(self.__service_url, name, connection=self.__conn) 91 | 92 | def _request(self, method, path, postdata): 93 | ''' 94 | Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout). 95 | This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5. 96 | ''' 97 | headers = {'Host': self.__url.hostname, 98 | 'User-Agent': USER_AGENT, 99 | 'Authorization': self.__auth_header, 100 | 'Content-type': 'application/json'} 101 | if os.name == 'nt': 102 | # Windows somehow does not like to re-use connections 103 | # TODO: Find out why the connection would disconnect occasionally and make it reusable on Windows 104 | # Avoid "ConnectionAbortedError: [WinError 10053] An established connection was aborted by the software in your host machine" 105 | self._set_conn() 106 | try: 107 | self.__conn.request(method, path, postdata, headers) 108 | return self._get_response() 109 | except (BrokenPipeError, ConnectionResetError): 110 | # Python 3.5+ raises BrokenPipeError when the connection was reset 111 | # ConnectionResetError happens on FreeBSD 112 | self.__conn.close() 113 | self.__conn.request(method, path, postdata, headers) 114 | return self._get_response() 115 | except OSError as e: 116 | retry = ( 117 | '[WinError 10053] An established connection was aborted by the software in your host machine' in str(e)) 118 | # Workaround for a bug on macOS. See https://bugs.python.org/issue33450 119 | retry = retry or ('[Errno 41] Protocol wrong type for socket' in str(e)) 120 | if retry: 121 | self.__conn.close() 122 | self.__conn.request(method, path, postdata, headers) 123 | return self._get_response() 124 | else: 125 | raise 126 | 127 | def get_request(self, *args, **argsn): 128 | AuthServiceProxy.__id_count += 1 129 | 130 | log.debug("-{}-> {} {}".format( 131 | AuthServiceProxy.__id_count, 132 | self._service_name, 133 | json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii), 134 | )) 135 | if args and argsn: 136 | raise ValueError('Cannot handle both named and positional arguments') 137 | return {'version': '1.1', 138 | 'method': self._service_name, 139 | 'params': args or argsn, 140 | 'id': AuthServiceProxy.__id_count} 141 | 142 | def __call__(self, *args, **argsn): 143 | postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) 144 | response, status = self._request('POST', self.__url.path, postdata.encode('utf-8')) 145 | if response['error'] is not None: 146 | raise JSONRPCException(response['error'], status) 147 | elif 'result' not in response: 148 | raise JSONRPCException({ 149 | 'code': -343, 'message': 'missing JSON-RPC result'}, status) 150 | elif status != HTTPStatus.OK: 151 | raise JSONRPCException({ 152 | 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) 153 | else: 154 | return response['result'] 155 | 156 | def batch(self, rpc_call_list): 157 | postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) 158 | log.debug("--> " + postdata) 159 | response, status = self._request('POST', self.__url.path, postdata.encode('utf-8')) 160 | if status != HTTPStatus.OK: 161 | raise JSONRPCException({ 162 | 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) 163 | return response 164 | 165 | def _get_response(self): 166 | req_start_time = time.time() 167 | try: 168 | http_response = self.__conn.getresponse() 169 | except socket.timeout: 170 | raise JSONRPCException({ 171 | 'code': -344, 172 | 'message': '%r RPC took longer than %f seconds. Consider ' 173 | 'using larger timeout for calls that take ' 174 | 'longer to return.' % (self._service_name, 175 | self.__conn.timeout)}) 176 | if http_response is None: 177 | raise JSONRPCException({ 178 | 'code': -342, 'message': 'missing HTTP response from server'}) 179 | 180 | content_type = http_response.getheader('Content-Type') 181 | if content_type != 'application/json': 182 | raise JSONRPCException( 183 | {'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}, 184 | http_response.status) 185 | 186 | responsedata = http_response.read().decode('utf8') 187 | response = json.loads(responsedata, parse_float=decimal.Decimal) 188 | elapsed = time.time() - req_start_time 189 | if "error" in response and response["error"] is None: 190 | log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) 191 | else: 192 | log.debug("<-- [%.6f] %s" % (elapsed, responsedata)) 193 | return response, http_response.status 194 | 195 | def __truediv__(self, relative_uri): 196 | return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn) 197 | 198 | def _set_conn(self, connection=None): 199 | port = 80 if self.__url.port is None else self.__url.port 200 | if connection: 201 | self.__conn = connection 202 | self.timeout = connection.timeout 203 | elif self.__url.scheme == 'https': 204 | self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=self.timeout) 205 | else: 206 | self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=self.timeout) 207 | --------------------------------------------------------------------------------