firmware/shared/wallet.py
2024-03-21 14:36:23 -04:00

124 lines
4.1 KiB
Python

# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# wallet.py - A place you find UTXO, addresses and descriptors.
#
import chains
from descriptor import Descriptor
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from stash import blank_object, SensitiveValues
class WalletABC:
# How to make this ABC useful without consuming memory/code space??
# - be more of an "interface" than a base class
# name
# addr_fmt
# chain
def yield_addresses(self, start_idx, count, change_idx=0, censored=True):
# TODO: returns various expected tuples?
pass
def render_address(self, change_idx, idx):
# make one single address
tmp = list(self.yield_addresses(idx, 1, change_idx=change_idx, censored=False))
assert len(tmp) == 1
assert tmp[0][0] == idx
return tmp[0][1]
def to_descriptor(self):
pass
class MasterSingleSigWallet(WalletABC):
# Refers to current master seed phrase, whichever is loaded
def __init__(self, addr_fmt, path=None, account_idx=0, chain_name=None):
# Construct a wallet based on current master secret, and chain.
# - path is optional, and then we use standard path for addr_fmt
# - path can be overriden when we come here via address explorer
if addr_fmt == AF_P2WPKH:
n = 'Segwit'
prefix = path or 'm/84h/{coin_type}h/{account}h'
elif addr_fmt == AF_CLASSIC:
n = 'Classic'
prefix = path or 'm/44h/{coin_type}h/{account}h'
elif addr_fmt == AF_P2WPKH_P2SH:
n = 'P2WPKH-in-P2SH'
prefix = path or 'm/49h/{coin_type}h/{account}h'
else:
raise ValueError(addr_fmt)
if chain_name:
self.chain = chains.get_chain(chain_name)
else:
self.chain = chains.current_chain()
if self.chain.ctype != 'BTC':
n += ' ' + self.chain.menu_name
self.name = n
self.addr_fmt = addr_fmt
# Figure out the derivation path
# - we want to store path w/o change and index part
p = prefix.format(account=account_idx, coin_type=self.chain.b44_cointype,
change='C', idx='I')
if p.endswith('/C/I'):
p = p[:-4]
if p.endswith('/I'):
# custom path in addr explorer can get this
p = p[:-2]
self._path = p
def yield_addresses(self, start_idx, count, change_idx=None, censored=True):
# Render a range of addresses. Slow to start, since accesses SE in general
# - if count==1, don't derive any subkey, just do path.
path = self._path
if change_idx is not None:
assert 0 <= change_idx <= 1
path += '/%d' % change_idx
with SensitiveValues() as sv:
node = sv.derive_path(path)
if count == 1:
address = self.chain.address(node, self.addr_fmt)
yield 0, address, path
return
path += '/'
for idx in range(start_idx, start_idx+count):
try:
here = node.copy()
here.derive(idx, False) # works in-place
address = self.chain.address(here, self.addr_fmt)
finally:
here.blank()
del here
yield idx, address, path+str(idx)
def render_address(self, change_idx, idx):
# Optimized for single address.
path = self._path + '/%d/%d' % (change_idx, idx)
with SensitiveValues() as sv:
node = sv.derive_path(path)
return self.chain.address(node, self.addr_fmt)
def render_path(self, change_idx, idx):
return self._path + '/%d/%d' % (change_idx, idx)
def to_descriptor(self):
from glob import settings
xfp = settings.get('xfp')
xpub = settings.get('xpub')
keys = (xfp, self._path, xpub)
return Descriptor([keys], self.addr_fmt)
# EOF