Compare commits
No commits in common. "master" and "v0.0.2" have entirely different histories.
70 changed files with 570 additions and 6124 deletions
12
MANIFEST.in
12
MANIFEST.in
|
|
@ -1,12 +0,0 @@
|
|||
include README.md LICENSE
|
||||
|
||||
include mmgen_node_tools/data/*
|
||||
|
||||
include test/init.sh
|
||||
include test/test-release.d/*.sh
|
||||
include test/modtest_d/*.py
|
||||
include test/cmdtest_d/*.py
|
||||
include test/cmdtest_d/include/cfg.py
|
||||
include test/cmdtest_d/httpd/ticker.py
|
||||
include test/overlay/fakemods/mmgen_node_tools/*.py
|
||||
include test/ref/*/*
|
||||
108
README.md
108
README.md
|
|
@ -1,105 +1,19 @@
|
|||
# MMGen Node Tools
|
||||
# MMGen node tools
|
||||
|
||||
### Terminal-based utilities for Bitcoin and forkcoin full nodes
|
||||
Optional helper programs for the [MMGen online/offline Bitcoin wallet][6]
|
||||
|
||||
Requires modules from the [MMGen online/offline cryptocurrency wallet][6].
|
||||
Included in: https://github.com/mmgen/MMGenLive
|
||||
|
||||
## Install:
|
||||
Requires: https://github.com/mmgen/mmgen
|
||||
|
||||
If installing as user (without venv), make sure that `~/.local/bin` is in `PATH`.
|
||||
|
||||
#### Windows/MSYS2:
|
||||
|
||||
> Install [MSYS2 and the MMGen Wallet dependencies][8], skipping installation of
|
||||
> scrypt, libsecp256k1 and the wallet itself if desired.
|
||||
>
|
||||
> Install some additional dependencies:
|
||||
> ```bash
|
||||
> $ pacman -S mingw-w64-ucrt-x86_64-python-pandas
|
||||
> $ python3 -m pip install requests-futures
|
||||
> $ python3 -m pip install --no-deps yahooquery
|
||||
> ```
|
||||
|
||||
#### Linux, macOS:
|
||||
|
||||
> Install some [required packages][7] with your package manager and pip.
|
||||
|
||||
### Stable version:
|
||||
|
||||
```bash
|
||||
$ python3 -m pip install --upgrade mmgen-node-tools
|
||||
```
|
||||
|
||||
### Development version:
|
||||
|
||||
First install the latest development version of [MMGen Wallet][6] for your
|
||||
platform. Then perform the following steps:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/mmgen/mmgen-node-tools
|
||||
$ cd mmgen-node-tools
|
||||
$ python3 -m build --no-isolation
|
||||
$ python3 -m pip install dist/*.whl
|
||||
```
|
||||
|
||||
## Test:
|
||||
|
||||
*NOTE: the tests require that the MMGen Wallet and MMGen Node Tools repositories be
|
||||
located in the same directory.*
|
||||
|
||||
#### Windows/MSYS2:
|
||||
|
||||
> *Tested only on NTFS – with ReFS your mileage may vary*
|
||||
>
|
||||
> Turn on Developer Mode to enable symlinks:
|
||||
> ```
|
||||
> Settings -> Update & Security -> For developers -> Developer Mode: On
|
||||
> ```
|
||||
> and add this to your `~/.bashrc`:
|
||||
> ```bash
|
||||
> export MSYS=winsymlinks:nativestrict
|
||||
> ```
|
||||
> Close and reopen the MSYS2 terminal to update your environment.
|
||||
|
||||
Initialize the test framework (must be run at least once after cloning, and
|
||||
possibly again after a pull if tests have been updated):
|
||||
```
|
||||
$ test/init.sh
|
||||
```
|
||||
BTC-only testing:
|
||||
```
|
||||
$ test/test-release.sh -A
|
||||
```
|
||||
Full testing:
|
||||
```
|
||||
$ test/test-release.sh
|
||||
```
|
||||
The node tools are Linux-only for now
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
Homepage:
|
||||
[Clearnet](https://mmgen.org) |
|
||||
[I2P](http://mmgen-wallet.i2p) |
|
||||
[Onion](http://mmgen55rtcahqfp2hn3v7syqv2wqanks5oeezqg3ykwfkebmouzjxlad.onion)
|
||||
Code repository:
|
||||
[Clearnet](https://mmgen.org/project/mmgen/mmgen-node-tools) |
|
||||
[I2P](http://mmgen-wallet.i2p/project/mmgen/mmgen-node-tools) |
|
||||
[Onion](http://mmgen55rtcahqfp2hn3v7syqv2wqanks5oeezqg3ykwfkebmouzjxlad.onion/project/mmgen/mmgen-node-tools)
|
||||
Code repository mirrors:
|
||||
[Github](https://github.com/mmgen/mmgen-node-tools) |
|
||||
[Gitlab](https://gitlab.com/mmgen/mmgen-node-tools) |
|
||||
[Codeberg](https://codeberg.org/mmgen/mmgen-node-tools)
|
||||
[Keybase](https://keybase.io/mmgen) |
|
||||
[Twitter](https://twitter.com/TheMMGenProject) |
|
||||
[Reddit](https://www.reddit.com/user/mmgen-py) |
|
||||
[Bitcointalk](https://bitcointalk.org/index.php?topic=567069.new#new)
|
||||
[PGP Signing Key][5]: 5C84 CB45 AEE2 250F 31A6 A570 3F8B 1861 E32B 7DA2
|
||||
Donate:
|
||||
⊙ BTC: *bc1qxmymxf8p5ckvlxkmkwgw8ap5t2xuaffmrpexap*
|
||||
⊙ BCH: *15TLdmi5NYLdqmtCqczUs5pBPkJDXRs83w*
|
||||
⊙ XMR: *8B14zb8wgLuKDdse5p8f3aKpFqRdB4i4xj83b7BHYABHMvHifWxiDXeKRELnaxL5FySfeRRS5girgUvgy8fQKsYMEzPUJ8h*
|
||||
[**Forum**][4] |
|
||||
[PGP Public Key][5] |
|
||||
Donate: 15TLdmi5NYLdqmtCqczUs5pBPkJDXRs83w
|
||||
|
||||
[5]: https://github.com/mmgen/mmgen-wallet/wiki/MMGen-Signing-Keys
|
||||
[6]: https://github.com/mmgen/mmgen-wallet/
|
||||
[7]: https://github.com/mmgen/mmgen-wallet/wiki/Install-MMGen-Wallet-on-Linux-or-macOS
|
||||
[8]: https://github.com/mmgen/mmgen-wallet/wiki/Install-MMGen-on-Microsoft-Windows#a_m
|
||||
[4]: https://bitcointalk.org/index.php?topic=567069.0
|
||||
[5]: https://github.com/mmgen/mmgen/wiki/MMGen-Signing-Key
|
||||
[6]: https://github.com/mmgen/mmgen/
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-addrbal: Get balances for arbitrary addresses in the blockchain
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='addrbal',package='mmgen_node_tools')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-blocks-info: Display information about a block or range of blocks
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='blocks_info',package='mmgen_node_tools')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-feeview: Visualize the fee structure of a node’s mempool
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='feeview',package='mmgen_node_tools')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-halving-calculator: Estimate date(s) of future block subsidy halving(s)
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='halving_calculator',package='mmgen_node_tools')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-netrate: Bitcoin daemon network rate monitor
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='netrate',package='mmgen_node_tools')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-peerblocks: List blocks in flight, disconnect stalling nodes
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='peerblocks',package='mmgen_node_tools')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-ticker: Display price information for cryptocurrency and other assets
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='ticker',package='mmgen_node_tools')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-txfind: Find a transaction in the blockchain or mempool
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
|
||||
launch(mod='txfind',package='mmgen_node_tools')
|
||||
BIN
data_files/audio/Counterpoint.wav
Normal file
BIN
data_files/audio/Counterpoint.wav
Normal file
Binary file not shown.
BIN
data_files/audio/Positive.wav
Normal file
BIN
data_files/audio/Positive.wav
Normal file
Binary file not shown.
BIN
data_files/audio/Rhodes.wav
Normal file
BIN
data_files/audio/Rhodes.wav
Normal file
Binary file not shown.
BIN
data_files/audio/ringtone.wav
Normal file
BIN
data_files/audio/ringtone.wav
Normal file
Binary file not shown.
26
mmgen/node_tools/Global.py
Normal file
26
mmgen/node_tools/Global.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
mmgen.node_tools.Global: global variables for MMGen node tools
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
class nt(object):
|
||||
system_data_dir = '/usr/local/share/mmgen/node_tools'
|
||||
data_dir = os.getenv('HOME') + '/.mmgen/node_tools'
|
||||
51
mmgen_node_tools/Sound.py → mmgen/node_tools/Sound.py
Executable file → Normal file
51
mmgen_node_tools/Sound.py → mmgen/node_tools/Sound.py
Executable file → Normal file
|
|
@ -16,68 +16,65 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
mmgen_node_tools.Sound: audio-related functions for MMGen node tools
|
||||
mmgen.node_tools.Sound: audio-related functions for MMGen node tools
|
||||
"""
|
||||
|
||||
import sys, os, time
|
||||
|
||||
from mmgen.util import die
|
||||
|
||||
from mmgen_node_tools.Util import do_system
|
||||
import sys,os,time
|
||||
from mmgen.node_tools.Util import *
|
||||
|
||||
_alsa_config_file = '/tmp/alsa-config-' + os.path.basename(sys.argv[0])
|
||||
_dvols = {'Master': 78, 'Speaker': 78, 'Headphone': 15, 'PCM': 190}
|
||||
_dvols = { 'Master': 78, 'Speaker': 78, 'Headphone': 15, 'PCM': 190 }
|
||||
|
||||
def timespec2secs(ts):
|
||||
import re
|
||||
mul = {'s': 1, 'm': 60, 'h': 60*60, 'd': 60*60*24}
|
||||
mul = { 's': 1, 'm': 60, 'h': 60*60, 'd': 60*60*24 }
|
||||
pat = r'^([0-9]+)([smhd]*)$'
|
||||
m = re.match(pat, ts)
|
||||
if m is None:
|
||||
m = re.match(pat,ts)
|
||||
if m == None:
|
||||
die(2,"'%s': invalid time specifier" % ts)
|
||||
a, b = m.groups()
|
||||
a,b = m.groups()
|
||||
return int(a) * (mul[b] if b else 1)
|
||||
|
||||
def parse_repeat_spec(rs):
|
||||
return [(timespec2secs(i), timespec2secs(j))
|
||||
for i, j in [a.split(':') for a in rs.split(',')]]
|
||||
return [(timespec2secs(i),timespec2secs(j))
|
||||
for i,j in [a.split(':') for a in rs.split(',')]]
|
||||
|
||||
def init_sound():
|
||||
def _restore_sound():
|
||||
# msg('Restoring sound volume')
|
||||
do_system('sudo alsactl restore -f ' + _alsa_config_file)
|
||||
do_system('alsactl restore -f ' + _alsa_config_file)
|
||||
os.unlink(_alsa_config_file)
|
||||
import atexit
|
||||
atexit.register(_restore_sound)
|
||||
do_system('sudo alsactl store -f ' + _alsa_config_file)
|
||||
do_system('alsactl store -f ' + _alsa_config_file)
|
||||
|
||||
def play_sound(fn, vol, repeat_spec='', remote_host='', kill_flg=None, testing=False):
|
||||
def play_sound(fn,vol,repeat_spec='',remote_host='',kill_flg=None,testing=False):
|
||||
if not remote_host:
|
||||
do_system('sudo alsactl store -f ' + _alsa_config_file)
|
||||
for k in 'Master', 'Speaker', 'Headphone':
|
||||
do_system(('sudo amixer -q set %s on' % k), testing)
|
||||
do_system('alsactl store -f ' + _alsa_config_file)
|
||||
for k in 'Master','Speaker','Headphone':
|
||||
do_system(('amixer -q set %s on' % k),testing)
|
||||
# do_system('amixer -q set Headphone off')
|
||||
|
||||
vols = dict([(k, int(_dvols[k] * float(vol) / 100)) for k in _dvols])
|
||||
vols = dict([(k,int(_dvols[k] * float(vol) / 100)) for k in _dvols])
|
||||
for k in vols:
|
||||
do_system('sudo amixer -q set %s %s' % (k, vols[k]), testing)
|
||||
do_system('amixer -q set %s %s' % (k,vols[k]),testing)
|
||||
|
||||
fn = os.path.expanduser(fn)
|
||||
cmd = (
|
||||
'aplay -q %s' % fn,
|
||||
'ssh %s mmnode-play-sound -v%d %s' % (remote_host, vol, fn)
|
||||
'ssh %s mmnode-play-sound -v%d %s' % (remote_host,vol,fn)
|
||||
)[bool(remote_host)]
|
||||
|
||||
if repeat_spec and kill_flg:
|
||||
for interval, duration in parse_repeat_spec(repeat_spec):
|
||||
for interval,duration in parse_repeat_spec(repeat_spec):
|
||||
start = time.time()
|
||||
while time.time() < start + duration:
|
||||
do_system(cmd, testing)
|
||||
do_system(cmd,testing)
|
||||
if kill_flg.wait(interval):
|
||||
if not remote_host:
|
||||
do_system('sudo alsactl restore -f ' + _alsa_config_file)
|
||||
do_system('alsactl restore -f ' + _alsa_config_file)
|
||||
return
|
||||
else: # Play once
|
||||
do_system(cmd, testing)
|
||||
do_system(cmd,testing)
|
||||
if not remote_host:
|
||||
do_system('sudo alsactl restore -f ' + _alsa_config_file)
|
||||
do_system('alsactl restore -f ' + _alsa_config_file)
|
||||
49
mmgen/node_tools/Term.py
Normal file
49
mmgen/node_tools/Term.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
mmgen.node_tools.Term: terminal routines for MMGen node tools
|
||||
"""
|
||||
|
||||
import sys,os,termios
|
||||
|
||||
def get_keypress(prompt="",esc_sequences=False):
|
||||
|
||||
import time,tty,select
|
||||
sys.stderr.write(prompt)
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
# old = termios.tcgetattr(fd) # see below
|
||||
tty.setcbreak(fd) # must do this, even if it was set at program launch
|
||||
|
||||
def osread_chk(n):
|
||||
while True:
|
||||
try:
|
||||
return os.read(fd,n)
|
||||
except:
|
||||
time.sleep(0.1)
|
||||
|
||||
# Must use os.read() for unbuffered read, otherwise select() will never return true
|
||||
s = osread_chk(1)
|
||||
if esc_sequences:
|
||||
if s == '\x1b':
|
||||
if select.select([sys.stdin],[],[],0)[0]:
|
||||
s += osread_chk(2)
|
||||
|
||||
# Leave the term in cbreak mode, restore at exit
|
||||
# termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
return s
|
||||
66
mmgen_node_tools/Util.py → mmgen/node_tools/Util.py
Executable file → Normal file
66
mmgen_node_tools/Util.py → mmgen/node_tools/Util.py
Executable file → Normal file
|
|
@ -16,57 +16,56 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
mmgen_node_tools.Util: utility functions for MMGen node tools
|
||||
mmgen.node_tools.Util: utility functions for MMGen node tools
|
||||
"""
|
||||
|
||||
import time
|
||||
import time,subprocess
|
||||
|
||||
def get_hms(t=None, utc=False, no_secs=False):
|
||||
def get_hms(t=None,utc=False,no_secs=False):
|
||||
secs = t or time.time()
|
||||
ret = (time.localtime, time.gmtime)[utc](secs)
|
||||
fs, n = (('{:02}:{:02}:{:02}', 6), ('{:02}:{:02}', 5))[no_secs]
|
||||
ret = (time.localtime,time.gmtime)[utc](secs)
|
||||
fs,n = (('{:02}:{:02}:{:02}',6),('{:02}:{:02}',5))[no_secs]
|
||||
return fs.format(*ret[3:n])
|
||||
|
||||
def get_day_hms(t=None, utc=False):
|
||||
def get_day_hms(t=None,utc=False):
|
||||
secs = t or time.time()
|
||||
ret = (time.localtime, time.gmtime)[utc](secs)
|
||||
ret = (time.localtime,time.gmtime)[utc](secs)
|
||||
return '{:04}-{:02}-{:02} {:02}:{:02}:{:02}'.format(*ret[0:6])
|
||||
|
||||
def do_system(cmd, testing=False, shell=False):
|
||||
def do_system(cmd,testing=False,shell=False):
|
||||
if testing:
|
||||
from mmgen.util import msg
|
||||
msg("Would execute: '%s'" % cmd)
|
||||
return True
|
||||
else:
|
||||
import subprocess
|
||||
return subprocess.call((cmd if shell else cmd.split()), shell, stderr=subprocess.PIPE)
|
||||
return subprocess.call((cmd if shell else cmd.split()),shell,stderr=subprocess.PIPE)
|
||||
|
||||
def get_url(url, gzip_ok=False, proxy=None, timeout=60, verbose=False, debug=False):
|
||||
def get_url(url,gzip_ok=False,proxy=None,timeout=60,verbose=False,debug=False):
|
||||
if debug:
|
||||
print('get_url():')
|
||||
print(' url', url)
|
||||
print(' gzip_ok:', gzip_ok, 'proxy:', proxy, 'timeout:', timeout, 'verbose:', verbose)
|
||||
import pycurl, io
|
||||
print(' gzip_ok:',gzip_ok, 'proxy:',proxy, 'timeout:',timeout, 'verbose:',verbose)
|
||||
import pycurl,io
|
||||
c = pycurl.Curl()
|
||||
c_out = io.StringIO()
|
||||
c.setopt(pycurl.WRITEFUNCTION, c_out.write)
|
||||
c.setopt(pycurl.TIMEOUT, timeout)
|
||||
c.setopt(pycurl.FOLLOWLOCATION, True)
|
||||
c.setopt(pycurl.COOKIEFILE, '')
|
||||
c.setopt(pycurl.VERBOSE, verbose)
|
||||
c.setopt(pycurl.WRITEFUNCTION,c_out.write)
|
||||
c.setopt(pycurl.TIMEOUT,timeout)
|
||||
c.setopt(pycurl.FOLLOWLOCATION,True)
|
||||
c.setopt(pycurl.COOKIEFILE,'')
|
||||
c.setopt(pycurl.VERBOSE,verbose)
|
||||
if gzip_ok:
|
||||
c.setopt(pycurl.USERAGENT, 'Lynx/2.8.9dev.8 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/3.4.9')
|
||||
c.setopt(pycurl.USERAGENT,'Lynx/2.8.9dev.8 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/3.4.9')
|
||||
c.setopt(pycurl.HTTPHEADER, [
|
||||
'Accept: text/html, text/plain, text/sgml, text/css, application/xhtml+xml, */*;q=0.01',
|
||||
'Accept-Encoding: gzip',
|
||||
'Accept-Language: en'])
|
||||
'Accept-Language: en']
|
||||
)
|
||||
if proxy:
|
||||
c.setopt(pycurl.PROXY, proxy)
|
||||
c.setopt(pycurl.URL, url)
|
||||
c.setopt(pycurl.PROXY,proxy)
|
||||
c.setopt(pycurl.URL,url)
|
||||
c.perform()
|
||||
text = c_out.getvalue()
|
||||
if text[:2] == '\x1f\x8b': # gzip magic number
|
||||
c_out.seek(0, 0)
|
||||
c_out.seek(0,0)
|
||||
import gzip
|
||||
with gzip.GzipFile(fileobj=c_out) as f:
|
||||
text = f.read()
|
||||
|
|
@ -102,19 +101,20 @@ big_digits = {
|
|||
"""
|
||||
}
|
||||
|
||||
_bnums_c, _bpunc_c = [[l.strip('\n') + ' ' * (big_digits[m]*big_digits['n'])
|
||||
_bnums_c,_bpunc_c = [[l.strip('\n') + ' ' * (big_digits[m]*big_digits['n'])
|
||||
for l in big_digits[k][1:].split('\n')]
|
||||
for k, m in (('nums', 'w'), ('punc', 'pw'))]
|
||||
for k,m in (('nums','w'),('punc','pw'))]
|
||||
|
||||
_bnums_n, _bpunc_n = [[[l[0+(j*w):w+(j*w)] for l in i]
|
||||
for j in range(big_digits[n])] for n, w, i in
|
||||
(('n', big_digits['w'], _bnums_c), ('pn', big_digits['pw'], _bpunc_c))]
|
||||
_bnums_n,_bpunc_n = [[[l[0+(j*w):w+(j*w)] for l in i]
|
||||
for j in range(big_digits[n])] for n,w,i in
|
||||
(('n',big_digits['w'],_bnums_c),('pn',big_digits['pw'],_bpunc_c))]
|
||||
|
||||
def display_big_digits(s, pre='', suf=''):
|
||||
s = [int((d, 10, 11)[(d in '.:')+(d==':')]) for d in s]
|
||||
def display_big_digits(s,pre='',suf=''):
|
||||
s = [int((d,10,11)[(d in '.:')+(d==':')]) for d in s]
|
||||
return pre + ('\n'+pre).join(
|
||||
[''.join([(_bnums_n+_bpunc_n)[d][l] for d in s]) + suf for l in range(big_digits['h'])])
|
||||
[''.join([(_bnums_n+_bpunc_n)[d][l] for d in s]) + suf for l in range(big_digits['h'])]
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
num = '2345.17'
|
||||
print(display_big_digits(num, pre='+ ', suf=' +'))
|
||||
print(display_big_digits(num,pre='+ ',suf=' +'))
|
||||
0
mmgen_node_tools/__init__.py → mmgen/node_tools/__init__.py
Executable file → Normal file
0
mmgen_node_tools/__init__.py → mmgen/node_tools/__init__.py
Executable file → Normal file
|
|
@ -1,755 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmgen_node_tools.BlocksInfo: Display information about a block or range of blocks
|
||||
"""
|
||||
|
||||
import re, json
|
||||
from collections import namedtuple
|
||||
from time import strftime, gmtime
|
||||
from decimal import Decimal
|
||||
|
||||
from mmgen.util import msg, Msg, Msg_r, die, suf, secs_to_ms, secs_to_dhms, is_int
|
||||
from mmgen.rpc.util import json_encoder
|
||||
|
||||
class RangeParser:
|
||||
|
||||
debug = False
|
||||
|
||||
def __init__(self, caller, arg):
|
||||
self.caller = caller
|
||||
self.arg = self.orig_arg = arg
|
||||
|
||||
def parse(self, target):
|
||||
ret = getattr(self, 'parse_'+target)()
|
||||
if self.debug:
|
||||
msg(f'arg after parse({target}): {self.arg}')
|
||||
return ret
|
||||
|
||||
def finalize(self):
|
||||
if self.arg:
|
||||
die(1, f'{self.orig_arg!r}: invalid range specifier')
|
||||
|
||||
def parse_from_tip(self):
|
||||
m = re.match(r'-([0-9]+)(.*)', self.arg)
|
||||
if m:
|
||||
res, self.arg = (m[1], m[2])
|
||||
return self.caller.check_nblocks(int(res))
|
||||
|
||||
def parse_abs_range(self):
|
||||
m = re.match(r'([^+-]+)(-([^+-]+)){0,1}(.*)', self.arg)
|
||||
if m:
|
||||
if self.debug:
|
||||
msg(f'abs_range parse: first={m[1]}, last={m[3]}')
|
||||
self.arg = m[4]
|
||||
return (
|
||||
self.caller.conv_blkspec(m[1]),
|
||||
self.caller.conv_blkspec(m[3]) if m[3] else None)
|
||||
return (None, None)
|
||||
|
||||
def parse_add(self):
|
||||
m = re.match(r'\+([0-9*]+)(.*)', self.arg)
|
||||
if m:
|
||||
res, self.arg = (m[1], m[2])
|
||||
if res.strip('*') != res:
|
||||
die(1, f"'+{res}': malformed nBlocks specifier")
|
||||
if len(res) > 30:
|
||||
die(1, f"'+{res}': overly long nBlocks specifier")
|
||||
return self.caller.check_nblocks(eval(res)) # res is only digits plus '*', so eval safe
|
||||
|
||||
class BlocksInfo:
|
||||
|
||||
total_bytes = 0
|
||||
total_weight = 0
|
||||
total_solve_time = 0
|
||||
header_printed = False
|
||||
|
||||
bf = namedtuple('block_info_fields', ['fmt_func', 'src', 'fs', 'hdr1', 'hdr2', 'key1', 'key2'])
|
||||
# bh=getblockheader, bs=getblockstats, lo=local
|
||||
fields = {
|
||||
'block': bf(None, 'bh', '{:<6}', '', 'Block', 'height', None),
|
||||
'hash': bf(None, 'bh', '{:<64}', '', 'Hash', 'hash', None),
|
||||
'date': bf('da', 'bh', '{:<19}', '', 'Date', 'time', None),
|
||||
'interval': bf('td', 'lo', '{:>8}', 'Solve', 'Time ', 'interval', None),
|
||||
'subsidy': bf('su', 'bs', '{:<5}', 'Sub-', 'sidy', 'subsidy', None),
|
||||
'totalfee': bf('tf', 'bs', '{:>10}', '', 'Total Fee', 'totalfee', None),
|
||||
'size': bf(None, 'bs', '{:>7}', '', 'Size', 'total_size', None),
|
||||
'weight': bf(None, 'bs', '{:>7}', '', 'Weight', 'total_weight', None),
|
||||
'fee90': bf('fe', 'bs', '{:>3}', '90%', 'Fee', 'feerate_percentiles', 4),
|
||||
'fee75': bf('fe', 'bs', '{:>3}', '75%', 'Fee', 'feerate_percentiles', 3),
|
||||
'fee50': bf('fe', 'bs', '{:>3}', '50%', 'Fee', 'feerate_percentiles', 2),
|
||||
'fee25': bf('fe', 'bs', '{:>3}', '25%', 'Fee', 'feerate_percentiles', 1),
|
||||
'fee10': bf('fe', 'bs', '{:>3}', '10%', 'Fee', 'feerate_percentiles', 0),
|
||||
'fee_max': bf('fe', 'bs', '{:>5}', 'Max', 'Fee', 'maxfeerate', None),
|
||||
'fee_avg': bf('fe', 'bs', '{:>3}', 'Avg', 'Fee', 'avgfeerate', None),
|
||||
'fee_min': bf('fe', 'bs', '{:>3}', 'Min', 'Fee', 'minfeerate', None),
|
||||
'nTx': bf(None, 'bh', '{:>5}', '', ' nTx ', 'nTx', None),
|
||||
'inputs': bf(None, 'bs', '{:>5}', 'In- ', 'puts', 'ins', None),
|
||||
'outputs': bf(None, 'bs', '{:>5}', 'Out-', 'puts', 'outs', None),
|
||||
'utxo_inc': bf(None, 'bs', '{:>6}', ' UTXO', ' Incr', 'utxo_increase', None),
|
||||
'version': bf(None, 'bh', '{:<8}', '', 'Version', 'versionHex', None),
|
||||
'difficulty': bf('di', 'bh', '{:<8}', 'Diffi-','culty', 'difficulty', None),
|
||||
'miner': bf(None, 'lo', '{:<5}', '', 'Miner', 'miner', None)}
|
||||
|
||||
dfl_fields = (
|
||||
'block',
|
||||
'date',
|
||||
'interval',
|
||||
'subsidy',
|
||||
'totalfee',
|
||||
'size',
|
||||
'weight',
|
||||
'fee50',
|
||||
'fee25',
|
||||
'fee10',
|
||||
'fee_avg',
|
||||
'fee_min',
|
||||
'version')
|
||||
|
||||
fixed_fields = (
|
||||
'block', # until ≈ 09/01/2028 (block 1000000)
|
||||
'hash',
|
||||
'date',
|
||||
'size', # until ≈ 6x block size increase
|
||||
'weight', # until ≈ 2.5x block size increase
|
||||
'version',
|
||||
'subsidy', # until ≈ 01/04/2028 (increases by 1 digit per halving until 9th halving [max 10 digits])
|
||||
'difficulty') # until 1.00e+100 (i.e. never)
|
||||
|
||||
# column width adjustment data:
|
||||
fs_lsqueeze = ('totalfee', 'inputs', 'outputs', 'nTx')
|
||||
fs_rsqueeze = ()
|
||||
fs_groups = (
|
||||
('fee10', 'fee25', 'fee50', 'fee75', 'fee90', 'fee_avg', 'fee_min', 'fee_max'))
|
||||
fs_lsqueeze2 = ('interval',)
|
||||
|
||||
all_stats = ['col_avg', 'range', 'avg', 'mini_avg', 'total', 'diff']
|
||||
dfl_stats = ['range', 'mini_avg', 'diff']
|
||||
noindent_stats = ['col_avg']
|
||||
|
||||
avg_stats_skip = {'block', 'hash', 'date', 'version', 'miner'}
|
||||
|
||||
range_data = namedtuple('parsed_range_data', ['first', 'last', 'from_tip', 'nblocks', 'step'])
|
||||
|
||||
t_fmt = lambda self, t: f'{t/86400:.2f} days' if t > 172800 else f'{t/3600:.2f} hrs'
|
||||
|
||||
@classmethod
|
||||
def parse_cslist(cls, uarg, full_set, dfl_set, desc):
|
||||
|
||||
def make_list(m, func):
|
||||
groups_lc = [set(e.lower() for e in gi.split(',')) for gi in m.groups()]
|
||||
for group in groups_lc:
|
||||
for e in group:
|
||||
if e not in full_set_lc:
|
||||
die(1, f'{e!r}: unrecognized {desc}')
|
||||
# display elements in order:
|
||||
return [e for e in full_set if e.lower() in func(groups_lc)]
|
||||
|
||||
full_set_lc = set(e.lower() for e in full_set)
|
||||
dfl_set_lc = set(e.lower() for e in dfl_set)
|
||||
cspat = r'(\w+(?:,\w+)*)'
|
||||
|
||||
for pat, func in (
|
||||
(rf'{cspat}$', lambda g: g[0]),
|
||||
(rf'\+{cspat}$', lambda g: dfl_set_lc | g[0]),
|
||||
(rf'\-{cspat}$', lambda g: dfl_set_lc - g[0]),
|
||||
(rf'\+{cspat}\-{cspat}$', lambda g: (dfl_set_lc | g[0]) - g[1]),
|
||||
(rf'\-{cspat}\+{cspat}$', lambda g: (dfl_set_lc - g[0]) | g[1]),
|
||||
(rf'all\-{cspat}$', lambda g: full_set_lc - g[0])):
|
||||
m = re.match(pat, uarg, re.ASCII|re.IGNORECASE)
|
||||
if m:
|
||||
return make_list(m, func)
|
||||
else:
|
||||
die(1, f'{uarg}: invalid parameter')
|
||||
|
||||
def __init__(self, cfg, cmd_args, rpc):
|
||||
|
||||
def parse_cs_uarg(uarg, full_set, dfl_set, desc):
|
||||
return (
|
||||
full_set if uarg == 'all' else [] if uarg == 'none' else
|
||||
self.parse_cslist(uarg, full_set, dfl_set, desc))
|
||||
|
||||
def get_fields():
|
||||
return parse_cs_uarg(self.cfg.fields, list(self.fields), self.dfl_fields, 'field')
|
||||
|
||||
def get_stats():
|
||||
return parse_cs_uarg(self.cfg.stats.lower(), self.all_stats, self.dfl_stats, 'stat')
|
||||
|
||||
def parse_cmd_args(): # => (block_list, first, last, step)
|
||||
match cmd_args:
|
||||
case [] | None:
|
||||
return (None, self.tip, self.tip, None)
|
||||
case [arg]:
|
||||
r = self.parse_rangespec(arg)
|
||||
return (
|
||||
list(range(r.first, r.last+1, r.step)) if r.step else None,
|
||||
r.first,
|
||||
r.last,
|
||||
r.step)
|
||||
case [*args]:
|
||||
return ([self.conv_blkspec(a) for a in args], None, None, None)
|
||||
|
||||
self.cfg = cfg
|
||||
self.rpc = rpc
|
||||
self.tip = rpc.blockcount
|
||||
|
||||
from_satoshi = self.rpc.proto.coin_amt.satoshi
|
||||
to_satoshi = 1 / from_satoshi
|
||||
|
||||
self.block_list, self.first, self.last, self.step = parse_cmd_args()
|
||||
|
||||
have_segwit = self.rpc.info('segwit_is_active')
|
||||
|
||||
if not have_segwit:
|
||||
del self.fields['weight']
|
||||
self.dfl_fields = tuple(f for f in self.dfl_fields if f != 'weight')
|
||||
|
||||
self.stats_deps = {
|
||||
'avg': set(self.fields) - self.avg_stats_skip,
|
||||
'col_avg': set(self.fields) - self.avg_stats_skip,
|
||||
'mini_avg': {'interval', 'size'} | ({'weight'} if have_segwit else set()),
|
||||
'total': {'interval', 'subsidy', 'totalfee', 'nTx', 'inputs', 'outputs', 'utxo_inc'},
|
||||
'range': {},
|
||||
'diff': {}}
|
||||
|
||||
self.fmt_funcs = {
|
||||
'da': lambda arg: strftime('%Y-%m-%d %X', gmtime(arg)),
|
||||
'td': lambda arg: (
|
||||
'-{:02}:{:02}'.format(abs(arg)//60, abs(arg)%60) if arg < 0 else
|
||||
' {:02}:{:02}'.format(arg//60, arg%60)),
|
||||
'tf': lambda arg: '{:.8f}'.format(arg * from_satoshi),
|
||||
'su': lambda arg: str(arg * from_satoshi).rstrip('0').rstrip('.'),
|
||||
'fe': lambda arg: str(arg),
|
||||
'di': lambda arg: '{:.2e}'.format(Decimal(arg))}
|
||||
|
||||
if self.cfg.coin == 'BCH':
|
||||
self.fmt_funcs.update({
|
||||
'su': lambda arg: str(arg).rstrip('0').rstrip('.'),
|
||||
'fe': lambda arg: str(int(Decimal(arg) * to_satoshi)),
|
||||
'tf': lambda arg: '{:.8f}'.format(Decimal(arg))})
|
||||
|
||||
self.fnames = tuple(
|
||||
[f for f in self.fields if self.fields[f].src == 'bh' or f == 'interval']
|
||||
if self.cfg.header_info
|
||||
else get_fields() if self.cfg.fields
|
||||
else self.dfl_fields)
|
||||
|
||||
if self.cfg.miner_info and 'miner' not in self.fnames:
|
||||
self.fnames += ('miner',)
|
||||
|
||||
self.stats = get_stats() if self.cfg.stats else self.dfl_stats
|
||||
|
||||
# Display diff stats by default only if user-requested range ends with chain tip
|
||||
if 'diff' in self.stats and not self.cfg.stats and self.last != self.tip:
|
||||
self.stats.remove('diff')
|
||||
|
||||
if {'avg', 'col_avg'} <= set(self.stats) and self.cfg.stats_only:
|
||||
self.stats.remove('col_avg')
|
||||
|
||||
if {'avg', 'mini_avg'} <= set(self.stats):
|
||||
self.stats.remove('mini_avg')
|
||||
|
||||
if self.cfg.full_stats:
|
||||
add_fnames = {fname for sname in self.stats for fname in self.stats_deps[sname]}
|
||||
self.fnames = tuple(f for f in self.fields if f in {'block'} | set(self.fnames) | add_fnames)
|
||||
else:
|
||||
if 'col_avg' in self.stats and not self.fnames:
|
||||
self.stats.remove('col_avg')
|
||||
|
||||
# self.fnames is now finalized
|
||||
|
||||
self.fvals = [self.fields[name] for name in self.fnames]
|
||||
self.fs = ''.join(self.gen_fs(self.fnames)).strip()
|
||||
|
||||
self.bs_keys = set(
|
||||
[v.key1 for v in self.fvals if v.src == 'bs'] +
|
||||
['total_size'] +
|
||||
(['total_weight'] if have_segwit else []))
|
||||
|
||||
if 'miner' in self.fnames:
|
||||
# capturing parens must contain only ASCII chars!
|
||||
self.miner_pats = [re.compile(pat) for pat in (
|
||||
rb'`/([_a-zA-Z0-9&. #/-]+)/',
|
||||
rb'[\xe3\xe4\xe5][\^/]([\x20-\x7e]+?)\xfa',
|
||||
rb'([a-zA-Z0-9&. -]+/Mined by [a-zA-Z0-9. ]+)',
|
||||
rb'\x08/(.*Mined by [a-zA-Z0-9. ]+)',
|
||||
rb'Mined by ([a-zA-Z0-9. ]+)',
|
||||
rb'[`]([_a-zA-Z0-9&. #/-]+)[/\xfa]',
|
||||
rb'([\x20-\x7e]{9,})',
|
||||
rb'[/^]([a-zA-Z0-9&. #/-]{5,})',
|
||||
rb'[/^]([_a-zA-Z0-9&. #/-]+)/',
|
||||
rb'^\x03...\W{0,5}([\\_a-zA-Z0-9&. #/-]+)[/\\]')]
|
||||
|
||||
self.block_data = namedtuple('block_data', self.fnames)
|
||||
self.deps = {v.src for v in self.fvals}
|
||||
|
||||
def gen_fs(self, fnames, fill=[], fill_char='-', add_name=False):
|
||||
for i in range(len(fnames)):
|
||||
name = fnames[i]
|
||||
ls = (' ', '')[name in self.fs_lsqueeze + self.fs_lsqueeze2]
|
||||
rs = (' ', '')[name in self.fs_rsqueeze]
|
||||
if i < len(fnames) - 1 and fnames[i+1] in self.fs_lsqueeze2:
|
||||
rs = ''
|
||||
if i:
|
||||
for group in self.fs_groups:
|
||||
if name in group and fnames[i-1] in group:
|
||||
ls = ''
|
||||
break
|
||||
repl = (name if add_name else '') + ':' + (fill_char if name in fill else '')
|
||||
yield (ls + self.fields[name].fs.replace(':', repl) + rs)
|
||||
|
||||
def conv_blkspec(self, arg):
|
||||
match arg:
|
||||
case str() if arg.lower() == 'cur':
|
||||
return self.tip
|
||||
case x if is_int(x):
|
||||
match int(arg):
|
||||
case x if x < 0:
|
||||
die(1, f'{x}: block number must be non-negative')
|
||||
case x if x > self.tip:
|
||||
die(1, f'{x}: requested block height greater than current chain tip!')
|
||||
case x:
|
||||
return x
|
||||
case _:
|
||||
die(1, f'{arg}: invalid block specifier')
|
||||
|
||||
def check_nblocks(self, arg):
|
||||
match arg:
|
||||
case x if x <= 0:
|
||||
die(1, 'nBlocks must be a positive integer')
|
||||
case x if x > self.tip:
|
||||
die(1, f'{arg}: nBlocks must be less than current chain height')
|
||||
case _:
|
||||
return arg
|
||||
|
||||
def parse_rangespec(self, arg):
|
||||
|
||||
p = RangeParser(self, arg)
|
||||
|
||||
from_tip = p.parse('from_tip')
|
||||
first, last = (self.tip-from_tip, None) if from_tip else p.parse('abs_range')
|
||||
add1 = p.parse('add')
|
||||
add2 = p.parse('add')
|
||||
p.finalize()
|
||||
|
||||
if add2 and last is not None:
|
||||
die(1, f'{arg!r}: invalid range specifier')
|
||||
|
||||
nblocks, step = (add1, add2) if last is None else (None, add1)
|
||||
|
||||
if p.debug:
|
||||
msg(repr(self.range_data(first, last, from_tip, nblocks, step)))
|
||||
|
||||
if nblocks:
|
||||
if first is None:
|
||||
first = self.tip - nblocks + 1
|
||||
last = first + nblocks - 1
|
||||
|
||||
first = self.conv_blkspec(first)
|
||||
last = self.conv_blkspec(last or first)
|
||||
|
||||
if p.debug:
|
||||
msg(repr(self.range_data(first, last, from_tip, nblocks, step)))
|
||||
|
||||
if first > last:
|
||||
die(1, f'{first}-{last}: invalid block range')
|
||||
|
||||
return self.range_data(first, last, from_tip, nblocks, step)
|
||||
|
||||
async def process_blocks(self):
|
||||
|
||||
async def get_hdrs(heights):
|
||||
hashes = await c.gathered_call('getblockhash',[(height,) for height in heights])
|
||||
return await c.gathered_call('getblockheader',[(H,) for H in hashes])
|
||||
|
||||
c = self.rpc
|
||||
|
||||
heights = self.block_list or range(self.first, self.last+1)
|
||||
self.hdrs = await get_hdrs(heights)
|
||||
|
||||
if self.block_list:
|
||||
self.prev_hdrs = await get_hdrs([(n-1 if n else 0) for n in self.block_list])
|
||||
self.first_prev_hdr = self.prev_hdrs[0]
|
||||
else:
|
||||
self.first_prev_hdr = (
|
||||
self.hdrs[0] if heights[0] == 0 else
|
||||
await c.call('getblockheader', await c.call('getblockhash', heights[0]-1)))
|
||||
|
||||
self.t_cur = self.first_prev_hdr['time']
|
||||
self.res = []
|
||||
|
||||
for n in range(len(heights)):
|
||||
if self.block_list:
|
||||
self.t_cur = self.prev_hdrs[n]['time']
|
||||
ret = await self.process_block(self.hdrs[n])
|
||||
self.res.append(ret)
|
||||
if self.fnames and not self.cfg.stats_only:
|
||||
self.output_block(ret, n)
|
||||
|
||||
def output_block(self, data, n):
|
||||
def gen():
|
||||
for k, v in data._asdict().items():
|
||||
func = self.fields[k].fmt_func
|
||||
yield self.fmt_funcs[func](v) if func else v
|
||||
Msg(self.fs.format(*gen()))
|
||||
|
||||
async def process_block(self, hdr):
|
||||
|
||||
self.t_diff = hdr['time'] - self.t_cur
|
||||
self.t_cur = hdr['time']
|
||||
self.total_solve_time += self.t_diff
|
||||
|
||||
blk_data = {
|
||||
'bh': hdr,
|
||||
'lo': {'interval': self.t_diff}}
|
||||
|
||||
if 'bs' in self.deps:
|
||||
bs = (
|
||||
self.genesis_stats if hdr['height'] == 0 else
|
||||
await self.rpc.call('getblockstats', hdr['hash'], list(self.bs_keys)))
|
||||
self.total_bytes += bs['total_size']
|
||||
if 'total_weight' in bs:
|
||||
self.total_weight += bs['total_weight']
|
||||
blk_data['bs'] = bs
|
||||
|
||||
if 'miner' in self.fnames:
|
||||
blk_data['lo']['miner'] = '-' if hdr['height'] == 0 else await self.get_miner_string(hdr['hash'])
|
||||
|
||||
def gen():
|
||||
for v in self.fvals:
|
||||
yield (
|
||||
blk_data[v.src][v.key1] if v.key2 is None else
|
||||
blk_data[v.src][v.key1][v.key2])
|
||||
|
||||
return self.block_data(*gen())
|
||||
|
||||
async def get_miner_string(self, H):
|
||||
tx0 = (await self.rpc.call('getblock', H))['tx'][0]
|
||||
bd = await self.rpc.call('getrawtransaction', tx0, 1)
|
||||
if type(bd) == tuple:
|
||||
return '---'
|
||||
else:
|
||||
cb = bytes.fromhex(bd['vin'][0]['coinbase'])
|
||||
if self.cfg.raw_miner_info:
|
||||
return repr(cb)
|
||||
else:
|
||||
trmap_in = {
|
||||
'\\': ' ',
|
||||
'/': ' ',
|
||||
',': ' '}
|
||||
trmap = {ord(a): b for a, b in trmap_in.items()}
|
||||
for pat in self.miner_pats:
|
||||
m = pat.search(cb)
|
||||
if m:
|
||||
return re.sub(r'\s+', ' ', m[1].decode().strip('^').translate(trmap).strip())
|
||||
return ''
|
||||
|
||||
def print_header(self):
|
||||
Msg('\n'.join(self.gen_header()))
|
||||
self.header_printed = True
|
||||
|
||||
def gen_header(self):
|
||||
hdr1 = [v.hdr1 for v in self.fvals]
|
||||
hdr2 = [v.hdr2 for v in self.fvals]
|
||||
if ''.join(hdr1):
|
||||
yield self.fs.format(*hdr1)
|
||||
yield self.fs.format(*hdr2)
|
||||
|
||||
def process_stats(self, sname):
|
||||
method = getattr(self, f'create_{sname}_stats', None)
|
||||
return self.output_stats(method() if method else self.create_stats(sname), sname)
|
||||
|
||||
def fmt_stat_item(self, fs, s):
|
||||
return fs.format(s) if type(fs) == str else fs(s)
|
||||
|
||||
async def output_stats(self, res, sname):
|
||||
|
||||
def gen(data):
|
||||
for d in data:
|
||||
match d:
|
||||
case [a, b]:
|
||||
yield (indent + a).format(**{k: self.fmt_stat_item(*v) for k, v in b.items()})
|
||||
case [a, _, b, c]:
|
||||
yield (indent + a).format(self.fmt_stat_item(b, c))
|
||||
case str():
|
||||
yield d
|
||||
case _:
|
||||
assert False, f'{d}: invalid stats data'
|
||||
|
||||
foo, data = await res
|
||||
indent = '' if sname in self.noindent_stats else ' '
|
||||
Msg('\n'.join(gen(data)))
|
||||
|
||||
async def create_range_stats(self):
|
||||
# These figures don’t include the Genesis Block:
|
||||
elapsed = self.hdrs[-1]['time'] - self.first_prev_hdr['time']
|
||||
nblocks = self.hdrs[-1]['height'] - self.first_prev_hdr['height']
|
||||
total_blks = len(self.hdrs)
|
||||
step_disp = f', nBlocks={total_blks}, step={self.step}' if self.step else ''
|
||||
def gen():
|
||||
yield 'Range Statistics:'
|
||||
yield (
|
||||
'Range: {start}-{end} ({range} blocks [{elapsed}]%s)' % step_disp, {
|
||||
'start': ('{}', self.hdrs[0]['height']),
|
||||
'end': ('{}', self.hdrs[-1]['height']),
|
||||
'range': ('{}', self.hdrs[-1]['height'] - self.hdrs[0]['height'] + 1),
|
||||
'elapsed': (self.t_fmt, elapsed),
|
||||
'nBlocks': ('{}', total_blks),
|
||||
'step': ('{}', self.step)})
|
||||
|
||||
if elapsed:
|
||||
yield ('Start: {}', 'start_date', self.fmt_funcs['da'], self.hdrs[0]['time'])
|
||||
yield ('End: {}', 'end_date', self.fmt_funcs['da'], self.hdrs[-1]['time'])
|
||||
yield ('Avg BDI: {} min', 'avg_bdi', '{:.2f}', elapsed / nblocks / 60)
|
||||
|
||||
return ('range', gen())
|
||||
|
||||
async def create_diff_stats(self):
|
||||
|
||||
c = self.rpc
|
||||
rel = self.tip % self.rpc.proto.diff_adjust_interval
|
||||
|
||||
tip_hdr = (
|
||||
self.hdrs[-1] if self.hdrs[-1]['height'] == self.tip else
|
||||
await c.call('getblockheader', await c.call('getblockhash', self.tip)))
|
||||
|
||||
min_sample_blks = 432 # ≈3 days
|
||||
rel_hdr = await c.call('getblockheader', await c.call('getblockhash', self.tip-rel))
|
||||
|
||||
if rel >= min_sample_blks:
|
||||
sample_blks = rel
|
||||
bdi = (tip_hdr['time'] - rel_hdr['time']) / rel
|
||||
else:
|
||||
sample_blks = min(min_sample_blks, self.tip)
|
||||
start_hdr = await c.call('getblockheader', await c.call('getblockhash', self.tip-sample_blks))
|
||||
diff_adj = Decimal(tip_hdr['difficulty']) / Decimal(start_hdr['difficulty'])
|
||||
time1 = rel_hdr['time'] - start_hdr['time']
|
||||
time2 = tip_hdr['time'] - rel_hdr['time']
|
||||
bdi = ((time1 * diff_adj) + time2) / sample_blks
|
||||
|
||||
rem = self.rpc.proto.diff_adjust_interval - rel
|
||||
|
||||
return ('difficulty', (
|
||||
'Difficulty Statistics:',
|
||||
('Current height: {}', 'chain_tip', '{}', self.tip),
|
||||
('Next diff adjust: {next_diff_adjust} (in {blks_remaining} block%s [{time_remaining}])' % suf(rem),
|
||||
{
|
||||
'next_diff_adjust': ('{}', self.tip + rem),
|
||||
'blks_remaining': ('{}', rem),
|
||||
'time_remaining': (self.t_fmt, rem * bdi)
|
||||
}
|
||||
),
|
||||
('Avg BDI: {avg_bdi} min (over {sample_blks}-block period)',
|
||||
{
|
||||
'avg_bdi': ('{:.2f}', bdi/60),
|
||||
'sample_blks': ('{}', sample_blks)
|
||||
}
|
||||
),
|
||||
('Cur difficulty: {}', 'cur_diff', '{:.2e}', Decimal(tip_hdr['difficulty'])),
|
||||
('Est. diff adjust: {}%', 'est_diff_adjust_pct', '{:+.2f}', ((600 / bdi) - 1) * 100),
|
||||
))
|
||||
|
||||
def sum_field_avg(self, field):
|
||||
return self.sum_field_total(field) // len(self.res)
|
||||
|
||||
def sum_field_total(self, field):
|
||||
if isinstance(getattr(self.res[0], field), str):
|
||||
return sum(Decimal(getattr(block, field)) for block in self.res)
|
||||
else:
|
||||
return sum(getattr(block, field) for block in self.res)
|
||||
|
||||
async def create_col_avg_stats(self):
|
||||
def gen():
|
||||
for field in self.fnames:
|
||||
if field in self.avg_stats_skip:
|
||||
yield (field, ('{}', ''))
|
||||
else:
|
||||
ret = self.sum_field_avg(field)
|
||||
func = self.fields[field].fmt_func
|
||||
yield (field, ((self.fmt_funcs[func] if func else '{}'), ret))
|
||||
if not self.header_printed:
|
||||
self.print_header()
|
||||
fs = ''.join(self.gen_fs(self.fnames, fill=self.avg_stats_skip, add_name=True)).strip()
|
||||
return ('column_averages', ('Column averages:', (fs, dict(gen()))))
|
||||
|
||||
def avg_stats_data(self, data, spec_conv, spec_val):
|
||||
coin = self.rpc.proto.coin
|
||||
|
||||
return data(
|
||||
hdr = 'Averages for processed blocks:',
|
||||
func = self.sum_field_avg,
|
||||
spec_sufs = {'subsidy': f' {coin}', 'totalfee': f' {coin}'},
|
||||
spec_convs = {
|
||||
'interval': spec_conv(0, lambda arg: secs_to_ms(arg)),
|
||||
'utxo_inc': spec_conv(-1, '{:<+}'),
|
||||
'mb_per_hour': spec_conv(0, '{}')},
|
||||
spec_vals = (
|
||||
spec_val(
|
||||
'mb_per_hour', 'MB/hr', 'interval',
|
||||
lambda values: 'bs' in self.deps,
|
||||
lambda values: (
|
||||
'{:.4f}'.format((self.total_bytes / 10000) / (self.total_solve_time / 36))
|
||||
if self.total_solve_time else 'N/A')),
|
||||
))
|
||||
|
||||
mini_avg_stats_data = avg_stats_data
|
||||
|
||||
def total_stats_data(self, data, spec_conv, spec_val):
|
||||
coin = self.rpc.proto.coin
|
||||
return data(
|
||||
hdr = 'Totals for processed blocks:',
|
||||
func = self.sum_field_total,
|
||||
spec_sufs = {'subsidy': f' {coin}', 'totalfee': f' {coin}', 'reward': f' {coin}'},
|
||||
spec_convs = {
|
||||
'interval': spec_conv(0, lambda arg: secs_to_dhms(arg)),
|
||||
'utxo_inc': spec_conv(-1, '{:<+}'),
|
||||
'reward': spec_conv(0, self.fmt_funcs['tf'])},
|
||||
spec_vals = (
|
||||
spec_val(
|
||||
'reward', 'Reward', 'totalfee',
|
||||
lambda values: {'subsidy', 'totalfee'} <= set(values),
|
||||
lambda values: values['subsidy'] + values['totalfee']),
|
||||
))
|
||||
|
||||
async def create_stats(self, sname):
|
||||
|
||||
def convert_stats_hdr(field):
|
||||
v = self.fields[field]
|
||||
return '{} {}'.format(
|
||||
v.hdr1.strip(), v.hdr2.strip()).replace('- ', '') if v.hdr1 else v.hdr2.strip()
|
||||
|
||||
d = getattr(self, f'{sname}_stats_data')(
|
||||
namedtuple('stats_data', ['hdr', 'func', 'spec_sufs', 'spec_convs', 'spec_vals']),
|
||||
namedtuple('spec_conv', ['width_adj', 'conv']),
|
||||
namedtuple('spec_val', ['name', 'lbl', 'insert_after', 'condition', 'code']))
|
||||
|
||||
fnames = [n for n in self.fnames if n in self.stats_deps[sname]]
|
||||
lbls = {n: convert_stats_hdr(n) for n in fnames}
|
||||
values = {n: d.func(n) for n in fnames}
|
||||
col1_w = max((len(l) for l in lbls.values()), default=0) + 2
|
||||
|
||||
for v in d.spec_vals:
|
||||
if v.condition(values):
|
||||
try: idx = fnames.index(v.insert_after) + 1
|
||||
except: idx = 0
|
||||
fnames.insert(idx, v.name)
|
||||
lbls[v.name] = v.lbl
|
||||
values[v.name] = v.code(values)
|
||||
|
||||
def gen():
|
||||
for n, fname in enumerate(fnames):
|
||||
spec_conv = d.spec_convs.get(fname)
|
||||
yield (
|
||||
'{lbl:{wid}} {{}}{suf}'.format(
|
||||
lbl = lbls[fname] + ':',
|
||||
wid = col1_w + (spec_conv.width_adj if spec_conv else 0),
|
||||
suf = d.spec_sufs.get(fname) or ''),
|
||||
fname,
|
||||
spec_conv.conv if spec_conv else (
|
||||
(lambda x: self.fmt_funcs[x] if x else '{}')(self.fields[fname].fmt_func)),
|
||||
values[fname])
|
||||
|
||||
return (sname, (d.hdr,) + tuple(gen()))
|
||||
|
||||
def process_stats_pre(self, i):
|
||||
if (self.fnames and not self.cfg.stats_only) or i != 0:
|
||||
Msg('')
|
||||
|
||||
def finalize_output(self): pass
|
||||
|
||||
# 'getblockstats' RPC raises exception on Genesis Block, so provide our own stats:
|
||||
genesis_stats = {
|
||||
'avgfee': 0,
|
||||
'avgfeerate': 0,
|
||||
'avgtxsize': 0,
|
||||
'feerate_percentiles': [ 0, 0, 0, 0, 0 ],
|
||||
'height': 0,
|
||||
'ins': 0,
|
||||
'maxfee': 0,
|
||||
'maxfeerate': 0,
|
||||
'maxtxsize': 0,
|
||||
'medianfee': 0,
|
||||
'mediantxsize': 0,
|
||||
'minfee': 0,
|
||||
'minfeerate': 0,
|
||||
'mintxsize': 0,
|
||||
'outs': 1,
|
||||
'subsidy': 5000000000,
|
||||
'swtotal_size': 0,
|
||||
'swtotal_weight': 0,
|
||||
'swtxs': 0,
|
||||
'total_out': 0,
|
||||
'total_size': 0,
|
||||
'total_weight': 0,
|
||||
'totalfee': 0,
|
||||
'txs': 1,
|
||||
'utxo_increase': 1,
|
||||
'utxo_size_inc': 117}
|
||||
|
||||
class JSONBlocksInfo(BlocksInfo):
|
||||
|
||||
def __init__(self, cfg, cmd_args, rpc):
|
||||
super().__init__(cfg, cmd_args, rpc)
|
||||
if self.cfg.json_raw:
|
||||
self.output_block = self.output_block_raw
|
||||
self.fmt_stat_item = self.fmt_stat_item_raw
|
||||
Msg_r('{')
|
||||
|
||||
async def process_blocks(self):
|
||||
Msg_r('"block_data": [')
|
||||
await super().process_blocks()
|
||||
Msg_r(']')
|
||||
|
||||
def output_block_raw(self, data, n):
|
||||
Msg_r((', ', '')[n==0] + json.dumps(data._asdict(), cls=json_encoder))
|
||||
|
||||
def output_block(self, data, n):
|
||||
def gen():
|
||||
for k, v in data._asdict().items():
|
||||
func = self.fields[k].fmt_func
|
||||
yield (k, (self.fmt_funcs[func](v) if func else v))
|
||||
Msg_r((', ', '')[n==0] + json.dumps(dict(gen()), cls=json_encoder))
|
||||
|
||||
def print_header(self): pass
|
||||
|
||||
def fmt_stat_item_raw(self, fs, s):
|
||||
return s
|
||||
|
||||
async def output_stats(self, res, sname):
|
||||
|
||||
def gen(data):
|
||||
for d in data:
|
||||
match d:
|
||||
case [_, a]:
|
||||
for k, v in a.items():
|
||||
yield (k, self.fmt_stat_item(*v))
|
||||
case [_, a, b, c]:
|
||||
yield (a, self.fmt_stat_item(b, c))
|
||||
case str():
|
||||
pass
|
||||
case _:
|
||||
assert False, f'{d}: invalid stats data'
|
||||
|
||||
varname, data = await res
|
||||
Msg_r(', "{}_data": {}'.format(varname, json.dumps(dict(gen(data)), cls=json_encoder)))
|
||||
|
||||
def process_stats_pre(self, i): pass
|
||||
|
||||
def finalize_output(self):
|
||||
Msg('}')
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-wallet https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmgen_node_tools.Misc: miscellaneous data and functions for the MMGen Node Tools suite
|
||||
"""
|
||||
|
||||
curl_exit_codes = {
|
||||
1: 'Unsupported protocol. This build of curl has no support for this protocol',
|
||||
2: 'Failed to initialize',
|
||||
3: 'URL malformed. The syntax was not correct',
|
||||
4: 'A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time. To make curl able to do this, you probably need another build of libcurl!',
|
||||
5: 'Couldn’t resolve proxy. The given proxy host could not be resolved',
|
||||
6: 'Couldn’t resolve host. The given remote host was not resolved',
|
||||
7: 'Failed to connect to host',
|
||||
8: 'Weird server reply. The server sent data curl couldn’t parse',
|
||||
9: 'FTP access denied. The server denied login or denied access to the particular resource or directory you wanted to reach. Most often you tried to change to a directory that doesn’t exist on the server',
|
||||
10: 'FTP accept failed. While waiting for the server to connect back when an active FTP session is used, an error code was sent over the control connection or similar',
|
||||
11: 'FTP weird PASS reply. Curl couldn’t parse the reply sent to the PASS request',
|
||||
12: 'During an active FTP session while waiting for the server to connect back to curl, the timeout expired',
|
||||
13: 'FTP weird PASV reply, Curl couldn’t parse the reply sent to the PASV request',
|
||||
14: 'FTP weird 227 format. Curl couldn’t parse the 227-line the server sent',
|
||||
15: 'FTP can’t get host. Couldn’t resolve the host IP we got in the 227-line',
|
||||
16: 'HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems, see the error message for details',
|
||||
17: 'FTP couldn’t set binary. Couldn’t change transfer method to binary',
|
||||
18: 'Partial file. Only a part of the file was transferred',
|
||||
19: 'FTP couldn’t download/access the given file, the RETR (or similar) command failed',
|
||||
21: 'FTP quote error. A quote command returned error from the server',
|
||||
22: 'HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f, --fail is used',
|
||||
23: 'Write error. Curl couldn’t write data to a local filesystem or similar',
|
||||
25: 'FTP couldn’t STOR file. The server denied the STOR operation, used for FTP uploading',
|
||||
26: 'Read error. Various reading problems',
|
||||
27: 'Out of memory. A memory allocation request failed',
|
||||
28: 'Operation timeout. The specified time-out period was reached according to the conditions',
|
||||
30: 'FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!',
|
||||
31: 'FTP couldn’t use REST. The REST command failed. This command is used for resumed FTP transfers',
|
||||
33: 'HTTP range error. The range "command" didn’t work',
|
||||
34: 'HTTP post error. Internal post-request generation error',
|
||||
35: 'SSL connect error. The SSL handshaking failed',
|
||||
36: 'Bad download resume. Couldn’t continue an earlier aborted download',
|
||||
37: 'FILE couldn’t read file. Failed to open the file. Permissions?',
|
||||
38: 'LDAP cannot bind. LDAP bind operation failed',
|
||||
39: 'LDAP search failed',
|
||||
41: 'Function not found. A required LDAP function was not found',
|
||||
42: 'Aborted by callback. An application told curl to abort the operation',
|
||||
43: 'Internal error. A function was called with a bad parameter',
|
||||
45: 'Interface error. A specified outgoing interface could not be used',
|
||||
47: 'Too many redirects. When following redirects, curl hit the maximum amount',
|
||||
48: 'Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was passed on to libcurl and rejected. Read up in the manual!',
|
||||
49: 'Malformed telnet option',
|
||||
51: 'The peer’s SSL certificate or SSH MD5 fingerprint was not OK',
|
||||
52: 'The server didn’t reply anything, which here is considered an error',
|
||||
53: 'SSL crypto engine not found',
|
||||
54: 'Cannot set SSL crypto engine as default',
|
||||
55: 'Failed sending network data',
|
||||
56: 'Failure in receiving network data',
|
||||
58: 'Problem with the local certificate',
|
||||
59: 'Couldn’t use specified SSL cipher',
|
||||
60: 'Peer certificate cannot be authenticated with known CA certificates',
|
||||
61: 'Unrecognized transfer encoding',
|
||||
62: 'Invalid LDAP URL',
|
||||
63: 'Maximum file size exceeded',
|
||||
64: 'Requested FTP SSL level failed',
|
||||
65: 'Sending the data requires a rewind that failed',
|
||||
66: 'Failed to initialise SSL Engine',
|
||||
67: 'The user name, password, or similar was not accepted and curl failed to log in',
|
||||
68: 'File not found on TFTP server',
|
||||
69: 'Permission problem on TFTP server',
|
||||
70: 'Out of disk space on TFTP server',
|
||||
71: 'Illegal TFTP operation',
|
||||
72: 'Unknown TFTP transfer ID',
|
||||
73: 'File already exists (TFTP)',
|
||||
74: 'No such user (TFTP)',
|
||||
75: 'Character conversion failed',
|
||||
76: 'Character conversion functions required',
|
||||
77: 'Problem with reading the SSL CA cert (path? access rights?)',
|
||||
78: 'The resource referenced in the URL does not exist',
|
||||
79: 'An unspecified error occurred during the SSH session',
|
||||
80: 'Failed to shut down the SSL connection',
|
||||
82: 'Could not load CRL file, missing or wrong format (added in 7.19.0)',
|
||||
83: 'Issuer check failed (added in 7.19.0)',
|
||||
84: 'The FTP PRET command failed',
|
||||
85: 'RTSP: mismatch of CSeq numbers',
|
||||
86: 'RTSP: mismatch of Session Identifiers',
|
||||
87: 'unable to parse FTP file list',
|
||||
88: 'FTP chunk callback reported error',
|
||||
89: 'No connection available, the session will be queued',
|
||||
90: 'SSL public key does not matched pinned public key',
|
||||
91: 'Invalid SSL certificate status',
|
||||
92: 'Stream error in HTTP/2 framing layer',
|
||||
93: 'An API function was called from inside a callback',
|
||||
94: 'An authentication function returned an error',
|
||||
95: 'A problem was detected in the HTTP/3 layer. This is somewhat generic and can be one out of several problems, see the error message for details',
|
||||
96: 'QUIC connection error. This error may be caused by an SSL library error. QUIC is the protocol used for HTTP/3 transfers',
|
||||
}
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
mmgen_node_tools.PeerBlocks: List blocks in flight, disconnect stalling nodes
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
from mmgen.util import msg, msg_r, is_int
|
||||
from mmgen.term import get_term, get_terminal_size, get_char
|
||||
from mmgen.ui import line_input
|
||||
from .PollDisplay import PollDisplay
|
||||
|
||||
RED, RESET = ('\033[31m', '\033[0m')
|
||||
COLORS = ['\033[38;5;%s;1m' % c for c in list(range(247, 256)) + [231]]
|
||||
ERASE_ALL, CUR_HOME = ('\033[J', '\033[H')
|
||||
CUR_HIDE, CUR_SHOW = ('\033[?25l', '\033[?25h')
|
||||
term = None
|
||||
|
||||
class Display(PollDisplay):
|
||||
|
||||
poll_secs = 2
|
||||
|
||||
def __init__(self, cfg):
|
||||
|
||||
super().__init__(cfg)
|
||||
|
||||
global term, term_width
|
||||
if not term:
|
||||
term = get_term()
|
||||
term.init(noecho=True)
|
||||
term_width = self.cfg.columns or get_terminal_size().width
|
||||
msg_r(CUR_HOME+ERASE_ALL+CUR_HOME)
|
||||
|
||||
async def get_info(self, rpc):
|
||||
return await rpc.call('getpeerinfo')
|
||||
|
||||
def display(self, count):
|
||||
msg_r(
|
||||
CUR_HOME
|
||||
+ (ERASE_ALL if count == 1 else '')
|
||||
+ 'CONNECTED PEERS ({a}) {b} - poll {c}'.format(
|
||||
a = len(self.info),
|
||||
b = self.desc,
|
||||
c = count).ljust(term_width)[:term_width]
|
||||
+ '\n'
|
||||
+ ('\n'.join(self.gen_display()) + '\n' if self.info else '')
|
||||
+ ERASE_ALL
|
||||
+ f"Type a peer number to disconnect, 'q' to quit, or any other key for {self.other_desc} display:"
|
||||
+ '\b')
|
||||
|
||||
async def disconnect_node(self, rpc, addr):
|
||||
return await rpc.call('disconnectnode', addr)
|
||||
|
||||
def get_input(self):
|
||||
s = get_char(immed_chars='q0123456789', prehold_protect=False, num_bytes=1)
|
||||
if not is_int(s):
|
||||
return s
|
||||
with self.info_lock:
|
||||
msg('')
|
||||
term.reset()
|
||||
# readline required for correct operation here; without it, user must re-type first digit
|
||||
ret = line_input(self.cfg, 'peer number> ', insert_txt=s, hold_protect=False)
|
||||
term.init(noecho=True)
|
||||
self.enable_display = False # prevent display from updating before process_input()
|
||||
return ret
|
||||
|
||||
async def process_input(self, rpc):
|
||||
|
||||
ids = tuple(str(i['id']) for i in self.info)
|
||||
ret = False
|
||||
msg_r(CUR_HIDE)
|
||||
|
||||
if self.input in ids:
|
||||
from mmgen.exception import RPCFailure
|
||||
addr = self.info[ids.index(self.input)]['addr']
|
||||
try:
|
||||
await self.disconnect_node(rpc, addr)
|
||||
except RPCFailure:
|
||||
msg_r(f'Unable to disconnect peer {self.input} ({addr})')
|
||||
else:
|
||||
msg_r(f'Disconnecting peer {self.input} ({addr})')
|
||||
await asyncio.sleep(1)
|
||||
elif self.input and is_int(self.input[0]):
|
||||
msg_r(f'{self.input}: invalid peer number ')
|
||||
await asyncio.sleep(0.5)
|
||||
else:
|
||||
ret = True
|
||||
|
||||
msg_r(CUR_SHOW)
|
||||
return ret
|
||||
|
||||
class BlocksDisplay(Display):
|
||||
|
||||
desc = 'Blocks in Flight'
|
||||
other_desc = 'address'
|
||||
|
||||
def gen_display(self):
|
||||
|
||||
pd = namedtuple('peer_data', ['id', 'blks_data', 'blks_width'])
|
||||
bd = namedtuple('block_datum', ['num', 'disp'])
|
||||
|
||||
def gen_block_data():
|
||||
global min_height
|
||||
min_height = None
|
||||
for d in self.info:
|
||||
if d.get('inflight'):
|
||||
blocks = d['inflight']
|
||||
min_height = min(blocks) if not min_height else min(min_height, min(blocks))
|
||||
line = ' '.join(map(str, blocks))[:blks_field_width]
|
||||
blocks_disp = line.split()
|
||||
yield pd(
|
||||
d['id'],
|
||||
[bd(blocks[i], blocks_disp[i]) for i in range(len(blocks_disp))],
|
||||
len(line))
|
||||
else:
|
||||
yield pd(d['id'], [], 0)
|
||||
|
||||
def gen_line(peer_data):
|
||||
for blk in peer_data.blks_data:
|
||||
yield (RED if blk.num == min_height else COLORS[blk.num % 10]) + blk.disp + RESET
|
||||
|
||||
id_width = max(2, max(len(str(i['id'])) for i in self.info))
|
||||
blks_field_width = term_width - 2 - id_width
|
||||
fs = '{:>%s}: {}' % id_width
|
||||
|
||||
# we must iterate through all data to get 'min_height' before calling gen_line():
|
||||
for peer_data in tuple(gen_block_data()):
|
||||
yield fs.format(
|
||||
peer_data.id,
|
||||
' '.join(gen_line(peer_data)) + ' ' * (blks_field_width - peer_data.blks_width))
|
||||
|
||||
class PeersDisplay(Display):
|
||||
|
||||
desc = 'Address Menu'
|
||||
other_desc = 'blocks'
|
||||
|
||||
def gen_display(self):
|
||||
id_width = max(2, max(len(str(i['id'])) for i in self.info))
|
||||
addr_width = max(len(str(i['addr'])) for i in self.info)
|
||||
for d in self.info:
|
||||
yield '{a:>{A}}: {b:{B}} {c}'.format(
|
||||
a = d['id'],
|
||||
A = id_width,
|
||||
b = d['addr'],
|
||||
B = addr_width,
|
||||
c = d['subver']).ljust(term_width)[:term_width]
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
mmgen_node_tools.PollDisplay: update and display RPC data; get input from user
|
||||
"""
|
||||
|
||||
import sys, threading
|
||||
from mmgen.util import msg
|
||||
from mmgen.term import get_char
|
||||
|
||||
class PollDisplay:
|
||||
|
||||
info = None
|
||||
input = None
|
||||
poll_secs = 1
|
||||
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.info_lock = threading.Lock() # self.info accessed by 2 threads
|
||||
self.display_kill_flag = threading.Event()
|
||||
|
||||
def get_input(self):
|
||||
return get_char(immed_chars='q', prehold_protect=False, num_bytes=1)
|
||||
|
||||
async def process_input(self, rpc):
|
||||
return True
|
||||
|
||||
async def run(self, rpc):
|
||||
|
||||
async def do_display():
|
||||
with self.info_lock:
|
||||
self.info = None
|
||||
self.input = None
|
||||
self.enable_display = True
|
||||
count = 1
|
||||
while True:
|
||||
with self.info_lock:
|
||||
if self.enable_display:
|
||||
self.info = await self.get_info(rpc)
|
||||
self.display(count)
|
||||
if self.display_kill_flag.wait(self.poll_secs):
|
||||
self.display_kill_flag.clear()
|
||||
return
|
||||
count += 1
|
||||
|
||||
async def process_input():
|
||||
if self.input is None:
|
||||
sys.exit(1)
|
||||
elif self.input == 'q':
|
||||
msg('')
|
||||
sys.exit(0)
|
||||
elif self.info:
|
||||
if await self.process_input(rpc):
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_input():
|
||||
self.input = self.get_input()
|
||||
self.display_kill_flag.set()
|
||||
|
||||
while True:
|
||||
threading.Thread(target=get_input, daemon=True).start()
|
||||
await do_display()
|
||||
if await process_input():
|
||||
break
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1 +0,0 @@
|
|||
Bitcoin, BTC, Ethereum, ETH, Monero, XMR, ERC20, cryptocurrency, wallet, cold storage, offline, signing, online, security, privacy, spending, financial, investment, open-source, command-line, Python, Linux, Microsoft Windows, macOS, Bitcoin Core, BIP32, BIP39, BIP44, BIP69, BIP125, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, MSYS2, MSWin, Armbian, Raspbian, Raspberry Pi, Orange Pi, Rock Pi, BCash, Bitcoin Cash Node, BCH, Litecoin, LTC, altcoin, ZEC, Zcash, SHA256Compress, monerod, token, deploy, contract, gas, fee, smart contract, solidity, Parity, OpenEthereum, testnet, devmode, regtest
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
### Indentation must be consistent! Do not mix leading tabs and spaces.
|
||||
|
||||
### See the curl manpage for supported --proxy parameters
|
||||
### For a direct connection, leave the right-hand side blank
|
||||
proxy: http://vpn-gw:8118
|
||||
# proxy2: http://gw2:8118
|
||||
|
||||
### Override the default cache directory (~/.cache/mmgen-node-tools):
|
||||
cachedir:
|
||||
|
||||
### Additional asset columns:
|
||||
# add_columns:
|
||||
# - cnhusd=x # Yuan
|
||||
# - 6j=f # Yen futures
|
||||
|
||||
### Asset rows
|
||||
### Asset labels are arbitrary strings. Use as many or few as you wish.
|
||||
### Invoke ‘mmnode-ticker --list-ids’ for a full list of supported asset IDs.
|
||||
assets:
|
||||
coin1:
|
||||
- btc-bitcoin
|
||||
- eth-ethereum
|
||||
- xmr-monero
|
||||
coin2:
|
||||
- ada-cardano
|
||||
- bnb-binance-coin
|
||||
commodity:
|
||||
- gc=f # gold futures
|
||||
- si=f # silver futures
|
||||
- bz=f # Brent futures
|
||||
fiat:
|
||||
- gbpusd=x # Pound Sterling
|
||||
- eurusd=x # Euro
|
||||
- chfusd=x # Swiss Franc
|
||||
index:
|
||||
- ^dji # Dow Jones Industrials
|
||||
- ^ixic # Nasdaq 100
|
||||
- ^gspc # S&P 500
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# List each asset and amount in your portfolio here.
|
||||
# Amounts should be strings rather than floating-point values.
|
||||
# Invoke `mmnode-ticker --list-ids` for a full list of supported asset IDs.
|
||||
btc-bitcoin: '1.23456789'
|
||||
eth-ethereum: '2.3456789012'
|
||||
xmr-monero: '4.567890123456'
|
||||
|
||||
# Nested values are supported. Values for each asset will be summed.
|
||||
wallet2:
|
||||
btc-bitcoin: '0.12345678'
|
||||
ltc-litecoin: '1.23456789'
|
||||
|
||||
exchange1:
|
||||
xmr-monero: '12.345678901234'
|
||||
|
|
@ -1 +0,0 @@
|
|||
3.6.dev12
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-wallet https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-addrbal: Get balances for arbitrary addresses in the blockchain
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from mmgen.obj import CoinTxID
|
||||
from mmgen.cfg import Config
|
||||
from mmgen.util import msg, Msg, die, suf, make_timestr, async_run
|
||||
from mmgen.color import red
|
||||
|
||||
opts_data = {
|
||||
'text': {
|
||||
'desc': 'Get balances for arbitrary addresses in the blockchain',
|
||||
'usage': '[opts] address [address..]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-f, --first-block With tabular output, additionally display first block info
|
||||
-t, --tabular Produce compact tabular output
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
def do_output(proto, addr_data, blk_hdrs):
|
||||
|
||||
col1w = len(str(len(addr_data)))
|
||||
indent = ' ' * (col1w + 2)
|
||||
|
||||
for n, (addr, unspents) in enumerate(addr_data.items(), 1):
|
||||
Msg(f'\n{n:{col1w}}) Address: {addr.hl(addr.view_pref)}')
|
||||
|
||||
if unspents:
|
||||
heights = {u['height'] for u in unspents}
|
||||
Msg('{}Balance: {}'.format(
|
||||
indent,
|
||||
sum(proto.coin_amt(u['amount']) for u in unspents).hl3(unit=True, fs='{:,}'))),
|
||||
Msg('{}{} unspent output{} in {} block{}'.format(
|
||||
indent,
|
||||
red(str(len(unspents))),
|
||||
suf(unspents),
|
||||
red(str(len(heights))),
|
||||
suf(heights)))
|
||||
blk_w = len(str(unspents[-1]['height']))
|
||||
fs = '%s{:%s} {:19} {:64} {:4} {}' % (indent, max(5, blk_w))
|
||||
Msg(fs.format('Block', 'Date', 'TxID', 'Vout', ' Amount'))
|
||||
for u in unspents:
|
||||
Msg(fs.format(
|
||||
u['height'],
|
||||
make_timestr(blk_hdrs[u['height']]['time']),
|
||||
CoinTxID(u['txid']).hl(),
|
||||
red(str(u['vout']).rjust(4)),
|
||||
proto.coin_amt(u['amount']).fmt(6, color=True, prec=8)))
|
||||
else:
|
||||
Msg(f'{indent}No balance')
|
||||
|
||||
def do_output_tabular(proto, addr_data, blk_hdrs):
|
||||
|
||||
col1w = len(str(len(addr_data))) + 1
|
||||
max_addrw = max(len(addr) for addr in addr_data)
|
||||
fb_heights = [str(unspents[0]['height']) if unspents else '' for unspents in addr_data.values()]
|
||||
lb_heights = [str(unspents[-1]['height']) if unspents else '' for unspents in addr_data.values()]
|
||||
fb_w = max(len(h) for h in fb_heights)
|
||||
lb_w = max(len(h) for h in lb_heights)
|
||||
|
||||
fs = (
|
||||
' {n:>%s} {a} {u} {b:>%s} {t:19} {B:>%s} {T:19} {A}' % (col1w, max(5, fb_w), max(4, lb_w))
|
||||
if cfg.first_block else
|
||||
' {n:>%s} {a} {u} {B:>%s} {T:19} {A}' % (col1w, max(4, lb_w)))
|
||||
|
||||
Msg('\n' + fs.format(
|
||||
n = '',
|
||||
a = 'Address'.ljust(max_addrw),
|
||||
u = 'UTXOs',
|
||||
b = 'First',
|
||||
t = 'Block',
|
||||
B = 'Last',
|
||||
T = 'Block',
|
||||
A = ' Amount'))
|
||||
|
||||
for n, (addr, unspents) in enumerate(addr_data.items(), 1):
|
||||
if unspents:
|
||||
Msg(fs.format(
|
||||
n = str(n) + ')',
|
||||
a = addr.fmt(addr.view_pref, max_addrw, color=True),
|
||||
u = red(str(len(unspents)).rjust(5)),
|
||||
b = unspents[0]['height'],
|
||||
t = make_timestr(blk_hdrs[unspents[0]['height']]['time']),
|
||||
B = unspents[-1]['height'],
|
||||
T = make_timestr(blk_hdrs[unspents[-1]['height']]['time']),
|
||||
A = sum(proto.coin_amt(u['amount']) for u in unspents).fmt(7, color=True, prec=8)))
|
||||
else:
|
||||
Msg(fs.format(
|
||||
n = str(n) + ')',
|
||||
a = addr.fmt(addr.view_pref, max_addrw, color=True),
|
||||
u = ' -',
|
||||
b = '-',
|
||||
t = '',
|
||||
B = '-',
|
||||
T = '',
|
||||
A = ' -'))
|
||||
|
||||
async def main(req_addrs):
|
||||
|
||||
proto = cfg._proto
|
||||
|
||||
from mmgen.addr import CoinAddr
|
||||
addrs = [CoinAddr(proto, addr) for addr in req_addrs]
|
||||
|
||||
from mmgen.rpc import rpc_init
|
||||
rpc = await rpc_init(cfg, ignore_wallet=True)
|
||||
|
||||
height = await rpc.call('getblockcount')
|
||||
Msg(f'{proto.coin} {proto.network.upper()} [height {height}]')
|
||||
|
||||
from mmgen.proto.btc.misc import scantxoutset
|
||||
res = await scantxoutset(cfg, rpc, [f'addr({addr})' for addr in addrs])
|
||||
|
||||
if not res['success']:
|
||||
die(1, 'UTXO scanning failed or was interrupted')
|
||||
elif not res['unspents']:
|
||||
msg('Address has no balance' if len(addrs) == 1 else
|
||||
'Addresses have no balances')
|
||||
else:
|
||||
addr_data = {k:[] for k in addrs}
|
||||
|
||||
if 'desc' in res['unspents'][0]:
|
||||
import re
|
||||
for unspent in sorted(res['unspents'], key=lambda x: x['height']):
|
||||
addr = re.match('addr\((.*?)\)', unspent['desc'])[1]
|
||||
addr_data[addr].append(unspent)
|
||||
else:
|
||||
from mmgen.proto.btc.tx.base import decodeScriptPubKey
|
||||
for unspent in sorted(res['unspents'], key=lambda x: x['height']):
|
||||
ds = decodeScriptPubKey(proto, unspent['scriptPubKey'])
|
||||
addr_data[ds.addr].append(unspent)
|
||||
|
||||
good_addrs = len([v for v in addr_data.values() if v])
|
||||
|
||||
Msg('Total: {} in {} address{}'.format(
|
||||
proto.coin_amt(res['total_amount']).hl3(unit=True, fs='{:,}'),
|
||||
red(str(good_addrs)),
|
||||
suf(good_addrs, 'es')))
|
||||
|
||||
blk_heights = {i['height'] for i in res['unspents']}
|
||||
blk_hashes = await rpc.batch_call('getblockhash', [(h,) for h in blk_heights])
|
||||
blk_hdrs = await rpc.batch_call('getblockheader', [(H,) for H in blk_hashes])
|
||||
|
||||
(do_output_tabular if cfg.tabular else do_output)(
|
||||
proto, addr_data, dict(zip(blk_heights, blk_hdrs)))
|
||||
|
||||
cfg = Config(opts_data=opts_data, init_opts={'rpc_backend': 'aiohttp'})
|
||||
|
||||
if len(cfg._args) < 1:
|
||||
die(1, 'This command requires at least one coin address argument')
|
||||
|
||||
try:
|
||||
async_run(cfg, main, args=[cfg._args])
|
||||
except KeyboardInterrupt:
|
||||
sys.stderr.write('\n')
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmnode-blocks-info: Display information about a block or range of blocks
|
||||
"""
|
||||
|
||||
from mmgen.cfg import gc, Config
|
||||
from mmgen.util import async_run, fmt_list
|
||||
from .BlocksInfo import BlocksInfo, JSONBlocksInfo
|
||||
|
||||
opts_data = {
|
||||
'sets': [
|
||||
('header_info', True, 'fields', None),
|
||||
('header_info', True, 'miner_info', None),
|
||||
('header_info', True, 'stats', 'range'),
|
||||
('json_raw', True, 'json', True),
|
||||
('raw_miner_info', True, 'miner_info', True),
|
||||
('stats_only', True, 'no_header', True),
|
||||
],
|
||||
'text': {
|
||||
'desc': 'Display information about a block or range of blocks',
|
||||
'usage': '[opts] blocknum ... | blocknum-blocknum[+step] | [blocknum|-nBlocks]+nBlocks[+step]',
|
||||
'usage2': [
|
||||
'[opts] blocknum ...',
|
||||
'[opts] blocknum-blocknum[+step]',
|
||||
'[opts] [blocknum|-nBlocks]+nBlocks[+step]',
|
||||
],
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-f, --full-stats Stats that relate to a specific field are shown only
|
||||
if that field is configured, whether by default or via
|
||||
the --fields option. This option adds the fields req-
|
||||
uired to produce a full display of configured stats.
|
||||
-H, --header-info Display information from block headers only
|
||||
-j, --json Produce JSON output
|
||||
-J, --json-raw Produce JSON output with unformatted values
|
||||
-m, --miner-info Display miner info in coinbase transaction
|
||||
-M, --raw-miner-info Display miner info in uninterpreted form
|
||||
-n, --no-header Don’t print the column header
|
||||
-o, --fields= Display the specified fields (comma-separated list).
|
||||
See AVAILABLE FIELDS below. Prefix the list with '+'
|
||||
to add the fields to the defaults, or '-' to remove
|
||||
them. The special values 'all' and 'none' select all
|
||||
available fields or none, respectively. The '+' and
|
||||
'-'-prefixed lists may be concatenated to specify both
|
||||
addition and removal of fields. A single '-'-prefixed
|
||||
list may be additionally prefixed by 'all'.
|
||||
-s, --stats= Display the specified stats (comma-separated list).
|
||||
See AVAILABLE STATS below. The prefixes and special
|
||||
values available to the --fields option are recognized.
|
||||
-S, --stats-only Display stats only. Suppress display of per-block data.
|
||||
""",
|
||||
'notes': """
|
||||
If no block number is specified, the current chain tip is assumed.
|
||||
|
||||
The special value 'cur' can be used to designate the chain tip wherever a
|
||||
block number is expected.
|
||||
|
||||
If the requested range ends at the current chain tip, an estimate of the next
|
||||
difficulty adjustment is also displayed. The estimate is based on the average
|
||||
Block Discovery Interval from the beginning of the current {I}-block period.
|
||||
|
||||
All fee fields except for 'totalfee' are in satoshis per virtual byte.
|
||||
|
||||
AVAILABLE FIELDS: {F}
|
||||
|
||||
AVAILABLE STATS: {S}
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
Display info for current block:
|
||||
$ {p}
|
||||
|
||||
Display info for the Genesis Block:
|
||||
$ {p} 0
|
||||
|
||||
Display info for the last 20 blocks:
|
||||
$ {p} +20
|
||||
|
||||
Display specified fields for blocks 165-190
|
||||
$ {p} -o block,date,size,inputs,nTx 165-190
|
||||
|
||||
Display info for 10 blocks beginning at block 600000:
|
||||
$ {p} 600000+10
|
||||
|
||||
Display info for every 5th block of 50-block range beginning at 1000
|
||||
blocks from chain tip:
|
||||
$ {p} -- -1000+50+5
|
||||
|
||||
Display info for block 152817, adding miner field:
|
||||
$ {p} -o +miner 152817
|
||||
|
||||
Display specified fields for listed blocks:
|
||||
$ {p} -o block,date,hash 245798 170 624044
|
||||
|
||||
Display every difficulty adjustment from Genesis Block to chain tip:
|
||||
$ {p} -o +difficulty 0-cur+{I}
|
||||
|
||||
Display roughly a block a day over the last two weeks. Note that
|
||||
multiplication is allowed in the nBlocks spec:
|
||||
$ {p} +144*14+144
|
||||
|
||||
Display only range stats for the last ten blocks:
|
||||
$ {p} -o none -s range +10
|
||||
|
||||
Display data for the last ten blocks, omitting the 'size' and 'subsidy'
|
||||
fields from the defaults and skipping stats:
|
||||
$ {p} -o -size,subsidy -s none +10
|
||||
|
||||
Display data for the last ten blocks, omitting the 'size' and 'version'
|
||||
fields from the defaults and adding the 'inputs' and 'utxo_inc' fields:
|
||||
$ {p} -o -version,size+utxo_inc,inputs +10
|
||||
|
||||
Display all fields and stats for the last ten blocks:
|
||||
$ {p} -o all -s all +10
|
||||
|
||||
Same as above, but omit the 'miner' and 'hash' fields:
|
||||
$ {p} -o all-miner,hash -s all +10
|
||||
|
||||
Same as above, but display only fields relating to stats:
|
||||
$ {p} -o none -s all -f +10
|
||||
|
||||
Same as above, but display stats only:
|
||||
$ {p} -o none -s all -fS +10
|
||||
|
||||
Display headers-only info for the last 1000 blocks. Speed up execution
|
||||
using the async RPC backend:
|
||||
$ {p} --rpc-backend=aio -H +1000
|
||||
|
||||
This program requires a txindex-enabled daemon for correct operation.
|
||||
"""},
|
||||
'code': {
|
||||
'notes': lambda cfg, proto, s: s.format(
|
||||
I = proto.diff_adjust_interval,
|
||||
F = fmt_list(BlocksInfo.fields, fmt='bare'),
|
||||
S = fmt_list(BlocksInfo.all_stats, fmt='bare'),
|
||||
p = gc.prog_name)
|
||||
}
|
||||
}
|
||||
|
||||
cfg = Config(opts_data=opts_data)
|
||||
|
||||
async def main():
|
||||
|
||||
from mmgen.rpc import rpc_init
|
||||
|
||||
cls = JSONBlocksInfo if cfg.json else BlocksInfo
|
||||
|
||||
m = cls(cfg, cfg._args, await rpc_init(cfg, ignore_wallet=True))
|
||||
|
||||
if m.fnames and not cfg.no_header:
|
||||
m.print_header()
|
||||
|
||||
await m.process_blocks()
|
||||
|
||||
if m.last:
|
||||
for i, sname in enumerate(m.stats):
|
||||
m.process_stats_pre(i)
|
||||
await m.process_stats(sname)
|
||||
|
||||
m.finalize_output()
|
||||
|
||||
async_run(cfg, main)
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmnode-feeview: Visualize the fee structure of a node’s mempool
|
||||
"""
|
||||
|
||||
from mmgen.cfg import Config
|
||||
from mmgen.util import async_run, die, fmt, make_timestr, check_int_between
|
||||
from mmgen.util2 import int2bytespec, parse_bytespec
|
||||
|
||||
min_prec, max_prec, dfl_prec = (0, 6, 4)
|
||||
fee_brackets = [
|
||||
1, 2, 3, 4, 5, 6,
|
||||
8, 10, 12, 14, 16, 18,
|
||||
20, 25, 30, 35, 40, 45,
|
||||
50, 60, 70, 80, 90,
|
||||
100, 120, 140, 160, 180,
|
||||
200, 250, 300, 350, 400, 450,
|
||||
500, 600, 700, 800, 900,
|
||||
1000, 1200, 1400, 1600, 1800,
|
||||
2000, 2500, 3000, 3500, 4000, 4500,
|
||||
5000, 6000, 7000, 8000, 9000,
|
||||
10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000,
|
||||
100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 2100000000000000,
|
||||
]
|
||||
|
||||
opts_data = {
|
||||
'sets': [
|
||||
('detail', True, 'ranges', True),
|
||||
('detail', True, 'show_mb_col', True),
|
||||
('detail', True, 'precision', 6),
|
||||
],
|
||||
'text': {
|
||||
'desc': 'Visualize the fee structure of a node’s mempool',
|
||||
'usage':'[opts]',
|
||||
'options': f"""
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-c, --include-current Include current bracket’s TXs in cumulative MB value
|
||||
-d, --outdir=D Write log data to directory 'D'
|
||||
-D, --detail Same as --ranges --show-mb-col --precision=6
|
||||
-e, --show-empty Show all fee brackets, including empty ones
|
||||
-i, --ignore-below=B Ignore fee brackets with less than 'B' bytes of TXs
|
||||
-l, --log Log JSON-RPC mempool data to 'mempool.json'
|
||||
-p, --precision=P Use 'P' decimal points of precision for megabyte amts
|
||||
(min: {min_prec}, max: {max_prec}, default: {dfl_prec})
|
||||
-P, --pager Pipe the output to a pager
|
||||
-r, --ranges Display fee brackets as ranges
|
||||
-s, --show-mb-col Display column with each fee bracket’s megabyte count
|
||||
""",
|
||||
'notes': """
|
||||
+ By default, fee bracket row labels include only the top of the range.
|
||||
+ By default, empty fee brackets are not displayed.
|
||||
+ Mempool amounts are shown in decimal megabytes.
|
||||
+ Values in the Total MB column are cumulative and represent megabytes of
|
||||
transactions in the mempool with fees higher than the TOP of the current
|
||||
fee bracket. To change this behavior, use the --include-current option.
|
||||
|
||||
Note that there is no global mempool in Bitcoin, and your node’s mempool may
|
||||
differ significantly from those of mining nodes depending on uptime and other
|
||||
factors.
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
cfg = Config(opts_data=opts_data)
|
||||
|
||||
if cfg.ignore_below:
|
||||
if cfg.show_empty:
|
||||
die(1, 'Conflicting options: --ignore-below, --show-empty')
|
||||
ignore_below = parse_bytespec(cfg.ignore_below)
|
||||
|
||||
precision = (
|
||||
check_int_between(cfg.precision, min_prec, max_prec, desc='--precision arg')
|
||||
if cfg.precision else dfl_prec)
|
||||
|
||||
from mmgen.term import get_terminal_size
|
||||
width = cfg.columns or get_terminal_size().width
|
||||
|
||||
class fee_bracket:
|
||||
def __init__(self, top, bottom):
|
||||
self.top = top
|
||||
self.bottom = bottom
|
||||
self.tx_bytes = 0
|
||||
self.tx_bytes_cum = 0
|
||||
self.skip = False
|
||||
|
||||
def log(data, fn):
|
||||
import json
|
||||
from mmgen.rpc.util import json_encoder
|
||||
from mmgen.fileutil import write_data_to_file
|
||||
write_data_to_file(
|
||||
cfg = cfg,
|
||||
outfile = fn,
|
||||
data = json.dumps(data, cls=json_encoder, sort_keys=True, indent=4),
|
||||
desc = 'mempool',
|
||||
ask_overwrite = False)
|
||||
|
||||
def create_data(coin_amt, mempool):
|
||||
out = [fee_bracket(fee_brackets[i], fee_brackets[i-1] if i else 0) for i in range(len(fee_brackets))]
|
||||
|
||||
# populate fee brackets:
|
||||
size_key = 'size' if proto.coin == 'BCH' else 'vsize'
|
||||
for tx in mempool.values():
|
||||
fee = coin_amt(tx['fees']['base']).to_unit('satoshi')
|
||||
size = tx[size_key]
|
||||
for bracket in out:
|
||||
if fee / size < bracket.top:
|
||||
bracket.tx_bytes += size
|
||||
break
|
||||
|
||||
# remove empty top brackets:
|
||||
while out and out[-1].tx_bytes == 0:
|
||||
out.pop()
|
||||
|
||||
out.reverse() # cumulative totals and display are top-down
|
||||
|
||||
# calculate cumulative byte totals, filter rows:
|
||||
tBytes = 0
|
||||
for i in out:
|
||||
if not (i.tx_bytes or cfg.show_empty):
|
||||
i.skip = True
|
||||
if cfg.ignore_below and i.tx_bytes < ignore_below:
|
||||
i.skip = True
|
||||
i.tx_bytes_cum = tBytes
|
||||
tBytes += i.tx_bytes
|
||||
|
||||
return out
|
||||
|
||||
def gen_header(host, mempool, blockcount):
|
||||
|
||||
yield fmt(f"""
|
||||
Mempool Fee Structure
|
||||
Date: {make_timestr()} UTC
|
||||
Host: {host}
|
||||
Network: {proto.coin.upper()} {proto.network.upper()}
|
||||
Block: {blockcount}
|
||||
TX count: {len(mempool)}
|
||||
""").strip()
|
||||
|
||||
if cfg.show_empty:
|
||||
yield 'Displaying all fee brackets'
|
||||
elif cfg.ignore_below:
|
||||
yield 'Ignoring fee brackets with less than {:,} bytes ({})'.format(
|
||||
ignore_below,
|
||||
int2bytespec(ignore_below, 'MB', '0.6', strip=True, add_space=True))
|
||||
|
||||
if cfg.include_current:
|
||||
yield 'Including transactions in current fee bracket in Total MB amounts'
|
||||
|
||||
def fmt_mb(n):
|
||||
return int2bytespec(n, 'MB', f'0.{precision}', print_sym=False)
|
||||
|
||||
def gen_body(data):
|
||||
tx_bytes_max = max((i.tx_bytes for i in data), default=0)
|
||||
top_max = max((i.top for i in data), default=0)
|
||||
bot_max = max((i.bottom for i in data), default=0)
|
||||
col1_w = max(len(f'{bot_max}-{top_max}') if cfg.ranges else len(f'{top_max}'), 6)
|
||||
col2_w = len(fmt_mb(tx_bytes_max)) if cfg.show_mb_col else 0
|
||||
col3_w = len(fmt_mb(data[-1].tx_bytes_cum)) if data else 0
|
||||
col4_w = width - col1_w - col2_w - col3_w - (4 if col2_w else 3)
|
||||
if cfg.show_mb_col:
|
||||
fs = '{a:<%i} {b:>%i} {c:>%i} {d}' % (col1_w, col2_w, col3_w)
|
||||
else:
|
||||
fs = '{a:<%i} {c:>%i} {d}' % (col1_w, col3_w)
|
||||
|
||||
yield fs.format(a='', b='', c=f'{"Total":<{col3_w}}', d='')
|
||||
yield fs.format(a='sat/B', b=f'{"MB":<{col2_w}}', c=f'{"MB":<{col3_w}}', d='')
|
||||
|
||||
for i in data:
|
||||
if not i.skip:
|
||||
cum_bytes = i.tx_bytes_cum + i.tx_bytes if cfg.include_current else i.tx_bytes_cum
|
||||
yield fs.format(
|
||||
a = '{}-{}'.format(i.bottom, i.top) if cfg.ranges else i.top,
|
||||
b = fmt_mb(i.tx_bytes),
|
||||
c = fmt_mb(cum_bytes),
|
||||
d = '-' * int(col4_w * (i.tx_bytes / tx_bytes_max)))
|
||||
|
||||
yield fs.format(
|
||||
a = 'TOTAL',
|
||||
b = '',
|
||||
c = fmt_mb(data[-1].tx_bytes_cum + data[-1].tx_bytes if data else 0),
|
||||
d = '')
|
||||
|
||||
async def main():
|
||||
|
||||
global proto
|
||||
proto = cfg._proto
|
||||
|
||||
from mmgen.rpc import rpc_init
|
||||
c = await rpc_init(cfg, ignore_wallet=True)
|
||||
|
||||
mempool = await c.call('getrawmempool', True)
|
||||
|
||||
if cfg.log:
|
||||
log(mempool, 'mempool.json')
|
||||
|
||||
data = create_data(proto.coin_amt, mempool)
|
||||
cfg._util.stdout_or_pager(
|
||||
'\n'.join(gen_header(
|
||||
c.host,
|
||||
mempool,
|
||||
await c.call('getblockcount'))) + '\n\n' +
|
||||
'\n'.join(gen_body(data)) + '\n')
|
||||
|
||||
async_run(cfg, main)
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmnode-halving-calculator: Estimate date(s) of future block subsidy halving(s)
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from mmgen.cfg import Config
|
||||
from mmgen.util import async_run
|
||||
|
||||
bdr_proj = 9.95
|
||||
|
||||
opts_data = {
|
||||
'sets': [('mined', True, 'list', True)],
|
||||
'text': {
|
||||
'desc': 'Estimate date(s) of future block subsidy halving(s)',
|
||||
'usage':'[opts]',
|
||||
'options': f"""
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-l, --list List historical and projected halvings
|
||||
-m, --mined Same as above, plus list coins mined
|
||||
-r, --bdr-proj=I Block discovery interval for projected halvings (default:
|
||||
{bdr_proj:.5f} min)
|
||||
-s, --sample-size=N Block range to calculate block discovery interval for next
|
||||
halving estimate (default: dynamically calculated)
|
||||
"""}
|
||||
}
|
||||
|
||||
cfg = Config(opts_data=opts_data)
|
||||
|
||||
if cfg.bdr_proj:
|
||||
bdr_proj = float(cfg.bdr_proj)
|
||||
|
||||
def date(t):
|
||||
return '{}-{:02}-{:02} {:02}:{:02}:{:02}'.format(*time.gmtime(t)[:6])
|
||||
|
||||
def dhms(t):
|
||||
t, neg = (-t, '-') if t < 0 else (t, ' ')
|
||||
return f'{neg}{t//60//60//24} days, {t//60//60%24:02}:{t//60%60:02}:{t%60:02} h/m/s'
|
||||
|
||||
def time_diff_warning(t_diff):
|
||||
if abs(t_diff) > 60*60:
|
||||
print('Warning: block tip time is {} {} clock time!'.format(
|
||||
dhms(abs(t_diff)),
|
||||
('behind', 'ahead of')[t_diff<0]))
|
||||
|
||||
async def main():
|
||||
|
||||
proto = cfg._proto
|
||||
|
||||
from mmgen.rpc import rpc_init
|
||||
c = await rpc_init(cfg, proto, ignore_wallet=True)
|
||||
|
||||
tip = await c.call('getblockcount')
|
||||
assert tip > 1, 'block tip must be > 1'
|
||||
remaining = proto.halving_interval - tip % proto.halving_interval
|
||||
sample_size = int(cfg.sample_size) if cfg.sample_size else min(tip-1, max(remaining, 144))
|
||||
|
||||
cur, old = await c.gathered_call('getblockstats', ((tip,), (tip - sample_size,)))
|
||||
|
||||
clock_time = int(time.time())
|
||||
time_diff_warning(clock_time - cur['time'])
|
||||
bdr = (cur['time'] - old['time']) / sample_size
|
||||
t_rem = remaining * int(bdr)
|
||||
t_next = cur['time'] + t_rem
|
||||
|
||||
if proto.name == 'BitcoinCash':
|
||||
sub = proto.coin_amt(str(cur['subsidy']))
|
||||
else:
|
||||
sub = cur['subsidy'] * proto.coin_amt.satoshi
|
||||
|
||||
def print_current_stats():
|
||||
print(
|
||||
f'Current block: {tip:>7}\n'
|
||||
f'Next halving block: {tip + remaining:>7}\n'
|
||||
f'Halving interval: {proto.halving_interval:>7}\n'
|
||||
f'Blocks since last halving: {proto.halving_interval - remaining:>7}\n'
|
||||
f'Blocks until next halving: {remaining:>7}\n\n'
|
||||
f'Current block subsidy: {str(sub).rstrip("0")} {proto.coin}\n'
|
||||
f'Current block discovery interval (over last {sample_size} blocks): {bdr/60:0.2f} min\n\n'
|
||||
f'Current clock time (UTC): {date(clock_time)}\n'
|
||||
f'Est. halving date (UTC): {date(t_next)}\n'
|
||||
f'Est. time until halving: {dhms(cur["time"] + t_rem - clock_time)}')
|
||||
|
||||
async def print_halvings():
|
||||
halving_blocknums = [i*proto.halving_interval for i in range(proto.max_halvings+1)][1:]
|
||||
hist_halvings = await c.gathered_call('getblockstats',([(n,) for n in halving_blocknums if n <= tip]))
|
||||
halving_secs = bdr_proj * 60 * proto.halving_interval
|
||||
nhist = len(hist_halvings)
|
||||
nSubsidy = int(proto.start_subsidy / proto.coin_amt.satoshi)
|
||||
|
||||
block0_hash = await c.call('getblockhash', 0)
|
||||
block0_date = (await c.call('getblock', block0_hash))['time']
|
||||
|
||||
def gen_data():
|
||||
total_mined = 0
|
||||
date = block0_date
|
||||
for n, blk in enumerate(halving_blocknums):
|
||||
mined = (nSubsidy >> n) * proto.halving_interval
|
||||
if n == 0:
|
||||
mined -= nSubsidy # subtract unspendable genesis block subsidy
|
||||
total_mined += mined
|
||||
sub = nSubsidy >> n+1 if n+1 < proto.max_halvings else 0
|
||||
bdi = (
|
||||
(hist_halvings[n]['time'] - date) / (proto.halving_interval * 60) if n < nhist
|
||||
else bdr/60 if n == nhist
|
||||
else bdr_proj)
|
||||
date = (
|
||||
hist_halvings[n]['time'] if n < nhist
|
||||
else t_next + int((n - nhist) * halving_secs))
|
||||
yield (n, sub, blk, mined, total_mined, bdi, date)
|
||||
if sub == 0:
|
||||
break
|
||||
|
||||
fs = (
|
||||
' {a:<7} {b:>8} {c:19}{d:2} {e:10} {f}',
|
||||
' {a:<7} {b:>8} {c:19}{d:2} {e:10} {f:17} {g:17} {h}'
|
||||
)[bool(cfg.mined)]
|
||||
|
||||
print(
|
||||
f'Historical/Estimated/Projected Halvings ({proto.coin}):\n\n'
|
||||
+ f' Sample size for next halving estimate (E): {sample_size} blocks\n'
|
||||
+ f' Block discovery interval for projected halvings (P): {bdr_proj:.5f} minutes\n\n'
|
||||
+ fs.format(
|
||||
a = 'HALVING',
|
||||
b = 'BLOCK',
|
||||
c = 'DATE',
|
||||
d = '',
|
||||
e = 'BDI (mins)',
|
||||
f = 'SUBSIDY ({proto.coin})',
|
||||
g = f'MINED ({proto.coin})',
|
||||
h = f'TOTAL MINED ({proto.coin})')
|
||||
+ '\n'
|
||||
+ fs.format(
|
||||
a = '-' * 7,
|
||||
b = '-' * 8,
|
||||
c = '-' * 19,
|
||||
d = '-' * 2,
|
||||
e = '-' * 10,
|
||||
f = '-' * 17,
|
||||
g = '-' * 17,
|
||||
h = '-' * 17)
|
||||
+ '\n'
|
||||
+ '\n'.join(fs.format(
|
||||
a = n + 1,
|
||||
b = blk,
|
||||
c = date(t),
|
||||
d = ' P' if n > nhist else '' if n < nhist else ' E',
|
||||
e = f'{bdr:8.5f}',
|
||||
f = proto.coin_amt(sub, from_unit='satoshi').fmt(2, prec=8),
|
||||
g = proto.coin_amt(mined, from_unit='satoshi').fmt(8, prec=8),
|
||||
h = proto.coin_amt(total_mined, from_unit='satoshi').fmt(8, prec=8)
|
||||
) for n, sub, blk, mined, total_mined, bdr, t in gen_data()))
|
||||
|
||||
if cfg.list:
|
||||
await print_halvings()
|
||||
else:
|
||||
print_current_stats()
|
||||
|
||||
async_run(cfg, main)
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmnode-netrate: Bitcoin daemon network rate monitor
|
||||
"""
|
||||
|
||||
import sys, time
|
||||
|
||||
from mmgen.cfg import Config
|
||||
from mmgen.util import async_run
|
||||
|
||||
opts_data = {
|
||||
'text': {
|
||||
'desc': 'Bitcoin daemon network rate monitor',
|
||||
'usage': '[opts]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
"""}
|
||||
}
|
||||
|
||||
cfg = Config(opts_data=opts_data)
|
||||
|
||||
ERASE_LINE, CUR_UP = '\033[K', '\033[1A'
|
||||
|
||||
async def main():
|
||||
|
||||
from mmgen.rpc import rpc_init
|
||||
c = await rpc_init(cfg, ignore_wallet=True)
|
||||
|
||||
async def get_data():
|
||||
d = await c.call('getnettotals')
|
||||
return [float(e) for e in (d['totalbytesrecv'], d['totalbytessent'], d['timemillis'])]
|
||||
|
||||
rs, ss, ts = (None, None, None)
|
||||
while True:
|
||||
r, s, t = await get_data()
|
||||
|
||||
if rs is not None:
|
||||
sys.stderr.write(
|
||||
'\rrcvd: {:9.2f} kB/s\nsent: {:9.2f} kB/s '.format(
|
||||
(r-rs)/(t-ts),
|
||||
(s-ss)/(t-ts)))
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
if rs is not None:
|
||||
sys.stderr.write('{}{}{}'.format(ERASE_LINE, CUR_UP, ERASE_LINE))
|
||||
|
||||
rs, ss, ts = (r, s, t)
|
||||
|
||||
try:
|
||||
async_run(cfg, main)
|
||||
except KeyboardInterrupt:
|
||||
sys.stderr.write('\n')
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-wallet https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
mmnode-ticker: Display price information for cryptocurrency and other assets
|
||||
"""
|
||||
|
||||
opts_data = {
|
||||
'sets': [
|
||||
('widest', True, 'percent_cols', 'd,w,m,y'),
|
||||
('widest', True, 'name_labels', True),
|
||||
('widest', True, 'thousands_comma', True),
|
||||
('widest', True, 'update_time', True),
|
||||
('wide', True, 'percent_cols', 'd,w'),
|
||||
('wide', True, 'name_labels', True),
|
||||
('wide', True, 'thousands_comma', True),
|
||||
('wide', True, 'update_time', True),
|
||||
],
|
||||
'text': {
|
||||
'desc': 'Display prices for cryptocurrency and other assets',
|
||||
'usage': '[opts] [TRADE_SPECIFIER | ASSET_RANGE]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-a, --asset-limit=N Retrieve data for top ‘N’ cryptocurrencies by market
|
||||
cap (default: {al}). To retrieve all available data,
|
||||
specify a value of zero.
|
||||
-A, --adjust=P Adjust prices by percentage ‘P’. In ‘trading’ mode,
|
||||
spot and adjusted prices are shown in separate columns.
|
||||
-b, --btc Fetch and display data for Bitcoin only
|
||||
-c, --add-columns=LIST Add columns for asset specifiers in LIST (comma-
|
||||
separated, see ASSET SPECIFIERS below). Can also be
|
||||
used to supply a USD exchange rate for missing assets.
|
||||
-C, --cached-data Use cached data from previous network query instead of
|
||||
live data from server
|
||||
-d, --download=D Retrieve and cache asset data ‘D’ from network (valid
|
||||
options: {ds})
|
||||
-D, --cachedir=D Read and write cached JSON data to directory ‘D’
|
||||
instead of ‘~/{dfl_cachedir}’
|
||||
-e, --add-precision=N Add ‘N’ digits of precision to columns
|
||||
-E, --elapsed Show elapsed time in UPDATED column (see --update-time)
|
||||
-F, --portfolio Display portfolio data
|
||||
-l, --list-ids List IDs of all available assets
|
||||
-n, --name-labels Label rows with asset names rather than symbols
|
||||
-p, --percent-cols=C Add daily, weekly, monthly, or yearly percentage change
|
||||
columns ‘C’ (specify with comma-separated letters
|
||||
{pc})
|
||||
-P, --pager Pipe the output to a pager
|
||||
-q, --quiet Produce quieter output
|
||||
-r, --add-rows=LIST Add rows for asset specifiers in LIST (comma-separated,
|
||||
see ASSET SPECIFIERS below). Can also be used to supply
|
||||
a USD exchange rate for missing assets.
|
||||
-s, --sort=P Sort output according to parameter P. Valid parameters
|
||||
are {sp_codes}. See SORT PARAMETERS below.
|
||||
To reverse the sort, prefix the parameter with ‘r’.
|
||||
-t, --testing Print command(s) to be executed to stdout and exit
|
||||
-T, --thousands-comma Use comma as a thousands separator
|
||||
-u, --update-time Include UPDATED (last update time) column
|
||||
-U, --pchg-unit=A Use asset ‘A’ as unit of reference for percentage
|
||||
change columns (default: USD)
|
||||
-v, --verbose Be more verbose
|
||||
-w, --wide Display most optional columns (same as -unT -p d,w)
|
||||
-W, --widest Display all optional columns (same as -unT -p d,w,m,y)
|
||||
-x, --proxy=P Connect via proxy ‘P’. Set to the empty string to
|
||||
completely disable or ‘none’ to allow override from
|
||||
environment. Consult the curl manpage for --proxy usage.
|
||||
-X, --proxy2=P Alternate proxy for non-crypto financial data. Defaults
|
||||
to value of --proxy
|
||||
""",
|
||||
'notes': """
|
||||
|
||||
The script has three display modes: ‘overview’, enabled when no arguments are
|
||||
given on the command line; ‘trading’, when a TRADE_SPECIFIER argument (see
|
||||
below) is given; and ‘market cap’, when an ASSET_RANGE (see below) is given.
|
||||
|
||||
Overview mode displays prices of all configured assets, and optionally the
|
||||
user’s portfolio; trading mode displays the price of a given quantity of an
|
||||
asset in relation to other assets, optionally comparing an offered price to
|
||||
the spot price; and market cap mode lists a range of crypto assets selected
|
||||
by current market cap.
|
||||
|
||||
The ASSET_RANGE argument can be either an integer N, in which case the top
|
||||
N assets by market cap will be displayed, or a hyphen-separated range N-M,
|
||||
in which case assets from N to M by market cap will be displayed.
|
||||
|
||||
ASSETS consist of either a symbol (e.g. ‘xmr’) or full ID (see --list-ids)
|
||||
consisting of symbol plus label (e.g. ‘xmr-monero’). In cases where the
|
||||
symbol is ambiguous, the full ID must be used. For Yahoo Finance assets
|
||||
the symbol and ID are identical:
|
||||
|
||||
Examples:
|
||||
|
||||
ltc - specify asset by symbol
|
||||
ltc-litecoin - same as above, but use full ID instead of symbol
|
||||
^dji - Dow Jones Industrial Average (Yahoo)
|
||||
gc=f - gold futures (Yahoo)
|
||||
|
||||
ASSET SPECIFIERS have the following format:
|
||||
|
||||
ASSET[:RATE[:RATE_ASSET]]
|
||||
|
||||
If the asset referred to by ASSET is not in the source data (see --list-ids),
|
||||
an arbitrarily chosen label may be used. RATE is the exchange rate of the
|
||||
asset in relation to RATE_ASSET, if present, otherwise USD. When RATE is
|
||||
postfixed with the letter ‘r’, its meaning is reversed, i.e. interpreted as
|
||||
‘ASSET/RATE_ASSET’ instead of ‘RATE_ASSET/ASSET’. Asset specifier examples:
|
||||
|
||||
inr:79.5 - INR is not in the source data, so supply rate of
|
||||
79.5 INR to the Dollar (USD/INR)
|
||||
inr:0.01257r - same as above, but use reverse rate (INR/USD)
|
||||
inr-indian-rupee:79.5 - same as first example, but add an arbitrary label
|
||||
omr-omani-rial:2.59r - Omani Rial is pegged to the Dollar at 2.59 USD
|
||||
bgn-bulgarian-lev:0.5113r:eurusd=x
|
||||
- Bulgarian Lev is pegged to the Euro at 0.5113 EUR
|
||||
|
||||
A TRADE_SPECIFIER is a single argument in the format:
|
||||
|
||||
ASSET:AMOUNT[:TO_ASSET[:TO_AMOUNT]]
|
||||
|
||||
Examples:
|
||||
|
||||
xmr:17.34 - price of 17.34 XMR in all configured assets
|
||||
xmr-monero:17.34 - same as above, but with full ID
|
||||
xmr:17.34:eurusd=x - price of 17.34 XMR in EUR only
|
||||
xmr:17.34:eurusd=x:2800 - commission on an offer of 17.34 XMR for 2800 EUR
|
||||
|
||||
TO_AMOUNT, if included, is used to calculate the percentage difference or
|
||||
commission on an offer compared to the spot price.
|
||||
|
||||
If either ASSET or TO_ASSET refer to assets not present in the source data,
|
||||
a USD rate for the missing asset(s) must be supplied via the --add-columns
|
||||
or --add-rows options.
|
||||
|
||||
SORT PARAMETERS:
|
||||
|
||||
{sp_fmt}
|
||||
|
||||
|
||||
PROXY NOTE
|
||||
|
||||
The remote server used to obtain the crypto price data, {cc.api_host},
|
||||
blocks Tor behind a Captcha wall, so a Tor proxy cannot be used directly.
|
||||
If you’re concerned about privacy, connect via a VPN, or better yet, VPN over
|
||||
Tor. Then set up an HTTP proxy (e.g. Privoxy) on the VPN’ed host and set the
|
||||
‘proxy’ option in the config file or --proxy on the command line accordingly.
|
||||
Or run the script directly on the VPN’ed host with ’proxy’ or --proxy set to
|
||||
the null string.
|
||||
|
||||
Alternatively, you may download the JSON source data in a Tor-proxied browser
|
||||
from {cc.api_url}, save it as ‘ticker.json’ in your
|
||||
configured cache directory and run the script with the --cached-data option.
|
||||
|
||||
Financial data is obtained from {fi.desc}, which currently allows Tor.
|
||||
|
||||
|
||||
RATE LIMITING NOTE
|
||||
|
||||
To protect user privacy, filtering and processing of cryptocurrency data is
|
||||
performed client side so that the remote server does not know which assets
|
||||
are being examined. This is done by fetching data for the top {al} crypto
|
||||
assets by market cap (configurable via the --asset-limit option) with each
|
||||
invocation of the script. A rate limit of {cc.ratelimit} seconds between calls is thus
|
||||
imposed to prevent abuse of the remote server. When the --btc option is in
|
||||
effect, this limit is reduced to {cc.btc_ratelimit} seconds. To bypass the rate limit
|
||||
entirely, use --cached-data.
|
||||
|
||||
Note that financial data obtained from {fi.api_host} is filtered in the
|
||||
request, which has privacy implications. The rate limit for financial data
|
||||
is {fi.ratelimit} seconds.
|
||||
|
||||
|
||||
EXAMPLES
|
||||
|
||||
# Basic display in ‘overview’ mode:
|
||||
$ mmnode-ticker
|
||||
|
||||
# Display BTC price only:
|
||||
$ mmnode-ticker --btc
|
||||
|
||||
# Wide display, add EUR and OMR columns, OMR/USD rate, extra precision and
|
||||
# proxy:
|
||||
$ mmnode-ticker -w -c eurusd=x,omr-omani-rial:2.59r -e2 -x http://vpnhost:8118
|
||||
|
||||
# Wide display, elapsed update time, add EUR, BGN columns and BGN/EUR rate:
|
||||
$ mmnode-ticker -wE -c eurusd=x,bgn-bulgarian-lev:0.5113r:eurusd=x
|
||||
|
||||
# Widest display with all percentage change columns, use cached data from
|
||||
# previous network query, show portfolio (see above), pipe output to pager,
|
||||
# add DOGE row:
|
||||
$ mmnode-ticker -WCFP -r doge
|
||||
|
||||
# Display 17.234 XMR priced in all configured assets (‘trading’ mode):
|
||||
$ mmnode-ticker xmr:17.234
|
||||
|
||||
# Same as above, but add INR price at specified USDINR rate:
|
||||
$ mmnode-ticker -c inr:79.5 xmr:17.234
|
||||
|
||||
# Same as above, but view INR price only at specified rate, adding label:
|
||||
$ mmnode-ticker -c inr-indian-rupee:79.5 xmr:17.234:inr
|
||||
|
||||
# Calculate commission on an offer of 2700 USD for 0.123 BTC, compared to
|
||||
# current spot price:
|
||||
$ mmnode-ticker usd:2700:btc:0.123
|
||||
|
||||
# Calculate commission on an offer of 200000 INR for 0.1 BTC, compared to
|
||||
# current spot price, at specified USDINR rate:
|
||||
$ mmnode-ticker -n -c inr-indian-rupee:79.5 inr:200000:btc:0.1
|
||||
|
||||
# Display top 20 crypto assets by market cap, adding a Euro column:
|
||||
$ mmnode-ticker -c eurusd=x 20
|
||||
|
||||
# Same as above, specifying assets using a range:
|
||||
$ mmnode-ticker -c eurusd=x 1-20
|
||||
|
||||
|
||||
CONFIGURED ASSETS:
|
||||
{assets}
|
||||
|
||||
Customize output by editing the file
|
||||
~/{cfg}
|
||||
|
||||
To add a portfolio, edit the file
|
||||
~/{pf_cfg}
|
||||
"""},
|
||||
'code': {
|
||||
'options': lambda s: s.format(
|
||||
dfl_cachedir = os.path.relpath(dfl_cachedir, start=homedir),
|
||||
ds = fmt_dict(DataSource.get_sources(), fmt='equal_compact'),
|
||||
al = DataSource.coinpaprika.dfl_asset_limit,
|
||||
sp_codes = fmt_list(sort_params, fmt='fancy'),
|
||||
pc = fmt_list(Ticker.percent_cols, fmt='fancy')),
|
||||
'notes': lambda s: s.format(
|
||||
assets = fmt_list(assets_list_gen(cfg_in), fmt='col', indent=' '),
|
||||
cfg = os.path.relpath(cfg_in.cfg_file, start=homedir),
|
||||
pf_cfg = os.path.relpath(cfg_in.portfolio_file, start=homedir),
|
||||
al = DataSource.coinpaprika.dfl_asset_limit,
|
||||
cc = src_cls['cc'](),
|
||||
sp_fmt = '\n '.join(f'‘{k}’ - {v.desc}' for k, v in sort_params.items()),
|
||||
fi = src_cls['fi']())
|
||||
}
|
||||
}
|
||||
|
||||
import os
|
||||
|
||||
from mmgen.util import fmt_list, fmt_dict
|
||||
from mmgen.cfg import Config
|
||||
from . import Ticker
|
||||
|
||||
gcfg = Config(opts_data=opts_data, caller_post_init=True)
|
||||
|
||||
src_cls, cfg_in = Ticker.make_cfg(gcfg)
|
||||
|
||||
from .Ticker import dfl_cachedir, homedir, DataSource, assets_list_gen, sort_params
|
||||
|
||||
gcfg._post_init()
|
||||
|
||||
Ticker.main()
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmnode-txfind: Find a transaction in the blockchain or mempool
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from mmgen.cfg import Config
|
||||
from mmgen.util import msg, Msg, die, is_hex_str, async_run
|
||||
|
||||
opts_data = {
|
||||
'text': {
|
||||
'desc': 'Find a transaction in the blockchain or mempool',
|
||||
'usage': '[opts] <transaction ID>',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-q, --quiet Be quieter
|
||||
-v, --verbose Be more verbose
|
||||
""",
|
||||
'notes': """
|
||||
If transaction is in blockchain, the block number and number of confirmations
|
||||
are displayed.
|
||||
|
||||
Requires --txindex for correct operation.
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
msg_data = {
|
||||
'normal': {
|
||||
'none': 'Transaction not found in blockchain or mempool',
|
||||
'block': 'Transaction is in block {b} ({c} confirmations)',
|
||||
'mem': 'Transaction is in mempool'},
|
||||
'quiet': {
|
||||
'none': 'None',
|
||||
'block': '{b} {c}',
|
||||
'mem': 'mempool'}}
|
||||
|
||||
async def main(txid):
|
||||
if len(txid) != 64 or not is_hex_str(txid):
|
||||
die(2, f'{txid}: invalid transaction ID')
|
||||
|
||||
if cfg.verbose:
|
||||
msg(f'TxID: {txid}')
|
||||
|
||||
from mmgen.rpc import rpc_init
|
||||
c = await rpc_init(cfg, ignore_wallet=True)
|
||||
|
||||
exitval = 0
|
||||
try:
|
||||
tip1 = await c.call('getblockcount')
|
||||
ret = await c.call('getrawtransaction', txid, True)
|
||||
tip2 = await c.call('getblockcount')
|
||||
except:
|
||||
Msg('\r' + msgs['none'])
|
||||
exitval = 1
|
||||
else:
|
||||
assert tip1 == tip2, 'Blockchain is updating. Try again later'
|
||||
if 'confirmations' in ret:
|
||||
confs = ret['confirmations']
|
||||
Msg('\r' + msgs['block'].format(b = tip1 - confs + 1, c = confs))
|
||||
else:
|
||||
Msg('\r' + msgs['mem'])
|
||||
|
||||
return exitval
|
||||
|
||||
cfg = Config(opts_data=opts_data)
|
||||
|
||||
msgs = msg_data['quiet' if cfg.quiet else 'normal']
|
||||
|
||||
if len(cfg._args) != 1:
|
||||
die(1, 'One transaction ID must be specified')
|
||||
|
||||
sys.exit(async_run(cfg, main, args=[cfg._args[0]]))
|
||||
169
mmnode-blocks-info
Executable file
169
mmnode-blocks-info
Executable file
|
|
@ -0,0 +1,169 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmgen-blocks-info: Display information about a block or range of blocks
|
||||
"""
|
||||
|
||||
import time,re
|
||||
from mmgen.common import *
|
||||
|
||||
opts_data = {
|
||||
'text': {
|
||||
'desc': 'Display information about or find a transaction within a range of blocks',
|
||||
'usage': '[opts] +<last n blocks>|<block num>|<block num range>',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-H, --hashes Display only block numbers and hashes
|
||||
-m, --miner-info Display miner info in coinbase transaction
|
||||
-n, --nya Display NYA (New York Agreement) support
|
||||
-M, --raw-miner-info Display miner info in uninterpreted form
|
||||
-s, --summary Print the summary only
|
||||
-t, --transaction=t Search for transaction 't' in specified block range
|
||||
|
||||
If no block number is specified, the current block is assumed
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
cmd_args = opts.init(opts_data)
|
||||
|
||||
if len(cmd_args) not in (0,1): opts.usage()
|
||||
if opt.raw_miner_info: opt.miner_info = True
|
||||
|
||||
c = rpc_init()
|
||||
cur_blk = c.getblockcount()
|
||||
|
||||
arg = cmd_args[0] if cmd_args else None
|
||||
|
||||
if not arg:
|
||||
first = last = cur_blk
|
||||
elif arg[0] == '+' and is_int(arg[1:]):
|
||||
last = cur_blk
|
||||
first = last - int(arg[1:]) + 1
|
||||
elif is_int(arg):
|
||||
first = last = int(arg)
|
||||
else:
|
||||
try:
|
||||
a,b = arg.split('-')
|
||||
assert is_int(a) and is_int(b)
|
||||
first,last = int(a),int(b)
|
||||
except:
|
||||
opts.usage()
|
||||
|
||||
if first > last:
|
||||
die(2,'{}: invalid block range'.format(arg))
|
||||
if last > cur_blk:
|
||||
die(2,'Requested block number ({}) greater than current block height'.format(last))
|
||||
|
||||
if opt.summary:
|
||||
pass
|
||||
elif opt.transaction:
|
||||
if len(opt.transaction) != 64 or not is_hex_str(opt.transaction):
|
||||
die(2,'{}: invalid transaction id'.format(opt.transaction))
|
||||
elif opt.hashes:
|
||||
Msg('{:<7} {}'.format('BLOCK','HASH'))
|
||||
else:
|
||||
if opt.miner_info:
|
||||
fs='{:<6} {:<19}{:>9} {:>6} {:>7} {:8} {}'
|
||||
Msg(fs.format('BLOCK','DATE','INTERVAL','CONFS','SIZE','VERSION','MINER'))
|
||||
else:
|
||||
fs='{:<6} {:<19}{:>9} {:>6} {:>7} {}'
|
||||
Msg(fs.format('BLOCK','DATE','INTERVAL','CONFS','SIZE','VERSION'))
|
||||
|
||||
miner_strings = {
|
||||
'Bixin':'Bixin',
|
||||
'AntPool':'AntPool',
|
||||
'Bitfury':'Bitfury',
|
||||
'BTCC':'BTCC',
|
||||
'BTC.COM':'BTC.COM',
|
||||
'BTPOOL':'BTPOOL',
|
||||
'ViaBTC':'ViaBTC',
|
||||
'slush':'Slush',
|
||||
'BitMinter':'BitMinter',
|
||||
'BW.COM':'BW.COM',
|
||||
'gbminers':'GBMiners',
|
||||
'BitClub Network':'BitClub Network',
|
||||
'bitcoin.com':'bitcoin.com',
|
||||
'KanoPool':'KanoPool',
|
||||
'BTC.TOP':'BTC.TOP',
|
||||
}
|
||||
|
||||
total,prev_time = 0,None
|
||||
for i in range(first,last+1):
|
||||
b = c.getblock(c.getblockhash(i))
|
||||
total += b['size']
|
||||
if opt.transaction:
|
||||
if opt.transaction in b['tx']:
|
||||
Msg('Requested transaction is in block {}'.format(i))
|
||||
sys.exit(0)
|
||||
msg('Checking block {}'.format(i))
|
||||
else:
|
||||
gmt = time.gmtime(b['mediantime'])
|
||||
date = '{}-{:02}-{:02} {:02}:{:02}:{:02}'.format(*gmt[:6])
|
||||
if prev_time == None:
|
||||
b_prev = b if i == 0 else c.getblock(c.getblockhash(i-1))
|
||||
first_time = prev_time = b_prev['mediantime']
|
||||
et = b['mediantime'] - prev_time
|
||||
prev_time = b['mediantime']
|
||||
if opt.summary:
|
||||
continue
|
||||
if opt.hashes:
|
||||
Msg('{:<7} {}'.format(i,b['hash']))
|
||||
else:
|
||||
d = (i,date,'{}:{:02}'.format(et//60,et%60),b['confirmations'],b['size'],b['versionHex'])
|
||||
if opt.miner_info:
|
||||
bd = c.getrawtransaction(b['tx'][0],1,on_fail='silent')
|
||||
if type(bd) == tuple:
|
||||
Msg(fs.format(*(d+('---',))))
|
||||
else:
|
||||
cb = unhexlify(bd['vin'][0]['coinbase'])
|
||||
scb = re.sub(b'[^\w /-:,;.]',b'',cb)[1:]
|
||||
if opt.raw_miner_info:
|
||||
Msg(fs.format(*(d+(scb.decode(),))))
|
||||
else:
|
||||
for ms in miner_strings:
|
||||
if ms.encode() in scb:
|
||||
s = miner_strings[ms]
|
||||
break
|
||||
else:
|
||||
try: s = scb.split('ined by')[1].strip()
|
||||
except: s = '??'
|
||||
nya_str = (b' ',b'NYA ')[b'NYA' in scb] if opt.nya else b''
|
||||
Msg(fs.format(*(d+(nya_str.decode()+s,))))
|
||||
else:
|
||||
Msg(fs.format(*d))
|
||||
|
||||
if opt.transaction:
|
||||
from mmgen.rpc import rpc_error
|
||||
if rpc_error(c.getmempoolentry(opt.transaction,on_fail='silent')):
|
||||
Msg('\rTransaction not found in block range {}-{} or in mempool'.format(first,last))
|
||||
else:
|
||||
Msg('\rTransaction is in mempool')
|
||||
else:
|
||||
blocks = last - first + 1
|
||||
if blocks > 1:
|
||||
fs2 = '{:<15} {}\n'
|
||||
et = b['mediantime'] - first_time
|
||||
ac = int(et / blocks)
|
||||
if not opt.summary: Msg('')
|
||||
Msg_r( fs2.format('Range:','{}-{} ({} blocks)'.format(first,last,blocks)) +
|
||||
fs2.format('Avg size:','{} bytes'.format(total//blocks)) +
|
||||
fs2.format('MB/hr:','{:0.5f}'.format(float(total)*36/10000/et)) +
|
||||
fs2.format('Avg conf time:','{}:{:02}'.format(ac//60,ac%60)))
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
||||
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
|
|
@ -17,34 +17,44 @@
|
|||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmnode-peerblocks: List blocks in flight, disconnect stalling nodes
|
||||
mmgen-netrate: Bitcoin daemon network rate monitor
|
||||
"""
|
||||
|
||||
import time
|
||||
from mmgen.common import *
|
||||
|
||||
opts_data = {
|
||||
'text': {
|
||||
'desc': 'List blocks in flight, disconnect stalling nodes',
|
||||
'desc': 'Bitcoin daemon network rate monitor',
|
||||
'usage': '[opts]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
"""}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
from mmgen.cfg import Config
|
||||
cfg = Config(opts_data=opts_data)
|
||||
cmd_args = opts.init(opts_data)
|
||||
|
||||
async def main():
|
||||
ERASE_LINE,CUR_UP = '\033[K','\033[1A'
|
||||
c = rpc_init()
|
||||
|
||||
from mmgen.rpc import rpc_init
|
||||
rpc = await rpc_init(cfg, ignore_wallet=True)
|
||||
|
||||
from .PeerBlocks import BlocksDisplay, PeersDisplay
|
||||
blocks = BlocksDisplay(cfg)
|
||||
peers = PeersDisplay(cfg)
|
||||
def do_loop():
|
||||
def get_data():
|
||||
d = c.getnettotals()
|
||||
return [float(e) for e in (d['totalbytesrecv'],d['totalbytessent'],d['timemillis'])]
|
||||
|
||||
r,s,t = get_data()
|
||||
time.sleep(0.2)
|
||||
while True:
|
||||
await blocks.run(rpc)
|
||||
await peers.run(rpc)
|
||||
rs,ss,ts = r,s,t
|
||||
r,s,t = get_data()
|
||||
td = t-ts
|
||||
sys.stderr.write('\rrcvd: {:9.2f} kB/s\nsent: {:9.2f} kB/s '.format((r-rs)/td,(s-ss)/td))
|
||||
time.sleep(2)
|
||||
sys.stderr.write('{}{}{}'.format(ERASE_LINE,CUR_UP,ERASE_LINE))
|
||||
|
||||
from mmgen.util import async_run
|
||||
async_run(cfg, main)
|
||||
try:
|
||||
do_loop()
|
||||
except KeyboardInterrupt:
|
||||
sys.stderr.write('\n')
|
||||
118
mmnode-peerblocks
Executable file
118
mmnode-peerblocks
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmgen-peerblocks: List blocks in flight, disconnect stalling nodes
|
||||
"""
|
||||
|
||||
import time,threading
|
||||
from mmgen.common import *
|
||||
|
||||
opts_data = {
|
||||
'text': {
|
||||
'desc': 'List blocks in flight, disconnect stalling nodes',
|
||||
'usage': '[opts]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
cmd_args = opts.init(opts_data)
|
||||
|
||||
colors = ['\033[38;5;%s;1m' % c for c in (238,240,242,244,246,247,249,251,253,255)]
|
||||
_red,_reset = '\033[31m','\033[0m'
|
||||
|
||||
ERASE_ALL,ERASE_LINE,CUR_HOME,CUR_HIDE,CUR_SHOW = \
|
||||
'\033[J','\033[K','\033[H','\033[?25l','\033[?25h'
|
||||
|
||||
import atexit
|
||||
def at_exit():
|
||||
import os
|
||||
os.system('stty sane')
|
||||
sys.stderr.write('\n')
|
||||
atexit.register(at_exit)
|
||||
|
||||
bc = rpc_init()
|
||||
|
||||
msg_r(CUR_HOME+ERASE_ALL)
|
||||
|
||||
def do_display():
|
||||
from mmgen.term import get_terminal_size
|
||||
global data
|
||||
count = 1
|
||||
while True:
|
||||
twid = get_terminal_size()[0]
|
||||
data = bc.getpeerinfo()
|
||||
min_t = None
|
||||
lines = []
|
||||
with lock:
|
||||
msg('{}{}{}ACTIVE PEERS ({}) - poll {}'.format(
|
||||
CUR_HOME,ERASE_ALL,CUR_HOME,len(data),count))
|
||||
for d in data:
|
||||
line = { 'id': d['id'], 'data': [] }
|
||||
if 'inflight' in d and d['inflight']:
|
||||
blks = [str(e) for e in d['inflight']]
|
||||
min_p = min(e for e in d['inflight'])
|
||||
if not min_t or min_t > min_p: min_t = min_p
|
||||
line_d = ' '.join(blks)[:twid-6]
|
||||
blks = blks[:len(line_d) - len(line_d.replace(' ','')) + 1]
|
||||
blks[-1] = blks[-1][:len(line_d.split(' ')[-1])]
|
||||
line['data'] = [[colors[int(i)%10],i,_reset] for i in blks if i]
|
||||
else:
|
||||
line['data'] = []
|
||||
lines.append(line)
|
||||
for line in lines:
|
||||
d = ' '.join([(a,_red)[int(b)==min_t]+b+c for a,b,c in line['data']])
|
||||
sys.stderr.write('\r{} {:>3}: {}\n'.format(ERASE_LINE,line['id'],d))
|
||||
msg_r(ERASE_ALL+'Hit ENTER for disconnect prompt: ')
|
||||
time.sleep(2)
|
||||
count += 1
|
||||
|
||||
lock = threading.Lock()
|
||||
data = {}
|
||||
|
||||
t = threading.Thread(target=do_display,name='display')
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def do_loop():
|
||||
global data
|
||||
while True:
|
||||
input()
|
||||
with lock:
|
||||
ids = [str(d['id']) for d in data]
|
||||
msg('{}{}{}ACTIVE PEERS ({})'.format(CUR_HOME,ERASE_ALL,CUR_HOME,len(data)))
|
||||
msg(' '+'\n '.join(['{:>3}: {:30} {}'.format(*[d[k] for k in ('id','addr','subver')]) for d in data]))
|
||||
reply = input('Enter a peer number to disconnect> ')
|
||||
if reply == '':
|
||||
pass
|
||||
elif reply in ids:
|
||||
idx = ids.index(reply)
|
||||
msg("Disconnecting peer {} ('{}')".format(reply,data[idx]['addr']))
|
||||
bc.disconnectnode(data[idx]['addr'])
|
||||
time.sleep(1.5)
|
||||
else:
|
||||
msg("'{}': invalid peer number".format(reply))
|
||||
time.sleep(0.5)
|
||||
|
||||
try:
|
||||
do_loop()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
56
mmnode-play-sound
Executable file
56
mmnode-play-sound
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
mmnode-play-sound: play a sound with controlled volume
|
||||
"""
|
||||
|
||||
import sys
|
||||
from mmgen.common import *
|
||||
from mmgen.util import die
|
||||
from mmgen.node_tools.Sound import *
|
||||
volume = 100
|
||||
|
||||
opts_data = {
|
||||
'text': {
|
||||
'desc': 'Play a sound file at controlled volume',
|
||||
'usage': '[opts]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
-v, --volume= n Adjust sound volume by percentage 'n' (default: {})
|
||||
""".format(volume)
|
||||
}
|
||||
}
|
||||
|
||||
args = opts.init(opts_data)
|
||||
|
||||
if opt.volume:
|
||||
volume = opt.volume
|
||||
try:
|
||||
volume = int(volume)
|
||||
assert 1 <= volume <= 120
|
||||
except:
|
||||
die(1,'Sound volume must be an integer between 1 and 120')
|
||||
|
||||
if len(args) != 1:
|
||||
die(1,'You must supply a sound file')
|
||||
|
||||
try: os.stat(args[0])
|
||||
except: die(1,"Couldn't stat file '{}'".format(args[0]))
|
||||
|
||||
play_sound(args[0],volume)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
Nix configuration directory for the MMGen Node Tools suite
|
||||
|
||||
Usage is as described in ‘nix/README’ in the mmgen-wallet repository, with the
|
||||
following differences:
|
||||
|
||||
a) all commands are executed from the repository root of mmgen-node-tools
|
||||
instead of mmgen-wallet
|
||||
|
||||
b) for NixOS, complete the steps as described up until the rebuild step.
|
||||
Copy the contents of this directory to ‘/etc/nixos/mmgen-project’ (this
|
||||
will overwrite ‘default.nix’), and continue with the rebuild step.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import (
|
||||
if builtins.pathExists ./merged-packages.nix then
|
||||
./merged-packages.nix
|
||||
else
|
||||
../../mmgen-wallet/nix/merged-packages.nix
|
||||
) { add_pkgs_path = ./node-tools-packages.nix; }
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{ pkgs, python }:
|
||||
|
||||
{
|
||||
system-packages = with pkgs; {
|
||||
cacert = cacert; # ticker (curl)
|
||||
};
|
||||
|
||||
python-packages = with python.pkgs; {
|
||||
yahooquery = (callPackage ./yahooquery.nix {}); # ticker
|
||||
pyyaml = pyyaml; # ticker
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import ../../mmgen-wallet/nix/shell.nix {
|
||||
repo = "mmgen-node-tools";
|
||||
add_pkgs_path = ./node-tools-packages.nix;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
diff --git a/scripts/build.py b/scripts/build.py
|
||||
index b705a0d..9bfcaab 100644
|
||||
--- a/scripts/build.py
|
||||
+++ b/scripts/build.py
|
||||
@@ -105,7 +105,6 @@ def get_curl_libraries():
|
||||
ffibuilder = FFI()
|
||||
system = platform.system()
|
||||
root_dir = Path(__file__).parent.parent
|
||||
-download_libcurl()
|
||||
|
||||
|
||||
ffibuilder.set_source(
|
||||
@@ -114,9 +113,7 @@ ffibuilder.set_source(
|
||||
#include "shim.h"
|
||||
""",
|
||||
# FIXME from `curl-impersonate`
|
||||
- libraries=get_curl_libraries(),
|
||||
- extra_objects=get_curl_archives(),
|
||||
- library_dirs=[arch["libdir"]],
|
||||
+ libraries=["curl-impersonate-chrome"],
|
||||
source_extension=".c",
|
||||
include_dirs=[
|
||||
str(root_dir / "include"),
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchPypi,
|
||||
python,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "yahooquery";
|
||||
version = "2.4.1";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchPypi {
|
||||
pname = "yahooquery";
|
||||
version = version;
|
||||
hash = "sha256-GQPGXq5qEtlelFAGNHkhbAeEbwE7riojkXkTUxt/rls=";
|
||||
};
|
||||
|
||||
build-system = with python.pkgs; [ hatchling ];
|
||||
|
||||
propagatedBuildInputs = with python.pkgs; [
|
||||
curl-cffi
|
||||
pandas
|
||||
requests-futures
|
||||
tqdm
|
||||
lxml
|
||||
beautifulsoup4
|
||||
];
|
||||
|
||||
doCheck = false; # skip tests
|
||||
|
||||
meta = with lib; {
|
||||
description = "Python wrapper for an unofficial Yahoo Finance API";
|
||||
homepage = "https://yahooquery.dpguthrie.com";
|
||||
license = licenses.mit;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 106
|
||||
indent-width = 4
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
indent-style = "tab"
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = [
|
||||
"E401", # multiple imports per line
|
||||
"E701", # multiple statements per line
|
||||
"E721", # use isinstance()
|
||||
"E731", # lambda instead of def
|
||||
"E402", # module import not top of file
|
||||
"E722", # bare except
|
||||
"E713", # membership 'not in'
|
||||
"E741", # ambiguous variable name
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"test/include/common.py" = [ "F821" ] # undefined name 'cfg'
|
||||
"test/misc/input_func.py" = [ "F401" ] # imported but unused
|
||||
"test/modtest_d/cashaddr.py" = [ "F841" ] # assigned to but never used
|
||||
"test/modtest_d/dep.py" = [ "F401" ] # imported but unused
|
||||
"test/modtest_d/testdep.py" = [ "F401" ] # imported but unused
|
||||
"test/modtest_d/obj.py" = [ "F841" ] # assigned to but never used
|
||||
"test/objtest_d/*" = [ "F401" ] # imported but unused
|
||||
"test/objattrtest_d/*" = [ "F401" ] # imported but unused
|
||||
"test/overlay/fakemods/*" = [ "F403", "F405" ] # `import *` used
|
||||
"test/*.py" = [ "F401" ] # imported but unused
|
||||
"test/colortest.py" = [ "F403", "F405" ] # `import *` used
|
||||
"test/tooltest2.py" = [ "F403", "F405" ] # `import *` used
|
||||
"test/overlay/tree/*" = [ "ALL" ]
|
||||
|
||||
[tool.pylint.format]
|
||||
indent-string = "\t"
|
||||
indent-after-paren = 2
|
||||
max-line-length = 110
|
||||
|
||||
[tool.pylint.main]
|
||||
py-version = "3.7"
|
||||
recursive = true
|
||||
jobs = 0
|
||||
|
||||
[tool.pylint."messages control"]
|
||||
ignored-modules = [
|
||||
"mmgen.term",
|
||||
"mmgen.color",
|
||||
]
|
||||
ignored-classes = [
|
||||
"mmgen_node_tools.Ticker.Ticker.base",
|
||||
"mmgen_node_tools.Ticker.DataSource.base",
|
||||
"mmgen_node_tools.PeerBlocks.Display",
|
||||
"mmgen_node_tools.PollDisplay.PollDisplay",
|
||||
]
|
||||
55
setup.cfg
55
setup.cfg
|
|
@ -1,55 +0,0 @@
|
|||
[metadata]
|
||||
name = mmgen-node-tools
|
||||
version = file: mmgen_node_tools/data/version
|
||||
description = Optional online tools for the MMGen wallet suite
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
author = The MMGen Project
|
||||
author_email = mmgen@tuta.io
|
||||
url = https://github.com/mmgen/mmgen-node-tools
|
||||
license = GNU GPL v3
|
||||
platforms = Linux, Armbian, Raspbian, MS Windows, MacOS
|
||||
keywords = file: mmgen_node_tools/data/keywords
|
||||
project_urls =
|
||||
Website = https://mmgen.org
|
||||
Bug Tracker = https://github.com/mmgen/mmgen-node-tools/issues
|
||||
classifiers =
|
||||
Operating System :: POSIX :: Linux
|
||||
Operating System :: Microsoft :: Windows
|
||||
Operating System :: MacOS
|
||||
Environment :: Console
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: C
|
||||
Framework :: AsyncIO
|
||||
Framework :: aiohttp
|
||||
Topic :: Office/Business :: Financial
|
||||
Topic :: Security :: Cryptography
|
||||
Topic :: Software Development :: Libraries :: Python Modules
|
||||
Topic :: Utilities
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: End Users/Desktop
|
||||
Intended Audience :: Financial and Insurance Industry
|
||||
Intended Audience :: System Administrators
|
||||
Development Status :: 5 - Production/Stable
|
||||
|
||||
[options]
|
||||
python_requires = >=3.11
|
||||
include_package_data = True
|
||||
|
||||
install_requires =
|
||||
mmgen-wallet>=16.1.dev26
|
||||
pyyaml
|
||||
yahooquery
|
||||
|
||||
packages =
|
||||
mmgen_node_tools
|
||||
mmgen_node_tools.data
|
||||
|
||||
scripts =
|
||||
cmds/mmnode-addrbal
|
||||
cmds/mmnode-blocks-info
|
||||
cmds/mmnode-feeview
|
||||
cmds/mmnode-halving-calculator
|
||||
cmds/mmnode-netrate
|
||||
cmds/mmnode-peerblocks
|
||||
cmds/mmnode-ticker
|
||||
56
setup.py
Executable file
56
setup.py
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from distutils.core import setup
|
||||
from distutils.command.install_data import install_data
|
||||
from mmgen.globalvars import g
|
||||
|
||||
class my_install_data(install_data):
|
||||
def run(self):
|
||||
sdir = os.path.join('data_files','audio')
|
||||
for f in [e for e in os.listdir(sdir) if e[-4:] == '.wav']:
|
||||
os.chmod(os.path.join(sdir,f),0o644)
|
||||
install_data.run(self)
|
||||
|
||||
setup(
|
||||
name = 'mmgen-node-tools',
|
||||
description = 'Optional tools for the MMGen wallet system',
|
||||
version = g.version,
|
||||
author = g.author,
|
||||
author_email = g.email,
|
||||
url = g.proj_url,
|
||||
license = 'GNU GPL v3',
|
||||
platforms = 'Linux, MS Windows, Raspberry Pi',
|
||||
keywords = g.keywords,
|
||||
packages = ['mmgen.node_tools'],
|
||||
scripts = [
|
||||
'mmnode-play-sound',
|
||||
'mmnode-netrate',
|
||||
'mmnode-peerblocks',
|
||||
'mmnode-blocks-info',
|
||||
],
|
||||
data_files = [('share/mmgen/node_tools/audio', [
|
||||
'data_files/audio/ringtone.wav', # source files must have 0644 mode
|
||||
'data_files/audio/Positive.wav',
|
||||
'data_files/audio/Rhodes.wav',
|
||||
'data_files/audio/Counterpoint.wav'
|
||||
])
|
||||
],
|
||||
cmdclass = { 'install_data': my_install_data },
|
||||
)
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# MMGen Node Tools, terminal-based programs for Bitcoin and forkcoin nodes
|
||||
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
test.cmdtest_d.httpd.ticker: Ticker WSGI http server
|
||||
"""
|
||||
|
||||
from . import HTTPD
|
||||
|
||||
class TickerServer(HTTPD):
|
||||
name = 'ticker server'
|
||||
port = 19900
|
||||
content_type = 'application/json'
|
||||
|
||||
def make_response_body(self, method, environ):
|
||||
|
||||
with open('test/ref/ticker/ticker.json') as fh:
|
||||
text = fh.read()
|
||||
|
||||
return text.encode()
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
test.cmdtest_d.include.cfg: configuration data for cmdtest.py
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
cmd_groups_altcoin = []
|
||||
|
||||
gd = namedtuple('cmd_groups_data', ['clsname', 'params'])
|
||||
|
||||
cmd_groups_dfl = {
|
||||
'main': gd('CmdTestMain', {}),
|
||||
'helpscreens': gd('CmdTestHelp', {'modname': 'misc', 'full_data': True}),
|
||||
'scripts': gd('CmdTestScripts', {'modname': 'misc'}),
|
||||
'regtest': gd('CmdTestRegtest', {}),
|
||||
}
|
||||
|
||||
cmd_groups_extra = {}
|
||||
|
||||
cfgs = {
|
||||
'1': {}, # regtest
|
||||
'2': {}, # scripts
|
||||
'3': {}, # main
|
||||
}
|
||||
|
||||
def fixup_cfgs():
|
||||
import os
|
||||
|
||||
for k in cfgs:
|
||||
cfgs[k]['tmpdir'] = os.path.join('test', 'tmp', str(k))
|
||||
|
||||
fixup_cfgs()
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
cmdtest_d.main: Basic operations tests for the cmdtest.py test suite
|
||||
"""
|
||||
|
||||
import sys, time
|
||||
|
||||
from ..include.common import cfg
|
||||
from .base import CmdTestBase
|
||||
|
||||
class CmdTestMain(CmdTestBase):
|
||||
'basic operations with fake RPC data'
|
||||
tmpdir_nums = [3]
|
||||
networks = ('btc',) # fake data, so test peerblocks for BTC mainnet only
|
||||
passthru_opts = ('daemon_data_dir','rpc_port','coin','testnet','rpc_backend')
|
||||
segwit_opts_ok = True
|
||||
color = True
|
||||
need_daemon = True
|
||||
|
||||
cmd_group_in = (
|
||||
('subgroup.peerblocks', []),
|
||||
)
|
||||
|
||||
cmd_subgroups = {
|
||||
'peerblocks': (
|
||||
"'mmnode-peerblocks' script",
|
||||
('peerblocks1', '--help'),
|
||||
('peerblocks2', 'interactive (popen spawn)'),
|
||||
('peerblocks3', 'interactive, 80 columns (pexpect_spawn [on Linux])'),
|
||||
),
|
||||
}
|
||||
|
||||
def peerblocks(self,args,expect_list=None,pexpect_spawn=False):
|
||||
t = self.spawn(
|
||||
'mmnode-peerblocks',
|
||||
args,
|
||||
pexpect_spawn = pexpect_spawn )
|
||||
if cfg.exact_output: # disable echoing of input
|
||||
t.p.logfile = None
|
||||
t.p.logfile_read = sys.stdout
|
||||
if expect_list:
|
||||
t.match_expect_list(expect_list)
|
||||
return t
|
||||
|
||||
def peerblocks1(self):
|
||||
t = self.peerblocks(['--help'])
|
||||
if t.pexpect_spawn:
|
||||
t.send('q')
|
||||
return t
|
||||
|
||||
def peerblocks2(self,args=[],pexpect_spawn=False):
|
||||
|
||||
t = self.peerblocks(args,pexpect_spawn=pexpect_spawn)
|
||||
|
||||
for i in range(5):
|
||||
t.expect('PEERS')
|
||||
|
||||
t.send('x')
|
||||
|
||||
for i in range(3):
|
||||
t.expect('PEERS')
|
||||
|
||||
sleep_secs = 0.2
|
||||
|
||||
t.send('0')
|
||||
time.sleep(sleep_secs)
|
||||
t.send('\n' if pexpect_spawn else '0\n') # TODO: check for readline availability
|
||||
t.expect('Unable to disconnect peer 0')
|
||||
t.expect('PEERS')
|
||||
|
||||
t.send('1')
|
||||
time.sleep(sleep_secs)
|
||||
t.send('1\n' if pexpect_spawn else '11\n')
|
||||
t.expect('11: invalid peer number')
|
||||
t.expect('PEERS')
|
||||
|
||||
t.send('2')
|
||||
time.sleep(sleep_secs)
|
||||
t.send('\n' if pexpect_spawn else '2\n')
|
||||
t.expect('Disconnecting peer 2')
|
||||
t.expect('PEERS')
|
||||
|
||||
t.send('q')
|
||||
|
||||
return t
|
||||
|
||||
def peerblocks3(self):
|
||||
return self.peerblocks2(
|
||||
['--columns=80'],
|
||||
pexpect_spawn = sys.platform == 'linux')
|
||||
|
|
@ -1,502 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
test.cmdtest_d.misc: Miscellaneous test groups for the cmdtest.py test suite
|
||||
"""
|
||||
|
||||
import os, shutil
|
||||
|
||||
from ..include.common import cfg
|
||||
from .base import CmdTestBase
|
||||
from .httpd.ticker import TickerServer
|
||||
|
||||
refdir = os.path.join('test','ref','ticker')
|
||||
|
||||
class CmdTestHelp(CmdTestBase):
|
||||
'help, info and usage screens'
|
||||
networks = ('btc','ltc','bch')
|
||||
tmpdir_nums = []
|
||||
passthru_opts = ('daemon_data_dir','rpc_port','coin','testnet')
|
||||
cmd_group = (
|
||||
('version', (1,'version message',[])),
|
||||
('helpscreens', (1,'help screens', [])),
|
||||
('longhelpscreens', (1,'help screens (--longhelp)',[])),
|
||||
)
|
||||
color = True
|
||||
|
||||
def version(self):
|
||||
t = self.spawn('mmnode-netrate', ['--version'])
|
||||
t.expect('MMNODE-NETRATE version')
|
||||
return t
|
||||
|
||||
def helpscreens(self,arg='--help',scripts=(),expect='USAGE:.*OPTIONS:'):
|
||||
|
||||
scripts = list(scripts) or [s for s in os.listdir('cmds') if s.startswith('mmnode-')]
|
||||
|
||||
for s in sorted(scripts):
|
||||
t = self.spawn(s,[arg],extra_desc=f'({s})')
|
||||
t.expect(expect,regex=True)
|
||||
t.read()
|
||||
t.ok()
|
||||
t.skip_ok = True
|
||||
|
||||
return t
|
||||
|
||||
def longhelpscreens(self):
|
||||
return self.helpscreens(arg='--longhelp',expect='USAGE:.*GLOBAL OPTIONS:')
|
||||
|
||||
class CmdTestScripts(CmdTestBase):
|
||||
'scripts not requiring a coin daemon'
|
||||
networks = ('btc',)
|
||||
tmpdir_nums = [2]
|
||||
passthru_opts = ()
|
||||
color = True
|
||||
|
||||
cmd_group_in = (
|
||||
('subgroup.ticker', []),
|
||||
)
|
||||
cmd_subgroups = {
|
||||
'ticker': (
|
||||
"'mmnode-ticker' script",
|
||||
('ticker1', 'ticker [--help]'),
|
||||
('copy_cache_files', 'copying JSON files to cache'),
|
||||
('ticker1a', 'ticker [--download=cc] (early caching)'),
|
||||
('ticker1b', 'ticker [--download=cc] (late caching)'),
|
||||
('ticker2', 'ticker (bad proxy)'),
|
||||
('ticker3', 'ticker [--cached-data]'),
|
||||
('ticker4', 'ticker [--cached-data --wide]'),
|
||||
('ticker5', 'ticker [--cached-data --wide --adjust=-0.766] (usr cfg file)'),
|
||||
('ticker6', 'ticker [--cached-data --wide --portfolio] (missing portfolio)'),
|
||||
('ticker7', 'ticker [--cached-data --wide --portfolio]'),
|
||||
('ticker8', 'ticker [--cached-data --wide --elapsed]'),
|
||||
('ticker9', 'ticker [--cached-data --wide --portfolio --elapsed --add-rows=fake-fakecoin:0.0123 --add-precision=2]'),
|
||||
('ticker10', 'ticker [--cached-data xmr:17.234]'),
|
||||
('ticker11', 'ticker [--cached-data xmr:17.234:btc]'),
|
||||
('ticker12', 'ticker [--cached-data --adjust=1.23 xmr:17.234:btc]'),
|
||||
('ticker13', 'ticker [--cached-data --wide --elapsed -c inr-indian-rupee:79.5 inr:200000:btc:0.1]'),
|
||||
('ticker14', 'ticker [--cached-data --wide --btc]'),
|
||||
('ticker15', 'ticker [--cached-data --wide --btc btc:2:usd:45000]'),
|
||||
('ticker16', 'ticker [--cached-data --wide --elapsed -c eur,omr-omani-rial:2.59r'),
|
||||
('ticker17', 'ticker [--cached-data --wide --elapsed -c bgn-bulgarian-lev:0.5113r:eur'),
|
||||
('ticker18', 'ticker [--cached-data --widest --add-columns eurusd=x 10]'),
|
||||
('ticker19', 'ticker [--cached-data 1-5]'),
|
||||
('ticker20', 'ticker [--cached-data 2-5]'),
|
||||
('ticker21', 'ticker [--cached-data 5-5]'),
|
||||
('ticker22', 'ticker [--sort=rp]'),
|
||||
('ticker23', 'ticker [--sort=rp xmr:10]'),
|
||||
('ticker24', 'ticker [--sort=p]'),
|
||||
('ticker25', 'ticker [--sort=p 200]'),
|
||||
('ticker26', 'ticker [--sort=c -r algo,ada]'),
|
||||
('ticker27', 'ticker [--sort=rp -r algo,ada]'),
|
||||
('ticker28', 'ticker [--sort=d -r algo,ada]'),
|
||||
('ticker29', 'ticker [--sort=y -r algo,ada]'),
|
||||
('ticker30', 'ticker [--cached-data --wide --pchg-unit=btc --sort=d] (cf with config file)'),
|
||||
('ticker31', 'ticker [--cached-data --wide --pchg-unit=usd] (cf with no USD)'),
|
||||
('ticker32', 'ticker [--cached-data --wide --pchg-unit=gc=f]'),
|
||||
('ticker33', 'ticker [--cached-data --wide --pchg-unit=btc --sort=c] (cfg file with USD)'),
|
||||
('ticker34', 'ticker [--cached-data --wide --pchg-unit=btc --sort=y] (cfg file with USD)'),
|
||||
('ticker35', 'ticker [--cached-data --wide --pchg-unit=btc --sort=p] (cfg file with USD)'),
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, cfg, trunner, cfgs, spawn):
|
||||
if not trunner:
|
||||
return
|
||||
self.ticker_server = TickerServer(cfg)
|
||||
self.ticker_server.start()
|
||||
self.dests = {
|
||||
'nt_datadir': os.path.join(cfg.data_dir_root, 'node_tools'),
|
||||
'cache': self.tmpdir}
|
||||
return super().__init__(cfg, trunner, cfgs, spawn)
|
||||
|
||||
def rm_file(self, fn, dest='nt_datadir'):
|
||||
os.unlink(os.path.join(self.dests[dest], fn))
|
||||
|
||||
def copy_file(self, src_fn, dest_fn=None, dest='nt_datadir'):
|
||||
shutil.copy2(
|
||||
os.path.join(refdir, src_fn),
|
||||
os.path.join(self.dests[dest], dest_fn or src_fn))
|
||||
|
||||
def copy_cache_files(self):
|
||||
self.spawn('', msg_only=True)
|
||||
self.copy_file('ticker-finance.json', dest='cache')
|
||||
self.copy_file('ticker-finance-history.json', dest='cache')
|
||||
self.copy_file('ticker-btc.json', dest='cache')
|
||||
return 'ok'
|
||||
|
||||
def ticker(
|
||||
self,
|
||||
args = [],
|
||||
expect_list = None,
|
||||
cached_data = True,
|
||||
add_opts = [],
|
||||
use_proxy = True,
|
||||
no_msg = False,
|
||||
exit_val = None):
|
||||
t = self.spawn(
|
||||
'mmnode-ticker',
|
||||
(['--cached-data'] if cached_data else [])
|
||||
+ [f'--cachedir={self.tmpdir}']
|
||||
+ (['--proxy=http://asdfzxcv:32459'] if use_proxy else [])
|
||||
+ add_opts
|
||||
+ args,
|
||||
no_msg = no_msg,
|
||||
exit_val = exit_val)
|
||||
if expect_list:
|
||||
t.match_expect_list(expect_list)
|
||||
return t
|
||||
|
||||
def ticker1(self):
|
||||
t = self.ticker(['--help'])
|
||||
t.expect('USAGE:')
|
||||
return t
|
||||
|
||||
def ticker1a(self, first_run=True):
|
||||
t = self.ticker(
|
||||
add_opts = ['--proxy', '', '--download=cc'],
|
||||
cached_data = False,
|
||||
use_proxy = False)
|
||||
if first_run and not cfg.skipping_deps:
|
||||
t.expect('Creating')
|
||||
t.expect('Creating')
|
||||
return t
|
||||
|
||||
def ticker1b(self):
|
||||
return self.ticker1a(first_run=False)
|
||||
|
||||
def ticker2(self):
|
||||
t = self.ticker(cached_data=False)
|
||||
ret = t.expect(['proxy host could not be resolved', 'unexpected keyword'])
|
||||
t.exit_val = 1 if ret else 3
|
||||
return t
|
||||
|
||||
def ticker3(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
[
|
||||
'USD BTC',
|
||||
'BTC 23250.77 1.00000000 ETH 1659.66 0.07138094'
|
||||
])
|
||||
|
||||
def ticker4(self):
|
||||
return self.ticker(
|
||||
['--widest','--add-columns=eurusd=x,inr-indian-rupee:79.5'],
|
||||
[
|
||||
r'EURUSD=X \(EUR/USD\) = 1.0642 USD ' +
|
||||
r'INR \(INDIAN RUPEE\) = 0.012579 USD',
|
||||
'USD EURUSD=X INR BTC CHG_1y CHG_30d CHG_7d CHG_24h UPDATED',
|
||||
'BITCOIN',
|
||||
r'ETHEREUM 1,659.66 1,559.5846 131,943.14 0.07138094 \+36.41 \+29.99 \+21.42 \+1.82',
|
||||
r'MONERO 158.97 149.3870 12,638.36 0.00683732 \+12.38 \+10.19 \+7.28 \+1.21 2022-08-02 18:25:59',
|
||||
r'S&P 500 4,320.06 4,059.5604 343,444.77 0.18580285 -1.71 \+12.93 \+9.05 -0.23',
|
||||
r'INDIAN RUPEE 0.01 0.0118 1.00 0.00000054 -- -- -- --',
|
||||
])
|
||||
|
||||
def ticker5(self):
|
||||
self.copy_file('ticker-cfg.yaml')
|
||||
t = self.ticker(
|
||||
['--wide','--adjust=-0.766'],
|
||||
[
|
||||
'Adjusting prices by -0.77%',
|
||||
'USD BTC CHG_7d CHG_24h UPDATED',
|
||||
r'LITECOIN 58.56 0.00251869 \+12.79 \+0.40 2022-08-02 18:25:59',
|
||||
r'MONERO 157.76 0.00678495 \+7.28 \+1.21'
|
||||
])
|
||||
self.rm_file('ticker-cfg.yaml')
|
||||
return t
|
||||
|
||||
def ticker6(self):
|
||||
t = self.ticker(['--wide','--portfolio'], None, exit_val=1)
|
||||
t.expect('No portfolio')
|
||||
return t
|
||||
|
||||
def ticker7(self): # demo
|
||||
self.copy_file('ticker-portfolio.yaml')
|
||||
t = self.ticker(
|
||||
['--wide','--portfolio'],
|
||||
[
|
||||
'USD BTC CHG_7d CHG_24h UPDATED',
|
||||
r'ETHEREUM 1,659.66 0.07138094 \+21.42 \+1.82 2022-08-02 18:25:59',
|
||||
'CARDANO','ALGORAND',
|
||||
'PORTFOLIO','BITCOIN','ETHEREUM','MONERO','CARDANO','ALGORAND','TOTAL'
|
||||
])
|
||||
self.rm_file('ticker-portfolio.yaml')
|
||||
return t
|
||||
|
||||
def ticker8(self):
|
||||
return self.ticker(
|
||||
['--wide','--elapsed'],
|
||||
[
|
||||
'USD BTC CHG_7d CHG_24h UPDATED',
|
||||
r'BITCOIN 23,250.77 1.00000000 \+11.15 \+0.89 10 minutes ago'
|
||||
])
|
||||
|
||||
def ticker9(self):
|
||||
self.copy_file('ticker-portfolio-bad.yaml', 'ticker-portfolio.yaml')
|
||||
t = self.ticker(
|
||||
['--wide','--portfolio','--elapsed','--add-rows=fake-fakecoin:0.0123','--add-precision=2'],
|
||||
[
|
||||
'USD BTC CHG_7d CHG_24h UPDATED',
|
||||
r'BITCOIN 23,250.7741 1.0000000000 \+11.15 \+0.89 10 minutes ago',
|
||||
r'FAKECOIN 81.3008 0.0034966927 -- -- --',
|
||||
r'\(no data for noc-nocoin\)',
|
||||
])
|
||||
self.rm_file('ticker-portfolio.yaml')
|
||||
return t
|
||||
|
||||
def ticker10(self):
|
||||
return self.ticker(
|
||||
['XMR:17.234'],
|
||||
[
|
||||
r'XMR \(MONERO\) = 158.97 USD ' +
|
||||
'Amount: 17.234 XMR',
|
||||
'SPOT PRICE',
|
||||
'BTC 0.11783441',
|
||||
'XMR 17.23400000',
|
||||
'GC=F',r'\^IXIC',
|
||||
])
|
||||
|
||||
def ticker11(self):
|
||||
return self.ticker(
|
||||
['XMR:17.234:BTC'],
|
||||
[
|
||||
r'XMR \(MONERO\) = 158.97 USD ' +
|
||||
r'BTC \(BITCOIN\) = 23250.77 USD ' +
|
||||
'Amount: 17.234 XMR',
|
||||
'SPOT PRICE',
|
||||
'XMR 17.23400000 BTC 0.11783441',
|
||||
])
|
||||
|
||||
def ticker12(self):
|
||||
return self.ticker(
|
||||
['--adjust=1.23','--wide','XMR:17.234:BTC'],
|
||||
[
|
||||
r'XMR \(MONERO\) = 158.97 USD ' +
|
||||
r'BTC \(BITCOIN\) = 23,250.77 USD ' +
|
||||
'Amount: 17.234 XMR',
|
||||
r'Adjusting prices by \+1.23%',
|
||||
'SPOT PRICE ADJUSTED PRICE',
|
||||
'MONERO 17.23400000 17.44597820 2022-08-02 18:25:59 ' +
|
||||
'BITCOIN 0.11783441 0.11928377 2022-08-02 18:25:59',
|
||||
])
|
||||
|
||||
def ticker13(self):
|
||||
return self.ticker(
|
||||
['-wE','-c','inr-indian-rupee:79.5','inr:200000:btc:0.1'],
|
||||
[
|
||||
'Offer: 200,000 INR',
|
||||
'Offered price differs from spot by -7.58%',
|
||||
'SPOT PRICE OFFERED PRICE UPDATED',
|
||||
'INDIAN RUPEE 200,000.00000000 184,843.65372424 -- ' +
|
||||
'BITCOIN 0.10819955 0.10000000 10 minutes ago'
|
||||
])
|
||||
|
||||
def ticker14(self):
|
||||
self.copy_file('ticker-portfolio.yaml')
|
||||
t = self.ticker(
|
||||
['--btc','--wide','--portfolio','--elapsed'],
|
||||
[
|
||||
'PRICES',
|
||||
r'BITCOIN 23,368.86 \+6.05 -1.87 1 day 9 hours 2 minutes ago',
|
||||
'PORTFOLIO',
|
||||
r'BITCOIN 28,850.44 \+6.05 -1.87 1.23456789'
|
||||
])
|
||||
self.rm_file('ticker-portfolio.yaml')
|
||||
return t
|
||||
|
||||
def ticker15(self):
|
||||
return self.ticker(
|
||||
['--btc','--wide','--elapsed','-r','inr:79.5','btc:2:usd:45000'],
|
||||
[
|
||||
r'BTC \(BITCOIN\) = 23,368.86 USD',
|
||||
'Offered price differs from spot by -3.72%',
|
||||
'SPOT PRICE OFFERED PRICE UPDATED',
|
||||
'BITCOIN 2.00000000 1.92563954 1 day 9 hours 2 minutes ago ' +
|
||||
'US DOLLAR 46,737.71911598 45,000.00000000 --',
|
||||
])
|
||||
|
||||
def ticker16(self):
|
||||
return self.ticker(
|
||||
['--wide','--elapsed','-c','eurusd=x,omr-omani-rial:2.59r'],
|
||||
[
|
||||
r'EURUSD=X \(EUR/USD\) = 1.0642 USD ' +
|
||||
r'OMR \(OMANI RIAL\) = 2.5900 USD',
|
||||
'USD EURUSD=X OMR BTC CHG_7d CHG_24h UPDATED',
|
||||
r'BITCOIN 23,250.77 21,848.7527 8,977.1328 1.00000000 \+11.15 \+0.89 10 minutes ago',
|
||||
'OMANI RIAL 2.59 2.4338 1.0000 0.00011139 -- -- --'
|
||||
])
|
||||
|
||||
def ticker17(self):
|
||||
# BGN pegged at 0.5113 EUR
|
||||
return self.ticker(
|
||||
['--wide','--elapsed','-c','bgn-bulgarian-lev:0.5113r:eurusd=x'],
|
||||
[
|
||||
r'BGN \(BULGARIAN LEV\) = 0.54411 USD',
|
||||
'USD BGN BTC CHG_7d CHG_24h UPDATED',
|
||||
'BITCOIN 23,250.77 42,731.767 1.00000000',
|
||||
'BULGARIAN LEV 0.54 1.000 0.00002340',
|
||||
])
|
||||
|
||||
def ticker18(self):
|
||||
return self.ticker(
|
||||
['10'],
|
||||
[
|
||||
r'1\) BITCOIN 444.33652 23,250.77 21,848.7527 1.00000000 \+18.96 \+15.61 \+11.15 \+0.89',
|
||||
r'33\) ALGORAND 2.30691 0.33 0.3120 0.00001428 \+16.47 \+13.57 \+9.69 \-0.82'
|
||||
],
|
||||
add_opts = ['--widest', '--add-columns=eurusd=x'])
|
||||
|
||||
def ticker19(self):
|
||||
return self.ticker(
|
||||
['1-5'],
|
||||
[
|
||||
r'MarketCap\(B\) USD EURUSD=X BTC '
|
||||
'---------------------------------------------------------- '
|
||||
r'1\) BTC 444.33652 23250.77 21848.7527 1.00000000',
|
||||
r'8\) ADA 17.11161 0.51 0.4764 0.00002180'
|
||||
' ----------------------------------------------------------'
|
||||
],
|
||||
add_opts = ['--add-columns=eurusd=x'])
|
||||
|
||||
def ticker20(self):
|
||||
return self.ticker(
|
||||
['2-5'],
|
||||
[
|
||||
r'MarketCap\(B\) USD EURUSD=X BTC '
|
||||
'---------------------------------------------------------- '
|
||||
r'2\) ETH 202.15129 1659.66 1559.5846 0.07138094',
|
||||
r'8\) ADA 17.11161 0.51 0.4764 0.00002180',
|
||||
],
|
||||
add_opts = ['--add-columns=eurusd=x'])
|
||||
|
||||
def ticker21(self):
|
||||
return self.ticker(
|
||||
['5-5'],
|
||||
[
|
||||
r'MarketCap\(B\) USD EURUSD=X BTC '
|
||||
'--------------------------------------------------------- '
|
||||
r'8\) ADA 17.11161 0.51 0.4764 0.00002180',
|
||||
],
|
||||
add_opts = ['--add-columns=eurusd=x'])
|
||||
|
||||
def ticker22(self):
|
||||
self.copy_file('ticker-cfg-bad.yaml', 'ticker-cfg.yaml')
|
||||
t = self.ticker(
|
||||
[],
|
||||
['MONERO', 'ETHEREUM', 'BITCOIN', 'SILVER', 'BRENT', 'GOLD'],
|
||||
add_opts = ['--name-labels', '--sort=rp'])
|
||||
self.rm_file('ticker-cfg.yaml')
|
||||
return t
|
||||
|
||||
def ticker23(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
['MONERO', 'ETHEREUM', 'BITCOIN', 'SILVER', 'BRENT', 'GOLD'],
|
||||
add_opts = ['--name-labels', '--sort=rp', 'xmr:10'])
|
||||
|
||||
def ticker24(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
['BITCOIN', 'ETHEREUM', 'MONERO', 'GOLD', 'BRENT', 'SILVER'],
|
||||
add_opts = ['--name-labels', '--sort=p'])
|
||||
|
||||
def ticker25(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
[
|
||||
r' 1\) BITCOIN',
|
||||
r' 2\) ETHEREUM',
|
||||
r'30\) MONERO',
|
||||
r'23\) LITECOIN',
|
||||
r' 8\) CARDANO',
|
||||
r'33\) ALGORAND'
|
||||
],
|
||||
add_opts = ['--name-labels', '--sort=p', '200'])
|
||||
|
||||
def ticker26(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
['BITCOIN', 'ETHEREUM', 'MONERO', 'CARDANO', 'ALGORAND'],
|
||||
add_opts = ['--name-labels', '--sort=c', '-r', 'ada,algo'])
|
||||
|
||||
def ticker27(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
['MONERO', 'ETHEREUM', 'BITCOIN', 'S&P', 'NASDAQ', 'DOW', 'ALGORAND', 'CARDANO'],
|
||||
add_opts = ['--name-labels', '--sort=rp', '--add-rows=ada-cardano,algo-algorand'])
|
||||
|
||||
def ticker28(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
['ETHEREUM', 'MONERO', 'BITCOIN', 'NASDAQ', 'S&P', 'DOW', 'CARDANO', 'ALGORAND'],
|
||||
add_opts = ['--widest', '--sort=d', '-r', 'ada,algo'])
|
||||
|
||||
def ticker29(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
['ETHEREUM', 'BITCOIN', 'MONERO', 'S&P', 'DOW', 'NASDAQ', 'CARDANO', 'ALGORAND'],
|
||||
add_opts = ['--widest', '-s', 'y', '-r', 'ada,algo'])
|
||||
|
||||
def ticker30(self):
|
||||
self.copy_file('ticker-cfg-sort-pchg.yaml', 'ticker-cfg.yaml')
|
||||
t = self.ticker(add_opts=['--wide'])
|
||||
chk1 = '\n'.join(t.read().splitlines()[5:-2])
|
||||
self.rm_file('ticker-cfg.yaml')
|
||||
|
||||
self.copy_file('ticker-cfg-bad.yaml', 'ticker-cfg.yaml')
|
||||
t = self.ticker(add_opts=['--wide', '--pchg-unit=btc', '--sort=d'], no_msg=True)
|
||||
chk2 = '\n'.join(t.read().splitlines()[5:-2])
|
||||
self.rm_file('ticker-cfg.yaml')
|
||||
|
||||
assert chk1 == chk2, f'\nOUTPUT 1\n{chk1}\n!= OUTPUT 2\n{chk2}\n'
|
||||
return t
|
||||
|
||||
def ticker31(self):
|
||||
t = self.ticker(add_opts=['--wide'])
|
||||
chk1 = '\n'.join(t.read().splitlines()[5:-2])
|
||||
|
||||
t = self.ticker(add_opts=['--wide', '--pchg-unit=usd'], no_msg=True)
|
||||
chk2 = '\n'.join(t.read().splitlines()[6:-2])
|
||||
|
||||
assert chk1 == chk2, f'\nOUTPUT 1\n{chk1}\n!= OUTPUT 2\n{chk2}\n'
|
||||
return t
|
||||
|
||||
def ticker32(self):
|
||||
return self.ticker(
|
||||
[],
|
||||
[
|
||||
'BITCOIN', r'\+10.99', r'\+7.06', '-1.18', r'\+1.05',
|
||||
'ETHEREUM',
|
||||
'GOLD', r'\+0.00', r'\+0.00', r'\+0.00', r'\+0.00',
|
||||
'SILVER'
|
||||
],
|
||||
add_opts = ['--widest', '--pchg-unit=gc=f', '--sort=c'])
|
||||
|
||||
def _ticker_cur(self, sort):
|
||||
self.copy_file('ticker-cfg-usd.yaml', 'ticker-cfg.yaml')
|
||||
t = self.ticker(
|
||||
[],
|
||||
[
|
||||
'BITCOIN 23,250.77 1.00000000 \+0.00',
|
||||
'US DOLLAR 1.00 0.00004301 -15.93',
|
||||
],
|
||||
add_opts = ['--widest', '--pchg-unit=btc', f'--sort={sort}'])
|
||||
self.rm_file('ticker-cfg.yaml')
|
||||
return t
|
||||
|
||||
def ticker33(self):
|
||||
return self._ticker_cur(sort='c')
|
||||
|
||||
def ticker34(self):
|
||||
return self._ticker_cur(sort='y')
|
||||
|
||||
def ticker35(self):
|
||||
return self._ticker_cur(sort='p')
|
||||
|
|
@ -1,439 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
test.cmdtest_d.regtest: Regtest tests for the cmdtest.py test suite
|
||||
"""
|
||||
|
||||
import sys, os
|
||||
from decimal import Decimal
|
||||
|
||||
from mmgen.util import msg_r, die, gmsg
|
||||
from mmgen.protocol import init_proto
|
||||
from mmgen.proto.btc.regtest import MMGenRegtest
|
||||
|
||||
from ..include.common import cfg, imsg, stop_test_daemons, joinpath
|
||||
from .base import CmdTestBase
|
||||
|
||||
args1 = ['--bob']
|
||||
args2 = ['--bob', '--rpc-backend=http']
|
||||
|
||||
def gen_addrs(proto, network, keys):
|
||||
from mmgen.tool.api import tool_api
|
||||
tool = tool_api(cfg)
|
||||
tool.init_coin(proto.coin, 'regtest')
|
||||
tool.addrtype = proto.mmtypes[-1]
|
||||
return [tool.privhex2addr('{:064x}'.format(key)) for key in keys]
|
||||
|
||||
class CmdTestRegtest(CmdTestBase):
|
||||
'various operations via regtest mode'
|
||||
networks = ('btc', 'ltc', 'bch')
|
||||
passthru_opts = ('coin',)
|
||||
tmpdir_nums = [1]
|
||||
color = True
|
||||
deterministic = False
|
||||
bdb_wallet = True
|
||||
|
||||
cmd_group_in = (
|
||||
('setup', 'regtest mode setup'),
|
||||
('subgroup.netrate', []),
|
||||
('subgroup.halving_calculator', []),
|
||||
('subgroup.fund_addrbal', []),
|
||||
('subgroup.addrbal', ['fund_addrbal']),
|
||||
('subgroup.blocks_info', ['addrbal']),
|
||||
('subgroup.feeview', []),
|
||||
('stop', 'stopping regtest daemon'),
|
||||
)
|
||||
cmd_subgroups = {
|
||||
'netrate': (
|
||||
"'mmnode-netrate' script",
|
||||
('netrate1', "netrate (--help)"),
|
||||
('netrate2', "netrate"),
|
||||
),
|
||||
'halving_calculator': (
|
||||
"'mmnode-halving-calculator' script",
|
||||
('halving_calculator1', "halving calculator (--help)"),
|
||||
('halving_calculator2', "halving calculator"),
|
||||
('halving_calculator3', "halving calculator (--list)"),
|
||||
('halving_calculator4', "halving calculator (--mined)"),
|
||||
('halving_calculator5', "halving calculator (--mined --bdr-proj=5)"),
|
||||
('halving_calculator6', "halving calculator (--mined --sample-size=20)"),
|
||||
),
|
||||
'fund_addrbal': (
|
||||
"funding addresses for 'addrbal' subgroup",
|
||||
('sendto1', 'sending funds to address #1 (1)'),
|
||||
('sendto2', 'sending funds to address #1 (2)'),
|
||||
('sendto3', 'sending funds to address #2'),
|
||||
),
|
||||
'addrbal': (
|
||||
"'mmnode-addrbal' script",
|
||||
('addrbal_single', 'getting address balance (single address)'),
|
||||
('addrbal_multiple', 'getting address balances (multiple addresses)'),
|
||||
('addrbal_multiple_tabular1', 'getting address balances (multiple addresses, tabular output)'),
|
||||
('addrbal_multiple_tabular2', 'getting address balances (multiple addresses, tabular, show first block)'),
|
||||
('addrbal_nobal1', 'getting address balances (no balance)'),
|
||||
('addrbal_nobal2', 'getting address balances (no balances)'),
|
||||
('addrbal_nobal3', 'getting address balances (one null balance)'),
|
||||
('addrbal_nobal3_tabular1', 'getting address balances (one null balance, tabular output)'),
|
||||
('addrbal_nobal3_tabular2', 'getting address balances (one null balance, tabular, show first block)'),
|
||||
),
|
||||
'blocks_info': (
|
||||
"'mmnode-blocks-info' script",
|
||||
('blocks_info1', "blocks-info (--help)"),
|
||||
('blocks_info2', "blocks-info (no args)"),
|
||||
('blocks_info3', "blocks-info +100"),
|
||||
('blocks_info4', "blocks-info --miner-info --fields=all --stats=all +1"),
|
||||
),
|
||||
'feeview': (
|
||||
"'mmnode-feeview' script",
|
||||
('feeview_setup', 'setting up feeview test'),
|
||||
('feeview1', "'mmnode-feeview'"),
|
||||
('feeview2', "'mmnode-feeview --columns=40 --include-current'"),
|
||||
('feeview3', "'mmnode-feeview --precision=6'"),
|
||||
('feeview4', "'mmnode-feeview --detail'"),
|
||||
('feeview5', "'mmnode-feeview --show-empty --log'"),
|
||||
('feeview6', "'mmnode-feeview --ignore-below=1MB'"),
|
||||
('feeview7', "'mmnode-feeview --ignore-below=20kB'"),
|
||||
('feeview8', "'mmnode-feeview' (empty mempool)"),
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, cfg, trunner, cfgs, spawn):
|
||||
CmdTestBase.__init__(self, cfg, trunner, cfgs, spawn)
|
||||
if trunner is None:
|
||||
return
|
||||
if cfg._proto.testnet:
|
||||
die(2, '--testnet and --regtest options incompatible with regtest test suite')
|
||||
self.proto = init_proto( cfg, self.proto.coin, network='regtest', need_amt=True )
|
||||
self.addrs = [a.views[a.view_pref] for a in gen_addrs(self.proto, 'regtest', [1, 2, 3, 4, 5])]
|
||||
|
||||
self.use_bdb_wallet = self.bdb_wallet and self.proto.coin != 'BTC'
|
||||
self.regtest = MMGenRegtest(cfg, self.proto.coin, bdb_wallet=self.use_bdb_wallet)
|
||||
|
||||
def setup(self):
|
||||
stop_test_daemons(self.proto.network_id, force=True, remove_datadir=True)
|
||||
from shutil import rmtree
|
||||
try:
|
||||
rmtree(joinpath(self.tr.data_dir, 'regtest'))
|
||||
except:
|
||||
pass
|
||||
t = self.spawn(
|
||||
'mmgen-regtest',
|
||||
(['--bdb-wallet'] if self.use_bdb_wallet else [])
|
||||
+ ['--setup-no-stop-daemon', 'setup'])
|
||||
for s in ('Starting', 'Creating', 'Creating', 'Creating', 'Mined', 'Setup complete'):
|
||||
t.expect(s)
|
||||
return t
|
||||
|
||||
def netrate(self, add_args, expect_str, exit_val=None):
|
||||
t = self.spawn('mmnode-netrate', args1 + add_args, exit_val=exit_val)
|
||||
t.expect(expect_str, regex=True)
|
||||
return t
|
||||
|
||||
def netrate1(self):
|
||||
return self.netrate( ['--help'], 'USAGE:.*' )
|
||||
|
||||
def netrate2(self):
|
||||
t = self.netrate([], r'sent:.*', exit_val=-15)
|
||||
t.kill(15)
|
||||
if sys.platform == 'win32':
|
||||
return 'ok'
|
||||
return t
|
||||
|
||||
def halving_calculator(self, add_args, expect_list):
|
||||
t = self.spawn('mmnode-halving-calculator', args1+add_args)
|
||||
t.match_expect_list(expect_list)
|
||||
return t
|
||||
|
||||
def halving_calculator1(self):
|
||||
return self.halving_calculator(['--help'], ['USAGE:'])
|
||||
|
||||
def halving_calculator2(self):
|
||||
return self.halving_calculator([], ['Current block: 393', f'Current block subsidy: 12.5 {cfg.coin}'])
|
||||
|
||||
def halving_calculator3(self):
|
||||
return self.halving_calculator(['--list'], ['33 4950', '0'])
|
||||
|
||||
def halving_calculator4(self):
|
||||
return self.halving_calculator(['--mined'], ['0 0.0000015 14949.9999835'])
|
||||
|
||||
def halving_calculator5(self):
|
||||
return self.halving_calculator(['--mined', '--bdr-proj=5'], ['5.00000 0 0.0000015 14949.9999835'])
|
||||
|
||||
def halving_calculator6(self):
|
||||
return self.halving_calculator(['--mined', '--sample-size=20'], ['33 4950', '0 0.0000015 14949.9999835'])
|
||||
|
||||
def sendto(self, addr, amt):
|
||||
return self.spawn('mmgen-regtest', ['send', addr, amt])
|
||||
|
||||
def sendto1(self): return self.sendto(self.addrs[0], '0.123')
|
||||
def sendto2(self): return self.sendto(self.addrs[0], '0.234')
|
||||
def sendto3(self): return self.sendto(self.addrs[1], '0.345')
|
||||
|
||||
def addrbal(self, args, expect_list):
|
||||
t = self.spawn('mmnode-addrbal', args2 + args)
|
||||
t.match_expect_list(expect_list)
|
||||
return t
|
||||
|
||||
def addrbal_single(self):
|
||||
return self.addrbal(
|
||||
[self.addrs[0]],
|
||||
[
|
||||
f'Balance: 0.357 {cfg.coin}',
|
||||
'2 unspent outputs in 2 blocks',
|
||||
'394', '0.123',
|
||||
'395', '0.234'
|
||||
])
|
||||
|
||||
def addrbal_multiple(self):
|
||||
return self.addrbal(
|
||||
[self.addrs[1], self.addrs[0]],
|
||||
[
|
||||
'396', '0.345',
|
||||
'394', '0.123',
|
||||
'395', '0.234'
|
||||
])
|
||||
|
||||
def addrbal_multiple_tabular1(self):
|
||||
return self.addrbal(
|
||||
['--tabular', self.addrs[1], self.addrs[0]],
|
||||
[
|
||||
self.addrs[1] + ' 1 396', '0.345',
|
||||
self.addrs[0] + ' 2 395', '0.357'
|
||||
])
|
||||
|
||||
def addrbal_multiple_tabular2(self):
|
||||
return self.addrbal(
|
||||
['--tabular', '--first-block', self.addrs[1], self.addrs[0]],
|
||||
[
|
||||
self.addrs[1] + ' 1 396', '396', '0.345',
|
||||
self.addrs[0] + ' 2 394', '395', '0.357'
|
||||
])
|
||||
|
||||
def addrbal_nobal1(self):
|
||||
return self.addrbal(
|
||||
[self.addrs[2]], ['Address has no balance'])
|
||||
|
||||
def addrbal_nobal2(self):
|
||||
return self.addrbal(
|
||||
[self.addrs[2], self.addrs[3]], ['Addresses have no balances'])
|
||||
|
||||
def addrbal_nobal3(self):
|
||||
return self.addrbal(
|
||||
[self.addrs[4], self.addrs[0], self.addrs[3]],
|
||||
[
|
||||
'No balance',
|
||||
'2 unspent outputs in 2 blocks',
|
||||
'394', '0.123', '395', '0.234',
|
||||
'No balance'
|
||||
])
|
||||
|
||||
def addrbal_nobal3_tabular1(self):
|
||||
return self.addrbal(
|
||||
['--tabular', self.addrs[4], self.addrs[0], self.addrs[3]],
|
||||
[
|
||||
self.addrs[4] + ' - - -',
|
||||
self.addrs[0] + ' 2 395', '0.357',
|
||||
self.addrs[3] + ' - - -',
|
||||
])
|
||||
|
||||
def addrbal_nobal3_tabular2(self):
|
||||
return self.addrbal(
|
||||
['--tabular', '--first-block', self.addrs[4], self.addrs[0], self.addrs[3]],
|
||||
[
|
||||
self.addrs[4] + ' - - - -',
|
||||
self.addrs[0] + ' 2 394', '395', '0.357',
|
||||
self.addrs[3] + ' - - - -',
|
||||
])
|
||||
|
||||
def blocks_info(self, args, expect_list):
|
||||
t = self.spawn('mmnode-blocks-info', args1 + args)
|
||||
t.match_expect_list(expect_list)
|
||||
return t
|
||||
|
||||
def blocks_info1(self):
|
||||
return self.blocks_info(
|
||||
['--help'],
|
||||
['USAGE:', 'OPTIONS:'])
|
||||
|
||||
def blocks_info2(self):
|
||||
return self.blocks_info(
|
||||
[],
|
||||
['Current height: 396'])
|
||||
|
||||
def blocks_info3(self):
|
||||
return self.blocks_info(
|
||||
['+100'],
|
||||
[
|
||||
'Range: 297-396',
|
||||
'Current height: 396',
|
||||
'Next diff adjust: 2016'
|
||||
])
|
||||
|
||||
def blocks_info4(self):
|
||||
n1, i1, o1, n2, i2, o2 = (2, 1, 3, 6, 3, 9) if cfg.coin == 'BCH' else (2, 1, 4, 6, 3, 12)
|
||||
return self.blocks_info(
|
||||
['--miner-info', '--fields=all', '--stats=all', '+3'],
|
||||
[
|
||||
'Averages',
|
||||
f'nTx: {n1}',
|
||||
f'Inputs: {i1}',
|
||||
f'Outputs: {o1}',
|
||||
'Totals',
|
||||
f'nTx: {n2}',
|
||||
f'Inputs: {i2}',
|
||||
f'Outputs: {o2}',
|
||||
'Current height: 396',
|
||||
'Next diff adjust: 2016'
|
||||
])
|
||||
|
||||
async def feeview_setup(self):
|
||||
|
||||
def create_pairs(nPairs):
|
||||
|
||||
from mmgen.tool.api import tool_api
|
||||
from collections import namedtuple
|
||||
|
||||
t = tool_api(cfg)
|
||||
t.init_coin(self.proto.coin, self.proto.network)
|
||||
t.addrtype = 'compressed' if self.proto.coin == 'BCH' else 'bech32'
|
||||
wp = namedtuple('wifaddrpair', ['wif', 'addr'])
|
||||
|
||||
def gen():
|
||||
for n in range(0xfaceface, nPairs+0xfaceface):
|
||||
wif = t.hex2wif(f'{n:064x}')
|
||||
yield wp( wif, t.wif2addr(wif) )
|
||||
|
||||
return list(gen())
|
||||
|
||||
def gen_fees(n_in, low, high):
|
||||
|
||||
# very approximate tx size estimation:
|
||||
ibytes, wbytes, obytes = (148, 0, 34) if self.proto.coin == 'BCH' else (43, 108, 31)
|
||||
x = (ibytes + (wbytes//4) + (obytes * nPairs)) * self.proto.coin_amt.satoshi
|
||||
|
||||
n = n_in - 1
|
||||
vmax = high - low
|
||||
|
||||
for i in range(n_in):
|
||||
yield Decimal(low + (i/n)**6 * vmax) * x
|
||||
|
||||
async def do_tx(inputs, outputs, wif):
|
||||
tx_hex = await r.rpc_call('createrawtransaction', inputs, outputs)
|
||||
if wif:
|
||||
tx = await r.rpc_call(
|
||||
'signrawtransactionwithkey',
|
||||
tx_hex,
|
||||
[wif],
|
||||
[],
|
||||
self.proto.sighash_type)
|
||||
else:
|
||||
tx = await r.rpc_call(
|
||||
'signrawtransactionwithwallet',
|
||||
tx_hex,
|
||||
None, # prevtxs
|
||||
self.proto.sighash_type,
|
||||
wallet = 'miner')
|
||||
assert tx['complete']
|
||||
return tx['hex']
|
||||
|
||||
async def do_tx1():
|
||||
us = await r.rpc_call('listunspent', wallet='miner')
|
||||
tx_input = us[7] # 25 BTC in coinbase -- us[0] could have < 25 BTC
|
||||
fee = self.proto.coin_amt('0.001')
|
||||
outputs = {p.addr: tx1_amt for p in pairs[:nTxs]}
|
||||
outputs.update({burn_addr: self.proto.coin_amt(tx_input['amount']) - (tx1_amt*nTxs) - fee})
|
||||
return await do_tx(
|
||||
[{'txid': tx_input['txid'], 'vout': 0}],
|
||||
outputs,
|
||||
await r.miner_wif)
|
||||
|
||||
async def do_tx2(tx, pairno):
|
||||
fee = self.proto.coin_amt(fees[pairno], from_decimal=True)
|
||||
outputs = {p.addr: tx2_amt for p in pairs}
|
||||
outputs.update({burn_addr: tx1_amt - (tx2_amt*len(pairs)) - fee})
|
||||
return await do_tx(
|
||||
[{'txid': tx['txid'], 'vout': pairno}],
|
||||
outputs,
|
||||
pairs[pairno].wif )
|
||||
|
||||
async def do_txs(tx_in):
|
||||
for pairno in range(nTxs):
|
||||
tx_hex = await do_tx2(tx_in, pairno)
|
||||
await r.rpc_call('sendrawtransaction', tx_hex)
|
||||
|
||||
self.spawn('', msg_only=True)
|
||||
|
||||
r = self.regtest
|
||||
nPairs = 100
|
||||
nTxs = 25
|
||||
tx1_amt = self.proto.coin_amt('{:0.4f}'.format(24 / nTxs)) # 25 BTC subsidy, leave extra for fee
|
||||
tx2_amt = self.proto.coin_amt('0.00005') # make this as small as possible
|
||||
|
||||
imsg(f'Creating {nPairs} key-address pairs')
|
||||
pairs = create_pairs(nPairs+1)
|
||||
burn_addr = pairs.pop()[1]
|
||||
|
||||
imsg(f'Creating funding transaction with {nTxs} outputs of value {tx1_amt} {self.proto.coin}')
|
||||
tx1_hex = await do_tx1()
|
||||
|
||||
imsg('Relaying funding transaction')
|
||||
await r.rpc_call('sendrawtransaction', tx1_hex)
|
||||
|
||||
imsg('Mining a block')
|
||||
await r.generate(1, silent=True)
|
||||
|
||||
imsg('Generating fees for mempool transactions')
|
||||
fees = list(gen_fees(nTxs, 2, 120))
|
||||
|
||||
imsg(f'Creating and relaying {nTxs} mempool transactions with {nPairs} outputs each')
|
||||
await do_txs(await r.rpc_call('decoderawtransaction', tx1_hex))
|
||||
|
||||
return 'ok'
|
||||
|
||||
def _feeview(self, args, expect_list=[]):
|
||||
t = self.spawn('mmnode-feeview', args1 + args)
|
||||
if expect_list:
|
||||
t.match_expect_list(expect_list)
|
||||
return t
|
||||
|
||||
def feeview1(self):
|
||||
return self._feeview([])
|
||||
|
||||
def feeview2(self):
|
||||
return self._feeview(['--columns=40', '--include-current'])
|
||||
|
||||
def feeview3(self):
|
||||
return self._feeview(['--precision=6'])
|
||||
|
||||
def feeview4(self):
|
||||
return self._feeview(['--detail'])
|
||||
|
||||
def feeview5(self):
|
||||
return self._feeview(['--show-empty', '--log', f'--outdir={self.tmpdir}'])
|
||||
|
||||
def feeview6(self):
|
||||
return self._feeview(['--ignore-below=1MB'])
|
||||
|
||||
def feeview7(self):
|
||||
return self._feeview(['--ignore-below=4kB'])
|
||||
|
||||
async def feeview8(self):
|
||||
imsg('Clearing mempool')
|
||||
await self.regtest.generate(1, silent=True)
|
||||
return self._feeview([])
|
||||
|
||||
def stop(self):
|
||||
if cfg.no_daemon_stop:
|
||||
self.spawn('', msg_only=True)
|
||||
msg_r('(leaving daemon running by user request)')
|
||||
return 'ok'
|
||||
else:
|
||||
return self.spawn('mmgen-regtest', ['stop'])
|
||||
165
test/init.sh
165
test/init.sh
|
|
@ -1,165 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
RED="\e[31;1m" GREEN="\e[32;1m" YELLOW="\e[33;1m" BLUE="\e[34;1m" RESET="\e[0m"
|
||||
|
||||
set -e
|
||||
set -o errtrace
|
||||
set -o functrace
|
||||
|
||||
trap 'echo -e "${GREEN}Exiting at user request$RESET"; exit' INT
|
||||
trap 'echo -e "${RED}Node Tools test suite initialization exited with error (line $BASH_LINENO) $RESET"' ERR
|
||||
umask 0022
|
||||
|
||||
for i in '-c' '-f'; do
|
||||
stat $i %i / >/dev/null 2>&1 && stat_fmt_opt=$i
|
||||
done
|
||||
|
||||
[ "$stat_fmt_opt" ] || { echo 'No suitable ‘stat’ binary found. Cannot proceed'; exit; }
|
||||
|
||||
STDOUT_DEVNULL='>/dev/null'
|
||||
STDERR_DEVNULL='2>/dev/null'
|
||||
|
||||
PROGNAME=$(basename $0)
|
||||
while getopts hcv OPT
|
||||
do
|
||||
case "$OPT" in
|
||||
h) printf " %-16s Initialize the MMGen Node Tools test suite\n" "${PROGNAME}:"
|
||||
echo " USAGE: $PROGNAME"
|
||||
echo " OPTIONS: '-h' Print this help message"
|
||||
echo " -c Create links from mmgen-wallet ‘cmds’ subdirectory"
|
||||
echo " -v Be more verbose"
|
||||
exit ;;
|
||||
v) VERBOSE=1 STDOUT_DEVNULL='' STDERR_DEVNULL='' ;;
|
||||
c) CMD_LINKS=1 ;;
|
||||
*) exit ;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
|
||||
wallet_repo='../mmgen-wallet'
|
||||
|
||||
die() { echo -e ${YELLOW}ERROR: $1$RESET; false; }
|
||||
becho() { echo -e $BLUE$1$RESET; }
|
||||
|
||||
check_mmgen_repo() {
|
||||
( cd $wallet_repo; python3 ./setup.py --url | grep -iq 'mmgen' )
|
||||
}
|
||||
|
||||
build_mmgen_extmod() {
|
||||
(
|
||||
cd $wallet_repo
|
||||
eval "python3 ./setup.py build_ext --inplace $STDOUT_DEVNULL $STDERR_DEVNULL"
|
||||
)
|
||||
}
|
||||
|
||||
create_dir_links() {
|
||||
for link_name in 'mmgen' 'scripts'; do
|
||||
target="$wallet_repo/$link_name"
|
||||
if [ -L $link_name ]; then
|
||||
[ "$(realpath --relative-to=. $link_name 2>/dev/null)" == $target ] || {
|
||||
[ "$VERBOSE" ] && echo "Removing broken symlink '$link_name'"
|
||||
rm $link_name
|
||||
}
|
||||
elif [ -e $link_name ]; then
|
||||
die "'$link_name' is not a symbolic link. Please remove or relocate it and re-run this script"
|
||||
fi
|
||||
if [ ! -e $link_name ]; then
|
||||
[ "$VERBOSE" ] && echo "Creating symlink: $link_name"
|
||||
ln -s $target
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
delete_old_stuff() {
|
||||
rm -rf test/unit_tests.py
|
||||
rm -rf test/cmdtest_d/common.py
|
||||
rm -rf test/cmdtest_d/ct_base.py
|
||||
rm -rf test/cmdtest_d/group_mgr.py
|
||||
rm -rf test/cmdtest_d/runner.py
|
||||
}
|
||||
|
||||
create_test_links() {
|
||||
paths='
|
||||
test/include symbolic
|
||||
test/overlay/__init__.py symbolic
|
||||
test/overlay/fakemods/mmgen symbolic
|
||||
test/__init__.py symbolic
|
||||
test/clean.py symbolic
|
||||
test/cmdtest.py hard
|
||||
test/modtest.py hard
|
||||
test/test-release.sh symbolic
|
||||
test/cmdtest_d/base.py symbolic
|
||||
test/cmdtest_d/httpd/__init__.py symbolic
|
||||
test/cmdtest_d/include/common.py symbolic
|
||||
test/cmdtest_d/include/runner.py symbolic
|
||||
test/cmdtest_d/include/group_mgr.py symbolic
|
||||
test/cmdtest_d/include/pexpect.py symbolic
|
||||
cmds/mmgen-regtest symbolic
|
||||
'
|
||||
while read path type; do
|
||||
[ "$path" ] || continue
|
||||
pfx=$(echo $path | sed -r 's/[^/]//g' | sed 's/\//..\//g')
|
||||
symlink_arg=$(if [ $type == 'symbolic' ]; then echo -s; fi)
|
||||
target="$wallet_repo/$path"
|
||||
if [ ! -e "$target" ]; then
|
||||
echo "Target path $target is missing! Cannot proceed"
|
||||
exit 1
|
||||
fi
|
||||
fs="%-8s %-16s %s -> %s\n"
|
||||
if [ $type == 'hard' ]; then
|
||||
if [ -L $path ]; then
|
||||
[ "$VERBOSE" ] && printf "$fs" "Deleting" "symbolic link:" $path $target
|
||||
rm -rf $path
|
||||
elif [ -e $path ]; then
|
||||
if [ "$(stat $stat_fmt_opt %i $path)" -ne "$(stat $stat_fmt_opt %i $target)" ]; then
|
||||
[ "$VERBOSE" ] && printf "$fs" "Deleting" "stale hard link:" $path "?"
|
||||
rm -rf $path
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ ! -e $path ]; then # link is either absent or a broken symlink
|
||||
[ "$VERBOSE" ] && printf "$fs" "Creating" "$type link:" $path $target
|
||||
( cd "$(dirname $path)" && ln -f $symlink_arg $pfx$target )
|
||||
fi
|
||||
done <<<$paths
|
||||
}
|
||||
|
||||
create_cmd_links() {
|
||||
[ "$VERBOSE" ] && becho 'Creating links to mmgen-wallet repo ‘cmds’ subdirectory'
|
||||
(
|
||||
filenames=$(cd $wallet_repo/cmds && ls)
|
||||
cd cmds
|
||||
for filename in $filenames; do
|
||||
[ -e $filename ] || ln -s "../$wallet_repo/cmds/$filename"
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
becho 'Initializing MMGen Node Tools Test Suite'
|
||||
|
||||
delete_old_stuff
|
||||
|
||||
check_mmgen_repo || die "MMGen Wallet repository not found at $wallet_repo!"
|
||||
|
||||
build_mmgen_extmod
|
||||
|
||||
[ "$VERBOSE" ] && becho 'Creating links to mmgen-wallet repo'
|
||||
|
||||
create_dir_links
|
||||
|
||||
create_test_links
|
||||
|
||||
[ "$CMD_LINKS" ] && create_cmd_links
|
||||
|
||||
[ "$VERBOSE" ] && becho 'OK'
|
||||
|
||||
true
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
test.modtest_d: shared data for module tests for the MMGen Node Tools suite
|
||||
"""
|
||||
|
||||
altcoin_tests = []
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
test.unit_tests_d.nt_BlocksInfo: BlocksInfo unit test for the MMGen Node Tools suite
|
||||
"""
|
||||
|
||||
from mmgen_node_tools.BlocksInfo import BlocksInfo
|
||||
|
||||
from ..include.common import vmsg
|
||||
|
||||
tip = 50000
|
||||
range_vecs = (
|
||||
# First Last FromTip nBlocks Step First Last BlockList
|
||||
( (), (), (tip, tip, None) ),
|
||||
( (199,2,37), (), (None, None, [199,2,37]) ),
|
||||
( '0', (0, 0, None, None, None), (0, 0, None) ),
|
||||
( '0-0', (0, 0, None, None, None), (0, 0, None) ),
|
||||
(f'-{tip}', (0, 0, tip, None, None), (0, 0, None) ),
|
||||
( '0-10', (0, 10, None, None, None), (0, 10, None) ),
|
||||
( '0+10', (0, 9, None, 10, None), (0, 9, None) ),
|
||||
( '0+10+2', (0, 9, None, 10, 2 ), (0, 9, [0,2,4,6,8]) ),
|
||||
|
||||
( '1', (1, 1, None, None, None), (1, 1, None) ),
|
||||
( '1-1', (1, 1, None, None, None), (1, 1, None) ),
|
||||
( '1-10', (1, 10, None, None, None), (1, 10, None) ),
|
||||
( '1+10', (1, 10, None, 10, None), (1, 10, None) ),
|
||||
( '1+10+2', (1, 10, None, 10, 2 ), (1, 10, [1,3,5,7,9]) ),
|
||||
|
||||
( '+1', (tip, tip, None, 1, None), (tip, tip, None) ),
|
||||
( '+10', (tip-9, tip, None, 10, None), (tip-9, tip, None) ),
|
||||
|
||||
( '-1', (tip-1, tip-1, 1, None, None), (tip-1, tip-1, None) ),
|
||||
( '-1+1', (tip-1, tip-1, 1, 1, None), (tip-1, tip-1, None) ),
|
||||
( '-1+2', (tip-1, tip, 1, 2, None), (tip-1, tip, None) ),
|
||||
( '-10', (tip-10, tip-10, 10, None, None), (tip-10, tip-10, None) ),
|
||||
( '-10+11', (tip-10, tip, 10, 11, None), (tip-10, tip, None) ),
|
||||
( '-10+11+2', (tip-10, tip, 10, 11, 2 ), (tip-10, tip, list(range(tip-10,tip+1,2))) ),
|
||||
|
||||
( 'cUr', (tip, tip, None, None, None), (tip, tip, None) ),
|
||||
( 'cur-cUR', (tip, tip, None, None, None), (tip, tip, None) ),
|
||||
( '0-cur', (0, tip, None, None, None), (0, tip, None) ),
|
||||
(f'{tip-1}-cur', (tip-1, tip, None, None, None), (tip-1, tip, None) ),
|
||||
( '0-cur+3000', (0, tip, None, None, 3000 ), (0, tip, list(range(0,tip+1,3000))) ),
|
||||
( '+1440+144', (tip-1439, tip, None, 1440, 144 ), (tip-1439, tip, list(range(tip-1439,tip+1,144))) ),
|
||||
( '+144*10+12*12', (tip-1439, tip, None, 1440, 144 ), (tip-1439, tip, list(range(tip-1439,tip+1,144))) ),
|
||||
)
|
||||
|
||||
full_set = ['aa','bbb','ccc_P2','ddddd','eeeeee','ffffffff','gg']
|
||||
dfl_set = ['aa','ddddd','ffffffff']
|
||||
fields_vecs = (
|
||||
( 'Ccc_P2', ['ccc_P2'] ),
|
||||
( '+CCC_P2', ['aa','ccc_P2','ddddd','ffffffff'] ),
|
||||
( '+Aa', dfl_set ),
|
||||
( '+ddDDD,FffffffF', dfl_set ),
|
||||
( '+bBb', ['aa','bbb','ddddd','ffffffff'] ),
|
||||
( '-ddddd', ['aa','ffffffff'] ),
|
||||
( '-DDDDD,fFffffff', ['aa'] ),
|
||||
( '-ffffffff,AA,DdDdD', [] ),
|
||||
( '+aa,gG,ccC_P2', ['aa','ccc_P2','ddddd','ffffffff','gg'] ),
|
||||
( '+BBB,gg-dDddD,fFffffff', ['aa','bbb','gg'] ),
|
||||
( '-dDddD,fFffffff+BBB,gg', ['aa','bbb','gg'] ),
|
||||
( 'aLL-Ccc_P2', [e for e in full_set if e != 'ccc_P2'] ),
|
||||
( 'All-dDddd,aa', [e for e in full_set if e not in ('aa','ddddd')] ),
|
||||
)
|
||||
|
||||
class dummyRPC:
|
||||
blockcount = tip
|
||||
def info(self,arg):
|
||||
return True
|
||||
class proto:
|
||||
class coin_amt:
|
||||
satoshi = 0.00000001
|
||||
|
||||
class dummyCfg:
|
||||
fields = None
|
||||
stats = None
|
||||
miner_info = None
|
||||
header_info = None
|
||||
full_stats = None
|
||||
coin = 'BTC'
|
||||
|
||||
class unit_tests:
|
||||
|
||||
def parse_field(self,name,ut):
|
||||
vmsg('{:28} => {}'.format('FULL SET:',full_set))
|
||||
vmsg('{:28} => {}'.format('DFL SET: ',dfl_set))
|
||||
b = BlocksInfo
|
||||
for opt,chk in fields_vecs:
|
||||
ret = b.parse_cslist(opt,full_set,dfl_set,'field')
|
||||
vmsg(f'{opt:28} => {ret}')
|
||||
assert ret == chk, f'{ret} != {chk}'
|
||||
return True
|
||||
|
||||
def parse_rangespec(self,name,ut):
|
||||
|
||||
b = BlocksInfo(dummyCfg(),None,dummyRPC())
|
||||
|
||||
def test(spec,chk,foo):
|
||||
ret = b.parse_rangespec(spec)
|
||||
vmsg(f'{spec:13} => {BlocksInfo.range_data(*chk)}')
|
||||
assert ret == chk, f'{ret} != {chk}'
|
||||
|
||||
for vec in range_vecs:
|
||||
if vec[1]:
|
||||
test(*vec)
|
||||
|
||||
return True
|
||||
|
||||
def parse_cmd_args(self,name,ut):
|
||||
|
||||
def test(spec,foo,chk):
|
||||
b = BlocksInfo(
|
||||
dummyCfg(),
|
||||
spec if type(spec) == tuple else [spec],
|
||||
dummyRPC() )
|
||||
ret = (b.first,b.last,b.block_list)
|
||||
vmsg('{:13} => {}'.format(
|
||||
(repr(spec) if type(spec) == tuple else spec),
|
||||
chk ))
|
||||
assert ret == chk, f'{ret} != {chk}'
|
||||
|
||||
for vec in range_vecs:
|
||||
test(*vec)
|
||||
|
||||
return True
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
test.unit_tests_d.ut_dep: dependency unit tests for the MMGen Node Tools
|
||||
|
||||
Test whether dependencies are installed and functional.
|
||||
No data verification is performed.
|
||||
"""
|
||||
|
||||
from ..include.common import vmsg,imsg
|
||||
from mmgen.color import yellow
|
||||
|
||||
class unit_tests:
|
||||
|
||||
def yahooquery(self,name,ut):
|
||||
try:
|
||||
from yahooquery import Ticker
|
||||
return True
|
||||
except ImportError:
|
||||
imsg(yellow('Unable to import Ticker from yahooquery'))
|
||||
return False
|
||||
|
|
@ -1,354 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
fakemods.mmgen_node_tools.PeerBlocks: List blocks in flight, disconnect stalling nodes - test data
|
||||
"""
|
||||
|
||||
from .PeerBlocks_orig import *
|
||||
|
||||
class fake_data:
|
||||
|
||||
gp_counter = 0
|
||||
dn_counter = 0
|
||||
poll_secs = 0.5
|
||||
|
||||
addresses = """
|
||||
0 3tokm3se4omuhhqgaxiam3yhl474ekbg5xb45kydfno57k7ooapitogv.onion:8333 /Satoshi:24.0.0/
|
||||
1 2rysrq5f2ec4zpnsohyb6mc6l6unosjwaydjefttt34ouogdpqtlo7a5.onion:8333 /Satoshi:0.21.1/
|
||||
2 5bkcnkrihwowji7zwko7ddtpcetz6572zbwdh6aguyt2yz3wgvzqa3ne.onion:8333 /Satoshi:23.0.0/
|
||||
3 ukzy7yvako4z6tvtocsm33yvdxwyx567ioqwfezzewlw2syawzkgrw64.onion:8333 /Satoshi:22.0.0/
|
||||
4 b3r3hhxiauujwj7afmji63forvnd7uhq7ov5x2n7w7hvrhutoq3lhul7.onion:8333 /Satoshi:24.0.0/
|
||||
5 ksownpb4zk4vuyfiowgyvz3kzc2djeiurnnh7pyal3if54n5wzup3afm.onion:8333 /Satoshi:22.0.0/
|
||||
6 xh6vhun75w5y2s2xze2n4rarnduqvwowwthhyiaefly3df2t4guwvjrl.onion:8333 /Satoshi:22.0.0/
|
||||
7 raqbaqcsoldmk7sn6fgoh5bicrlzjdoexz5b2z7jemxb2u6z62vpxto2.onion:8333 /Satoshi:23.0.0/
|
||||
8 5gu3m7jr4tog5tuyisek2fxl5erjgasrxzarnii6cpemmvqhate36hrg.onion:8333 /Satoshi:25.1.0(Aldebaran 3.2.1)/
|
||||
9 ayiyxqgckls4fmzrbbh35ppquhstzhm453xezvzmuoglhnibrxu5ebos.onion:8333 /Satoshi:22.0.0/
|
||||
10 3zmyku3xqrb4mebi2r6l65l5ovmf5yjou2zeywgz5g2ehiqbclppwy6t.onion:8333 /Satoshi:23.0.0/
|
||||
11 ljyfsi4wponyw52o5y75bn6oxktw7kiaes3vdtzf26scapei2ed6yw4m.onion:8333 /Satoshi:0.21.0/
|
||||
12 n5hsupofuhis2xfx7neahntqjl2l3jjp7ait6jsegrp4utj7573qgqsp.onion:8333 /Satoshi:22.0.0/
|
||||
13 klw3dgcuzr4etnkgw5ysnukfl46urhnqrlvjt53v544p32o7acakyps4.onion:8333 /Satoshi:23.0.0/
|
||||
14 icmdqoko4nnqp4aiamiqihbvqlumqrmidyb7n54jdif2uki3qkprwsvd.onion:8333 /Satoshi:24.1.0/
|
||||
"""
|
||||
|
||||
# connected peers for each iteration
|
||||
iterations = """
|
||||
01 0 1 2 3 4 5 6 7 8
|
||||
02 0 1 2 3 4 5 6 7
|
||||
03 0 1 2 3 4 5 6 7 8
|
||||
04 0 2 3 4 5 6 7 8 9
|
||||
05 0 2 3 4 5 6 7 8 9
|
||||
06 0 2 3 4 5 6 7 8 9 10
|
||||
07 0 2 3 4 5 6 7 8 9 10
|
||||
08 0 2 3 4 5 6 7 8 9 10
|
||||
09 0 2 3 4 5 6 7 8 9 10
|
||||
10 0 2 3 4 5 6 7 8 9 10
|
||||
11 2 3 4 5 6 7 8 9 10
|
||||
12 2 3 4 5 6 7 8 9 10
|
||||
13 2 3 4 5 6 7 8 9 10
|
||||
14 2 3 4 5 6 7 8 9 10
|
||||
15 2 3 4 5 6 7 8 9 10 12
|
||||
16 2 3 4 5 6 7 8 9 10 12
|
||||
17 2 3 4 5 6 7 8 9 10 12
|
||||
18 2 3 4 5 6 7 8 9 10 12
|
||||
19 2 3 5 6 7 8 9 10 12
|
||||
20 2 3 5 6 7 8 9 10 12
|
||||
21 2 3 5 6 7 8 9 10 12
|
||||
22 2 3 5 6 7 8 9 10 12
|
||||
"""
|
||||
|
||||
# blocks in flight for each iteration for each peer
|
||||
blocks = {
|
||||
'0': """
|
||||
01 6917 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083
|
||||
02 6917 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083
|
||||
03 6917 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083
|
||||
04 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083 6913
|
||||
05 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083 6913
|
||||
06 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083 6913
|
||||
07 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083 6913
|
||||
08 6918 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083 6913
|
||||
09 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083 6913 7183
|
||||
10 6920 6937 6939 6942 6944 6946 6947 6950 6951 6953 6971 6976 6985 7083 6913 7183
|
||||
""",
|
||||
'1': """
|
||||
01 6906 6907 6908 6909 6910 6911 6913 6915 6919 6940 6948 6982 6988 7000 7023 7062
|
||||
02 6906 6907 6908 6909 6910 6911 6913 6915 6919 6940 6948 6982 6988 7000 7023 7062
|
||||
03 6908 6909 6910 6911 6913 6915 6919 6940 6948 6982 6988 7000 7023 7062 7088 7107
|
||||
""",
|
||||
'2': """
|
||||
01 6990 6994 6996 6997 6999 7017 7018 7019 7020 7021 7025 7059 7060 7061 7082 7084
|
||||
02 6990 6994 6996 6997 6999 7017 7018 7019 7020 7021 7025 7059 7060 7061 7082 7084
|
||||
03 6997 6999 7017 7018 7019 7020 7021 7025 7059 7060 7061 7082 7084 7085 7089 7108
|
||||
04 7018 7019 7020 7021 7025 7059 7060 7061 7082 7084 7085 7089 7108 7113 6909 7000
|
||||
05 7018 7019 7020 7021 7025 7059 7060 7061 7082 7084 7085 7089 7108 7113 6909 7000
|
||||
06 7020 7021 7025 7059 7060 7061 7082 7084 7085 7089 7108 7113 6909 7000 7112 7142
|
||||
07 7025 7059 7060 7061 7082 7084 7085 7089 7108 7113 6909 7000 7112 7142 7164 7173
|
||||
08 7059 7060 7061 7082 7084 7085 7089 7108 7113 6909 7000 7112 7142 7164 7173 7179
|
||||
09 7060 7061 7082 7084 7085 7089 7108 7113 6909 7000 7112 7142 7164 7173 7179 7190
|
||||
10 7061 7082 7084 7085 7089 7108 7113 6909 7000 7112 7142 7164 7173 7179 7190 7201
|
||||
11 7084 7085 7089 7108 7113 6909 7000 7112 7142 7164 7173 7179 7190 7201 7209 6942
|
||||
12 7085 7089 7108 7113 6909 7000 7112 7142 7164 7173 7179 7190 7201 7209 6942 6985
|
||||
13 7085 7089 7108 7113 6909 7000 7112 7142 7164 7173 7179 7190 7201 7209 6942 6985
|
||||
14 7089 7108 7113 6909 7000 7112 7142 7164 7173 7179 7190 7201 7209 6942 6985 7217
|
||||
15 7108 7113 6909 7000 7112 7142 7164 7173 7179 7190 7201 7209 6942 6985 7217 7245
|
||||
16 6909 7000 7112 7142 7164 7173 7179 7190 7201 7209 6942 6985 7217 7245 7250 7261
|
||||
17 6909 7000 7112 7142 7164 7173 7179 7190 7201 7209 6942 6985 7217 7245 7250 7261
|
||||
18 7173 7179 7190 7201 7209 6942 6985 7217 7245 7250 7261 7289 7314 7319 7325 7360
|
||||
19 7179 7190 7201 7209 6942 6985 7217 7245 7250 7261 7289 7314 7319 7325 7360 7379
|
||||
20 7190 7201 7209 6942 6985 7217 7245 7250 7261 7289 7314 7319 7325 7360 7379 7188
|
||||
21 7209 6942 6985 7217 7245 7250 7261 7289 7314 7319 7325 7360 7379 7188 7324 7388
|
||||
22 7209 6942 6985 7217 7245 7250 7261 7289 7314 7319 7325 7360 7379 7188 7324 7388
|
||||
""",
|
||||
'3': """
|
||||
01 6974 6977 6978 6979 6983 6986 6991 6992 6993 6995 6998 7022 7024 7058 7063 7081
|
||||
02 6974 6977 6978 6979 6983 6986 6991 6992 6993 6995 6998 7022 7024 7058 7063 7081
|
||||
03 6977 6978 6979 6983 6986 6991 6992 6993 6995 6998 7022 7024 7058 7063 7081 7106
|
||||
04 6979 6983 6986 6991 6992 6993 6995 6998 7022 7024 7058 7063 7081 7106 7119 6982
|
||||
05 6979 6983 6986 6991 6992 6993 6995 6998 7022 7024 7058 7063 7081 7106 7119 6982
|
||||
06 6986 6991 6992 6993 6995 6998 7022 7024 7058 7063 7081 7106 7119 6982 7136 7161
|
||||
07 6991 6992 6993 6995 6998 7022 7024 7058 7063 7081 7106 7119 6982 7136 7161 7170
|
||||
08 6992 6993 6995 6998 7022 7024 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177
|
||||
09 6993 6995 6998 7022 7024 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177 7186
|
||||
10 6995 6998 7022 7024 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177 7186 7195
|
||||
11 7022 7024 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177 7186 7195 6937 6946
|
||||
12 7022 7024 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177 7186 7195 6937 6946
|
||||
13 7024 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177 7186 7195 6937 6946 7214
|
||||
14 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177 7186 7195 6937 6946 7214
|
||||
15 7058 7063 7081 7106 7119 6982 7136 7161 7170 7177 7186 7195 6937 6946 7214 7223
|
||||
16 7081 7106 7119 6982 7136 7161 7170 7177 7186 7195 6937 6946 7214 7223 7247 7257
|
||||
17 7106 7119 6982 7136 7161 7170 7177 7186 7195 6937 6946 7214 7223 7247 7257 7272
|
||||
18 7257 7272 7279 7287 7297 7300 7305 7318 7326 7332 7339 7341 7352 7359 7369 7373
|
||||
19 7279 7287 7297 7300 7305 7318 7326 7332 7339 7341 7352 7359 7369 7373 7381 7383
|
||||
20 7287 7297 7300 7305 7318 7326 7332 7339 7341 7352 7359 7369 7373 7381 7383 7259
|
||||
21 7297 7300 7305 7318 7326 7332 7339 7341 7352 7359 7369 7373 7381 7383 7259 7389
|
||||
22 7297 7300 7305 7318 7326 7332 7339 7341 7352 7359 7369 7373 7381 7383 7259 7389
|
||||
""",
|
||||
'4': """
|
||||
01 7002 7003 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080
|
||||
02 7002 7003 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080
|
||||
03 7003 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109
|
||||
04 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915
|
||||
05 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915
|
||||
06 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160
|
||||
07 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172
|
||||
08 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172
|
||||
09 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188
|
||||
10 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188
|
||||
11 7008 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188 7205
|
||||
12 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188 7205 7083
|
||||
13 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188 7205 7083
|
||||
14 7009 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188 7205 7083
|
||||
15 7010 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188 7205 7083 7225
|
||||
16 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188 7205 7083 7225 7259
|
||||
17 7011 7012 7013 7014 7015 7016 7080 7109 6915 7160 7172 7188 7205 7083 7225 7259
|
||||
18 6915 7160 7172 7188 7205 7083 7225 7259 7278 7291 7317 7324 7345 7358 7364 7370
|
||||
""",
|
||||
'5': """
|
||||
01 7026 7027 7028 7029 7030 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041
|
||||
02 7026 7027 7028 7029 7030 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041
|
||||
03 7028 7029 7030 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041 7086 7110
|
||||
04 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041 7086 7110 7116 6910 7088
|
||||
05 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041 7086 7110 7116 6910 7088
|
||||
06 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041 7086 7110 7116 6910 7088 7143
|
||||
07 7033 7034 7035 7036 7037 7038 7039 7040 7041 7086 7110 7116 6910 7088 7143 7167
|
||||
08 7034 7035 7036 7037 7038 7039 7040 7041 7086 7110 7116 6910 7088 7143 7167 7180
|
||||
09 7035 7036 7037 7038 7039 7040 7041 7086 7110 7116 6910 7088 7143 7167 7180 7185
|
||||
10 7037 7038 7039 7040 7041 7086 7110 7116 6910 7088 7143 7167 7180 7185 7194 7200
|
||||
11 7041 7086 7110 7116 6910 7088 7143 7167 7180 7185 7194 7200 7206 7207 6939 6944
|
||||
12 7116 6910 7088 7143 7167 7180 7185 7194 7200 7206 7207 6939 6944 6950 6951 6953
|
||||
13 6910 7088 7143 7167 7180 7185 7194 7200 7206 7207 6939 6944 6950 6951 6953 7211
|
||||
14 7088 7143 7167 7180 7185 7194 7200 7206 7207 6939 6944 6950 6951 6953 7211 7220
|
||||
15 7143 7167 7180 7185 7194 7200 7206 7207 6939 6944 6950 6951 6953 7211 7220 7224
|
||||
16 7180 7185 7194 7200 7206 7207 6939 6944 6950 6951 6953 7211 7220 7224 7246 7263
|
||||
17 7185 7194 7200 7206 7207 6939 6944 6950 6951 6953 7211 7220 7224 7246 7263 7268
|
||||
18 7220 7224 7246 7263 7268 7275 7283 7296 7308 7321 7330 7335 7336 7349 7353 7371
|
||||
19 7224 7246 7263 7268 7275 7283 7296 7308 7321 7330 7335 7336 7349 7353 7371 6915
|
||||
20 7224 7246 7263 7268 7275 7283 7296 7308 7321 7330 7335 7336 7349 7353 7371 6915
|
||||
21 7268 7275 7283 7296 7308 7321 7330 7335 7336 7349 7353 7371 6915 7385 7391 7392
|
||||
22 7283 7296 7308 7321 7330 7335 7336 7349 7353 7371 6915 7385 7391 7392 7400 7408
|
||||
""",
|
||||
'6': """
|
||||
01 7042 7043 7044 7045 7046 7047 7048 7049 7050 7051 7052 7053 7054 7055 7056 7057
|
||||
02 7043 7044 7045 7046 7047 7048 7049 7050 7051 7052 7053 7054 7055 7056 7057
|
||||
03 7045 7046 7047 7048 7049 7050 7051 7052 7053 7054 7055 7056 7057 7087
|
||||
04 7048 7049 7050 7051 7052 7053 7054 7055 7056 7057 7087 7115 6940
|
||||
05 7049 7050 7051 7052 7053 7054 7055 7056 7057 7087 7115 6940
|
||||
06 7051 7052 7053 7054 7055 7056 7057 7087 7115 6940 7141
|
||||
07 7052 7053 7054 7055 7056 7057 7087 7115 6940 7141
|
||||
08 7054 7055 7056 7057 7087 7115 6940 7141 7176
|
||||
09 7055 7056 7057 7087 7115 6940 7141 7176
|
||||
10 7056 7057 7087 7115 6940 7141 7176
|
||||
11 7057 7087 7115 6940 7141 7176
|
||||
12 7087 7115 6940 7141 7176
|
||||
13 6940 7141 7176 7213
|
||||
14 7141 7176 7213
|
||||
15 7141 7176 7213
|
||||
16 7141 7176 7213
|
||||
17 7141 7176 7213
|
||||
18 7141 7176 7213
|
||||
19 7176 7213
|
||||
20 7176 7213
|
||||
21 7213
|
||||
22
|
||||
""",
|
||||
'7': """
|
||||
01 7064 7065 7066 7067 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079
|
||||
02 7064 7065 7066 7067 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079
|
||||
03 7064 7065 7066 7067 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079
|
||||
04 7067 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988
|
||||
05 7067 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988
|
||||
06 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988 7139
|
||||
07 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166
|
||||
08 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175
|
||||
09 7071 7072 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175 7187
|
||||
10 7072 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198
|
||||
11 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198 6913
|
||||
12 7073 7074 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198 6913
|
||||
13 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198 6913 7210 7212
|
||||
14 7075 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198 6913 7210 7212
|
||||
15 7076 7077 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198 6913 7210 7212 7226
|
||||
16 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198 6913 7210 7212 7226 7254 7265
|
||||
17 7078 7079 7111 7118 6988 7139 7166 7175 7187 7198 6913 7210 7212 7226 7254 7265
|
||||
18 6913 7210 7212 7226 7254 7265 7276 7284 7285 7293 7304 7306 7323 7333 7346 7355
|
||||
19 7212 7226 7254 7265 7276 7284 7285 7293 7304 7306 7323 7333 7346 7355 7375 7083
|
||||
20 7212 7226 7254 7265 7276 7284 7285 7293 7304 7306 7323 7333 7346 7355 7375 7083
|
||||
21 7254 7265 7276 7284 7285 7293 7304 7306 7323 7333 7346 7355 7375 7083 7345 7395
|
||||
22 7276 7284 7285 7293 7304 7306 7323 7333 7346 7355 7375 7083 7345 7395 7396 7405
|
||||
""",
|
||||
'8': """
|
||||
03 7090 7091 7092 7093 7094 7095 7096 7097 7098 7099 7100 7101 7102 7103 7104 7105
|
||||
04 7095 7096 7097 7098 7099 7100 7101 7102 7103 7104 7105 7114 7117 6911 6948 7023
|
||||
05 7095 7096 7097 7098 7099 7100 7101 7102 7103 7104 7105 7114 7117 6911 6948 7023
|
||||
06 7097 7098 7099 7100 7101 7102 7103 7104 7105 7114 7117 6911 6948 7023 7137 7140
|
||||
07 7099 7100 7101 7102 7103 7104 7105 7114 7117 6911 6948 7023 7137 7140 7162 7169
|
||||
08 7100 7101 7102 7103 7104 7105 7114 7117 6911 6948 7023 7137 7140 7162 7169 7178
|
||||
09 7103 7104 7105 7114 7117 6911 6948 7023 7137 7140 7162 7169 7178 7182 7189 7191
|
||||
10 7105 7114 7117 6911 6948 7023 7137 7140 7162 7169 7178 7182 7189 7191 7193 7199
|
||||
11 7114 7117 6911 6948 7023 7137 7140 7162 7169 7178 7182 7189 7191 7193 7199 6920
|
||||
12 7117 6911 6948 7023 7137 7140 7162 7169 7178 7182 7189 7191 7193 7199 6920 6971
|
||||
13 7117 6911 6948 7023 7137 7140 7162 7169 7178 7182 7189 7191 7193 7199 6920 6971
|
||||
14 6911 6948 7023 7137 7140 7162 7169 7178 7182 7189 7191 7193 7199 6920 6971 7222
|
||||
15 6911 6948 7023 7137 7140 7162 7169 7178 7182 7189 7191 7193 7199 6920 6971 7222
|
||||
16 7162 7169 7178 7182 7189 7191 7193 7199 6920 6971 7222 7248 7249 7256 7260 7264
|
||||
17 7169 7178 7182 7189 7191 7193 7199 6920 6971 7222 7248 7249 7256 7260 7264 7270
|
||||
18 7199 6920 6971 7222 7248 7249 7256 7260 7264 7270 7280 7290 7309 7329 7348 7366
|
||||
19 6971 7222 7248 7249 7256 7260 7264 7270 7280 7290 7309 7329 7348 7366 7380 7172
|
||||
20 6971 7222 7248 7249 7256 7260 7264 7270 7280 7290 7309 7329 7348 7366 7380 7172
|
||||
21 7248 7249 7256 7260 7264 7270 7280 7290 7309 7329 7348 7366 7380 7172 7291 7387
|
||||
22 7260 7264 7270 7280 7290 7309 7329 7348 7366 7380 7172 7291 7387 7399 7402 7406
|
||||
""",
|
||||
'9': """
|
||||
04 7122 7123 7124 7125 7126 7127 7128 7129 7130 7131 7132 7133 7134 7135 6919 7062
|
||||
05 7123 7124 7125 7126 7127 7128 7129 7130 7131 7132 7133 7134 7135 6919 7062 7107
|
||||
06 7124 7125 7126 7127 7128 7129 7130 7131 7132 7133 7134 7135 6919 7062 7107 7138
|
||||
07 7125 7126 7127 7128 7129 7130 7131 7132 7133 7134 7135 6919 7062 7107 7138 7168
|
||||
08 7126 7127 7128 7129 7130 7131 7132 7133 7134 7135 6919 7062 7107 7138 7168 7181
|
||||
09 7127 7128 7129 7130 7131 7132 7133 7134 7135 6919 7062 7107 7138 7168 7181 7184
|
||||
10 7131 7132 7133 7134 7135 6919 7062 7107 7138 7168 7181 7184 7196 7197 7202 7204
|
||||
11 7132 7133 7134 7135 6919 7062 7107 7138 7168 7181 7184 7196 7197 7202 7204 7208
|
||||
12 7134 7135 6919 7062 7107 7138 7168 7181 7184 7196 7197 7202 7204 7208 6947 6976
|
||||
13 7135 6919 7062 7107 7138 7168 7181 7184 7196 7197 7202 7204 7208 6947 6976 7215
|
||||
14 6919 7062 7107 7138 7168 7181 7184 7196 7197 7202 7204 7208 6947 6976 7215 7219
|
||||
15 7062 7107 7138 7168 7181 7184 7196 7197 7202 7204 7208 6947 6976 7215 7219 7244
|
||||
16 7138 7168 7181 7184 7196 7197 7202 7204 7208 6947 6976 7215 7219 7244 7252 7262
|
||||
17 7168 7181 7184 7196 7197 7202 7204 7208 6947 6976 7215 7219 7244 7252 7262 7269
|
||||
18 7215 7219 7244 7252 7262 7269 7286 7301 7313 7328 7337 7340 7351 7362 7367 7372
|
||||
19 7244 7252 7262 7269 7286 7301 7313 7328 7337 7340 7351 7362 7367 7372 7382 7160
|
||||
20 7252 7262 7269 7286 7301 7313 7328 7337 7340 7351 7362 7367 7372 7382 7160 7205
|
||||
21 7301 7313 7328 7337 7340 7351 7362 7367 7372 7382 7160 7205 7278 7370 7390 7393
|
||||
22 7337 7340 7351 7362 7367 7372 7382 7160 7205 7278 7370 7390 7393 7397 7404 7407
|
||||
""",
|
||||
'10': """
|
||||
06 7144 7145 7146 7147 7148 7149 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159
|
||||
07 7147 7148 7149 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159 7163 7165 7171
|
||||
08 7148 7149 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159 7163 7165 7171 7174
|
||||
09 7149 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159 7163 7165 7171 7174 7192
|
||||
10 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159 7163 7165 7171 7174 7192 7203
|
||||
11 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159 7163 7165 7171 7174 7192 7203
|
||||
12 7151 7152 7153 7154 7155 7156 7157 7158 7159 7163 7165 7171 7174 7192 7203 7183
|
||||
13 7152 7153 7154 7155 7156 7157 7158 7159 7163 7165 7171 7174 7192 7203 7183 7216
|
||||
14 7154 7155 7156 7157 7158 7159 7163 7165 7171 7174 7192 7203 7183 7216 7218 7221
|
||||
15 7155 7156 7157 7158 7159 7163 7165 7171 7174 7192 7203 7183 7216 7218 7221 7227
|
||||
16 7158 7159 7163 7165 7171 7174 7192 7203 7183 7216 7218 7221 7227 7251 7258 7267
|
||||
17 7159 7163 7165 7171 7174 7192 7203 7183 7216 7218 7221 7227 7251 7258 7267 7273
|
||||
18 7277 7282 7288 7294 7302 7312 7320 7327 7331 7338 7347 7354 7357 7361 7365 7368
|
||||
19 7288 7294 7302 7312 7320 7327 7331 7338 7347 7354 7357 7361 7365 7368 7376 7378
|
||||
20 7288 7294 7302 7312 7320 7327 7331 7338 7347 7354 7357 7361 7365 7368 7376 7378
|
||||
21 7302 7312 7320 7327 7331 7338 7347 7354 7357 7361 7365 7368 7376 7378 7364 7394
|
||||
22 7327 7331 7338 7347 7354 7357 7361 7365 7368 7376 7378 7364 7394 7401 7403 7410
|
||||
""",
|
||||
'12': """
|
||||
15 7228 7229 7230 7231 7232 7233 7234 7235 7236 7237 7238 7239 7240 7241 7242 7243
|
||||
16 7230 7231 7232 7233 7234 7235 7236 7237 7238 7239 7240 7241 7242 7243 7253 7266
|
||||
17 7231 7232 7233 7234 7235 7236 7237 7238 7239 7240 7241 7242 7243 7253 7266 7274
|
||||
18 7281 7292 7298 7299 7303 7307 7310 7311 7316 7322 7334 7343 7344 7350 7356 7363
|
||||
19 7299 7303 7307 7310 7311 7316 7322 7334 7343 7344 7350 7356 7363 7374 7377 7384
|
||||
20 7303 7307 7310 7311 7316 7322 7334 7343 7344 7350 7356 7363 7374 7377 7384 7225
|
||||
21 7310 7311 7316 7322 7334 7343 7344 7350 7356 7363 7374 7377 7384 7225 7317 7386
|
||||
22 7316 7322 7334 7343 7344 7350 7356 7363 7374 7377 7384 7225 7317 7386 7398 7409
|
||||
"""}
|
||||
|
||||
def make_data():
|
||||
|
||||
def gen_address_data():
|
||||
for line in fake_data.addresses.strip().split('\n'):
|
||||
data = line.split(maxsplit=2)
|
||||
yield (data[0], {k: v for k, v in zip(('id', 'addr', 'subver'), data)})
|
||||
|
||||
def gen_iterations_data():
|
||||
for line in fake_data.iterations.strip().split('\n'):
|
||||
data = line.split(maxsplit=1)
|
||||
yield (data[0], data[1].split())
|
||||
|
||||
def gen_blocks_data(iterations):
|
||||
for peer,blocks_str in fake_data.blocks.items():
|
||||
iter_strs = dict([s.lstrip().split(maxsplit=1) for s in blocks_str.strip().split('\n') if ' ' in s])
|
||||
yield (peer,dict((i,iter_strs.get(i,'').split()) for i in iterations))
|
||||
|
||||
def make_peerinfo(peer_id,blocks,iter_no):
|
||||
d = address_data[peer_id]
|
||||
return {
|
||||
'id': int(d['id']),
|
||||
'addr': d['addr'],
|
||||
'subver': d['subver'],
|
||||
'inflight': [int(n)+830000 for n in blocks[iter_no]]}
|
||||
|
||||
def gen_data():
|
||||
for iter_no in iterations_data:
|
||||
yield (
|
||||
iter_no,
|
||||
[make_peerinfo(peer_id,blocks,iter_no) for peer_id,blocks in blocks_data.items()
|
||||
if peer_id in iterations_data[iter_no] ]
|
||||
)
|
||||
|
||||
address_data = dict(gen_address_data())
|
||||
iterations_data = dict(gen_iterations_data())
|
||||
blocks_data = dict(gen_blocks_data(iterations_data))
|
||||
|
||||
fake_data.peerinfo = dict(gen_data())
|
||||
|
||||
async def get_info(self,rpc):
|
||||
fake_data.gp_counter = (fake_data.gp_counter % 22) + 1
|
||||
return fake_data.peerinfo[f'{fake_data.gp_counter:02d}']
|
||||
|
||||
async def disconnect_node(self,rpc,addr):
|
||||
from mmgen.exception import RPCFailure
|
||||
fake_data.dn_counter += 1
|
||||
if fake_data.dn_counter % 2:
|
||||
raise RPCFailure
|
||||
else:
|
||||
pass
|
||||
|
||||
fake_data.make_data()
|
||||
|
||||
Display.poll_secs = fake_data.poll_secs
|
||||
Display.get_info = fake_data.get_info
|
||||
Display.disconnect_node = fake_data.disconnect_node
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# MMGen Node Tools, terminal-based programs for Bitcoin and forkcoin nodes
|
||||
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
"""
|
||||
fakemods.mmgen_node_tools.Ticker: fake module for Ticker class
|
||||
"""
|
||||
|
||||
from .Ticker_orig import *
|
||||
|
||||
class overlay_fake_DataSource:
|
||||
class coinpaprika:
|
||||
api_host = 'localhost:19900'
|
||||
api_proto = 'http'
|
||||
|
||||
DataSource.coinpaprika.api_host = overlay_fake_DataSource.coinpaprika.api_host
|
||||
DataSource.coinpaprika.api_proto = overlay_fake_DataSource.coinpaprika.api_proto
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"id":"btc-bitcoin","name":"Bitcoin","symbol":"BTC","rank":"1","circulating_supply":"19109225","total_supply":"19109231","max_supply":"21000000","last_updated":"2022-08-01T09:34:05Z","quotes":{"USD":{"price":23368.859557988893,"percent_change_1h":-0.23,"percent_change_6h":-1.4960000000000002,"percent_change_24h":-1.87,"percent_change_7d":6.05,"percent_change_30d":8.469999999999999,"percent_change_1y":10.285,"volume_24h":24116251608.791744,"market_cap":446560795287}}}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
assets:
|
||||
coin1:
|
||||
- btc-bitcoin
|
||||
- ltc-litecoin
|
||||
- eth-ethereum
|
||||
- xmr-monero
|
||||
- bad-badcoin
|
||||
commodity:
|
||||
- gc=f
|
||||
- si=f
|
||||
- bz=f
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
sort: d
|
||||
pchg_unit: btc
|
||||
|
||||
assets:
|
||||
coin1:
|
||||
- btc-bitcoin
|
||||
- ltc-litecoin
|
||||
- eth-ethereum
|
||||
- xmr-monero
|
||||
- bad-badcoin
|
||||
commodity:
|
||||
- gc=f
|
||||
- si=f
|
||||
- bz=f
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
assets:
|
||||
coin1:
|
||||
- btc-bitcoin
|
||||
- eth-ethereum
|
||||
- xmr-monero
|
||||
commodity:
|
||||
- gc=f
|
||||
- si=f
|
||||
currency:
|
||||
- usd-us-dollar
|
||||
- eurusd=x
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
proxy: http://asdfzxcv:32459
|
||||
|
||||
cachedir:
|
||||
|
||||
assets:
|
||||
coin1:
|
||||
- btc-bitcoin
|
||||
- ltc-litecoin
|
||||
- eth-ethereum
|
||||
- xmr-monero
|
||||
coin2:
|
||||
- ada-cardano
|
||||
- algo-algorand
|
||||
commodity:
|
||||
- gc=f # gold futures
|
||||
- si=f # silver futures
|
||||
- bz=f # Brent futures
|
||||
fiat:
|
||||
- chfusd=x # Swiss Franc
|
||||
- eurusd=x # Euro
|
||||
index:
|
||||
- ^dji # Dow Jones Industrials
|
||||
- ^ixic # Nasdaq 100
|
||||
- ^gspc # S&P 500
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +0,0 @@
|
|||
btc-bitcoin: '1.23456789'
|
||||
eth-ethereum: '2.345678901234567890'
|
||||
xmr-monero: '4.567890123456'
|
||||
ada-cardano: '123.45678901'
|
||||
algo-algorand: '234.5678901'
|
||||
noc-nocoin: '777.1234'
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
btc-bitcoin: '1.23456789'
|
||||
eth-ethereum: '2.345678901234567890'
|
||||
xmr-monero: '3.333390123456'
|
||||
ada-cardano: '123.45678901'
|
||||
|
||||
wallet2:
|
||||
algo-algorand: '234.5678901'
|
||||
|
||||
exchange1:
|
||||
xmr-monero: '1.2345'
|
||||
|
|
@ -1 +0,0 @@
|
|||
[{"id":"btc-bitcoin","name":"Bitcoin","symbol":"BTC","rank":"1","circulating_supply":"19110612","total_supply":"19110619","max_supply":"21000000","last_updated":"2022-08-02T18:25:59Z","quotes":{"USD":{"price":23250.774053363122,"percent_change_1h":-0.27,"percent_change_6h":0.7120000000000001,"percent_change_24h":0.89,"percent_change_7d":11.15,"percent_change_30d":15.61,"percent_change_1y":18.955000000000002,"volume_24h":28307579008.4866,"market_cap":444336521633}}},{"id":"eth-ethereum","name":"Ethereum","symbol":"ETH","rank":"2","circulating_supply":"121802674","total_supply":"121802721","max_supply":"","last_updated":"2022-08-02T18:25:59Z","quotes":{"USD":{"price":1659.6621665887371,"percent_change_1h":-0.03,"percent_change_6h":1.4560000000000002,"percent_change_24h":1.82,"percent_change_7d":21.42,"percent_change_30d":29.988,"percent_change_1y":36.414,"volume_24h":18216561308.363518,"market_cap":202151289827}}},{"id":"ltc-litecoin","name":"Litecoin","symbol":"LTC","rank":"23","circulating_supply":"70856606","total_supply":"70856631","max_supply":"84000000","last_updated":"2022-08-02T18:25:59Z","quotes":{"USD":{"price":59.013589192027936,"percent_change_1h":-0.43,"percent_change_6h":0.32000000000000006,"percent_change_24h":0.4,"percent_change_7d":12.79,"percent_change_30d":17.906,"percent_change_1y":21.743,"volume_24h":510336800.72056556,"market_cap":4181502638}}},{"id":"xmr-monero","name":"Monero","symbol":"XMR","rank":"30","circulating_supply":"18155770","total_supply":"18155768","max_supply":"","last_updated":"2022-08-02T18:25:59Z","quotes":{"USD":{"price":158.97302629813817,"percent_change_1h":0.21,"percent_change_6h":0.968,"percent_change_24h":1.21,"percent_change_7d":7.28,"percent_change_30d":10.192,"percent_change_1y":12.376,"volume_24h":78159392.30003875,"market_cap":2886277702}}},{"id":"ada-cardano","name":"Cardano","symbol":"ADA","rank":"8","circulating_supply":"33752565071","total_supply":"34277702082","max_supply":"45000000000","last_updated":"2022-08-02T18:25:59Z","quotes":{"USD":{"price":0.5069722536476557,"percent_change_1h":0.05,"percent_change_6h":-0.08800000000000001,"percent_change_24h":-0.11,"percent_change_7d":11.68,"percent_change_30d":16.352,"percent_change_1y":19.855999999999998,"volume_24h":507973898.04662097,"market_cap":17111613980}}},{"id":"algo-algorand","name":"Algorand","symbol":"ALGO","rank":"33","circulating_supply":"6949173585","total_supply":"7350412337","max_supply":"","last_updated":"2022-08-02T18:25:59Z","quotes":{"USD":{"price":0.33196893931152616,"percent_change_1h":-0.06,"percent_change_6h":-0.656,"percent_change_24h":-0.82,"percent_change_7d":9.69,"percent_change_30d":13.565999999999999,"percent_change_1y":16.473,"volume_24h":62207779.35759487,"market_cap":2306909784}}}]
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-node-tools
|
||||
# https://gitlab.com/mmgen/mmgen-node-tools
|
||||
|
||||
# Testing status
|
||||
# mmnode-addrbal OK
|
||||
# mmnode-blocks-info OK
|
||||
# mmnode-feeview OK
|
||||
# mmnode-halving-calculator OK
|
||||
# mmnode-netrate -
|
||||
# mmnode-peerblocks OK
|
||||
# mmnode-ticker OK
|
||||
# mmnode-txfind -
|
||||
|
||||
all_tests='mod lint misc scripts btc btc_rt bch_rt ltc_rt'
|
||||
|
||||
groups_desc="
|
||||
default - All tests minus the extra tests
|
||||
extra - All tests minus the default tests
|
||||
noalt - BTC-only tests
|
||||
quick - Default tests minus bch_rt and ltc_rt
|
||||
qskip - The tests skipped in the 'quick' test group
|
||||
"
|
||||
|
||||
init_groups() {
|
||||
dfl_tests='mod misc scripts btc btc_rt bch_rt ltc_rt'
|
||||
extra_tests='lint'
|
||||
noalt_tests='mod misc scripts btc btc_rt'
|
||||
quick_tests='mod misc scripts btc btc_rt'
|
||||
qskip_tests='lint bch_rt ltc_rt'
|
||||
}
|
||||
|
||||
init_tests() {
|
||||
|
||||
d_lint="code errors with static code analyzer"
|
||||
t_lint="
|
||||
- $pylint --errors-only mmgen_node_tools
|
||||
- $pylint --errors-only test
|
||||
- $pylint --errors-only --disable=relative-beyond-top-level test/cmdtest_d
|
||||
"
|
||||
|
||||
d_mod="low-level subsystems"
|
||||
t_mod="- $modtest_py"
|
||||
|
||||
d_misc="miscellaneous features"
|
||||
t_misc="- $cmdtest_py helpscreens"
|
||||
|
||||
d_scripts="scripts not requiring a coin daemon"
|
||||
t_scripts="- $cmdtest_py scripts"
|
||||
|
||||
d_btc="Bitcoin with emulated RPC data"
|
||||
t_btc="- $cmdtest_py main"
|
||||
|
||||
d_btc_rt="Bitcoin regtest"
|
||||
t_btc_rt="- $cmdtest_py regtest"
|
||||
|
||||
d_bch_rt="Bitcoin Cash Node (BCH) regtest"
|
||||
t_bch_rt="- $cmdtest_py --coin=bch regtest"
|
||||
|
||||
d_ltc_rt="Litecoin regtest"
|
||||
t_ltc_rt="- $cmdtest_py --coin=ltc regtest"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue