# assert keys == ['mpsMLTNqBNrsQuYNmZPj7ifqqMTSnZMMWH', 'mjoj9a1cFNPhvFkbrwzNPTBCWxhteAJHE5', # 'mkjqteuKMDApEzsZbdphtufvVPmCFafLhM', 'mvRSS7xmYBjDUEQsxvNefXLbwQHpwm76wb', # 'mhGBcrA9xDuBWttQLZFGRBJHcGEZyQpT3b', 'mkYFhxXQY6mMZbKxcuk6j6FD2Ff1gX6zgC', # 'mmgkFCdHKxCHuTMcJ9CPncRMA2UPainW6j', 'mg5fNCy7TJiZ8L4uxU3XerW2twNYAY3hmU', # 'myY1Xmhx6CdvFn6uzdUDo5EM2HxmsPXPJB', 'mozpwp3z32g9vBZxbpN6ySxx7A5EWw4Zfi'] addr = BitcoinMain.p2sh_address(AF_P2SH, script) assert addr[0] == '3' assert addr == '3Kt6KxjirrFS7GexJiXLLhmuaMzSbjp275' addr = BitcoinTestnet.p2sh_address(AF_P2SH, script) assert addr[0] == '2' assert addr == '2NBSJPhfkUJknK4HVyr9CxemAniCcRfhqp4' addr = BitcoinMain.p2sh_address(AF_P2WSH, script) assert addr[0:4] == 'bc1q', addr assert len(addr) >= 62 assert addr == 'bc1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkqftu4jr' addr = BitcoinTestnet.p2sh_address(AF_P2WSH, script) assert addr[0:4] == 'tb1q', addr assert len(addr) >= 62 assert addr == 'tb1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkq7r26gv' if 1: from utils import xfp2str, str2xfp assert xfp2str(0x10203040) == '40302010' for i in 0, 1, 0x12345678: assert str2xfp(xfp2str(i)) == i
async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH): # collect all xpub- exports on current SD card (must be > 1) # - ask for M value # - create wallet, save and also export # - also create electrum skel to go with that # - only expected to work with our ccxp-foo.json export files. from actions import file_picker import uos, ujson from utils import get_filesize from main import settings chain = chains.current_chain() my_xfp = settings.get('xfp') xpubs = [] files = [] has_mine = False deriv = None try: with CardSlot() as card: for path in card.get_paths(): for fn, ftype, *var in uos.ilistdir(path): if ftype == 0x4000: # ignore subdirs continue if not fn.startswith('ccxp-') or not fn.endswith('.json'): # wrong prefix/suffix: ignore continue full_fname = path + '/' + fn # Conside file size # sigh, OS/filesystem variations file_size = var[1] if len(var) == 2 else get_filesize( full_fname) if not (0 <= file_size <= 1000): # out of range size continue try: with open(full_fname, 'rt') as fp: vals = ujson.load(fp) ln = vals.get(mode) # value in file is BE32, but we want LE32 internally xfp = str2xfp(vals['xfp']) if not deriv: deriv = vals[mode + '_deriv'] else: assert deriv == vals[mode + '_deriv'], "wrong derivation" node, _, _ = import_xpub(ln) if xfp == my_xfp: has_mine = True xpubs.append( (xfp, chain.serialize_public(node, AF_P2SH))) files.append(fn) except CardMissingError: raise except Exception as exc: # show something for coders, but no user feedback sys.print_exception(exc) continue except CardMissingError: await needs_microsd() return # remove dups; easy to happen if you double-tap the export delme = set() for i in range(len(xpubs)): for j in range(len(xpubs)): if j in delme: continue if i == j: continue if xpubs[i] == xpubs[j]: delme.add(j) if delme: xpubs = [x for idx, x in enumerate(xpubs) if idx not in delme] if not xpubs or len(xpubs) == 1 and has_mine: await ux_show_story( "Unable to find any Coldcard exported keys on this card. Must have filename: ccxp-....json" ) return # add myself if not included already if not has_mine: with stash.SensitiveValues() as sv: node = sv.derive_path(deriv) xpubs.append((my_xfp, chain.serialize_public(node, AF_P2SH))) N = len(xpubs) if N > MAX_SIGNERS: await ux_show_story("Too many signers, max is %d." % MAX_SIGNERS) return # pick useful M value to start assert N >= 2 M = (N - 1) if N < 4 else ((N // 2) + 1) while 1: msg = '''How many need to sign?\n %d of %d Press (7 or 9) to change M value, or OK \ to continue. If you expected more or less keys (N=%d #files=%d), \ then check card and file contents. Coldcard multisig setup file and an Electrum wallet file will be created automatically.\ ''' % (M, N, N, len(files)) ch = await ux_show_story(msg, escape='123479') if ch in '1234': M = min(N, int(ch)) # undocumented shortcut elif ch == '9': M = min(N, M + 1) elif ch == '7': M = max(1, M - 1) elif ch == 'x': await ux_dramatic_pause('Aborted.', 2) return elif ch == 'y': break # create appropriate object assert 1 <= M <= N <= MAX_SIGNERS name = 'CC-%d-of-%d' % (M, N) ms = MultisigWallet(name, (M, N), xpubs, chain_type=chain.ctype, common_prefix=deriv[2:], addr_fmt=addr_fmt) from auth import NewEnrollRequest, active_request active_request = NewEnrollRequest(ms, auto_export=True) # menu item case: add to stack from ux import the_ux the_ux.push(active_request)
def from_file(cls, config, name=None): # Given a simple text file, parse contents and create instance (unsaved). # format is: label: value # where label is: # name: nameforwallet # policy: M of N # (8digithex): xpub of cosigner # # quick checks: # - name: 1-20 ascii chars # - M of N line (assume N of N if not spec'd) # - xpub: any bip32 serialization we understand, but be consistent # from main import settings my_xfp = settings.get('xfp') common_prefix = None xpubs = [] path_tops = set() M, N = -1, -1 has_mine = False addr_fmt = AF_P2SH expect_chain = chains.current_chain().ctype lines = config.split('\n') for ln in lines: # remove comments comm = ln.find('#') if comm != -1: ln = ln[0:comm] ln = ln.strip() if ':' not in ln: if 'pub' in ln: # optimization: allow bare xpub if we can calc xfp label = '0' * 8 value = ln else: # complain? if ln: print("no colon: " + ln) continue else: label, value = ln.split(':') label = label.lower() value = value.strip() if label == 'name': name = value elif label == 'policy': try: # accepts: 2 of 3 2/3 2,3 2 3 etc mat = ure.search(r'(\d+)\D*(\d+)', value) assert mat M = int(mat.group(1)) N = int(mat.group(2)) assert 1 <= M <= N <= MAX_SIGNERS except: raise AssertionError('bad policy line') elif label == 'derivation': # reveal the **common** path derivation for all keys try: mat = ure.search(r"(m/)([0123456789/']+)", value) assert mat common_prefix = mat.group(2) assert common_prefix assert 1 <= len(common_prefix) < 30 except: raise AssertionError('bad derivation line') elif label == 'format': # pick segwit vs. classic vs. wrapped version value = value.lower() for fmt_code, fmt_label in cls.FORMAT_NAMES: if value == fmt_label: addr_fmt = fmt_code break else: raise AssertionError('bad format line') elif len(label) == 8: try: xfp = str2xfp(label) except: # complain? #print("Bad xfp: " + ln) continue # deserialize, update list and lots of checks xfp = cls.check_xpub(xfp, value, expect_chain, xpubs, path_tops) if xfp == my_xfp: # not conclusive, but enough for error catching. has_mine = True assert len(xpubs), 'need xpubs' if M == N == -1: # default policy: all keys N = M = len(xpubs) if not name: # provide a default name name = '%d-of-%d' % (M, N) try: name = str(name, 'ascii') assert 1 <= len(name) <= 20 except: raise AssertionError('name must be ascii, 1..20 long') assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range' assert N == len(xpubs), 'wrong # of xpubs, expect %d' % N assert addr_fmt & AFC_SCRIPT, 'script style addr fmt' # check we're included... do not insert ourselves, even tho we # have enough info, simply because other signers need to know my xpubkey anyway assert has_mine, 'my key not included' if not common_prefix and len(path_tops) == 1: # fill in the common prefix iff we can deduce it from xpubs common_prefix = path_tops.pop() # done. have all the parts return cls(name, (M, N), xpubs, addr_fmt=addr_fmt, chain_type=expect_chain, common_prefix=common_prefix)
def from_file(cls, config, name=None): # Given a simple text file, parse contents and create instance (unsaved). # format is: label: value # where label is: # name: nameforwallet # policy: M of N # format: p2sh (+etc) # derivation: m/45'/0 (common prefix) # (8digithex): xpub of cosigner # # quick checks: # - name: 1-20 ascii chars # - M of N line (assume N of N if not spec'd) # - xpub: any bip32 serialization we understand, but be consistent # from main import settings my_xfp = settings.get('xfp') deriv = None xpubs = [] M, N = -1, -1 has_mine = 0 addr_fmt = AF_P2SH expect_chain = chains.current_chain().ctype lines = config.split('\n') for ln in lines: # remove comments comm = ln.find('#') if comm == 0: continue if comm != -1: if not ln[comm+1:comm+2].isdigit(): ln = ln[0:comm] ln = ln.strip() if ':' not in ln: if 'pub' in ln: # pointless optimization: allow bare xpub if we can calc xfp label = '0'*8 value = ln else: # complain? #if ln: print("no colon: " + ln) continue else: label, value = ln.split(':', 1) label = label.lower() value = value.strip() if label == 'name': name = value elif label == 'policy': try: # accepts: 2 of 3 2/3 2,3 2 3 etc mat = ure.search(r'(\d+)\D*(\d+)', value) assert mat M = int(mat.group(1)) N = int(mat.group(2)) assert 1 <= M <= N <= MAX_SIGNERS except: raise AssertionError('bad policy line') elif label == 'derivation': # reveal the path derivation for following key(s) try: assert value, 'blank' deriv = cleanup_deriv_path(value) except BaseException as exc: raise AssertionError('bad derivation line: ' + str(exc)) elif label == 'format': # pick segwit vs. classic vs. wrapped version value = value.lower() for fmt_code, fmt_label in cls.FORMAT_NAMES: if value == fmt_label: addr_fmt = fmt_code break else: raise AssertionError('bad format line') elif len(label) == 8: try: xfp = str2xfp(label) except: # complain? #print("Bad xfp: " + ln) continue # deserialize, update list and lots of checks is_mine = cls.check_xpub(xfp, value, deriv, expect_chain, my_xfp, xpubs) if is_mine: has_mine += 1 assert len(xpubs), 'need xpubs' if M == N == -1: # default policy: all keys N = M = len(xpubs) if not name: # provide a default name name = '%d-of-%d' % (M, N) try: name = str(name, 'ascii') assert 1 <= len(name) <= 20 except: raise AssertionError('name must be ascii, 1..20 long') assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range' assert N == len(xpubs), 'wrong # of xpubs, expect %d' % N assert addr_fmt & AFC_SCRIPT, 'script style addr fmt' # check we're included... do not insert ourselves, even tho we # have enough info, simply because other signers need to know my xpubkey anyway assert has_mine != 0, 'my key not included' assert has_mine == 1 # 'my key included more than once' # done. have all the parts return cls(name, (M, N), xpubs, addr_fmt=addr_fmt, chain_type=expect_chain)