Browse Source

RUNE tracking wallet support

Testing/demo:

    $ test/cmdtest.py --demo --coin=rune rune
The MMGen Project 6 months ago
parent
commit
2993fdba9e

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev41
+15.1.dev42

+ 4 - 0
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'

+ 18 - 0
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 <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+proto.rune.addrdata: THORChain TwAddrData class
+"""
+
+from ...addrdata import TwAddrDataWithStore
+
+class THORChainTwAddrData(TwAddrDataWithStore):
+	pass

+ 15 - 1
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

+ 50 - 0
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 <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+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')

+ 20 - 0
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 <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+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

+ 18 - 0
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 <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+proto.rune.tw.ctl: THORChain tracking wallet control class
+"""
+
+from ....tw.store import TwCtlWithStore
+
+class THORChainTwCtl(TwCtlWithStore):
+	use_cached_balances = True

+ 19 - 0
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 <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+proto.rune.tw.unspent: THORChain tracking wallet unspent outputs class
+"""
+
+from ....tw.unspent import TwUnspentOutputs
+from .view import THORChainTwView
+
+class THORChainTwUnspentOutputs(THORChainTwView, TwUnspentOutputs):
+	pass

+ 21 - 0
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 <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+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

+ 2 - 0
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

+ 16 - 0
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()

+ 2 - 0
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
 }

+ 86 - 0
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 <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+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')

+ 8 - 3
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