From 2993fdba9e89e542a720cc86a7f66039cab0cb2a Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 28 May 2025 11:40:40 +0000 Subject: [PATCH] RUNE tracking wallet support Testing/demo: $ test/cmdtest.py --demo --coin=rune rune --- mmgen/data/version | 2 +- mmgen/obj.py | 4 ++ mmgen/proto/rune/addrdata.py | 18 +++++++ mmgen/proto/rune/params.py | 16 +++++- mmgen/proto/rune/rpc/remote.py | 50 +++++++++++++++++++ mmgen/proto/rune/tw/addresses.py | 20 ++++++++ mmgen/proto/rune/tw/ctl.py | 18 +++++++ mmgen/proto/rune/tw/unspent.py | 19 +++++++ mmgen/proto/rune/tw/view.py | 21 ++++++++ setup.cfg | 2 + test/cmdtest_d/httpd/thornode.py | 16 ++++++ test/cmdtest_d/include/cfg.py | 2 + test/cmdtest_d/rune.py | 86 ++++++++++++++++++++++++++++++++ test/test-release.d/cfg.sh | 11 ++-- 14 files changed, 280 insertions(+), 5 deletions(-) create mode 100755 mmgen/proto/rune/addrdata.py create mode 100755 mmgen/proto/rune/rpc/remote.py create mode 100755 mmgen/proto/rune/tw/addresses.py create mode 100755 mmgen/proto/rune/tw/ctl.py create mode 100755 mmgen/proto/rune/tw/unspent.py create mode 100755 mmgen/proto/rune/tw/view.py create mode 100755 test/cmdtest_d/rune.py diff --git a/mmgen/data/version b/mmgen/data/version index 96b1978d..90f19ad1 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -15.1.dev41 +15.1.dev42 diff --git a/mmgen/obj.py b/mmgen/obj.py index e12d70fd..362087f1 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -415,3 +415,7 @@ class MMGenPWIDString(MMGenLabel): desc = 'password ID string' forbidden = list(' :/\\') trunc_ok = False + +class Hostname(MMGenLabel): + max_len = 256 + color = 'pink' diff --git a/mmgen/proto/rune/addrdata.py b/mmgen/proto/rune/addrdata.py new file mode 100755 index 00000000..c342ab58 --- /dev/null +++ b/mmgen/proto/rune/addrdata.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +proto.rune.addrdata: THORChain TwAddrData class +""" + +from ...addrdata import TwAddrDataWithStore + +class THORChainTwAddrData(TwAddrDataWithStore): + pass diff --git a/mmgen/proto/rune/params.py b/mmgen/proto/rune/params.py index e3379188..795705c2 100755 --- a/mmgen/proto/rune/params.py +++ b/mmgen/proto/rune/params.py @@ -13,6 +13,7 @@ proto.rune.params: THORChain protocol """ from ...protocol import CoinProtocol, decoded_addr, _nw +from ...obj import Hostname from ...addr import CoinAddr from ...contrib import bech32 @@ -27,7 +28,7 @@ class mainnet(CoinProtocol.Secp256k1): coin_amt = 'UniAmt' max_tx_fee = 1 # TODO caps = () - mmcaps = () + mmcaps = ('tw', 'rpc_init', 'rpc_remote') base_proto = 'THORChain' base_proto_coin = 'RUNE' base_coin = 'RUNE' @@ -41,6 +42,10 @@ class mainnet(CoinProtocol.Secp256k1): encode_wif = btc_mainnet.encode_wif decode_wif = btc_mainnet.decode_wif + rpc_remote_params = {'server_domain': Hostname('ninerealms.com')} + rpc_remote_http_params = {'host': Hostname('thornode.ninerealms.com')} + rpc_remote_rpc_params = {'host': Hostname('rpc.ninerealms.com')} + def decode_addr(self, addr): hrp, data = bech32.bech32_decode(addr) assert hrp == self.bech32_hrp, f'{hrp!r}: invalid bech32 hrp (should be {self.bech32_hrp!r})' @@ -58,6 +63,15 @@ class mainnet(CoinProtocol.Secp256k1): class testnet(mainnet): # testnet is stagenet bech32_hrp = 'sthor' + rpc_remote_http_params = {'host': Hostname('stagenet-thornode.ninerealms.com')} + rpc_remote_rpc_params = {'host': Hostname('stagenet-rpc.ninerealms.com')} class regtest(testnet): # regtest is deprecated testnet bech32_hrp = 'tthor' + rpc_remote_params = { + 'server_domain': Hostname('localhost')} + rpc_remote_http_params = { + 'proto': 'http', + 'host': Hostname('localhost:18800'), + 'verify': False} + rpc_remote_rpc_params = rpc_remote_http_params diff --git a/mmgen/proto/rune/rpc/remote.py b/mmgen/proto/rune/rpc/remote.py new file mode 100755 index 00000000..b2018f78 --- /dev/null +++ b/mmgen/proto/rune/rpc/remote.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +proto.rune.rpc.remote: THORChain base protocol remote RPC client for the MMGen Project +""" + +import json + +from ....http import HTTPClient +from ....rpc.remote import RemoteRPCClient + +class ThornodeRemoteHTTPClient(HTTPClient): + + http_hdrs = {'Content-Type': 'application/json'} + timeout = 5 + + def __init__(self, cfg, *, proto=None, host=None): + for k, v in cfg._proto.rpc_remote_http_params.items(): + setattr(self, k, v) + super().__init__(cfg, proto=proto, host=host) + +class THORChainRemoteRPCClient(RemoteRPCClient): + server_proto = 'THORChain' + + def __init__(self, cfg, proto): + for k, v in proto.rpc_remote_params.items(): + setattr(self, k, v) + super().__init__(cfg, proto) + self.caps = ('lbl_id',) + self.http = ThornodeRemoteHTTPClient(cfg) + + # throws exception on error + def get_balance(self, addr, *, block): + http_res = self.http.get(path=f'/bank/balances/{addr}') + data = json.loads(http_res) + if data['result'] is None: + from ....util import die + die('RPCFailure', f'address ‘{addr}’ not found in blockchain') + else: + rune_res = [d for d in data['result'] if d['denom'] == 'rune'] + assert len(rune_res) == 1, f'{rune_res}: result length is not one!' + return self.proto.coin_amt(int(rune_res[0]['amount']), from_unit='satoshi') diff --git a/mmgen/proto/rune/tw/addresses.py b/mmgen/proto/rune/tw/addresses.py new file mode 100755 index 00000000..b7a44980 --- /dev/null +++ b/mmgen/proto/rune/tw/addresses.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +proto.rune.tw.addresses: THORChain protocol tracking wallet address list class +""" + +from ....tw.addresses import TwAddresses + +from .view import THORChainTwView + +class THORChainTwAddresses(THORChainTwView, TwAddresses): + pass diff --git a/mmgen/proto/rune/tw/ctl.py b/mmgen/proto/rune/tw/ctl.py new file mode 100755 index 00000000..5003ea54 --- /dev/null +++ b/mmgen/proto/rune/tw/ctl.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +proto.rune.tw.ctl: THORChain tracking wallet control class +""" + +from ....tw.store import TwCtlWithStore + +class THORChainTwCtl(TwCtlWithStore): + use_cached_balances = True diff --git a/mmgen/proto/rune/tw/unspent.py b/mmgen/proto/rune/tw/unspent.py new file mode 100755 index 00000000..2b771458 --- /dev/null +++ b/mmgen/proto/rune/tw/unspent.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +proto.rune.tw.unspent: THORChain tracking wallet unspent outputs class +""" + +from ....tw.unspent import TwUnspentOutputs +from .view import THORChainTwView + +class THORChainTwUnspentOutputs(THORChainTwView, TwUnspentOutputs): + pass diff --git a/mmgen/proto/rune/tw/view.py b/mmgen/proto/rune/tw/view.py new file mode 100755 index 00000000..081b104c --- /dev/null +++ b/mmgen/proto/rune/tw/view.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +proto.rune.tw.view: THORChain protocol base class for tracking wallet view classes +""" + +class THORChainTwView: + + def gen_subheader(self, cw, color): + yield from super().gen_subheader(cw, color) + if self.proto.network == 'mainnet': + from ....color import red + yield red('For demonstration purposes only! DO NOT SPEND to these addresses!') # TODO diff --git a/setup.cfg b/setup.cfg index 78bdf8c8..1781db64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -94,6 +94,8 @@ packages = mmgen.proto.eth.tw mmgen.proto.ltc mmgen.proto.rune + mmgen.proto.rune.rpc + mmgen.proto.rune.tw mmgen.proto.secp256k1 mmgen.proto.xchain mmgen.proto.xmr diff --git a/test/cmdtest_d/httpd/thornode.py b/test/cmdtest_d/httpd/thornode.py index 261ecfc6..10842d30 100755 --- a/test/cmdtest_d/httpd/thornode.py +++ b/test/cmdtest_d/httpd/thornode.py @@ -12,9 +12,25 @@ test.cmdtest_d.httpd.thornode: Thornode WSGI http server """ +import re, json +from wsgiref.util import request_uri + from . import HTTPD class ThornodeServer(HTTPD): name = 'thornode server' port = 18800 content_type = 'application/json' + request_pat = r'/bank/balances/(\S+)' + + def make_response_body(self, method, environ): + req_str = request_uri(environ) + m = re.search(self.request_pat, req_str) + assert m[1], f'‘{req_str}’: malformed query path' + data = { + 'result': [ + {'denom': 'foocoin', 'amount': 321321321321}, + {'denom': 'rune', 'amount': 987654321321}, + {'denom': 'barcoin', 'amount': 123123123123}, + ]} + return json.dumps(data).encode() diff --git a/test/cmdtest_d/include/cfg.py b/test/cmdtest_d/include/cfg.py index 35cba35a..db3e67cd 100755 --- a/test/cmdtest_d/include/cfg.py +++ b/test/cmdtest_d/include/cfg.py @@ -43,6 +43,7 @@ cmd_groups_dfl = { # 'chainsplit': ('CmdTestChainsplit', {}), 'ethdev': ('CmdTestEthdev', {}), 'ethbump': ('CmdTestEthBump', {}), + 'rune': ('CmdTestRune', {}), 'xmrwallet': ('CmdTestXMRWallet', {}), 'xmr_autosign': ('CmdTestXMRAutosign', {}), } @@ -250,6 +251,7 @@ cfgs = { # addr_idx_lists (except 31, 32, 33, 34) must contain exactly 8 address '47': {}, # ethswap '48': {}, # ethswap_eth '49': {}, # autosign_automount + '50': {}, # rune '59': {}, # autosign_eth '99': {}, # dummy } diff --git a/test/cmdtest_d/rune.py b/test/cmdtest_d/rune.py new file mode 100755 index 00000000..5b8677ec --- /dev/null +++ b/test/cmdtest_d/rune.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +test.cmdtest_d.rune: THORChain RUNE tests for the cmdtest.py test suite +""" + +from .include.common import dfl_sid +from .httpd.thornode import ThornodeServer +from .ethdev import CmdTestEthdevMethods +from .base import CmdTestBase +from .shared import CmdTestShared +from .swap import CmdTestSwapMethods + +class CmdTestRune(CmdTestEthdevMethods, CmdTestBase, CmdTestShared): + 'THORChain RUNE tracking wallet and transacting operations' + networks = ('rune',) + passthru_opts = ('coin', 'http_timeout') + tmpdir_nums = [50] + color = True + menu_prompt = 'efresh balance:\b' + + cmd_group_in = ( + ('subgroup.init', []), + ('subgroup.main', ['init']), + ) + cmd_subgroups = { + 'init': ( + 'initializing wallets', + ('addrgen', 'generating addresses'), + ('addrimport', 'importing addresses'), + ), + 'main': ( + 'tracking wallet and transaction operations', + ('twview', 'viewing unspent outputs in tracking wallet'), + ('bal_refresh', 'refreshing address balance in tracking wallet'), + ('thornode_server_stop', 'stopping Thornode server'), + ), + } + + def __init__(self, cfg, trunner, cfgs, spawn): + CmdTestBase.__init__(self, cfg, trunner, cfgs, spawn) + if trunner is None: + return + + self.eth_opts = [f'--outdir={self.tmpdir}', '--regtest=1', '--quiet'] + self.eth_opts_noquiet = [f'--outdir={self.tmpdir}', '--regtest=1'] + + self.rune_opts = self.eth_opts + + from mmgen.protocol import init_proto + self.proto = init_proto(cfg, network_id=self.proto.coin + '_rt', need_amt=True) + self.spawn_env['MMGEN_BOGUS_SEND'] = '' + + self.thornode_server = ThornodeServer() + self.thornode_server.start() + + def addrgen(self): + return self._addrgen() + + def addrimport(self): + return self._addrimport() + + def twview(self): + return self.spawn('mmgen-tool', self.rune_opts + ['twview']) + + def bal_refresh(self): + t = self.spawn('mmgen-tool', self.rune_opts + ['listaddresses', 'interactive=1']) + t.expect(self.menu_prompt, 'R') + t.expect('menu): ', '3\n') + t.expect('(y/N): ', 'y') + t.expect(r'Total RUNE: \S*\D9876.54321321\D', regex=True) + t.expect('address #3 refreshed') + t.expect(self.menu_prompt, 'q') + return t + + def thornode_server_stop(self): + return CmdTestSwapMethods._thornode_server_stop( + self, attrname='thornode_server', name='thornode server') diff --git a/test/test-release.d/cfg.sh b/test/test-release.d/cfg.sh index 351ab352..39480f10 100755 --- a/test/test-release.d/cfg.sh +++ b/test/test-release.d/cfg.sh @@ -19,10 +19,10 @@ groups_desc=" " init_groups() { - dfl_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt altref altgen bch bch_rt ltc ltc_rt eth etc xmr' + dfl_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt altref altgen bch bch_rt ltc ltc_rt eth etc rune xmr' extra_tests='dep dev lint pylint autosign_live ltc_tn bch_tn' - noalt_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt pylint' - quick_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_rt altref altgen eth etc xmr' + noalt_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_tn btc_rt' + quick_tests='dep alt obj color daemon mod hash ref tool tool2 gen help autosign btc btc_rt altref altgen eth etc rune xmr' qskip_tests='lint btc_tn bch bch_rt ltc ltc_rt' noalt_ok_tests='lint' @@ -273,6 +273,11 @@ init_tests() { " [ "$SKIP_PARITY" ] && t_etc_skip='parity' + d_rune="operations for THORChain RUNE using testnet" + t_rune=" + - $cmdtest_py --coin=rune rune + " + d_xmr="Monero xmrwallet operations" t_xmr=" - $HTTP_LONG_TIMEOUT$cmdtest_py$PEXPECT_LONG_TIMEOUT --coin=xmr --exclude help