| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- #!/usr/bin/env python3
- #
- # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
- # Copyright (C)2013-2022 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
- # https://gitlab.com/mmgen/mmgen
- """
- proto.btc.rpc: Bitcoin base protocol RPC client class
- """
- import os
- from ...globalvars import g
- from ...base_obj import AsyncInit
- from ...util import ymsg,vmsg,die,fmt
- from ...fileutil import get_lines_from_file
- from ...rpc import RPCClient,auth_data
- no_credentials_errmsg = """
- Error: no {proto_name} RPC authentication method found
- RPC credentials must be supplied using one of the following methods:
- 1) If daemon is local and running as same user as you:
- - no credentials required, or matching rpcuser/rpcpassword and
- rpc_user/rpc_password values in {cf_name}.conf and mmgen.cfg
- 2) If daemon is running remotely or as different user:
- - matching credentials in {cf_name}.conf and mmgen.cfg as described
- above
- The --rpc-user/--rpc-password options may be supplied on the MMGen command
- line. They override the corresponding values in mmgen.cfg. Set them to an
- empty string to use cookie authentication with a local server when the
- options are set in mmgen.cfg.
- For better security, rpcauth should be used in {cf_name}.conf instead of
- rpcuser/rpcpassword.
- """
- class CallSigs:
- class bitcoin_core:
- @classmethod
- def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
- """
- Quirk: when --datadir is specified (even if standard), wallet is created directly in
- datadir, otherwise in datadir/wallets
- """
- return (
- 'createwallet',
- wallet_name, # 1. wallet_name
- no_keys, # 2. disable_private_keys
- blank, # 3. blank (no keys or seed)
- passphrase, # 4. passphrase (empty string for non-encrypted)
- False, # 5. avoid_reuse (track address reuse)
- False, # 6. descriptors (native descriptor wallet)
- load_on_startup # 7. load_on_startup
- )
- @classmethod
- def gettransaction(cls,txid,include_watchonly,verbose):
- return (
- 'gettransaction',
- txid, # 1. transaction id
- include_watchonly, # 2. optional, default=true for watch-only wallets, otherwise false
- verbose, # 3. optional, default=false -- include a `decoded` field containing
- # the decoded transaction (equivalent to RPC decoderawtransaction)
- )
- class litecoin_core(bitcoin_core):
- @classmethod
- def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
- return (
- 'createwallet',
- wallet_name, # 1. wallet_name
- no_keys, # 2. disable_private_keys
- blank, # 3. blank (no keys or seed)
- )
- @classmethod
- def gettransaction(cls,txid,include_watchonly,verbose):
- return (
- 'gettransaction',
- txid, # 1. transaction id
- include_watchonly, # 2. optional, default=true for watch-only wallets, otherwise false
- )
- class bitcoin_cash_node(litecoin_core):
- pass
- class BitcoinRPCClient(RPCClient,metaclass=AsyncInit):
- auth_type = 'basic'
- has_auth_cookie = True
- wallet_path = '/'
- async def __init__(self,proto,daemon,backend,ignore_wallet):
- self.proto = proto
- self.daemon = daemon
- self.call_sigs = getattr(CallSigs,daemon.id,None)
- super().__init__(
- host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
- port = daemon.rpc_port )
- self.set_auth()
- await self.set_backend_async(backend) # backend requires self.auth
- self.cached = {}
- self.caps = ('full_node',)
- for func,cap in (
- ('setlabel','label_api'),
- ('getdeploymentinfo','deployment_info'),
- ('signrawtransactionwithkey','sign_with_key') ):
- if len((await self.call('help',func)).split('\n')) > 3:
- self.caps += (cap,)
- call_group = [
- ('getblockcount',()),
- ('getblockhash',(0,)),
- ('getnetworkinfo',()),
- ('getblockchaininfo',()),
- ] + (
- [('getdeploymentinfo',())] if 'deployment_info' in self.caps else []
- )
- (
- self.blockcount,
- block0,
- self.cached['networkinfo'],
- self.cached['blockchaininfo'],
- self.cached['deploymentinfo'],
- ) = (
- await self.gathered_call(None,tuple(call_group))
- ) + (
- [] if 'deployment_info' in self.caps else [None]
- )
- self.daemon_version = self.cached['networkinfo']['version']
- self.daemon_version_str = self.cached['networkinfo']['subversion']
- self.chain = self.cached['blockchaininfo']['chain']
- tip = await self.call('getblockhash',self.blockcount)
- self.cur_date = (await self.call('getblockheader',tip))['time']
- if self.chain != 'regtest':
- self.chain += 'net'
- assert self.chain in self.proto.networks
- async def check_chainfork_mismatch(block0):
- try:
- if block0 != self.proto.block0:
- raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol')
- for fork in self.proto.forks:
- if fork.height == None or self.blockcount < fork.height:
- break
- if fork.hash != await self.call('getblockhash',fork.height):
- die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?')
- except Exception as e:
- die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin))
- if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change
- await check_chainfork_mismatch(block0)
- if not ignore_wallet:
- await self.check_or_create_daemon_wallet()
- # for regtest, wallet path must remain '/' until Carol’s user wallet has been created
- if g.regtest_user:
- self.wallet_path = f'/wallet/{g.regtest_user}'
- def set_auth(self):
- """
- MMGen's credentials override coin daemon's
- """
- if g.rpc_user:
- user,passwd = (g.rpc_user,g.rpc_password)
- else:
- user,passwd = self.get_daemon_cfg_options(('rpcuser','rpcpassword')).values()
- if not (user and passwd):
- user,passwd = (self.daemon.rpc_user,self.daemon.rpc_password)
- if user and passwd:
- self.auth = auth_data(user,passwd)
- return
- if self.has_auth_cookie:
- cookie = self.get_daemon_auth_cookie()
- if cookie:
- self.auth = auth_data(*cookie.split(':'))
- return
- die(1, '\n\n' + fmt(no_credentials_errmsg,strip_char='\t',indent=' ').format(
- proto_name = self.proto.name,
- cf_name = (self.proto.is_fork_of or self.proto.name).lower(),
- ))
- def make_host_path(self,wallet):
- return f'/wallet/{wallet}' if wallet else self.wallet_path
- async def check_or_create_daemon_wallet(self,called=[],wallet_create=True):
- """
- Returns True if the correct tracking wallet is currently loaded or if a new one
- is created, False otherwise
- """
- if called or (self.chain == 'regtest' and g.regtest_user != 'carol'):
- return False
- twname = self.daemon.tracking_wallet_name
- loaded_wnames = await self.call('listwallets')
- wnames = [i['name'] for i in (await self.call('listwalletdir'))['wallets']]
- m = f'Please fix your {self.daemon.desc} wallet installation or cmdline options'
- ret = False
- if g.carol:
- if 'carol' in loaded_wnames:
- ret = True
- elif wallet_create:
- await self.icall('createwallet',wallet_name='carol')
- ymsg(f'Created {self.daemon.coind_name} wallet {"carol"!r}')
- ret = True
- elif len(loaded_wnames) == 1:
- loaded_wname = loaded_wnames[0]
- if twname in wnames and loaded_wname != twname:
- await self.call('unloadwallet',loaded_wname)
- await self.call('loadwallet',twname)
- elif loaded_wname == '':
- ymsg(f'WARNING: use of default wallet as tracking wallet is not recommended!\n{m}')
- elif loaded_wname != twname:
- ymsg(f'WARNING: loaded wallet {loaded_wname!r} is not {twname!r}\n{m}')
- ret = True
- elif len(loaded_wnames) == 0:
- if twname in wnames:
- await self.call('loadwallet',twname)
- ret = True
- elif wallet_create:
- await self.icall('createwallet',wallet_name=twname)
- ymsg(f'Created {self.daemon.coind_name} wallet {twname!r}')
- ret = True
- else: # support only one loaded wallet for now
- die(4,f'ERROR: more than one {self.daemon.coind_name} wallet loaded: {loaded_wnames}')
- if wallet_create:
- called.append(True)
- return ret
- def get_daemon_cfg_fn(self):
- # Use dirname() to remove 'bob' or 'alice' component
- return os.path.join(
- (os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon.datadir),
- self.daemon.cfg_file )
- def get_daemon_cfg_options(self,req_keys):
- fn = self.get_daemon_cfg_fn()
- from ...opts import opt
- try:
- lines = get_lines_from_file(fn,'daemon config file',silent=not opt.verbose)
- except:
- vmsg(f'Warning: {fn!r} does not exist or is unreadable')
- return dict((k,None) for k in req_keys)
- def gen():
- for key in req_keys:
- val = None
- for l in lines:
- if l.startswith(key):
- res = l.split('=',1)
- if len(res) == 2 and not ' ' in res[1].strip():
- val = res[1].strip()
- yield (key,val)
- return dict(gen())
- def get_daemon_auth_cookie(self):
- fn = self.daemon.auth_cookie_fn
- return get_lines_from_file(fn,'cookie',quiet=True)[0] if os.access(fn,os.R_OK) else ''
- def info(self,info_id):
- def segwit_is_active():
- if 'deployment_info' in self.caps:
- return (
- self.cached['deploymentinfo']['deployments']['segwit']['active']
- or ( g.test_suite and not self.chain == 'regtest' )
- )
- d = self.cached['blockchaininfo']
- try:
- if d['softforks']['segwit']['active'] == True:
- return True
- except:
- pass
- try:
- if d['bip9_softforks']['segwit']['status'] == 'active':
- return True
- except:
- pass
- if g.test_suite and not self.chain == 'regtest':
- return True
- return False
- return locals()[info_id]()
- rpcmethods = (
- 'backupwallet',
- 'createrawtransaction',
- 'decoderawtransaction',
- 'disconnectnode',
- 'estimatefee',
- 'estimatesmartfee',
- 'getaddressesbyaccount',
- 'getaddressesbylabel',
- 'getblock',
- 'getblockchaininfo',
- 'getblockcount',
- 'getblockhash',
- 'getblockheader',
- 'getblockstats', # mmgen-node-tools
- 'getmempoolinfo',
- 'getmempoolentry',
- 'getnettotals',
- 'getnetworkinfo',
- 'getpeerinfo',
- 'getrawmempool',
- 'getmempoolentry',
- 'getrawtransaction',
- 'gettransaction',
- 'importaddress', # address (address or script) label rescan p2sh (Add P2SH version of the script)
- 'listaccounts',
- 'listlabels',
- 'listunspent',
- 'setlabel',
- 'sendrawtransaction',
- 'signrawtransaction',
- 'signrawtransactionwithkey', # method new to Core v0.17.0
- 'validateaddress',
- 'walletpassphrase',
- )
|