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