Compare commits

...

61 commits

Author SHA1 Message Date
f88b70c2e2
mmnode-ticker: support nested assets in ticker-portfolio.yaml 2026-01-27 08:47:29 +00:00
b97b4f5f63
update for Bitcoin Core v30.0, nixpkgs 25.11 2025-12-08 14:57:32 +00:00
e5bc33a6d6
update for MMGen Wallet v16.1.dev21 2025-12-08 14:57:32 +00:00
f5c165c362
whitespace, minor cleanups and fixes 2025-12-08 14:57:31 +00:00
d02c9936ef
mmnode-ticker: fix market cap sort with USD row 2025-10-22 10:33:16 +00:00
84e8ea65d0
mmnode-ticker: display percent change columns in terms of non-USD assets
Any crypto or finance asset may be specified.

Examples:

    # Display percentage changes in relation to Bitcoin:
    $ mmnode-ticker --widest --pchg-unit=btc

    # In relation to Gold:
    $ mmnode-ticker --widest --pchg-unit=gc=f

    # In relation to Euros:
    $ mmnode-ticker --widest --pchg-unit=eurusd=x

    # In relation to the Nasdaq Index:
    $ mmnode-ticker --widest --pchg-unit=^ixic
2025-10-20 09:14:34 +00:00
2647fa1fe3
mmnode-ticker: minor cleanups 2025-10-20 09:14:30 +00:00
8f9c4ba48c
mmnode-ticker --sort: handle missing entries in source data 2025-10-17 09:08:00 +00:00
13234e990b
cmdtest.py misc.ticker: cleanups 2025-10-17 09:07:55 +00:00
0a953e3ca0
mmnode-ticker: sort output by various parameters
Supported parameters:

    d - 1-day % change
    w - 1-week % change
    m - 1-month % change
    y - 1-year % change
    p - asset price
    c - market cap

Examples:

    # Display top 50 assets by market cap, sorting by price change
    # in last 24 hours:
    $ mmnode-ticker --sort=d 50
2025-10-16 17:09:15 +00:00
e6d62fd18b
mmnode-ticker: display coin ranking in first column 2025-10-16 17:09:15 +00:00
e71ef141bf
mmnode-ticker: minor cleanups 2025-10-16 17:09:09 +00:00
9aa4b4dcfe
mmnode-ticker: add MarketCap column
Column is enabled automatically when script is invoked in market cap mode.
2025-10-15 10:14:20 +00:00
253aa14a26
Ticker.py: new RowDict class 2025-10-15 10:14:20 +00:00
3f921d333c
Ticker.py: parse_asset_id(): unify call signature 2025-10-15 10:14:20 +00:00
c1f42fc25b
mmnode-ticker: various fixes and cleanups 2025-10-15 10:14:14 +00:00
060b968ad4
mmnode-ticker: display crypto assets by market cap
Examples:

    # Display top 2000 assets by market cap:
    $ mmnode-ticker 2000

    # Display assets 201-300 by market cap, displaying all available columns:
    $ mmnode-ticker --widest 201-300

    # Display asset 32 by market cap:
    $ mmnode-ticker 32-32

Testing/demo:

    $ test/cmdtest.py -e scripts.ticker
2025-10-13 14:59:00 +00:00
083b29eae8
mmnode-ticker: minor fixes and cleanups 2025-10-13 14:59:00 +00:00
9ffaed6a91
mmnode-ticker: improve options setting
Options set in the cfg file can now be unset on the command line, e.g.:

    $ mmnode-ticker --no-quiet --no-cached-data
2025-10-13 14:58:54 +00:00
e7de689079
mmnode-ticker: cache network data after parsing 2025-10-12 10:01:51 +00:00
3b8aede9ba
Ticker.gen_data(): new process_data class 2025-10-12 10:01:51 +00:00
a8adef0be5
mmgen-ticker: various fixes and cleanups 2025-10-12 10:01:51 +00:00
313c7af4bb
mmgen-ticker: test caching JSON data 2025-10-12 10:01:51 +00:00
de833d75af
whitespace, variable renames 2025-10-12 10:01:47 +00:00
06340ed521
restore Nix support 2025-10-04 12:44:54 +00:00
b9957f9182
mmnode-ticker: support --http-timeout option 2025-10-04 09:57:02 +00:00
0b79ef719b
whitespace, minor changes (16 files) 2025-10-04 09:56:55 +00:00
0bad23b77b
update for MMGen Wallet v16.1.dev4 2025-10-03 10:31:49 +00:00
1f12baac4c
update for MMGen Wallet v16.1.dev3 2025-10-01 15:27:52 +00:00
fe45fbaa23
minor cleanups 2025-10-01 15:26:42 +00:00
MMGen@trixie
37c74e361c
use match statement where practicable 2025-10-01 15:26:42 +00:00
MMGen@trixie
bfb2dd839a
setup.cfg: bump requirements 2025-10-01 15:26:41 +00:00
MMGen@trixie
4d1f4577a7
lint, whitespace 2025-10-01 15:26:41 +00:00
MMGen@trixie
5c9d705301
test suite: lint, whitespace 2025-10-01 15:26:41 +00:00
MMGen@trixie
cab8be0167
pyproject.toml: add ruff data 2025-10-01 15:26:37 +00:00
5a24cbfc0a
Version 3.5.0
- released concurrently with MMGen Wallet v16.0.0
2025-09-24 10:45:50 +00:00
106f201300
update for MMGen Wallet v15.1.dev39 2025-05-24 10:10:24 +00:00
ab93f54d54
update test suite for MMGen Wallet v15.1.dev26 2025-03-30 10:18:52 +00:00
4688ee94c0
update test suite for MMGen Wallet v15.1.dev25 2025-03-29 12:12:35 +00:00
d63cb9817a
update for MMGen Wallet v15.1.dev23 2025-03-23 10:12:56 +00:00
966c171650
update for MMGen Wallet v15.1.dev20 2025-03-15 18:23:16 +00:00
b62ff991fe
update for MMGen Wallet v15.1.dev14 2025-02-06 10:07:57 +00:00
7cb2fc5b08
nix/shell.nix: add repo arg; test/init.sh: add -c option 2025-01-16 11:06:28 +00:00
bec7df1d6b
support Nix and NixOS
Quick Start for BTC:

    $ git clone https://github.com/mmgen/mmgen-wallet
    $ git clone https://github.com/mmgen/mmgen-node-tools
    $ cd mmgen-node-tools
    $ test/init.sh
    $ nix-shell --pure nix

Enable altcoins and additional packages:

    # From the mmgen-node-tools repository root:
    $ mkdir -p ~/.mmgen
    $ cp ../mmgen-wallet/nix/user-packages.nix ~/.mmgen
    # ... edit ~/.mmgen/user-packages.nix as required ...
    $ nix-shell --pure nix

For NixOS installation and other information, see:

    nix/README.node-tools
    ../mmgen-wallet/nix/README
2025-01-03 13:52:53 +00:00
30772b3699
Update for MMGen Wallet 15.1.dev6 2024-10-18 10:33:48 +00:00
b9b289c56d
directory rename:
- test/cmdtest_py_d -> test/cmdtest_d
2024-10-18 10:33:48 +00:00
dee57d8886
update for MMGen Wallet CoinAmt changes 2024-10-18 10:33:47 +00:00
cb42eaa8cd
update for MMGen Wallet 15.1.dev3 2024-10-08 13:19:15 +00:00
92fdfc047e
Support BCH cashaddr format 2024-09-29 14:29:10 +00:00
c9d6d8f047
Version 3.4.0
- released concurrently with MMGen Wallet v15.0.0
- adds macOS support
2024-09-22 16:38:00 +00:00
1a9184debc
version bump 2024-07-20 16:45:56 +00:00
0c54cb2c97
Version 3.3.0 2024-07-19 09:54:03 +00:00
18a92cd461
mmnode-ticker: add --quiet option, back up cached JSON data 2024-07-19 09:53:52 +00:00
867026e8ed
mmnode-ticker: add config file vars
The following command-line opts are now configurable in ‘ticker-cfg.yaml’:

    add_precision
    asset_limit
    btc
    cached_data
    elapsed
    name_labels
    pager
    percent_cols
    thousands_comma
    update_time
    verbose
