Nostr key/address generation
Examples:
# Generate 10 Nostr addresses from your default wallet:
$ mmgen-addrgen --coin=nostr 1-10
# Generate 10 Nostr key/address pairs from your default wallet:
$ mmgen-keygen --coin=nostr 1-10
# Generate a random Nostr key/address pair:
$ mmgen-tool --coin=nostr randpair
# Convert a Nostr address to a hexadecimal pubkey:
$ mmgen-tool --coin=nostr addr2pubhex npub1r7m96hpfeegjcj8un2wjgkw4rwv36x9mtmtek5duv3uq29tcwqgq93nwkz
# Convert a Nostr hexadecimal pubkey to an address:
$ mmgen-tool --coin=nostr pubhex2addr 1fb65d5c29ce512c48fc9a9d2459d51b991d18bb5ed79b51bc64780515787010
Testing:
$ test/modtest.py -v nostr
$ test/cmdtest.py --coin=nostr ref3_addr
$ test/tooltest2.py --coin=nostr
This commit is contained in:
parent
9a9627c8ed
commit
e322b4338c
16 changed files with 201 additions and 6 deletions
|
|
@ -45,12 +45,14 @@ class MMGenAddrType(HiliteStr, InitErrors, MMGenObject):
|
|||
desc = ImmutableAttr(str)
|
||||
|
||||
pubhash_fmts = ('p2pkh', 'bech32', 'ethereum')
|
||||
pubkey_fmts = ('bech32pk')
|
||||
mmtypes = {
|
||||
'L': ati('legacy', 'std', False,'p2pkh', 'p2pkh', 'wif', (), 'Legacy uncompressed address'),
|
||||
'C': ati('compressed','std', True, 'p2pkh', 'p2pkh', 'wif', (), 'Compressed P2PKH 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'),
|
||||
'X': ati('bech32x', 'std', True, 'p2pkh', 'bech32', 'wif', (), 'Cross-chain Bech32 address'),
|
||||
'K': ati('bech32pk', 'std', True, 'bech32pk','bech32', 'wif', (), 'Cross-chain pubkey Bech32 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'),
|
||||
'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address')}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ def AddrGenerator(cfg, proto, addr_type):
|
|||
'segwit': 'btc',
|
||||
'bech32': 'btc',
|
||||
'bech32x': 'xchain',
|
||||
'bech32pk': 'xchain',
|
||||
'monero': 'xmr',
|
||||
'ethereum': 'eth',
|
||||
'zcash_z': 'zec'}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ def check_privkey(key_int):
|
|||
|
||||
class BipHDConfig(Lockable):
|
||||
|
||||
supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch', 'rune')
|
||||
supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch', 'rune', 'nostr')
|
||||
|
||||
def __init__(self, base_cfg, coin, *, network, addr_type, from_path, no_path_checks):
|
||||
|
||||
|
|
@ -149,7 +149,8 @@ class BipHDConfig(Lockable):
|
|||
'coin': coin,
|
||||
'network': network,
|
||||
'type': addr_type or None,
|
||||
'quiet': True})
|
||||
'quiet': True},
|
||||
need_amt = False)
|
||||
|
||||
dfl_type = base_cfg._proto.dfl_mmtype
|
||||
addr_type = MMGenAddrType(
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ IDX CHAIN CURVE NW ADDR_CLS VB_PRV VB_PUB VB_WIF VB_ADDR DFL_PA
|
|||
996 OKT x m Okex x x - - x OKChain Token
|
||||
1023 ONE x m One x x - - x HARMONY-ONE (Legacy)
|
||||
1024 ONT nist m Neo x x - spec x Ontology
|
||||
1237 NOSTR x m BechPK x x - h:npub x Nostr
|
||||
1729 XTZ edw m Xtz x x - spec 0'/0' Tezos
|
||||
1815 ADA khol m AdaByronIcarus 0f4331d4 x - spec x Cardano
|
||||
9000 AVAX x m AvaxXChain x x - - x Avalanche
|
||||
|
|
@ -958,7 +959,6 @@ IDX CHAIN NAME
|
|||
1170 HOO - Hoo Smart Chain
|
||||
1234 ALPH - Alephium
|
||||
1236 --- - Masca
|
||||
1237 --- - Nostr
|
||||
1280 --- - Kudos Setler
|
||||
1284 GLMR - Moonbeam
|
||||
1285 MOVR - Moonriver
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
May 2026
|
||||
June 2026
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
16.2.dev1
|
||||
16.2.dev2
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ mods = {
|
|||
),
|
||||
'coin': (
|
||||
'addr2pubhash',
|
||||
'addr2pubhex',
|
||||
'addr2scriptpubkey',
|
||||
'eth_checksummed_addr',
|
||||
'hex2wif',
|
||||
|
|
|
|||
63
mmgen/proto/nostr/params.py
Executable file
63
mmgen/proto/nostr/params.py
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||
# Copyright (C)2013-2026 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.nostr.params: Nostr protocol
|
||||
"""
|
||||
|
||||
from ...protocol import CoinProtocol, decoded_wif, decoded_addr, _nw
|
||||
from ...addr import CoinAddr
|
||||
from ...contrib import bech32
|
||||
|
||||
class mainnet(CoinProtocol.Secp256k1):
|
||||
mod_clsname = 'Nostr'
|
||||
network_names = _nw('mainnet', None, None)
|
||||
mmtypes = ('K',)
|
||||
dfl_mmtype = 'K'
|
||||
base_proto = 'Nostr'
|
||||
base_proto_coin = 'NOSTR'
|
||||
base_coin = 'NOSTR'
|
||||
bech32_hrp = 'npub'
|
||||
bech32_hrp_prv = 'nsec'
|
||||
|
||||
def encode_wif(self, privbytes, pubkey_type, *, compressed): # input is preprocessed
|
||||
return bech32.bech32_encode(
|
||||
hrp = self.bech32_hrp_prv,
|
||||
data = bech32.convertbits(privbytes, 8, 5))
|
||||
|
||||
def decode_wif(self, wif):
|
||||
hrp, data = bech32.bech32_decode(wif)
|
||||
assert hrp == self.bech32_hrp_prv, f'Bech32 HRP does not match! ({hrp} != {self.bech32_hrp_prv})'
|
||||
return decoded_wif(
|
||||
sec = bytes(bech32.convertbits(data, 5, 8, False)),
|
||||
pubkey_type = None,
|
||||
compressed = True) if data else False
|
||||
|
||||
def decode_addr(self, addr):
|
||||
hrp, data = bech32.bech32_decode(addr)
|
||||
assert hrp == self.bech32_hrp, f'Bech32 HRP does not match! ({hrp} != {self.bech32_hrp})'
|
||||
return decoded_addr(
|
||||
bytes(bech32.convertbits(data, 5, 8, False)),
|
||||
ver_bytes = None,
|
||||
fmt = 'bech32pk') if data else False
|
||||
|
||||
def encode_addr_bech32pk(self, pubkey):
|
||||
match len(pubkey):
|
||||
case 33:
|
||||
assert pubkey[0] in (2, 3), f'{pubkey[0]}: invalid first byte for {self.name} protocol public key'
|
||||
case 32:
|
||||
pass
|
||||
case x:
|
||||
raise ValueError(f'{x}: invalid public key length for {self.name} protocol')
|
||||
return CoinAddr(
|
||||
self,
|
||||
bech32.bech32_encode(
|
||||
hrp = self.bech32_hrp,
|
||||
data = bech32.convertbits(pubkey[-32:], 8, 5)))
|
||||
|
|
@ -20,3 +20,9 @@ class bech32x(addr_generator.base):
|
|||
@check_data
|
||||
def to_addr(self, data):
|
||||
return self.proto.encode_addr_bech32x(hash160(data.pubkey))
|
||||
|
||||
class bech32pk(addr_generator.base):
|
||||
|
||||
@check_data
|
||||
def to_addr(self, data):
|
||||
return self.proto.encode_addr_bech32pk(data.pubkey)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class CoinProtocol(MMGenObject):
|
|||
'etc': proto_info('EthereumClassic', 4),
|
||||
'zec': proto_info('Zcash', 2),
|
||||
'xmr': proto_info('Monero', 5),
|
||||
'nostr': proto_info('Nostr', 4),
|
||||
'rune': proto_info('THORChain', 4)}
|
||||
|
||||
class Base(Lockable):
|
||||
|
|
|
|||
|
|
@ -183,6 +183,10 @@ class tool_cmd(tool_cmd_base):
|
|||
die(2, f'{ap.fmt} addresses cannot be converted to {ptype}')
|
||||
return ap.bytes.hex()
|
||||
|
||||
def addr2pubhex(self, addr: 'sstr'):
|
||||
"convert coin address to public key"
|
||||
return self._addr2pub(addr, ptype='pubkey')
|
||||
|
||||
def addr2pubhash(self, addr: 'sstr'):
|
||||
"convert coin address to public key hash"
|
||||
return self._addr2pub(addr, ptype='pubhash')
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ packages =
|
|||
mmgen.proto.eth.tx
|
||||
mmgen.proto.eth.tw
|
||||
mmgen.proto.ltc
|
||||
mmgen.proto.nostr
|
||||
mmgen.proto.rune
|
||||
mmgen.proto.rune.rpc
|
||||
mmgen.proto.rune.tw
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ class CmdTestRef3Seed(CmdTestBase, CmdTestShared):
|
|||
|
||||
class CmdTestRef3Addr(CmdTestRef3Seed):
|
||||
'generated reference address and key-address files for 128-, 192- and 256-bit seeds'
|
||||
networks = ('btc', 'btc_tn', 'ltc', 'ltc_tn', 'bch', 'bch_tn')
|
||||
networks = ('btc', 'btc_tn', 'ltc', 'ltc_tn', 'bch', 'bch_tn', 'nostr')
|
||||
passthru_opts = ('coin', 'testnet', 'cashaddr')
|
||||
tmpdir_nums = [26, 27, 28]
|
||||
shared_deps = ['mmdat', pwfile]
|
||||
|
|
@ -330,6 +330,24 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
|
|||
'btc': ('D0DD BDE3 87BE 15AE', '7552 D70C AAB8 DEAA'),
|
||||
'ltc': ('74A0 7DD5 963B 6326', '2CDA A007 4B9F E9A5'),
|
||||
},
|
||||
'refaddrgen_bech32pk_1': {
|
||||
'nostr': ('005B CFEC B290 32FE',),
|
||||
},
|
||||
'refaddrgen_bech32pk_2': {
|
||||
'nostr': ('9BB5 F044 6048 2007',),
|
||||
},
|
||||
'refaddrgen_bech32pk_3': {
|
||||
'nostr': ('652F D99A 174F 9055',),
|
||||
},
|
||||
'refkeyaddrgen_bech32pk_1': {
|
||||
'nostr': ('84B5 4714 A15C D72E',),
|
||||
},
|
||||
'refkeyaddrgen_bech32pk_2': {
|
||||
'nostr': ('3802 94B3 27D3 F42A',),
|
||||
},
|
||||
'refkeyaddrgen_bech32pk_3': {
|
||||
'nostr': ('9A54 4897 DC3D 80D0',),
|
||||
},
|
||||
}
|
||||
|
||||
cmd_group = (
|
||||
|
|
@ -338,10 +356,12 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
|
|||
('refaddrgen_compressed', 'new refwallet addr chksum (compressed)'),
|
||||
('refaddrgen_segwit', 'new refwallet addr chksum (segwit)'),
|
||||
('refaddrgen_bech32', 'new refwallet addr chksum (bech32)'),
|
||||
('refaddrgen_bech32pk', 'new refwallet addr chksum (bech32pk)'),
|
||||
('refkeyaddrgen_legacy', 'new refwallet key-addr chksum (uncompressed)'),
|
||||
('refkeyaddrgen_compressed', 'new refwallet key-addr chksum (compressed)'),
|
||||
('refkeyaddrgen_segwit', 'new refwallet key-addr chksum (segwit)'),
|
||||
('refkeyaddrgen_bech32', 'new refwallet key-addr chksum (bech32)'),
|
||||
('refkeyaddrgen_bech32pk', 'new refwallet key-addr chksum (bech32pk)'),
|
||||
)
|
||||
|
||||
def call_addrgen(self, mmtype, name='addrgen'):
|
||||
|
|
@ -362,6 +382,9 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
|
|||
def refaddrgen_bech32(self):
|
||||
return self.call_addrgen('bech32')
|
||||
|
||||
def refaddrgen_bech32pk(self):
|
||||
return self.call_addrgen('bech32pk')
|
||||
|
||||
def refkeyaddrgen_legacy(self):
|
||||
return self.call_addrgen('legacy', 'keyaddrgen')
|
||||
|
||||
|
|
@ -374,6 +397,9 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
|
|||
def refkeyaddrgen_bech32(self):
|
||||
return self.call_addrgen('bech32', 'keyaddrgen')
|
||||
|
||||
def refkeyaddrgen_bech32pk(self):
|
||||
return self.call_addrgen('bech32pk', 'keyaddrgen')
|
||||
|
||||
class CmdTestRef3Passwd(CmdTestRef3Seed):
|
||||
'generated reference password files for 128-, 192- and 256-bit seeds'
|
||||
tmpdir_nums = [26, 27, 28]
|
||||
|
|
|
|||
63
test/modtest_d/nostr.py
Executable file
63
test/modtest_d/nostr.py
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
test.modtest_d.nostr: Nostr unit test for the MMGen suite
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from mmgen.bip_hd import BipHDNode, MasterNode
|
||||
from mmgen.bip39 import bip39
|
||||
|
||||
from ..include.common import cfg, vmsg
|
||||
|
||||
# Source: https://nostr-nips.com/nip-06
|
||||
nv = namedtuple('nostr_test_vector', 'mnemonic privhex nsec pubhex npub')
|
||||
vecs = [
|
||||
nv( 'leader monkey parrot ring guide accident before fence cannon height naive bean',
|
||||
'7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a',
|
||||
'nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp',
|
||||
'17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917',
|
||||
'npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu'),
|
||||
nv( 'what bleak badge arrange retreat wolf trade produce cricket blur garlic valid '
|
||||
'proud rude strong choose busy staff weather area salt hollow arm fade',
|
||||
'c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add',
|
||||
'nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel',
|
||||
'd41b22899549e1f3d335a31002cfd382174006e166d3e658e3a5eecdb6463573',
|
||||
'npub16sdj9zv4f8sl85e45vgq9n7nsgt5qphpvmf7vk8r5hhvmdjxx4es8rq74h')]
|
||||
|
||||
path = "m/44'/1237'/0'/0/0"
|
||||
|
||||
class unit_tests:
|
||||
|
||||
def path(self, name, ut):
|
||||
for v in vecs:
|
||||
vmsg(f'mnemonic: {v.mnemonic}')
|
||||
seed = bip39().generate_seed(v.mnemonic.split())
|
||||
res = BipHDNode.from_path(cfg, seed, path, coin='nostr')
|
||||
xprv = res.key_extended(public=False)
|
||||
xpub = res.key_extended(public=True)
|
||||
vmsg(f'prv: {xprv.key.hex()}')
|
||||
vmsg(f'pub: {xpub.key.hex()}')
|
||||
vmsg(f'addr: {res.address}')
|
||||
vmsg(f'wif: {res.privkey.wif}')
|
||||
assert res.privkey.hex() == v.privhex
|
||||
assert res.address.bytes.hex() == v.pubhex
|
||||
assert xprv.key.hex() == v.privhex
|
||||
assert xpub.key.hex()[2:] == v.pubhex
|
||||
assert res.address == v.npub
|
||||
assert res.privkey.wif == v.nsec
|
||||
vmsg('')
|
||||
return True
|
||||
|
||||
def derive(self, name, ut):
|
||||
for v in vecs:
|
||||
vmsg(f'mnemonic: {v.mnemonic}')
|
||||
seed = bip39().generate_seed(v.mnemonic.split())
|
||||
res = MasterNode(cfg, seed).to_chain(idx=0, coin='nostr').derive_private(0)
|
||||
vmsg(f'addr: {res.address}')
|
||||
vmsg(f'wif: {res.privkey.wif}')
|
||||
assert res.address == v.npub
|
||||
assert res.privkey.wif == v.nsec
|
||||
vmsg('')
|
||||
return True
|
||||
|
|
@ -135,6 +135,7 @@ init_tests() {
|
|||
- $gentest_py --coin=xmr --use-internal-keccak-module 1 $rounds10x
|
||||
- $gentest_py --coin=zec 1 $rounds10x
|
||||
- $gentest_py --coin=zec --type=zcash_z 1 $rounds10x
|
||||
- $gentest_py --coin=nostr 1 $rounds10x
|
||||
- # verification against external libraries and tools:
|
||||
- # pycoin
|
||||
- $gentest_py --all-coins --type=legacy 1:pycoin $rounds
|
||||
|
|
@ -321,6 +322,7 @@ init_tests() {
|
|||
e $tooltest2_py --coin=eth --token=mm1 --testnet=1
|
||||
e $tooltest2_py --coin=etc
|
||||
t $tooltest2_py --coin=rune
|
||||
t $tooltest2_py --coin=nostr
|
||||
- $tooltest2_py --fork # run once with --fork so commands are actually executed
|
||||
"
|
||||
[ "$SKIP_ALT_DEP" ] && t_tool2_skip='a e t'
|
||||
|
|
|
|||
|
|
@ -113,6 +113,16 @@ zec_pubhex1 = 'e6a4edbff547f21bcc2a825b6cf70f06e266a452d2da9d6dc5c1da3d99d7e996f
|
|||
|
||||
rune_addr1 = 'thor1xptlvmwaymaxa7pxkr2u5pn7c0508stcr9tw2z'
|
||||
|
||||
nostr_privhex1 = 'c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add'
|
||||
nostr_pubhex1 = 'd41b22899549e1f3d335a31002cfd382174006e166d3e658e3a5eecdb6463573'
|
||||
nostr_wif1 = 'nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel'
|
||||
nostr_addr1 = 'npub16sdj9zv4f8sl85e45vgq9n7nsgt5qphpvmf7vk8r5hhvmdjxx4es8rq74h'
|
||||
|
||||
nostr_privhex2 = '7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a'
|
||||
nostr_pubhex2 = '17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917'
|
||||
nostr_wif2 = 'nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp'
|
||||
nostr_addr2 = 'npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu'
|
||||
|
||||
tests = {
|
||||
'Mnemonic': {
|
||||
'hex2mn': (
|
||||
|
|
@ -414,6 +424,12 @@ tests = {
|
|||
([rune_addr1], pubhash2),
|
||||
],
|
||||
},
|
||||
'addr2pubhex': {
|
||||
'nostr_mainnet': [
|
||||
([nostr_addr1], nostr_pubhex1),
|
||||
([nostr_addr2], nostr_pubhex2),
|
||||
],
|
||||
},
|
||||
'eth_checksummed_addr': {
|
||||
'eth_mainnet': [
|
||||
(['00a329c0648769a73afac7f9381e08fb43dbea72'], '00a329c0648769A73afAc7F9381E08FB43dBEA72'),
|
||||
|
|
@ -462,6 +478,10 @@ tests = {
|
|||
([privhex7], [btc_wif2, btc_addr3], ['--type=segwit'], 'segwit'),
|
||||
([privhex7], [btc_wif2, btc_addr4], ['--type=bech32'], 'bech32'),
|
||||
],
|
||||
'nostr_mainnet': [
|
||||
([nostr_privhex1], [nostr_wif1, nostr_addr1], None, 'bech32pk'),
|
||||
([nostr_privhex2], [nostr_wif2, nostr_addr2], None, 'bech32pk'),
|
||||
],
|
||||
},
|
||||
'privhex2addr': {
|
||||
'btc_mainnet': [
|
||||
|
|
@ -529,6 +549,10 @@ tests = {
|
|||
'zec_mainnet': [
|
||||
([zec_pubhex1], zec_addr1, ['--type=zcash_z'], 'zcash_z'),
|
||||
],
|
||||
'nostr_mainnet': [
|
||||
([nostr_pubhex1], nostr_addr1, None, 'bech32pk'),
|
||||
([nostr_pubhex2], nostr_addr2, None, 'bech32pk'),
|
||||
],
|
||||
},
|
||||
'pubhex2redeem_script': {
|
||||
'btc_mainnet': [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue