Initial commit

master
Daniel Edgecumbe 7 years ago
commit 0d4a8777e6

6
.gitignore vendored

@ -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

113
rpc.py

@ -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…
Cancel
Save