support THORChain address generation

Example:

    $ mmgen-addrgen --coin=rune 1-10

Testing:

    $ test/tooltest2.py -q --coin=rune
    $ test/modtest.py -v bip_hd.multicoin
This commit is contained in:
The MMGen Project 2025-05-21 09:13:42 +00:00
commit ba3b40c5b6
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
14 changed files with 135 additions and 5 deletions

View file

@ -50,6 +50,7 @@ class MMGenAddrType(HiliteStr, InitErrors, MMGenObject):
'C': ati('compressed','std', True, 'p2pkh', 'p2pkh', 'wif', (), 'Compressed P2PKH address'), 'C': ati('compressed','std', True, 'p2pkh', 'p2pkh', 'wif', (), 'Compressed P2PKH address'),
'S': ati('segwit', 'std', True, 'segwit', 'p2sh', 'wif', (), 'Segwit P2SH-P2WPKH address'), 'S': ati('segwit', 'std', True, 'segwit', 'p2sh', 'wif', (), 'Segwit P2SH-P2WPKH address'),
'B': ati('bech32', 'std', True, 'bech32', 'bech32', 'wif', (), 'Native Segwit (Bech32) address'), 'B': ati('bech32', 'std', True, 'bech32', 'bech32', 'wif', (), 'Native Segwit (Bech32) address'),
'X': ati('bech32x', 'std', True, 'p2pkh', 'bech32', 'wif', (), 'Cross-chain Bech32 address'),
'E': ati('ethereum', 'std', False,'ethereum','p2pkh', 'privkey', ('wallet_passwd',),'Ethereum address'), 'E': ati('ethereum', 'std', False,'ethereum','p2pkh', 'privkey', ('wallet_passwd',),'Ethereum address'),
'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif', ('viewkey',), 'Zcash z-address'), 'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif', ('viewkey',), 'Zcash z-address'),
'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address'), 'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address'),

View file

@ -59,6 +59,7 @@ def AddrGenerator(cfg, proto, addr_type):
'compressed': 'btc', 'compressed': 'btc',
'segwit': 'btc', 'segwit': 'btc',
'bech32': 'btc', 'bech32': 'btc',
'bech32x': 'xchain',
'monero': 'xmr', 'monero': 'xmr',
'ethereum': 'eth', 'ethereum': 'eth',
'zcash_z': 'zec', 'zcash_z': 'zec',

View file

@ -135,7 +135,7 @@ def check_privkey(key_int):
class BipHDConfig(Lockable): class BipHDConfig(Lockable):
supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch') supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch', 'rune')
def __init__(self, base_cfg, coin, *, network, addr_type, from_path, no_path_checks): def __init__(self, base_cfg, coin, *, network, addr_type, from_path, no_path_checks):

View file

@ -58,6 +58,8 @@ def parse_data():
return out return out
# RUNE derivation is SLIP-10, not BIP-44, but we treat them as equivalent
_data_in = """ _data_in = """
[defaults] [defaults]
@ -105,6 +107,7 @@ IDX CHAIN CURVE NW ADDR_CLS VB_PRV VB_PUB VB_WIF VB_ADDR DFL_PA
784 SUI edw m Sui x x - - 0'/0'/0' Sui 784 SUI edw m Sui x x - - 0'/0'/0' Sui
818 VET x m Eth x x - - x VeChain Token 818 VET x m Eth x x - - x VeChain Token
888 NEO nist m Neo x x - spec x NEO 888 NEO nist m Neo x x - spec x NEO
931 RUNE x m BechP2PKH x x - h:thor x THORChain
996 OKT x m Okex x x - - x OKChain Token 996 OKT x m Okex x x - - x OKChain Token
1023 ONE x m One x x - - x HARMONY-ONE (Legacy) 1023 ONE x m One x x - - x HARMONY-ONE (Legacy)
1024 ONT nist m Neo x x - spec x Ontology 1024 ONT nist m Neo x x - spec x Ontology
@ -896,7 +899,6 @@ IDX CHAIN NAME
921 AVN - Avian Network 921 AVN - Avian Network
925 DIP - Dipper Network 925 DIP - Dipper Network
928 GHM - HermitMatrixNetwork 928 GHM - HermitMatrixNetwork
931 RUNE - THORChain (RUNE)
941 --- - reserved 941 --- - reserved
945 UNLOCK - Jasiri protocol 945 UNLOCK - Jasiri protocol
955 LTP - LifetionCoin 955 LTP - LifetionCoin

View file

@ -1 +1 @@
15.1.dev37 15.1.dev38

63
mmgen/proto/rune/params.py Executable file
View file

@ -0,0 +1,63 @@
#!/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.params: THORChain protocol
"""
from ...protocol import CoinProtocol, decoded_addr, _nw
from ...addr import CoinAddr
from ...contrib import bech32
from ..btc.params import mainnet as btc_mainnet
class mainnet(CoinProtocol.Secp256k1):
mod_clsname = 'THORChain'
network_names = _nw('mainnet', 'stagenet', 'testnet')
mmtypes = ('X',)
preferred_mmtypes = ('X',)
dfl_mmtype = 'X'
coin_amt = 'UniAmt'
max_tx_fee = 1 # TODO
caps = ()
mmcaps = ()
base_proto = 'THORChain'
base_proto_coin = 'RUNE'
base_coin = 'RUNE'
bech32_hrp = 'thor'
sign_mode = 'standalone'
avg_bdi = 6 # TODO
address_reuse_ok = False
wif_ver_num = btc_mainnet.wif_ver_num
coin_cfg_opts = btc_mainnet.coin_cfg_opts
encode_wif = btc_mainnet.encode_wif
decode_wif = btc_mainnet.decode_wif
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})'
return decoded_addr(
bytes(bech32.convertbits(data, 5, 8)),
None,
'bech32') if data else False
def encode_addr_bech32x(self, pubhash):
return CoinAddr(
self,
bech32.bech32_encode(
hrp = self.bech32_hrp,
data = bech32.convertbits(list(pubhash), 8, 5)))
class stagenet(mainnet):
bech32_hrp = 'sthor'
class testnet(stagenet): # testnet is deprecated
bech32_hrp = 'tthor'

22
mmgen/proto/xchain/addrgen.py Executable file
View file

@ -0,0 +1,22 @@
#!/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.xchain.addrgen: Cross-chain address generation classes for the MMGen suite
"""
from ...addrgen import addr_generator, check_data
from ..btc.common import hash160
class bech32x(addr_generator.base):
@check_data
def to_addr(self, data):
return self.proto.encode_addr_bech32x(hash160(data.pubkey))

View file

@ -46,7 +46,8 @@ class CoinProtocol(MMGenObject):
'eth': proto_info('Ethereum', 4), 'eth': proto_info('Ethereum', 4),
'etc': proto_info('EthereumClassic', 4), 'etc': proto_info('EthereumClassic', 4),
'zec': proto_info('Zcash', 2), 'zec': proto_info('Zcash', 2),
'xmr': proto_info('Monero', 4) 'xmr': proto_info('Monero', 4),
'rune': proto_info('THORChain', 2)
} }
class Base(Lockable): class Base(Lockable):

View file

@ -165,6 +165,8 @@ class tool_cmd(tool_cmd_base):
return self.proto.pubhash2segwitaddr(pubhash) return self.proto.pubhash2segwitaddr(pubhash)
elif self.mmtype.name == 'bech32': elif self.mmtype.name == 'bech32':
return self.proto.pubhash2bech32addr(pubhash) return self.proto.pubhash2bech32addr(pubhash)
elif self.mmtype.name == 'bech32x':
return self.proto.encode_addr_bech32x(pubhash)
else: else:
return self.proto.pubhash2addr(pubhash, self.mmtype.addr_fmt) return self.proto.pubhash2addr(pubhash, self.mmtype.addr_fmt)

View file

@ -91,7 +91,9 @@ packages =
mmgen.proto.eth.tx mmgen.proto.eth.tx
mmgen.proto.eth.tw mmgen.proto.eth.tw
mmgen.proto.ltc mmgen.proto.ltc
mmgen.proto.rune
mmgen.proto.secp256k1 mmgen.proto.secp256k1
mmgen.proto.xchain
mmgen.proto.xmr mmgen.proto.xmr
mmgen.proto.zec mmgen.proto.zec
mmgen.swap mmgen.swap

View file

@ -146,6 +146,7 @@ vectors_multicoin = {
'bch_compressed': 'bitcoincash:qpqpcllprftg4s0chdgkpxhxv23wfymq3gj7n0a9vw', 'bch_compressed': 'bitcoincash:qpqpcllprftg4s0chdgkpxhxv23wfymq3gj7n0a9vw',
'bsc_smart': '0x373731f4d885Fc7Da05498F9f0804a87A14F891b', 'bsc_smart': '0x373731f4d885Fc7Da05498F9f0804a87A14F891b',
'bnb_beacon': 'bnb179c3ymltqm4utlp089zxqeta5dvn48a305rhe5', 'bnb_beacon': 'bnb179c3ymltqm4utlp089zxqeta5dvn48a305rhe5',
'rune': 'thor1nr6fye3nznyn20m5w6fey6w8a8l4q599cdqmpc',
} }
def wif2addr(cfg, wif): def wif2addr(cfg, wif):

View file

