RUNE tracking wallet support

Testing/demo:

    $ test/cmdtest.py --demo --coin=rune rune
This commit is contained in:
The MMGen Project 2025-05-28 11:40:40 +00:00
commit 2993fdba9e
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
14 changed files with 280 additions and 5 deletions

View file

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

View file

@ -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
mmgen/proto/rune/addrdata.py Executable file
View file

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

View file

@ -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
mmgen/proto/rune/rpc/remote.py Executable file
View file

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

View file

@ -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
mmgen/proto/rune/tw/ctl.py Executable file
View file

@ -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
mmgen/proto/rune/tw/unspent.py Executable file
View file

@ -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
mmgen/proto/rune/tw/view.py Executable file
View file

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

View file

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

View file

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

View file

@ -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
test/cmdtest_d/rune.py Executable file
View file

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

View file

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