Initial commit
commit
0d4a8777e6
@ -0,0 +1,6 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
*.swp
|
||||
*.log
|
||||
*.conf
|
||||
.ropeproject
|
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,64 @@
|
||||
# bitcoind-ncurses2 v0.2.0-dev
|
||||
|
||||
## This repository is under heavy development. Safety not guaranteed.
|
||||
|
||||
Python ncurses front-end for bitcoind. Uses the JSON-RPC API.
|
||||
|
||||
![ScreenShot](/img/bitcoind-ncurses2.gif)
|
||||
|
||||
- esotericnonsense (Daniel Edgecumbe)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Developed with python 3.6.2, Bitcoin Core 0.15.0.1
|
||||
* PyPi packages: aiohttp and async-timeout (see requirements.txt)
|
||||
|
||||
## Features
|
||||
|
||||
* Updating monitor mode showing bitcoind's status, including:
|
||||
* Current block information: hash, height, fees, timestamp, age, diff, ...
|
||||
* Wallet balance
|
||||
* Total bandwidth used (up / down)
|
||||
* Connected peers, IP addresses, user agents, ...
|
||||
|
||||
## Installation and usage
|
||||
|
||||
```
|
||||
git clone https://github.com/esotericnonsense/bitcoind-ncurses2
|
||||
```
|
||||
|
||||
```
|
||||
pip install -r bitcoind-ncurses2/requirements.txt
|
||||
```
|
||||
or, on Arch Linux:
|
||||
```
|
||||
pacman -S python-aiohttp python-async-timeout
|
||||
```
|
||||
|
||||
```
|
||||
cd bitcoind-ncurses2
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
bitcoind-ncurses2 will automatically use the cookie file available in
|
||||
~/.bitcoin/, or the RPC settings in ~/.bitcoin/bitcoin.conf. To use a different
|
||||
datadir, specify the --datadir flag:
|
||||
|
||||
```
|
||||
python3 main.py --datadir /some/path/to/your/datadir
|
||||
```
|
||||
|
||||
This is an early development release and a complete rewrite of the original
|
||||
bitcoind-ncurses. Expect the unexpected.
|
||||
|
||||
Donations
|
||||
---------
|
||||
|
||||
If you have found bitcoind-ncurses2 useful, please consider donating.
|
||||
|
||||
All funds go towards the operating costs of my hardware and future
|
||||
Bitcoin development projects.
|
||||
|
||||
![ScreenShot](/img/3BYFucUnVNhZjUDf6tZweuZ5r9PPjPEcRv.png)
|
||||
|
||||
**bitcoin 3BYFucUnVNhZjUDf6tZweuZ5r9PPjPEcRv**
|
@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
|
||||
def parse_file(filename):
|
||||
with open(filename, "r") as f:
|
||||
cfg = {}
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
try:
|
||||
# replace maintains compatibility with older config files
|
||||
(key, value) = line.replace(' = ', '=').split('=', 1)
|
||||
cfg[key] = value
|
||||
except ValueError:
|
||||
# Happens when line has no '=', ignore
|
||||
pass
|
||||
|
||||
return cfg
|
@ -0,0 +1,65 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
import curses
|
||||
|
||||
from macros import MODES
|
||||
|
||||
|
||||
class FooterView(object):
|
||||
def __init__(self):
|
||||
self._pad = None
|
||||
|
||||
self._mode = None
|
||||
self._dt = None
|
||||
|
||||
self._callbacks = set()
|
||||
|
||||
def add_callback(self, callback):
|
||||
self._callbacks.add(callback)
|
||||
|
||||
def draw(self):
|
||||
# TODO: figure out window width etc.
|
||||
if self._pad is None:
|
||||
self._pad = curses.newpad(2, 100)
|
||||
else:
|
||||
self._pad.clear()
|
||||
|
||||
CYELLOW = curses.color_pair(5)
|
||||
CREVERSE = curses.A_REVERSE
|
||||
|
||||
x = 1
|
||||
for mode_string in MODES:
|
||||
first_char, rest = mode_string[0], mode_string[1:]
|
||||
modifier = curses.A_BOLD
|
||||
if self._mode == mode_string:
|
||||
# first_char = first_char.upper()
|
||||
modifier += CREVERSE
|
||||
self._pad.addstr(0, x, first_char, modifier + CYELLOW)
|
||||
self._pad.addstr(0, x+1, rest, modifier)
|
||||
x += len(mode_string) + 4
|
||||
|
||||
if self._dt:
|
||||
self._pad.addstr(0, 81, self._dt.isoformat(timespec="seconds")[:19])
|
||||
|
||||
self._pad.refresh(0, 0, 25, 0, 27, 100)
|
||||
|
||||
async def on_mode_change(self, newmode, seek=None):
|
||||
if seek is not None:
|
||||
assert newmode is None
|
||||
if self._mode is None:
|
||||
return
|
||||
idx = MODES.index(self._mode)
|
||||
idx = (idx + seek) % len(MODES)
|
||||
newmode = MODES[idx]
|
||||
|
||||
self._mode = newmode
|
||||
self.draw()
|
||||
|
||||
for callback in self._callbacks:
|
||||
await callback(newmode)
|
||||
|
||||
async def on_tick(self, dt):
|
||||
self._dt = dt
|
||||
self.draw()
|
@ -0,0 +1,151 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
import curses
|
||||
import platform
|
||||
|
||||
from macros import VERSION_STRING
|
||||
|
||||
|
||||
class HeaderView(object):
|
||||
def __init__(self):
|
||||
# one larger than we will ever draw otherwise we can't populate
|
||||
# the bottom-right
|
||||
self._pad = curses.newpad(2, 101)
|
||||
|
||||
self._platform = "{} {} {}".format(
|
||||
platform.system(),
|
||||
platform.release(),
|
||||
platform.machine(),
|
||||
)
|
||||
|
||||
self._subversion = None
|
||||
self._chain = None
|
||||
self._connectioncount = None
|
||||
self._nettotals = None
|
||||
self._balance = None
|
||||
|
||||
def draw(self):
|
||||
# TODO: figure out window width etc.
|
||||
|
||||
self._pad.clear()
|
||||
|
||||
CGREEN = curses.color_pair(1)
|
||||
CCYAN = curses.color_pair(2)
|
||||
CRED = curses.color_pair(3)
|
||||
CYELLOW = curses.color_pair(5)
|
||||
CBOLD = curses.A_BOLD
|
||||
|
||||
colors = {
|
||||
"main": CGREEN + CBOLD,
|
||||
"test": CCYAN + CBOLD,
|
||||
"regtest": CRED + CBOLD,
|
||||
}
|
||||
currencies = {
|
||||
"main": "BTC",
|
||||
"test": "tBC",
|
||||
"regtest": "rBC",
|
||||
}
|
||||
version_color = colors.get(self._chain, CBOLD)
|
||||
currency = currencies.get(self._chain, "???")
|
||||
chn = self._chain if self._chain is not None else "???"
|
||||
|
||||
self._pad.addstr(0, 1, "{} ({})".format(
|
||||
VERSION_STRING[:30],
|
||||
chn
|
||||
), version_color)
|
||||
|
||||
if self._connectioncount is not None:
|
||||
if self._connectioncount > 8:
|
||||
peercolor = CGREEN + CBOLD
|
||||
elif self._connectioncount > 0:
|
||||
peercolor = CBOLD
|
||||
else:
|
||||
peercolor = CRED + CBOLD
|
||||
|
||||
self._pad.addstr(0, 37, "{: 4d} {}".format(
|
||||
self._connectioncount,
|
||||
"peers" if self._connectioncount != 1 else "peer"
|
||||
), peercolor)
|
||||
|
||||
if self._subversion:
|
||||
self._pad.addstr(1, 1, "{} / {}".format(
|
||||
self._platform[:27],
|
||||
self._subversion.strip("/").strip(":")[:18]
|
||||
), version_color)
|
||||
|
||||
if self._nettotals is not None:
|
||||
self._pad.addstr(0, 51, "Up: {: 9.2f} MB".format(
|
||||
self._nettotals[1] / 1048576,
|
||||
), CBOLD + CCYAN)
|
||||
self._pad.addstr(1, 51, "Down: {: 9.2f} MB".format(
|
||||
self._nettotals[0] / 1048576,
|
||||
), CBOLD + CGREEN)
|
||||
|
||||
if self._balance is not None:
|
||||
self._pad.addstr(0, 82, "{: 14.8f} {}".format(
|
||||
self._balance[0],
|
||||
currency
|
||||
), CBOLD)
|
||||
|
||||
# We only show unconfirmed if we have both unc/imm. So it goes.
|
||||
if self._balance[1] != 0:
|
||||
self._pad.addstr(1, 69, "unconfirmed: {: 14.8f} {}".format(
|
||||
self._balance[1],
|
||||
currency,
|
||||
), CBOLD + CYELLOW)
|
||||
elif self._balance[2] != 0:
|
||||
self._pad.addstr(1, 72, "immature: {: 14.8f} {}".format(
|
||||
self._balance[2],
|
||||
currency,
|
||||
), CBOLD + CRED)
|
||||
else:
|
||||
self._pad.addstr(0, 74, "wallet disabled")
|
||||
|
||||
self._pad.refresh(0, 0, 1, 0, 2, 100)
|
||||
|
||||
async def on_networkinfo(self, key, obj):
|
||||
try:
|
||||
self._subversion = obj["result"]["subversion"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.draw()
|
||||
|
||||
async def on_blockchaininfo(self, key, obj):
|
||||
try:
|
||||
self._chain = obj["result"]["chain"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.draw()
|
||||
|
||||
async def on_peerinfo(self, key, obj):
|
||||
try:
|
||||
self._connectioncount = len(obj["result"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.draw()
|
||||
|
||||
async def on_nettotals(self, key, obj):
|
||||
try:
|
||||
tbr = obj["result"]["totalbytesrecv"]
|
||||
tbs = obj["result"]["totalbytessent"]
|
||||
self._nettotals = (tbr, tbs)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.draw()
|
||||
|
||||
async def on_walletinfo(self, key, obj):
|
||||
try:
|
||||
bal = obj["result"]["balance"]
|
||||
ubal = obj["result"]["unconfirmed_balance"]
|
||||
ibal = obj["result"]["immature_balance"]
|
||||
self._balance = (bal, ubal, ibal)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.draw()
|
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
import curses
|
||||
|
||||
|
||||
def init_curses():
|
||||
window = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.curs_set(0)
|
||||
|
||||
curses.start_color()
|
||||
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
||||
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
||||
curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||||
|
||||
window.timeout(50)
|
||||
window.keypad(1)
|
||||
|
||||
return window
|
||||
|
||||
|
||||
def end_curses():
|
||||
curses.nocbreak()
|
||||
curses.endwin()
|
@ -0,0 +1,12 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
VERSION_STRING = "bitcoind-ncurses v0.2.0-dev"
|
||||
|
||||
# MODES = [
|
||||
# "monitor", "wallet", "peers", "block",
|
||||
# "tx", "console", "net", "forks",
|
||||
# ]
|
||||
MODES = ["monitor", "peers"]
|
||||
DEFAULT_MODE = "monitor"
|
@ -0,0 +1,169 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
import rpc
|
||||
import interface
|
||||
import header
|
||||
import footer
|
||||
import monitor
|
||||
import peers
|
||||
|
||||
from macros import MODES, DEFAULT_MODE
|
||||
|
||||
|
||||
async def handle_hotkeys(window, callback):
|
||||
|
||||
async def handle_key(key):
|
||||
if key == "KEY_LEFT":
|
||||
await callback(None, seek=-1)
|
||||
return
|
||||
|
||||
if key == "KEY_RIGHT":
|
||||
await callback(None, seek=1)
|
||||
return
|
||||
|
||||
if len(key) > 1:
|
||||
return
|
||||
|
||||
lower = key.lower()
|
||||
|
||||
for mode in MODES:
|
||||
if mode[0] == lower:
|
||||
await callback(mode)
|
||||
|
||||
first = True
|
||||
while True:
|
||||
# This is basically spinning which is really annoying.
|
||||
# TODO: find a way of having async blocking getch/getkey.
|
||||
try:
|
||||
key = window.getkey()
|
||||
except Exception:
|
||||
# This is bonkers and I don't understand it.
|
||||
if first:
|
||||
await callback(DEFAULT_MODE)
|
||||
first = False
|
||||
|
||||
await asyncio.sleep(0.05)
|
||||
continue
|
||||
|
||||
await handle_key(key)
|
||||
|
||||
|
||||
async def poll_client(client, method, callback, sleeptime):
|
||||
# Allow the rest of the program to start.
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
while True:
|
||||
j = await client.request(method)
|
||||
await callback(method, j)
|
||||
await asyncio.sleep(sleeptime)
|
||||
|
||||
|
||||
async def tick(callback, sleeptime):
|
||||
# Allow the rest of the program to start.
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
while True:
|
||||
dt = datetime.datetime.utcnow()
|
||||
await callback(dt)
|
||||
await asyncio.sleep(sleeptime)
|
||||
|
||||
|
||||
def initialize():
|
||||
# parse commandline arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--datadir",
|
||||
help="path to bitcoin datadir [~/.bitcoin/]",
|
||||
default=os.path.expanduser("~/.bitcoin/"))
|
||||
args = parser.parse_args()
|
||||
|
||||
url = rpc.get_url_from_datadir(args.datadir)
|
||||
auth = rpc.get_auth_from_datadir(args.datadir)
|
||||
client = rpc.BitcoinRPCClient(url, auth)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def check_disablewallet(client):
|
||||
""" Check if the wallet is enabled. """
|
||||
|
||||
# Ugly, a synchronous RPC request mechanism would be nice here.
|
||||
x = asyncio.gather(client.request("getwalletinfo"))
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(x)
|
||||
|
||||
try:
|
||||
x.result()[0]["result"]["walletname"]
|
||||
except (KeyError, TypeError):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def create_tasks(client, window):
|
||||
headerview = header.HeaderView()
|
||||
footerview = footer.FooterView()
|
||||
|
||||
monitorview = monitor.MonitorView(client)
|
||||
peerview = peers.PeersView()
|
||||
footerview.add_callback(monitorview.on_mode_change)
|
||||
footerview.add_callback(peerview.on_mode_change)
|
||||
|
||||
async def on_peerinfo(key, obj):
|
||||
await headerview.on_peerinfo(key, obj)
|
||||
await peerview.on_peerinfo(key, obj)
|
||||
|
||||
async def on_tick(dt):
|
||||
await footerview.on_tick(dt)
|
||||
await monitorview.on_tick(dt)
|
||||
|
||||
tasks = [
|
||||
poll_client(client, "getbestblockhash",
|
||||
monitorview.on_bestblockhash, 1.0),
|
||||
poll_client(client, "getblockchaininfo",
|
||||
headerview.on_blockchaininfo, 5.0),
|
||||
poll_client(client, "getnetworkinfo",
|
||||
headerview.on_networkinfo, 5.0),
|
||||
poll_client(client, "getnettotals",
|
||||
headerview.on_nettotals, 5.0),
|
||||
poll_client(client, "getpeerinfo",
|
||||
on_peerinfo, 5.0),
|
||||
tick(on_tick, 1.0),
|
||||
handle_hotkeys(window, footerview.on_mode_change)
|
||||
]
|
||||
|
||||
if not check_disablewallet(client):
|
||||
tasks.append(
|
||||
poll_client(client, "getwalletinfo", headerview.on_walletinfo, 1.0)
|
||||
)
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def mainfn():
|
||||
client = initialize()
|
||||
|
||||
try:
|
||||
window = interface.init_curses()
|
||||
tasks = create_tasks(client, window)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
t = asyncio.gather(*tasks)
|
||||
loop.run_until_complete(t)
|
||||
|
||||
finally:
|
||||
try:
|
||||
loop.close()
|
||||
except BaseException:
|
||||
pass
|
||||
interface.end_curses()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mainfn()
|
@ -0,0 +1,155 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
import datetime
|
||||
import math
|
||||
import curses
|
||||
import asyncio
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class MonitorView(object):
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
|
||||
self._pad = None
|
||||
|
||||
self._visible = False
|
||||
|
||||
self._lock = asyncio.Lock()
|
||||
self._bestblockhash = None
|
||||
self._bestblockheader = None # raw json blockheader
|
||||
self._bestblock = None # raw json block
|
||||
self._bestcoinbase = None # raw json tx
|
||||
self._dt = None
|
||||
|
||||
def _draw(self):
|
||||
# TODO: figure out window width etc.
|
||||
|
||||
if self._pad is not None:
|
||||
self._pad.clear()
|
||||
else:
|
||||
self._pad = curses.newpad(20, 100)
|
||||
|
||||
if self._bestblockheader:
|
||||
bbh = self._bestblockheader
|
||||
self._pad.addstr(0, 1, "Height: {: 8d}".format(bbh["height"]))
|
||||
self._pad.addstr(0, 36, bbh["hash"])
|
||||
|
||||
if self._bestblock:
|
||||
bb = self._bestblock
|
||||
self._pad.addstr(1, 1, "Size: {: 8d} bytes Weight: {: 8d} WU".format(
|
||||
bb["size"],
|
||||
bb["weight"]
|
||||
))
|
||||
|
||||
self._pad.addstr(1, 64, "Block timestamp: {}".format(
|
||||
datetime.datetime.utcfromtimestamp(bb["time"]),
|
||||
))
|
||||
|
||||
if self._dt:
|
||||
stampdelta = int(
|
||||
(self._dt - datetime.datetime.utcfromtimestamp(bb["time"]))
|
||||
.total_seconds())
|
||||
|
||||
if stampdelta > 3600*3: # probably syncing
|
||||
stampdelta_string = " (syncing)"
|
||||
|
||||
elif stampdelta > 0:
|
||||
m, s = divmod(stampdelta, 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
stampdelta_string = "({:d}d {:02d}:{:02d}:{:02d} by stamp)".format(d,h,m,s)
|
||||
|
||||
else:
|
||||
stampdelta_string = " (stamp in future)"
|
||||
|
||||
self._pad.addstr(2, 64, "Age: {}".format(
|
||||
stampdelta_string))
|
||||
|
||||
self._pad.addstr(2, 1, "Transactions: {} ({} bytes/tx, {} WU/tx)".format(
|
||||
len(bb["tx"]),
|
||||
bb["size"] // len(bb["tx"]),
|
||||
bb["weight"] // len(bb["tx"]),
|
||||
))
|
||||
|
||||
if self._bestcoinbase:
|
||||
bcb = self._bestcoinbase
|
||||
reward = sum(vout["value"] for vout in bcb["vout"])
|
||||
|
||||
# TODO: if chain is regtest, this is different
|
||||
halvings = bb["height"] // 210000
|
||||
block_subsidy = Decimal(50 * (0.5 ** halvings))
|
||||
|
||||
total_fees = Decimal(reward) - block_subsidy
|
||||
|
||||
self._pad.addstr(4, 1, "Block reward: {:.6f} BTC".format(
|
||||
reward))
|
||||
|
||||
if len(bb["tx"]) > 1:
|
||||
if reward > 0:
|
||||
fee_pct = total_fees * 100 / Decimal(reward)
|
||||
else:
|
||||
fee_pct = 0
|
||||
mbtc_per_tx = (total_fees / (len(bb["tx"]) - 1)) * 1000
|
||||
|
||||
# 80 bytes for the block header.
|
||||
total_tx_size = bb["size"] - 80 - bcb["size"]
|
||||
if total_tx_size > 0:
|
||||
sat_per_kb = ((total_fees * 1024) / total_tx_size) * 100000000
|
||||
else:
|
||||
sat_per_kb = 0
|
||||
self._pad.addstr(4, 34, "Fees: {: 8.6f} BTC ({: 6.2f}%, avg {: 6.2f} mBTC/tx, ~{: 7.0f} sat/kB)".format(total_fees, fee_pct, mbtc_per_tx, sat_per_kb))
|
||||
|
||||
self._pad.addstr(6, 1, "Diff: {:,}".format(
|
||||
int(bb["difficulty"]),
|
||||
))
|
||||
self._pad.addstr(7, 1, "Chain work: 2**{:.6f}".format(
|
||||
math.log(int(bb["chainwork"], 16), 2),
|
||||
))
|
||||
|
||||
self._pad.refresh(0, 0, 4, 0, 24, 100)
|
||||
|
||||
async def draw(self):
|
||||
with await self._lock:
|
||||
self._draw()
|
||||
|
||||
async def on_bestblockhash(self, key, obj):
|
||||
try:
|
||||
bestblockhash = obj["result"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
draw = False
|
||||
with await self._lock:
|
||||
if bestblockhash != self._bestblockhash:
|
||||
draw = True
|
||||
self._bestblockhash = bestblockhash
|
||||
|
||||
j = await self._client.request("getblockheader", [bestblockhash])
|
||||
self._bestblockheader = j["result"]
|
||||
|
||||
j = await self._client.request("getblock", [bestblockhash])
|
||||
self._bestblock = j["result"]
|
||||
|
||||
j = await self._client.request("getrawtransaction", [j["result"]["tx"][0], 1])
|
||||
self._bestcoinbase = j["result"]
|
||||
|
||||
if draw and self._visible:
|
||||
await self.draw()
|
||||
|
||||
async def on_tick(self, dt):
|
||||
with await self._lock:
|
||||
self._dt = dt
|
||||
|
||||
if self._visible:
|
||||
await self.draw()
|
||||
|
||||
async def on_mode_change(self, newmode):
|
||||
if newmode != "monitor":
|
||||
self._visible = False
|
||||
return
|
||||
|
||||
self._visible = True
|
||||
await self.draw()
|
@ -0,0 +1,105 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import math
|
||||
import curses
|
||||
import asyncio
|
||||
|
||||
|
||||
class PeersView(object):
|
||||
def __init__(self):
|
||||
self._pad = None
|
||||
self._visible = False
|
||||
self._peerinfo = None # raw data from getpeerinfo
|
||||
|
||||
def draw(self):
|
||||
# TODO: figure out window width etc.
|
||||
|
||||
if self._pad is not None:
|
||||
self._pad.clear()
|
||||
else:
|
||||
self._pad = curses.newpad(20, 100) # y, x
|
||||
|
||||
if self._peerinfo:
|
||||
po = self._peerinfo
|
||||
|
||||
self._pad.addstr(0, 1, "Node IP Version Recv Sent Time Height", curses.A_BOLD + curses.color_pair(5))
|
||||
|
||||
window_height = 20
|
||||
offset = 0
|
||||
for index in range(offset, offset+window_height):
|
||||
if index < len(po):
|
||||
peer = po[index]
|
||||
|
||||
condition = (index == offset+window_height-1) and (index+1 < len(state['peerinfo']))
|
||||
condition = condition or ( (index == offset) and (index > 0) )
|
||||
|
||||
if condition:
|
||||
# scrolling up or down is possible
|
||||
self._pad.addstr(1+index-offset, 3, "...")
|
||||
|
||||
else:
|
||||
if peer['inbound']:
|
||||
self._pad.addstr(1+index-offset, 1, 'I')
|
||||
|
||||
elif 'syncnode' in peer:
|
||||
if peer['syncnode']:
|
||||
# syncnodes are outgoing only
|
||||
self._pad.addstr(1+index-offset, 1, 'S')
|
||||
|
||||
addr_str = peer['addr'].replace(".onion","").replace(":8333","").replace(": 18333","").strip("[").strip("]")
|
||||
|
||||
# truncate long ip addresses (ipv6)
|
||||
addr_str = (addr_str[:17] + '...') if len(addr_str) > 20 else addr_str
|
||||
|
||||
self._pad.addstr(1+index-offset, 1, addr_str)
|
||||
self._pad.addstr(1+index-offset, 22,
|
||||
peer['subver'][1:40][:-1]
|
||||
)
|
||||
|
||||
mbrecv = "% 7.1f" % ( float(peer['bytesrecv']) / 1048576 )
|
||||
mbsent = "% 7.1f" % ( float(peer['bytessent']) / 1048576 )
|
||||
|
||||
self._pad.addstr(1+index-offset, 60, mbrecv + 'MB')
|
||||
self._pad.addstr(1+index-offset, 70, mbsent + 'MB')
|
||||
|
||||
timedelta = int(time.time() - peer['conntime'])
|
||||
m, s = divmod(timedelta, 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
|
||||
time_string = ""
|
||||
if d:
|
||||
time_string += ("%d" % d + "d").rjust(3) + " "
|
||||
time_string += "%02d" % h + ":"
|
||||
elif h:
|
||||
time_string += "%02d" % h + ":"
|
||||
time_string += "%02d" % m + ":"
|
||||
time_string += "%02d" % s
|
||||
|
||||
self._pad.addstr(1+index-offset, 79, time_string.rjust(12))
|
||||
|
||||
if 'synced_headers' in peer:
|
||||
self._pad.addstr(1+index-offset, 93, str(peer['synced_headers']).rjust(7) )
|
||||
|
||||
self._pad.refresh(0, 0, 4, 0, 24, 100)
|
||||
|
||||
async def on_peerinfo(self, key, obj):
|
||||
try:
|
||||
self._peerinfo = obj["result"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if self._visible:
|
||||
self.draw()
|
||||
|
||||
async def on_mode_change(self, newmode):
|
||||
if newmode != "peers":
|
||||
self._visible = False
|
||||
return
|
||||
|
||||
self._visible = True
|
||||
self.draw()
|
@ -0,0 +1,2 @@
|
||||
aiohttp>=2.2.5
|
||||
async-timeout>=1.4.0
|
@ -0,0 +1,113 @@
|
||||
# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://opensource.org/licenses/mit-license.php
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import base64
|
||||
import os
|
||||
|
||||
import ujson as json
|
||||
|
||||
import config
|
||||
|
||||
|
||||
def craft_url(proto, ip, port):
|
||||
return "{}://{}:{}".format(proto, ip, port)
|
||||
|
||||
|
||||
def get_url_from_datadir(datadir):
|
||||
configfile = os.path.join(datadir, "bitcoin.conf")
|
||||
|
||||
try:
|
||||
cfg = config.parse_file(configfile)
|
||||
except IOError:
|
||||
return craft_url("http", "localhost", 8332)
|
||||
|
||||
proto = cfg["protocol"] if "protocol" in cfg else "http"
|
||||
ip = cfg["rpcip"] if "rpcip" in cfg else "localhost"
|
||||
try:
|
||||
port = cfg["rpcport"]
|
||||
except KeyError:
|
||||
# If both regtest and testnet are set, bitcoind will not run.
|
||||
if "regtest" in cfg and cfg["regtest"] == "1":
|
||||
port = 18332
|
||||
elif "testnet" in cfg and cfg["testnet"] == "1":
|
||||
port = 18332
|
||||
else:
|
||||
port = 8332
|
||||
|
||||
return craft_url(proto, ip, port)
|
||||
|
||||
|
||||
def get_auth_from_datadir(datadir):
|
||||
def craft_auth_from_credentials(user, password):
|
||||
details = ":".join([user, password])
|
||||
return base64.b64encode(bytes(details, "utf-8")).decode("utf-8")
|
||||
|
||||
def get_auth_from_cookiefile(cookiefile):
|
||||
# Raises IOError if file does not exist
|
||||
with open(cookiefile, "r") as f:
|
||||
return base64.b64encode(bytes(f.readline(), "utf-8")).decode("utf-8")
|
||||
|
||||
cookiefile = os.path.join(datadir, ".cookie")
|
||||
|
||||
try:
|
||||
return get_auth_from_cookiefile(cookiefile)
|
||||
except FileNotFoundError:
|
||||
print("cookiefile not found, falling back to password authentication")
|
||||
# Fall back to credential-based authentication
|
||||
configfile = os.path.join(datadir, "bitcoin.conf")
|
||||
|
||||
try:
|
||||
cfg = config.parse_file(configfile)
|
||||
except IOError:
|
||||
print("configuration file not found; aborting.")
|
||||
raise
|
||||
|
||||
try:
|
||||
rpcuser = cfg["rpcuser"]
|
||||
rpcpassword = cfg["rpcpassword"]
|
||||
except KeyError:
|
||||
if not ("rpcuser" in cfg):
|
||||
print("rpcuser not in configuration file.")
|
||||
if not ("rpcpassword" in cfg):
|
||||
print("rpcpassword not in configuration file.")
|
||||
raise
|
||||
|
||||
return craft_auth_from_credentials(rpcuser, rpcpassword)
|
||||
|
||||
|
||||
class BitcoinRPCClient(object):
|
||||
def __init__(self, url, auth):
|
||||
self._url = url
|
||||
self._headers = {
|
||||
"Authorization": "Basic {}".format(auth),
|
||||
"Content-Type": "text/plain",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def _craft_request(req, params, ident):
|
||||
d = {
|
||||
# "jsonrpc": "2.0", # Currently ignored by Bitcoin Core.
|
||||
"method": req,
|
||||
}
|
||||
|
||||
if params is not None:
|
||||
d["params"] = params
|
||||
|
||||
if ident is not None:
|
||||
d["id"] = ident
|
||||
|
||||
return json.dumps(d)
|
||||
|
||||
async def _fetch(self, session, req):
|
||||
with async_timeout.timeout(5):
|
||||
async with session.post(self._url, headers=self._headers, data=req) as response:
|
||||
return await response.text()
|
||||
|
||||
async def request(self, method, params=None, ident=None, callback=None):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
req = await self._craft_request(method, params, ident)
|
||||
html = await self._fetch(session, req)
|
||||
return json.loads(html)
|
Loading…
Reference in New Issue