2024-03-10 14:44:57 +00:00
c4ace71049
minor fixes and cleanups 2024-03-10 14:44:57 +00:00
db4ba26dce
update testing for MMGen Wallet v14.1.dev19 2024-03-08 15:32:54 +00:00
7c7c2c7da8
mmnode-ticker: backport fix for Python 3.9 2024-02-10 15:15:18 +00:00
6f2925f797
test/init.sh, MANIFEST.in: fixes, cleanups 2024-02-10 15:15:18 +00:00
cc0f4729c6
update for mmgen-wallet v14.1.dev2 2023-12-12 09:58:40 +00:00
de29b213ff
bump version and wallet dependency 2023-11-30 11:25:34 +00:00
3dace6b188
README.md: fix MSWin deps, fix repo URLs, add Twitter link 2023-11-30 09:19:10 +00:00
52 changed files with 2253 additions and 1483 deletions

View file

@ -1,4 +1,12 @@
include README.md LICENSE
include test/test-release.sh
include test/unit_tests_d/*.py
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/*/*

View file

@ -15,17 +15,14 @@ If installing as user (without venv), make sure that `~/.local/bin` is in `PATH`
>
> Install some additional dependencies:
> ```bash
> $ pacman -S \
> mingw-w64-ucrt-x86_64-python-pandas \
> mingw-w64-ucrt-x86_64-python-tqdm \
> mingw-w64-ucrt-x86_64-python-lxml
> $ pacman -S mingw-w64-ucrt-x86_64-python-pandas
> $ python3 -m pip install requests-futures
> $ python3 -m pip install --no-deps yahooquery
> ```
#### Linux:
#### Linux, macOS:
> Install the [required MMGen Wallet packages][7] for your Linux distribution.
> Install some [required packages][7] with your package manager and pip.
### Stable version:
@ -35,7 +32,8 @@ $ python3 -m pip install --upgrade mmgen-node-tools
### Development version:
Install the latest development version of [MMGen Wallet][6] for your platform.
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
@ -65,32 +63,34 @@ located in the same directory.*
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
```
$ test/init.sh
```
BTC-only testing:
$ test/test-release.sh -A
```
$ test/test-release.sh -A
```
Full testing:
$ test/test-release.sh
```
$ test/test-release.sh
```
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Homepage:
[Clearnet](https://mmgen-wallet.cc) |
[Clearnet](https://mmgen.org) |
[I2P](http://mmgen-wallet.i2p) |
[Onion](http://mmgen55rtcahqfp2hn3v7syqv2wqanks5oeezqg3ykwfkebmouzjxlad.onion)
Code repository:
[Clearnet](https://mmgen.org/project/mmgen/mmgen-wallet) |
[I2P](http://mmgen-wallet.i2p/project/mmgen/mmgen-wallet) |
[Onion](http://mmgen55rtcahqfp2hn3v7syqv2wqanks5oeezqg3ykwfkebmouzjxlad.onion/project/mmgen/mmgen-wallet)
[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-wallet) |
[Gitlab](https://gitlab.com/mmgen/mmgen-wallet) |
[Gitflic](https://gitflic.ru/project/mmgen/mmgen-wallet)
[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
@ -99,8 +99,7 @@ Donate:
 ⊙ BCH: *15TLdmi5NYLdqmtCqczUs5pBPkJDXRs83w*
 ⊙ XMR: *8B14zb8wgLuKDdse5p8f3aKpFqRdB4i4xj83b7BHYABHMvHifWxiDXeKRELnaxL5FySfeRRS5girgUvgy8fQKsYMEzPUJ8h*
[4]: https://bitcointalk.org/index.php?topic=567069.0
[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-on-Linux
[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

View file

@ -14,4 +14,4 @@ mmnode-addrbal: Get balances for arbitrary addresses in the blockchain
from mmgen.main import launch
launch('addrbal',package='mmgen_node_tools')
launch(mod='addrbal',package='mmgen_node_tools')

View file

@ -14,4 +14,4 @@ mmnode-blocks-info: Display information about a block or range of blocks
from mmgen.main import launch
launch('blocks_info',package='mmgen_node_tools')
launch(mod='blocks_info',package='mmgen_node_tools')

View file

@ -14,4 +14,4 @@ mmnode-feeview: Visualize the fee structure of a node’s mempool
from mmgen.main import launch
launch('feeview',package='mmgen_node_tools')
launch(mod='feeview',package='mmgen_node_tools')

View file

@ -14,4 +14,4 @@ mmnode-halving-calculator: Estimate date(s) of future block subsidy halving(s)
from mmgen.main import launch
launch('halving_calculator',package='mmgen_node_tools')
launch(mod='halving_calculator',package='mmgen_node_tools')

View file

@ -14,4 +14,4 @@ mmnode-netrate: Bitcoin daemon network rate monitor
from mmgen.main import launch
launch('netrate',package='mmgen_node_tools')
launch(mod='netrate',package='mmgen_node_tools')

View file

@ -14,4 +14,4 @@ mmnode-peerblocks: List blocks in flight, disconnect stalling nodes
from mmgen.main import launch
launch('peerblocks',package='mmgen_node_tools')
launch(mod='peerblocks',package='mmgen_node_tools')

View file

@ -14,4 +14,4 @@ mmnode-ticker: Display price information for cryptocurrency and other assets
from mmgen.main import launch
launch('ticker',package='mmgen_node_tools')
launch(mod='ticker',package='mmgen_node_tools')

View file

@ -14,4 +14,4 @@ mmnode-txfind: Find a transaction in the blockchain or mempool
from mmgen.main import launch
launch('txfind',package='mmgen_node_tools')
launch(mod='txfind',package='mmgen_node_tools')

View file

@ -20,57 +20,57 @@
mmgen_node_tools.BlocksInfo: Display information about a block or range of blocks
"""
import re,json
import re, json
from collections import namedtuple
from time import strftime,gmtime
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 import json_encoder
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):
def __init__(self, caller, arg):
self.caller = caller
self.arg = self.orig_arg = arg
def parse(self,target):
ret = getattr(self,'parse_'+target)()
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')
die(1, f'{self.orig_arg!r}: invalid range specifier')
def parse_from_tip(self):
m = re.match(r'-([0-9]+)(.*)',self.arg)
m = re.match(r'-([0-9]+)(.*)', self.arg)
if m:
res,self.arg = (m[1],m[2])
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)
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)
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)
m = re.match(r'\+([0-9*]+)(.*)', self.arg)
if m:
res,self.arg = (m[1],m[2])
res, self.arg = (m[1], m[2])
if res.strip('*') != res:
die(1,f"'+{res}': malformed nBlocks specifier")
die(1, f"'+{res}': malformed nBlocks specifier")
if len(res) > 30:
die(1,f"'+{res}': overly long nBlocks specifier")
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:
@ -80,33 +80,33 @@ class BlocksInfo:
total_solve_time = 0
header_printed = False
bf = namedtuple('block_info_fields',['fmt_func','src','fs','hdr1','hdr2','key1','key2'])
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 ),
}
'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',
@ -120,8 +120,8 @@ class BlocksInfo:
'fee10',
'fee_avg',
'fee_min',
'version',
)
'version')
fixed_fields = (
'block', # until ≈ 09/01/2028 (block 1000000)
'hash',
@ -130,36 +130,34 @@ class BlocksInfo:
'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)
)
'difficulty') # until 1.00e+100 (i.e. never)
# column width adjustment data:
fs_lsqueeze = ('totalfee','inputs','outputs','nTx')
fs_lsqueeze = ('totalfee', 'inputs', 'outputs', 'nTx')
fs_rsqueeze = ()
fs_groups = (
('fee10','fee25','fee50','fee75','fee90','fee_avg','fee_min','fee_max'),
)
('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']
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'}
avg_stats_skip = {'block', 'hash', 'date', 'version', 'miner'}
range_data = namedtuple('parsed_range_data',['first','last','from_tip','nblocks','step'])
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'
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 parse_cslist(cls, uarg, full_set, dfl_set, desc):
def make_list(m,func):
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}')
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)]
@ -167,47 +165,45 @@ class BlocksInfo:
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)
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)
return make_list(m, func)
else:
die(1,f'{uarg}: invalid parameter')
die(1, f'{uarg}: invalid parameter')
def __init__(self,cfg,cmd_args,rpc):
def __init__(self, cfg, cmd_args, rpc):
def parse_cs_uarg(uarg,full_set,dfl_set,desc):
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)
)
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')
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')
return parse_cs_uarg(self.cfg.stats.lower(), self.all_stats, self.dfl_stats, 'stat')
def parse_cmd_args(): # => (block_list, first, last, step)
if not cmd_args:
return (None,self.tip,self.tip,None)
elif len(cmd_args) == 1:
r = self.parse_rangespec(cmd_args[0])
return (
list(range(r.first,r.last+1,r.step)) if r.step else None,
r.first,
r.last,
r.step
)
else:
return ([self.conv_blkspec(a) for a in cmd_args],None,None,None)
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
@ -216,7 +212,7 @@ class BlocksInfo:
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()
self.block_list, self.first, self.last, self.step = parse_cmd_args()
have_segwit = self.rpc.info('segwit_is_active')
@ -227,35 +223,33 @@ class BlocksInfo:
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'},
'mini_avg': {'interval', 'size'} | ({'weight'} if have_segwit else set()),
'total': {'interval', 'subsidy', 'totalfee', 'nTx', 'inputs', 'outputs', 'utxo_inc'},
'range': {},
'diff': {},
}
'diff': {}}
self.fmt_funcs = {
'da': lambda arg: strftime('%Y-%m-%d %X',gmtime(arg)),
'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) ),
'-{: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(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(arg * to_satoshi)),
'tf': lambda arg: '{:.8f}'.format(arg),
})
'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
)
[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',)
@ -265,15 +259,15 @@ class BlocksInfo:
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:
if {'avg', 'col_avg'} <= set(self.stats) and self.cfg.stats_only:
self.stats.remove('col_avg')
if {'avg','mini_avg'} <= set(self.stats):
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 )
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')
@ -286,8 +280,7 @@ class BlocksInfo:
self.bs_keys = set(
[v.key1 for v in self.fvals if v.src == 'bs'] +
['total_size'] +
(['total_weight'] if have_segwit else [])
)
(['total_weight'] if have_segwit else []))
if 'miner' in self.fnames:
# capturing parens must contain only ASCII chars!
@ -301,17 +294,16 @@ class BlocksInfo:
rb'([\x20-\x7e]{9,})',
rb'[/^]([a-zA-Z0-9&. #/-]{5,})',
rb'[/^]([_a-zA-Z0-9&. #/-]+)/',
rb'^\x03...\W{0,5}([\\_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 }
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):
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]
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:
@ -320,47 +312,52 @@ class BlocksInfo:
ls = ''
break
repl = (name if add_name else '') + ':' + (fill_char if name in fill else '')
yield (ls + self.fields[name].fs.replace(':',repl) + rs)
yield (ls + self.fields[name].fs.replace(':', repl) + rs)
def conv_blkspec(self,arg):
if str(arg).lower() == 'cur':
return self.tip
elif is_int(arg):
if int(arg) < 0:
die(1,f'{arg}: block number must be non-negative')
elif int(arg) > self.tip:
die(1,f'{arg}: requested block height greater than current chain tip!')
else:
return int(arg)
else:
die(1,f'{arg}: invalid block specifier')
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):
if arg <= 0:
die(1,'nBlocks must be a positive integer')
elif arg > self.tip:
die(1, f"'{arg}': nBlocks must be less than current chain height")
return arg
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):
def parse_rangespec(self, arg):
p = RangeParser(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')
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')
die(1, f'{arg!r}: invalid range specifier')
nblocks,step = (add1,add2) if last is None else (None,add1)
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 p.debug:
msg(repr(self.range_data(first, last, from_tip, nblocks, step)))
if nblocks:
if first == None:
if first is None:
first = self.tip - nblocks + 1
last = first + nblocks - 1
@ -368,12 +365,12 @@ class BlocksInfo:
last = self.conv_blkspec(last or first)
if p.debug:
msg(repr(self.range_data(first,last,from_tip,nblocks,step)))
msg(repr(self.range_data(first, last, from_tip, nblocks, step)))
if first > last:
die(1,f'{first}-{last}: invalid block range')
die(1, f'{first}-{last}: invalid block range')
return self.range_data(first,last,from_tip,nblocks,step)
return self.range_data(first, last, from_tip, nblocks, step)
async def process_blocks(self):
@ -383,7 +380,7 @@ class BlocksInfo:
c = self.rpc
heights = self.block_list or range(self.first,self.last+1)
heights = self.block_list or range(self.first, self.last+1)
self.hdrs = await get_hdrs(heights)
if self.block_list:
@ -392,8 +389,7 @@ class BlocksInfo:
else:
self.first_prev_hdr = (
self.hdrs[0] if heights[0] == 0 else
await c.call('getblockheader',await c.call('getblockhash',heights[0]-1))
)
await c.call('getblockheader', await c.call('getblockhash', heights[0]-1)))
self.t_cur = self.first_prev_hdr['time']
self.res = []
@ -404,16 +400,16 @@ class BlocksInfo:
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)
self.output_block(ret, n)
def output_block(self,data,n):
def output_block(self, data, n):
def gen():
for k,v in data._asdict().items():
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):
async def process_block(self, hdr):
self.t_diff = hdr['time'] - self.t_cur
self.t_cur = hdr['time']
@ -421,14 +417,12 @@ class BlocksInfo:
blk_data = {
'bh': hdr,
'lo': { 'interval': self.t_diff }
}
'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))
)
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']
@ -441,14 +435,13 @@ class BlocksInfo:
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]
)
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)
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:
@ -459,13 +452,12 @@ class BlocksInfo:
trmap_in = {
'\\': ' ',
'/': ' ',
',': ' ',
}
trmap = { ord(a):b for a,b in trmap_in.items() }
',': ' '}
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 re.sub(r'\s+', ' ', m[1].decode().strip('^').translate(trmap).strip())
return ''
def print_header(self):
@ -479,27 +471,28 @@ class BlocksInfo:
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 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):
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):
async def output_stats(self, res, sname):
def gen(data):
for d in data:
if len(d) == 2:
yield (indent+d[0]).format(**{k:self.fmt_stat_item(*v) for k,v in d[1].items()})
elif len(d) == 4:
yield (indent+d[0]).format(self.fmt_stat_item(d[2],d[3]))
elif type(d) == str:
yield d
else:
assert False, f'{d}: invalid stats 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
foo, data = await res
indent = '' if sname in self.noindent_stats else ' '
Msg('\n'.join(gen(data)))
@ -518,15 +511,14 @@ class BlocksInfo:
'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 )
'step': ('{}', self.step)})
return ( 'range', gen() )
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):
@ -535,26 +527,25 @@ class BlocksInfo:
tip_hdr = (
self.hdrs[-1] if self.hdrs[-1]['height'] == self.tip else
await c.call('getblockheader',await c.call('getblockhash',self.tip))
)
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))
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
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 = float(tip_hdr['difficulty'] / start_hdr['difficulty'])
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', (
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),
@ -570,112 +561,113 @@ class BlocksInfo:
'sample_blks': ('{}', sample_blks)
}
),
('Cur difficulty: {}', 'cur_diff', '{:.2e}', tip_hdr['difficulty']),
('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, ('{}','') )
yield (field, ('{}', ''))
else:
ret = sum(getattr(block,field) for block in self.res) // len(self.res)
ret = self.sum_field_avg(field)
func = self.fields[field].fmt_func
yield ( field, ( (self.fmt_funcs[func] if func else '{}'), ret ))
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())) ))
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):
def avg_stats_data(self, data, spec_conv, spec_val):
coin = self.rpc.proto.coin
return data(
hdr = 'Averages for processed blocks:',
func = lambda field: sum(getattr(block,field) for block in self.res) // len(self.res),
spec_sufs = { 'subsidy': f' {coin}', 'totalfee': f' {coin}' },
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)),
'interval': spec_conv(0, lambda arg: secs_to_ms(arg)),
'utxo_inc': spec_conv(-1, '{:<+}'),
'mb_per_hour': spec_conv(0, '{}'),
},
'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' ),
),
)
)
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):
def total_stats_data(self, data, spec_conv, spec_val):
coin = self.rpc.proto.coin
return data(
hdr = 'Totals for processed blocks:',
func = lambda field: sum(getattr(block,field) for block in self.res),
spec_sufs = { 'subsidy': f' {coin}', 'totalfee': f' {coin}', 'reward': f' {coin}' },
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)),
'interval': spec_conv(0, lambda arg: secs_to_dhms(arg)),
'utxo_inc': spec_conv(-1, '{:<+}'),
'reward': spec_conv(0, self.fmt_funcs['tf']),
},
'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']
),
)
)
lambda values: {'subsidy', 'totalfee'} <= set(values),
lambda values: values['subsidy'] + values['totalfee']),
))
async def create_stats(self,sname):
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()
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'])
)
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
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)
fnames.insert(idx, v.name)
lbls[v.name] = v.lbl
values[v.name] = v.code(values)
def gen():
for n,fname in enumerate(fnames):
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 ''
),
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]
)
(lambda x: self.fmt_funcs[x] if x else '{}')(self.fields[fname].fmt_func)),
values[fname])
return ( sname, (d.hdr,) + tuple(gen()) )
return (sname, (d.hdr,) + tuple(gen()))
def process_stats_pre(self,i):
def process_stats_pre(self, i):
if (self.fnames and not self.cfg.stats_only) or i != 0:
Msg('')
@ -708,13 +700,12 @@ class BlocksInfo:
'totalfee': 0,
'txs': 1,
'utxo_increase': 1,
'utxo_size_inc': 117
}
'utxo_size_inc': 117}
class JSONBlocksInfo(BlocksInfo):
def __init__(self,cfg,cmd_args,rpc):
super().__init__(cfg,cmd_args,rpc)
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
@ -725,37 +716,40 @@ class JSONBlocksInfo(BlocksInfo):
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_raw(self, data, n):
Msg_r((', ', '')[n==0] + json.dumps(data._asdict(), cls=json_encoder))
def output_block(self,data,n):
def output_block(self, data, n):
def gen():
for k,v in data._asdict().items():
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) )
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):
def fmt_stat_item_raw(self, fs, s):
return s
async def output_stats(self,res,sname):
async def output_stats(self, res, sname):
def gen(data):
for d in data:
if len(d) == 2:
for k,v in d[1].items():
yield (k,self.fmt_stat_item(*v))
elif len(d) == 4:
yield (d[1],self.fmt_stat_item(d[2],d[3]))
elif type(d) != str:
assert False, f'{d}: invalid stats 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) ))
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 process_stats_pre(self, i): pass
def finalize_output(self):
Msg('}')

View file

@ -14,66 +14,66 @@ 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.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')
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):
def __init__(self, cfg):
super().__init__(cfg)
global term,term_width
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):
async def get_info(self, rpc):
return await rpc.call('getpeerinfo')
def display(self,count):
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]
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' )
+ '\b')
async def disconnect_node(self,rpc,addr):
return await rpc.call('disconnectnode',addr)
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)
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 )
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):
async def process_input(self, rpc):
ids = tuple(str(i['id']) for i in self.info)
ret = False
@ -83,7 +83,7 @@ class Display(PollDisplay):
from mmgen.exception import RPCFailure
addr = self.info[ids.index(self.input)]['addr']
try:
await self.disconnect_node(rpc,addr)
await self.disconnect_node(rpc, addr)
except RPCFailure:
msg_r(f'Unable to disconnect peer {self.input} ({addr})')
else:
@ -105,8 +105,8 @@ class BlocksDisplay(Display):
def gen_display(self):
pd = namedtuple('peer_data',['id','blks_data','blks_width'])
bd = namedtuple('block_datum',['num','disp'])
pd = namedtuple('peer_data', ['id', 'blks_data', 'blks_width'])
bd = namedtuple('block_datum', ['num', 'disp'])
def gen_block_data():
global min_height
@ -114,15 +114,15 @@ class BlocksDisplay(Display):
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]
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) )
[bd(blocks[i], blocks_disp[i]) for i in range(len(blocks_disp))],
len(line))
else:
yield pd(d['id'],[],0)
yield pd(d['id'], [], 0)
def gen_line(peer_data):
for blk in peer_data.blks_data:
@ -136,7 +136,7 @@ class BlocksDisplay(Display):
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) )
' '.join(gen_line(peer_data)) + ' ' * (blks_field_width - peer_data.blks_width))
class PeersDisplay(Display):
@ -152,5 +152,4 @@ class PeersDisplay(Display):
A = id_width,
b = d['addr'],
B = addr_width,
c = d['subver']
).ljust(term_width)[:term_width]
c = d['subver']).ljust(term_width)[:term_width]

View file

@ -12,7 +12,7 @@
mmgen_node_tools.PollDisplay: update and display RPC data; get input from user
"""
import sys,threading
import sys, threading
from mmgen.util import msg
from mmgen.term import get_char
@ -22,18 +22,18 @@ class PollDisplay:
input = None
poll_secs = 1
def __init__(self,cfg):
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)
return get_char(immed_chars='q', prehold_protect=False, num_bytes=1)
async def process_input(self,rpc):
async def process_input(self, rpc):
return True
async def run(self,rpc):
async def run(self, rpc):
async def do_display():
with self.info_lock:
@ -52,7 +52,7 @@ class PollDisplay:
count += 1
async def process_input():
if self.input == None:
if self.input is None:
sys.exit(1)
elif self.input == 'q':
msg('')
@ -68,7 +68,7 @@ class PollDisplay:
self.display_kill_flag.set()
while True:
threading.Thread(target=get_input,daemon=True).start()
threading.Thread(target=get_input, daemon=True).start()
await do_display()
if await process_input():
break

View file

@ -19,28 +19,28 @@
mmgen_node_tools.Sound: audio-related functions for MMGen node tools
"""
import sys,os,time
import sys, os, time
from mmgen.util import die
from mmgen_node_tools.Util import do_system
_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 == None:
m = re.match(pat, ts)
if m is 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():
@ -51,33 +51,33 @@ def init_sound():
atexit.register(_restore_sound)
do_system('sudo 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)
for k in 'Master', 'Speaker', 'Headphone':
do_system(('sudo 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('sudo 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)
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)

File diff suppressed because it is too large Load diff

View file

@ -20,55 +20,53 @@ mmgen_node_tools.Util: utility functions for MMGen node tools
"""
import time
from mmgen.util import suf
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()
@ -104,20 +102,19 @@ 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=' +'))

View file

@ -1 +1 @@
Bitcoin, BTC, Ethereum, ETH, Monero, XMR, ERC20, cryptocurrency, wallet, BIP32, cold storage, offline, online, spending, open-source, command-line, Python, Linux, Bitcoin Core, 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, PowerShell, MSYS, MSYS2, MinGW, MinGW64, MSWin, Armbian, Raspbian, Raspberry Pi, Orange Pi, BCash, BCH, Litecoin, LTC, altcoin, ZEC, Zcash, DASH, Dashpay, SHA256Compress, monerod, EMC, Emercoin, token, deploy, contract, gas, fee, smart contract, solidity, Parity, OpenEthereum, testnet, devmode, Kovan
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

View file

@ -3,4 +3,12 @@
# Invoke `mmnode-ticker --list-ids` for a full list of supported asset IDs.
btc-bitcoin: '1.23456789'
eth-ethereum: '2.3456789012'
xmr-monero: '4.5678901234'
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'

View file

@ -1 +1 @@
3.2.0
3.6.dev12

View file

@ -14,9 +14,9 @@ mmnode-addrbal: Get balances for arbitrary addresses in the blockchain
import sys
from mmgen.obj import CoinTxID,Int
from mmgen.obj import CoinTxID
from mmgen.cfg import Config
from mmgen.util import msg,Msg,die,suf,make_timestr,async_run
from mmgen.util import msg, Msg, die, suf, make_timestr, async_run
from mmgen.color import red
opts_data = {
@ -32,40 +32,39 @@ opts_data = {
}
}
def do_output(proto,addr_data,blk_hdrs):
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()}')
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 }
heights = {u['height'] for u in unspents}
Msg('{}Balance: {}'.format(
indent,
proto.coin_amt(sum(u['amount'] for u in unspents)).hl2(unit=True,fs='{:,}') )),
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) ))
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'))
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'] ),
make_timestr(blk_hdrs[u['height']]['time']),
CoinTxID(u['txid']).hl(),
red(str(u['vout']).rjust(4)),
proto.coin_amt(u['amount']).fmt(color=True,iwidth=6,prec=8)
))
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):
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)
@ -75,9 +74,9 @@ def do_output_tabular(proto,addr_data,blk_hdrs):
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))
' {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)) )
' {n:>%s} {a} {u} {B:>%s} {T:19} {A}' % (col1w, max(4, lb_w)))
Msg('\n' + fs.format(
n = '',
@ -87,86 +86,85 @@ def do_output_tabular(proto,addr_data,blk_hdrs):
t = 'Block',
B = 'Last',
T = 'Block',
A = ' Amount' ))
A = ' Amount'))
for n,(addr,unspents) in enumerate(addr_data.items(),1):
for n, (addr, unspents) in enumerate(addr_data.items(), 1):
if unspents:
Msg(fs.format(
n = str(n) + ')',
a = addr.fmt(width=max_addrw,color=True),
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'] ),
t = make_timestr(blk_hdrs[unspents[0]['height']]['time']),
B = unspents[-1]['height'],
T = make_timestr( blk_hdrs[unspents[-1]['height']]['time'] ),
A = proto.coin_amt(sum(u['amount'] for u in unspents)).fmt(color=True,iwidth=7,prec=8)
))
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(width=max_addrw,color=True),
a = addr.fmt(addr.view_pref, max_addrw, color=True),
u = ' -',
b = '-',
t = '',
B = '-',
T = '',
A = ' -' ))
A = ' -'))
async def main(req_addrs):
proto = cfg._proto
from mmgen.addr import CoinAddr
addrs = [CoinAddr(proto,addr) for addr in req_addrs]
addrs = [CoinAddr(proto, addr) for addr in req_addrs]
from mmgen.rpc import rpc_init
rpc = await rpc_init(cfg,ignore_wallet=True)
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] )
res = await scantxoutset(cfg, rpc, [f'addr({addr})' for addr in addrs])
if not res['success']:
die(1,'UTXO scanning failed or was interrupted')
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' )
'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]
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 scriptPubKey2addr
for unspent in sorted(res['unspents'],key=lambda x: x['height']):
addr = scriptPubKey2addr( proto, unspent['scriptPubKey'] )[0]
addr_data[addr].append(unspent)
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']).hl2(unit=True,fs='{:,}'),
proto.coin_amt(res['total_amount']).hl3(unit=True, fs='{:,}'),
red(str(good_addrs)),
suf(good_addrs,'es')
))
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)) )
(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'} )
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')
die(1, 'This command requires at least one coin address argument')
try:
async_run(main(cfg._args))
async_run(cfg, main, args=[cfg._args])
except KeyboardInterrupt:
sys.stderr.write('\n')

View file

@ -20,9 +20,9 @@
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
from mmgen.cfg import gc, Config
from mmgen.util import async_run, fmt_list
from .BlocksInfo import BlocksInfo, JSONBlocksInfo
opts_data = {
'sets': [
@ -145,14 +145,13 @@ EXAMPLES:
$ {p} --rpc-backend=aio -H +1000
This program requires a txindex-enabled daemon for correct operation.
""" },
"""},
'code': {
'notes': lambda cfg,proto,s: s.format(
'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,
)
F = fmt_list(BlocksInfo.fields, fmt='bare'),
S = fmt_list(BlocksInfo.all_stats, fmt='bare'),
p = gc.prog_name)
}
}
@ -164,7 +163,7 @@ async def main():
cls = JSONBlocksInfo if cfg.json else BlocksInfo
m = cls( cfg, cfg._args, await rpc_init(cfg,ignore_wallet=True) )
m = cls(cfg, cfg._args, await rpc_init(cfg, ignore_wallet=True))
if m.fnames and not cfg.no_header:
m.print_header()
@ -172,10 +171,10 @@ async def main():
await m.process_blocks()
if m.last:
for i,sname in enumerate(m.stats):
for i, sname in enumerate(m.stats):
m.process_stats_pre(i)
await m.process_stats(sname)
m.finalize_output()
async_run(main())
async_run(cfg, main)

View file

@ -21,10 +21,10 @@ 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
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)
min_prec, max_prec, dfl_prec = (0, 6, 4)
fee_brackets = [
1, 2, 3, 4, 5, 6,
8, 10, 12, 14, 16, 18,
@ -42,9 +42,9 @@ fee_brackets = [
opts_data = {
'sets': [
('detail',True,'ranges',True),
('detail',True,'show_mb_col',True),
('detail',True,'precision',6),
('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',
@ -83,37 +83,37 @@ cfg = Config(opts_data=opts_data)
if cfg.ignore_below:
if cfg.show_empty:
die(1,'Conflicting options: --ignore-below, --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,'--precision arg')
if cfg.precision else dfl_prec )
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):
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):
def log(data, fn):
import json
from mmgen.rpc import json_encoder
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),
data = json.dumps(data, cls=json_encoder, sort_keys=True, indent=4),
desc = 'mempool',
ask_overwrite = False )
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))]
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'
@ -143,7 +143,7 @@ def create_data(coin_amt,mempool):
return out
def gen_header(host,mempool,blockcount):
def gen_header(host, mempool, blockcount):
yield fmt(f"""
Mempool Fee Structure
@ -159,27 +159,26 @@ def gen_header(host,mempool,blockcount):
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),
)
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)
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)
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)
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)
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='')
@ -188,16 +187,16 @@ def gen_body(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,
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 )) )
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 = '' )
d = '')
async def main():
@ -205,19 +204,19 @@ async def main():
proto = cfg._proto
from mmgen.rpc import rpc_init
c = await rpc_init(cfg,ignore_wallet=True)
c = await rpc_init(cfg, ignore_wallet=True)
mempool = await c.call('getrawmempool',True)
mempool = await c.call('getrawmempool', True)
if cfg.log:
log(mempool,'mempool.json')
log(mempool, 'mempool.json')
data = create_data(proto.coin_amt,mempool)
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' )
await c.call('getblockcount'))) + '\n\n' +
'\n'.join(gen_body(data)) + '\n')
async_run(main())
async_run(cfg, main)

View file

@ -21,7 +21,6 @@ mmnode-halving-calculator: Estimate date(s) of future block subsidy halving(s)
"""
import time
from decimal import Decimal
from mmgen.cfg import Config
from mmgen.util import async_run
@ -29,7 +28,7 @@ from mmgen.util import async_run
bdr_proj = 9.95
opts_data = {
'sets': [('mined',True,'list',True)],
'sets': [('mined', True, 'list', True)],
'text': {
'desc': 'Estimate date(s) of future block subsidy halving(s)',
'usage':'[opts]',
@ -42,7 +41,7 @@ opts_data = {
{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)
@ -54,28 +53,28 @@ 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,' ')
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]))
('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 )
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))
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,)))
cur, old = await c.gathered_call('getblockstats', ((tip,), (tip - sample_size,)))
clock_time = int(time.time())
time_diff_warning(clock_time - cur['time'])
@ -99,8 +98,7 @@ async def main():
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)}'
)
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:]
@ -109,13 +107,13 @@ async def main():
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']
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):
for n, blk in enumerate(halving_blocknums):
mined = (nSubsidy >> n) * proto.halving_interval
if n == 0:
mined -= nSubsidy # subtract unspendable genesis block subsidy
@ -124,13 +122,11 @@ async def main():
bdi = (
(hist_halvings[n]['time'] - date) / (proto.halving_interval * 60) if n < nhist
else bdr/60 if n == nhist
else bdr_proj
)
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 )
else t_next + int((n - nhist) * halving_secs))
yield (n, sub, blk, mined, total_mined, bdi, date)
if sub == 0:
break
@ -148,11 +144,10 @@ async def main():
b = 'BLOCK',
c = 'DATE',
d = '',
e = f'BDI (mins)',
f = f'SUBSIDY ({proto.coin})',
e = 'BDI (mins)',
f = 'SUBSIDY ({proto.coin})',
g = f'MINED ({proto.coin})',
h = f'TOTAL MINED ({proto.coin})'
)
h = f'TOTAL MINED ({proto.coin})')
+ '\n'
+ fs.format(
a = '-' * 7,
@ -160,26 +155,24 @@ async def main():
c = '-' * 19,
d = '-' * 2,
e = '-' * 10,
f = '-' * 13,
f = '-' * 17,
g = '-' * 17,
h = '-' * 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(iwidth=2,prec=8),
g = proto.coin_amt(mined,from_unit='satoshi').fmt(iwidth=8,prec=8),
h = proto.coin_amt(total_mined,from_unit='satoshi').fmt(iwidth=8,prec=8)
) for n,sub,blk,mined,total_mined,bdr,t in gen_data())
)
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(main())
async_run(cfg, main)

View file

@ -20,7 +20,7 @@
mmnode-netrate: Bitcoin daemon network rate monitor
"""
import sys,time
import sys, time
from mmgen.cfg import Config
from mmgen.util import async_run
@ -32,41 +32,40 @@ opts_data = {
'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'
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)
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'])]
return [float(e) for e in (d['totalbytesrecv'], d['totalbytessent'], d['timemillis'])]
rs,ss,ts = (None,None,None)
rs, ss, ts = (None, None, None)
while True:
r,s,t = await get_data()
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) ))
(s-ss)/(t-ts)))
time.sleep(2)
if rs is not None:
sys.stderr.write('{}{}{}'.format(ERASE_LINE,CUR_UP,ERASE_LINE))
sys.stderr.write('{}{}{}'.format(ERASE_LINE, CUR_UP, ERASE_LINE))
rs,ss,ts = (r,s,t)
rs, ss, ts = (r, s, t)
try:
async_run(main())
async_run(cfg, main)
except KeyboardInterrupt:
sys.stderr.write('\n')

View file

@ -27,19 +27,18 @@ opts_data = {
'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)
async def main():
from mmgen.cfg import Config
cfg = Config(opts_data=opts_data)
from mmgen.rpc import rpc_init
rpc = await rpc_init(cfg,ignore_wallet=True)
rpc = await rpc_init(cfg, ignore_wallet=True)
from .PeerBlocks import BlocksDisplay,PeersDisplay
from .PeerBlocks import BlocksDisplay, PeersDisplay
blocks = BlocksDisplay(cfg)
peers = PeersDisplay(cfg)
@ -48,4 +47,4 @@ async def main():
await peers.run(rpc)
from mmgen.util import async_run
async_run(main())
async_run(cfg, main)

View file

@ -25,7 +25,7 @@ opts_data = {
],
'text': {
'desc': 'Display prices for cryptocurrency and other assets',
'usage': '[opts] [TRADE_SPECIFIER]',
'usage': '[opts] [TRADE_SPECIFIER | ASSET_RANGE]',
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
@ -40,10 +40,10 @@ opts_data = {
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, --cachedir=D Read and write cached JSON data to directory D
instead of ~/{dfl_cachedir}
-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
@ -53,12 +53,18 @@ opts_data = {
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)
@ -70,14 +76,19 @@ opts_data = {
""",
'notes': """
The script has two display modes: overview, the default, and trading, the
latter being enabled when a TRADE_SPECIFIER argument (see below) is supplied
on the command line.
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
users portfolio, while 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.
users 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
@ -127,6 +138,10 @@ A TRADE_SPECIFIER is a single argument in the format:
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
@ -198,6 +213,12 @@ $ mmnode-ticker usd:2700:btc:0.123
# 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}
@ -207,37 +228,36 @@ Customize output by editing the file
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'),
dfl_cachedir = os.path.relpath(dfl_cachedir, start=homedir),
ds = fmt_dict(DataSource.get_sources(), fmt='equal_compact'),
al = DataSource.coinpaprika.dfl_asset_limit,
pc = fmt_list(Ticker.percent_cols,fmt='bare'),
),
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),
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'](),
fi = src_cls['fi'](),
)
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.util import fmt_list, fmt_dict
from mmgen.cfg import Config
from . import Ticker
gcfg = Config( opts_data=opts_data, do_post_init=True )
gcfg = Config(opts_data=opts_data, caller_post_init=True)
Ticker.make_cfg(gcfg)
src_cls, cfg_in = Ticker.make_cfg(gcfg)
from .Ticker import dfl_cachedir,homedir,DataSource,assets_list_gen,cfg_in,src_cls
from .Ticker import dfl_cachedir, homedir, DataSource, assets_list_gen, sort_params
gcfg._post_init()

View file

@ -23,7 +23,7 @@ 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
from mmgen.util import msg, Msg, die, is_hex_str, async_run
opts_data = {
'text': {
@ -48,29 +48,26 @@ msg_data = {
'normal': {
'none': 'Transaction not found in blockchain or mempool',
'block': 'Transaction is in block {b} ({c} confirmations)',
'mem': 'Transaction is in mempool',
},
'mem': 'Transaction is in mempool'},
'quiet': {
'none': 'None',
'block': '{b} {c}',
'mem': 'mempool',
}
}
'mem': 'mempool'}}
async def main(txid):
if len(txid) != 64 or not is_hex_str(txid):
die(2,f'{txid}: invalid transaction ID')
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)
c = await rpc_init(cfg, ignore_wallet=True)
exitval = 0
try:
tip1 = await c.call('getblockcount')
ret = await c.call('getrawtransaction',txid,True)
ret = await c.call('getrawtransaction', txid, True)
tip2 = await c.call('getblockcount')
except:
Msg('\r' + msgs['none'])
@ -90,6 +87,6 @@ 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')
die(1, 'One transaction ID must be specified')
sys.exit(async_run(main(cfg._args[0])))
sys.exit(async_run(cfg, main, args=[cfg._args[0]]))

11
nix/README.node-tools Normal file
View file

@ -0,0 +1,11 @@
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.

6
nix/default.nix Normal file
View file

@ -0,0 +1,6 @@
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; }

View file

@ -0,0 +1,12 @@
{ pkgs, python }:
{
system-packages = with pkgs; {
cacert = cacert; # ticker (curl)
};
python-packages = with python.pkgs; {
yahooquery = (callPackage ./yahooquery.nix {}); # ticker
pyyaml = pyyaml; # ticker
};
}

4
nix/shell.nix Normal file
View file

@ -0,0 +1,4 @@
import ../../mmgen-wallet/nix/shell.nix {
repo = "mmgen-node-tools";
add_pkgs_path = ./node-tools-packages.nix;
}

23
nix/use-system-libs.patch Normal file
View file

@ -0,0 +1,23 @@
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"),

37
nix/yahooquery.nix Normal file
View file

@ -0,0 +1,37 @@
{
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;
};
}

View file

@ -5,6 +5,41 @@ requires = [
]
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

View file

@ -8,27 +8,36 @@ 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
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 =
Programming Language :: Python :: 3
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
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.8
python_requires = >=3.11
include_package_data = True
install_requires =
mmgen-wallet==14.0.0
mmgen-wallet>=16.1.dev26
pyyaml
yahooquery

27
test/cmdtest_d/httpd/ticker.py Executable file
View file

@ -0,0 +1,27 @@
#!/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()

View file

@ -9,16 +9,20 @@
# https://gitlab.com/mmgen/mmgen-node-tools
"""
test.cmdtest_py_d.cfg: configuration data for cmdtest.py
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': ('CmdTestMain',{}),
'helpscreens': ('CmdTestHelp',{'modname':'misc','full_data':True}),
'scripts': ('CmdTestScripts',{'modname':'misc'}),
'regtest': ('CmdTestRegtest',{}),
'main': gd('CmdTestMain', {}),
'helpscreens': gd('CmdTestHelp', {'modname': 'misc', 'full_data': True}),
'scripts': gd('CmdTestScripts', {'modname': 'misc'}),
'regtest': gd('CmdTestRegtest', {}),
}
cmd_groups_extra = {}
@ -30,4 +34,9 @@ cfgs = {
}
def fixup_cfgs():
pass
import os
for k in cfgs:
cfgs[k]['tmpdir'] = os.path.join('test', 'tmp', str(k))
fixup_cfgs()

View file

@ -9,13 +9,13 @@
# https://gitlab.com/mmgen/mmgen-wallet
"""
cmdtest_py_d.ct_main: Basic operations tests for the cmdtest.py test suite
cmdtest_d.main: Basic operations tests for the cmdtest.py test suite
"""
import sys,time
import sys, time
from ..include.common import cfg
from .ct_base import CmdTestBase
from .base import CmdTestBase
class CmdTestMain(CmdTestBase):
'basic operations with fake RPC data'
@ -41,7 +41,7 @@ class CmdTestMain(CmdTestBase):
def peerblocks(self,args,expect_list=None,pexpect_spawn=False):
t = self.spawn(
f'mmnode-peerblocks',
'mmnode-peerblocks',
args,
pexpect_spawn = pexpect_spawn )
if cfg.exact_output: # disable echoing of input
@ -96,4 +96,4 @@ class CmdTestMain(CmdTestBase):
def peerblocks3(self):
return self.peerblocks2(
['--columns=80'],
pexpect_spawn = sys.platform != 'win32' )
pexpect_spawn = sys.platform == 'linux')

502
test/cmdtest_d/misc.py Executable file
View file

@ -0,0 +1,502 @@
#!/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')

View file

@ -9,36 +9,38 @@
# https://gitlab.com/mmgen/mmgen-node-tools
"""
test.cmdtest_py_d.ct_regtest: Regtest tests for the cmdtest.py test suite
test.cmdtest_d.regtest: Regtest tests for the cmdtest.py test suite
"""
import sys,os
import sys, os
from decimal import Decimal
from mmgen.util import msg_r,die,gmsg
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 .ct_base import CmdTestBase
from ..include.common import cfg, imsg, stop_test_daemons, joinpath
from .base import CmdTestBase
args1 = ['--bob']
args2 = ['--bob','--rpc-backend=http']
args2 = ['--bob', '--rpc-backend=http']
def gen_addrs(proto,network,keys):
def gen_addrs(proto, network, keys):
from mmgen.tool.api import tool_api
tool = tool_api(cfg)
tool.init_coin(proto.coin,'regtest')
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')
networks = ('btc', 'ltc', 'bch')
passthru_opts = ('coin',)
extra_spawn_args = ['--regtest=1']
tmpdir_nums = [1]
color = True
deterministic = False
bdb_wallet = True
cmd_group_in = (
('setup', 'regtest mode setup'),
('subgroup.netrate', []),
@ -103,182 +105,194 @@ class CmdTestRegtest(CmdTestBase):
),
}
def __init__(self,trunner,cfgs,spawn):
CmdTestBase.__init__(self,trunner,cfgs,spawn)
if trunner == None:
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')
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 = gen_addrs(self.proto,'regtest',[1,2,3,4,5])
self.regtest = MMGenRegtest(cfg,self.proto.coin)
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)
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',['-n','setup'])
for s in ('Starting','Creating','Creating','Creating','Mined','Setup complete'):
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):
t = self.spawn( 'mmnode-netrate', args1 + add_args )
t.expect(expect_str,regex=True)
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:.*' )
t = self.netrate([], r'sent:.*', exit_val=-15)
t.kill(15)
if sys.platform == 'win32':
return 'ok'
t.req_exit_val = -15
return t
def halving_calculator(self,add_args,expect_list):
t = self.spawn('mmnode-halving-calculator',args1+add_args)
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:'])
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}'])
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'])
return self.halving_calculator(['--list'], ['33 4950', '0'])
def halving_calculator4(self):
return self.halving_calculator(['--mined'],['0 0.0000015 14949.9999835'])
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'])
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'])
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 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 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',args)
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(
args2 + [self.addrs[0]],
[self.addrs[0]],
[
f'Balance: 0.357 {cfg.coin}',
'2 unspent outputs in 2 blocks',
'394','0.123',
'395','0.234'
'394', '0.123',
'395', '0.234'
])
def addrbal_multiple(self):
return self.addrbal(
args2 + [self.addrs[1],self.addrs[0]],
[self.addrs[1], self.addrs[0]],
[
'396','0.345',
'394','0.123',
'395','0.234'
'396', '0.345',
'394', '0.123',
'395', '0.234'
])
def addrbal_multiple_tabular1(self):
return self.addrbal(
args2 + ['--tabular',self.addrs[1],self.addrs[0]],
['--tabular', self.addrs[1], self.addrs[0]],
[
self.addrs[1] + ' 1 396','0.345',
self.addrs[0] + ' 2 395','0.357'
self.addrs[1] + ' 1 396', '0.345',
self.addrs[0] + ' 2 395', '0.357'
])
def addrbal_multiple_tabular2(self):
return self.addrbal(
args2 + ['--tabular','--first-block',self.addrs[1],self.addrs[0]],
['--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'
self.addrs[1] + ' 1 396', '396', '0.345',
self.addrs[0] + ' 2 394', '395', '0.357'
])
def addrbal_nobal1(self):
return self.addrbal(
args2 + [self.addrs[2]], ['Address has no balance'] )
[self.addrs[2]], ['Address has no balance'])
def addrbal_nobal2(self):
return self.addrbal(
args2 + [self.addrs[2],self.addrs[3]], ['Addresses have no balances'] )
[self.addrs[2], self.addrs[3]], ['Addresses have no balances'])
def addrbal_nobal3(self):
return self.addrbal(
args2 + [self.addrs[4],self.addrs[0],self.addrs[3]],
[self.addrs[4], self.addrs[0], self.addrs[3]],
[
'No balance',
'2 unspent outputs in 2 blocks',
'394','0.123','395','0.234',
'394', '0.123', '395', '0.234',
'No balance'
])
def addrbal_nobal3_tabular1(self):
return self.addrbal(
args2 + ['--tabular',self.addrs[4],self.addrs[0],self.addrs[3]],
['--tabular', self.addrs[4], self.addrs[0], self.addrs[3]],
[
self.addrs[4] + ' - - -',
self.addrs[0] + ' 2 395','0.357',
self.addrs[0] + ' 2 395', '0.357',
self.addrs[3] + ' - - -',
])
def addrbal_nobal3_tabular2(self):
return self.addrbal(
args2 + ['--tabular','--first-block',self.addrs[4],self.addrs[0],self.addrs[3]],
['--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[0] + ' 2 394', '395', '0.357',
self.addrs[3] + ' - - - -',
])
def blocks_info(self,args,expect_list):
t = self.spawn('mmnode-blocks-info',args)
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( args1 + ['--help'], ['USAGE:','OPTIONS:'])
return self.blocks_info(
['--help'],
['USAGE:', 'OPTIONS:'])
def blocks_info2(self):
return self.blocks_info( args1, [
'Current height: 396',
])
return self.blocks_info(
[],
['Current height: 396'])
def blocks_info3(self):
return self.blocks_info( args1 + ['+100'], [
'Range: 297-396',
'Current height: 396',
'Next diff adjust: 2016'
])
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( args1 + ['--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'
])
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):
@ -288,61 +302,74 @@ class CmdTestRegtest(CmdTestBase):
from collections import namedtuple
t = tool_api(cfg)
t.init_coin(self.proto.coin,self.proto.network)
t.init_coin(self.proto.coin, self.proto.network)
t.addrtype = 'compressed' if self.proto.coin == 'BCH' else 'bech32'
wp = namedtuple('wifaddrpair',['wif','addr'])
wp = namedtuple('wifaddrpair', ['wif', 'addr'])
def gen():
for n in range(0xfaceface,nPairs+0xfaceface):
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):
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(self.proto.coin_amt.satoshi)
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 (low + (i/n)**6 * vmax) * x
yield Decimal(low + (i/n)**6 * vmax) * x
async def do_tx(inputs,outputs,wif):
tx_hex = await r.rpc_call( 'createrawtransaction', inputs, outputs )
tx = await r.rpc_call( 'signrawtransactionwithkey', tx_hex, [wif], [], self.proto.sighash_type )
assert tx['complete'] == True
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')
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: tx_input['amount'] - (tx1_amt*nTxs) - fee})
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 }],
[{'txid': tx_input['txid'], 'vout': 0}],
outputs,
r.miner_wif )
await r.miner_wif)
async def do_tx2(tx,pairno):
fee = fees[pairno]
outputs = {p.addr:tx2_amt for p in pairs}
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 }],
[{'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)
tx_hex = await do_tx2(tx_in, pairno)
await r.rpc_call('sendrawtransaction', tx_hex)
self.spawn('',msg_only=True)
self.spawn('', msg_only=True)
r = self.regtest
nPairs = 100
@ -357,22 +384,22 @@ class CmdTestRegtest(CmdTestBase):
imsg(f'Creating funding transaction with {nTxs} outputs of value {tx1_amt} {self.proto.coin}')
tx1_hex = await do_tx1()
imsg(f'Relaying funding transaction')
await r.rpc_call('sendrawtransaction',tx1_hex)
imsg('Relaying funding transaction')
await r.rpc_call('sendrawtransaction', tx1_hex)
imsg(f'Mining a block')
await r.generate(1,silent=True)
imsg('Mining a block')
await r.generate(1, silent=True)
imsg(f'Generating fees for mempool transactions')
fees = list(gen_fees(nTxs,2,120))
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))
await do_txs(await r.rpc_call('decoderawtransaction', tx1_hex))
return 'ok'
def _feeview(self,args,expect_list=[]):
t = self.spawn('mmnode-feeview',args)
def _feeview(self, args, expect_list=[]):
t = self.spawn('mmnode-feeview', args1 + args)
if expect_list:
t.match_expect_list(expect_list)
return t
@ -381,7 +408,7 @@ class CmdTestRegtest(CmdTestBase):
return self._feeview([])
def feeview2(self):
return self._feeview(['--columns=40','--include-current'])
return self._feeview(['--columns=40', '--include-current'])
def feeview3(self):
return self._feeview(['--precision=6'])
@ -390,7 +417,7 @@ class CmdTestRegtest(CmdTestBase):
return self._feeview(['--detail'])
def feeview5(self):
return self._feeview(['--show-empty','--log',f'--outdir={self.tmpdir}'])
return self._feeview(['--show-empty', '--log', f'--outdir={self.tmpdir}'])
def feeview6(self):
return self._feeview(['--ignore-below=1MB'])
@ -400,13 +427,13 @@ class CmdTestRegtest(CmdTestBase):
async def feeview8(self):
imsg('Clearing mempool')
await self.regtest.generate(1,silent=True)
await self.regtest.generate(1, silent=True)
return self._feeview([])
def stop(self):
if cfg.no_daemon_stop:
self.spawn('',msg_only=True)
self.spawn('', msg_only=True)
msg_r('(leaving daemon running by user request)')
return 'ok'
else:
return self.spawn('mmgen-regtest',['stop'])
return self.spawn('mmgen-regtest', ['stop'])