@ -0,0 +1,20 @@
# MMGen address file
#
# This file is editable.
# Everything following a hash symbol '#' is a comment and ignored by MMGen.
# A text label of 80 screen cells or less may be added to the right of each
# address, and it will be appended to the tracking wallet label upon import.
# The label may contain any printable ASCII symbol.
#
# Address data checksum for 98831F3A-RUNE-X[1,31-33,500-501,1010-1011]: 00C6 1930 557F 5E99
# Record this value to a secure location.
98831F3A RUNE:BECH32X {
1 thor137aqwpky5muxw7rvrdy6kxegt838k04xp4rnge
31 thor1w758u285uqv5h3wuksylvxtahql9tjsdllurug
32 thor18srpqhu8q8ppp4zc3zje3yp5lf3jpmq8hu4phh
33 thor16hgd6g3cutx8dhslvdgxljss4w7prq32u9x20y
500 thor14q7m5jyajjnlqgccu3vh2dx0m2323558faxlz6
501 thor1gyaexk8l3rvt7vsfd693y33ate989r64ak65gk
1010 thor1g95knu2elqt8l0d5vcyf0hyuspn4mhsjrym8g9
1011 thor12f6lgkjy7amxdz7eznv33xypvzux9kxslkqvzq
}

View file

@ -299,9 +299,10 @@ init_tests() {
e $tooltest2_py --coin=eth --token=mm1 e $tooltest2_py --coin=eth --token=mm1
e $tooltest2_py --coin=eth --token=mm1 --testnet=1 e $tooltest2_py --coin=eth --token=mm1 --testnet=1
e $tooltest2_py --coin=etc e $tooltest2_py --coin=etc
t $tooltest2_py --coin=rune
- $tooltest2_py --fork # run once with --fork so commands are actually executed - $tooltest2_py --fork # run once with --fork so commands are actually executed
" "
[ "$SKIP_ALT_DEP" ] && t_tool2_skip='a e' # skip ETH,ETC: txview requires py_ecc [ "$SKIP_ALT_DEP" ] && t_tool2_skip='a e t' # skip ETH,ETC: txview requires py_ecc
d_tool="'mmgen-tool' utility (all supported coins)" d_tool="'mmgen-tool' utility (all supported coins)"
t_tool=" t_tool="

View file

@ -110,6 +110,8 @@ eth_pubhex2 = '9166c289b9f905e55f9e3df9f69d7f356b4a22095f894f4715714aa4b56606aff
xmr_pubhex1 = '1ed49357e217e79dab3c5503822f2bdb561e302e24476ee6ff33242c7551d4e78944790c0cfa9998c2f196061be89b2b8387f9d397db20ea8e049899cdc947d1' xmr_pubhex1 = '1ed49357e217e79dab3c5503822f2bdb561e302e24476ee6ff33242c7551d4e78944790c0cfa9998c2f196061be89b2b8387f9d397db20ea8e049899cdc947d1'
zec_pubhex1 = 'e6a4edbff547f21bcc2a825b6cf70f06e266a452d2da9d6dc5c1da3d99d7e996f488704dcdfe8d92cafe47772b3f692a98d59de1e99e00ff815f64ae59910f0c' zec_pubhex1 = 'e6a4edbff547f21bcc2a825b6cf70f06e266a452d2da9d6dc5c1da3d99d7e996f488704dcdfe8d92cafe47772b3f692a98d59de1e99e00ff815f64ae59910f0c'
rune_addr1 = 'thor1xptlvmwaymaxa7pxkr2u5pn7c0508stcr9tw2z'
tests = { tests = {
'Mnemonic': { 'Mnemonic': {
'hex2mn': ( 'hex2mn': (
@ -407,6 +409,9 @@ tests = {
([btc_addr5], pubhash1), ([btc_addr5], pubhash1),
([btc_addr6], pubhash2), ([btc_addr6], pubhash2),
], ],
'rune_mainnet': [
([rune_addr1], pubhash2),
],
}, },
'eth_checksummed_addr': { 'eth_checksummed_addr': {
'eth_mainnet': [ 'eth_mainnet': [
@ -423,6 +428,9 @@ tests = {
['--type=segwit'], 'segwit'), ['--type=segwit'], 'segwit'),
([pubhash2], btc_addr6, ['--type=bech32'], 'bech32'), ([pubhash2], btc_addr6, ['--type=bech32'], 'bech32'),
], ],
'rune_mainnet': [
([pubhash2], rune_addr1, ['--type=X'], 'bech32x'),
],
}, },
'addr2scriptpubkey': { 'addr2scriptpubkey': {
'btc_mainnet': [ 'btc_mainnet': [
@ -665,6 +673,12 @@ tests = {
'E97A D796 B495 E8BC' 'E97A D796 B495 E8BC'
), ),
], ],
'rune_mainnet': [
(
['test/ref/thorchain/98831F3A-RUNE-X[1,31-33,500-501,1010-1011].addrs'],
'00C6 1930 557F 5E99'
),
],
}, },
'viewkeyaddrfile_chksum': { 'viewkeyaddrfile_chksum': {
'xmr_mainnet': [ 'xmr_mainnet': [