View file

@ -1,300 +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_py_d.ct_misc: Miscellaneous test groups for the cmdtest.py test suite
"""
import os,shutil
from ..include.common import cfg
from .ct_base import CmdTestBase
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(f'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:.*LONG OPTIONS:')
class CmdTestScripts(CmdTestBase):
'scripts not requiring a coin daemon'
networks = ('btc',)
tmpdir_nums = [2]
passthru_opts = ()
color = True
cmd_group_in = (
('subgroup.ticker_setup', []),
('subgroup.ticker', ['ticker_setup']),
)
cmd_subgroups = {
'ticker_setup': (
"setup for 'ticker' subgroup",
('ticker_setup', 'ticker setup'),
),
'ticker': (
"'mmnode-ticker' script",
('ticker1', 'ticker [--help)'),
('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'),
)
}
@property
def ticker_args(self):
return [ f'--cachedir={self.tmpdir}', '--proxy=http://asdfzxcv:32459' ]
@property
def nt_datadir(self):
return os.path.join( cfg.data_dir_root, 'node_tools' )
def ticker_setup(self):
self.spawn('',msg_only=True)
shutil.copy2(os.path.join(refdir,'ticker.json'),self.tmpdir)
shutil.copy2(os.path.join(refdir,'ticker-finance.json'),self.tmpdir)
shutil.copy2(os.path.join(refdir,'ticker-finance-history.json'),self.tmpdir)
shutil.copy2(os.path.join(refdir,'ticker-btc.json'),self.tmpdir)
return 'ok'
def ticker(self,args=[],expect_list=None,cached=True):
t = self.spawn(
f'mmnode-ticker',
(['--cached-data'] if cached else []) + self.ticker_args + args )
if expect_list:
t.match_expect_list(expect_list)
return t
def ticker1(self):
t = self.ticker(['--help'])
t.expect('USAGE:')
return t
def ticker2(self):
t = self.ticker(cached=False)
if not cfg.skipping_deps:
t.expect('Creating')
t.expect('Creating')
ret = t.expect(['proxy host could not be resolved','ProxyError'])
t.req_exit_val = 3 if ret == 0 else 1
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):
shutil.copy2(os.path.join(refdir,'ticker-cfg.yaml'),self.nt_datadir)
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'
])
os.unlink(os.path.join(self.nt_datadir,'ticker-cfg.yaml'))
return t
def ticker6(self):
t = self.ticker( ['--wide','--portfolio'], None )
t.expect('No portfolio')
t.req_exit_val = 1
return t
def ticker7(self): # demo
shutil.copy2(os.path.join(refdir,'ticker-portfolio.yaml'),self.nt_datadir)
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'
])
os.unlink(os.path.join(self.nt_datadir,'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):
shutil.copy2(
os.path.join(refdir,'ticker-portfolio-bad.yaml'),
os.path.join(self.nt_datadir,'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\)',
])
os.unlink(os.path.join(self.nt_datadir,'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):
shutil.copy2(os.path.join(refdir,'ticker-portfolio.yaml'),self.nt_datadir)
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'
])
os.unlink(os.path.join(self.nt_datadir,'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',
])

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
@ -10,6 +10,7 @@
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
@ -17,14 +18,27 @@ 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 h OPT
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
@ -41,7 +55,10 @@ check_mmgen_repo() {
}
build_mmgen_extmod() {
( cd $wallet_repo; python3 ./setup.py build_ext --inplace )
(
cd $wallet_repo
eval "python3 ./setup.py build_ext --inplace $STDOUT_DEVNULL $STDERR_DEVNULL"
)
}
create_dir_links() {
@ -49,36 +66,49 @@ create_dir_links() {
target="$wallet_repo/$link_name"
if [ -L $link_name ]; then
[ "$(realpath --relative-to=. $link_name 2>/dev/null)" == $target ] || {
echo "Removing broken symlink '$link_name'"
[ "$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
echo "Creating symlink: $link_name"
[ "$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/cmdtest.py hard
test/unit_tests.py hard
test/test-release.sh symbolic
test/cmdtest_py_d/common.py symbolic
test/cmdtest_py_d/ct_base.py symbolic
cmds/mmgen-regtest symbolic
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 --symbolic; fi)
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"
@ -87,32 +117,49 @@ create_test_links() {
fs="%-8s %-16s %s -> %s\n"
if [ $type == 'hard' ]; then
if [ -L $path ]; then
printf "$fs" "Deleting" "symbolic link:" $path $target
[ "$VERBOSE" ] && printf "$fs" "Deleting" "symbolic link:" $path $target
rm -rf $path
elif [ -e $path ]; then
if [ "$(stat --printf=%i $path)" -ne "$(stat --printf=%i $target)" ]; then
printf "$fs" "Deleting" "stale hard link:" $path "?"
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
printf "$fs" "Creating" "$type link:" $path $target
[ "$VERBOSE" ] && printf "$fs" "Creating" "$type link:" $path $target
( cd "$(dirname $path)" && ln -f $symlink_arg $pfx$target )
fi
done <<<$paths
}
set -e
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
becho 'OK'
[ "$CMD_LINKS" ] && create_cmd_links
[ "$VERBOSE" ] && becho 'OK'
true

7
test/modtest_d/__init__.py Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env python3
"""
test.modtest_d: shared data for module tests for the MMGen Node Tools suite
"""
altcoin_tests = []

21
test/modtest_d/ut_dep.py Executable file
View file

@ -0,0 +1,21 @@
#!/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

View file

@ -294,15 +294,14 @@ class fake_data:
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)})
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'):
@ -320,8 +319,7 @@ class fake_data:
'id': int(d['id']),
'addr': d['addr'],
'subver': d['subver'],
'inflight': [int(n)+830000 for n in blocks[iter_no]],
}
'inflight': [int(n)+830000 for n in blocks[iter_no]]}
def gen_data():
for iter_no in iterations_data:

View file

@ -0,0 +1,23 @@
#!/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

View file

@ -0,0 +1,11 @@
assets:
coin1:
- btc-bitcoin
- ltc-litecoin
- eth-ethereum
- xmr-monero
- bad-badcoin
commodity:
- gc=f
- si=f
- bz=f

View file

@ -0,0 +1,14 @@
sort: d
pchg_unit: btc
assets:
coin1:
- btc-bitcoin
- ltc-litecoin
- eth-ethereum
- xmr-monero
- bad-badcoin
commodity:
- gc=f
- si=f
- bz=f

View file

@ -0,0 +1,11 @@
assets:
coin1:
- btc-bitcoin
- eth-ethereum
- xmr-monero
commodity:
- gc=f
- si=f
currency:
- usd-us-dollar
- eurusd=x

View file

@ -1,5 +1,10 @@
btc-bitcoin: '1.23456789'
eth-ethereum: '2.345678901234567890'
xmr-monero: '4.567890123456'
xmr-monero: '3.333390123456'
ada-cardano: '123.45678901'
algo-algorand: '234.5678901'
wallet2:
algo-algorand: '234.5678901'
exchange1:
xmr-monero: '1.2345'

View file

@ -18,7 +18,7 @@
# mmnode-ticker OK
# mmnode-txfind -
all_tests='unit lint misc scripts btc btc_rt bch_rt ltc_rt'
all_tests='mod lint misc scripts btc btc_rt bch_rt ltc_rt'
groups_desc="
default - All tests minus the extra tests
@ -29,10 +29,10 @@ groups_desc="
"
init_groups() {
dfl_tests='unit misc scripts btc btc_rt bch_rt ltc_rt'
dfl_tests='mod misc scripts btc btc_rt bch_rt ltc_rt'
extra_tests='lint'
noalt_tests='unit misc scripts btc btc_rt'
quick_tests='unit misc scripts btc btc_rt'
noalt_tests='mod misc scripts btc btc_rt'
quick_tests='mod misc scripts btc btc_rt'
qskip_tests='lint bch_rt ltc_rt'
}
@ -42,11 +42,11 @@ init_tests() {
t_lint="
- $pylint --errors-only mmgen_node_tools
- $pylint --errors-only test
- $pylint --errors-only --disable=relative-beyond-top-level test/cmdtest_py_d
- $pylint --errors-only --disable=relative-beyond-top-level test/cmdtest_d
"
d_unit="low-level subsystems"
t_unit="- $unit_tests_py"
d_mod="low-level subsystems"
t_mod="- $modtest_py"
d_misc="miscellaneous features"
t_misc="- $cmdtest_py helpscreens"