# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# actions.py
#
# Every function here is called directly by a menu item. They should all be async.
#
import ckcc, pyb, version, uasyncio, sys, uos, chains
from uhashlib import sha256
from uasyncio import sleep_ms
from ubinascii import hexlify as b2a_hex
from utils import imported, problem_file_line, get_filesize, encode_seed_qr
from utils import xfp2str, B2A, txid_from_fname, wipe_if_deltamode
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X, ux_render_words
from export import export_contents, make_summary_file, make_descriptor_wallet_export
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
from export import generate_unchained_export, generate_electrum_wallet, make_key_expression_export
from files import CardSlot, CardMissingError, needs_microsd
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH
from glob import settings
from pincodes import pa
from menu import start_chooser, MenuSystem, MenuItem
from version import MAX_TXN_LEN
from charcodes import KEY_NFC, KEY_QR, KEY_CANCEL


async def start_selftest(*args):
    # selftest is harmless, no need to warn anymore,
    # but this layer saves memory in typical cases
    with imported('selftest') as st:
        await st.start_selftest()

async def show_bag_number(*a):
    import callgate
    bn = callgate.get_bag_number() or 'UNBAGGED!'

    await ux_show_story('''\
Your new Coldcard should have arrived SEALED in a bag with the above number. Please take a moment to confirm the number and look for any signs of tampering.

Take pictures and contact support@coinkite if you have concerns.''', title=bn)

async def accept_terms(*a):
    # do nothing if they have accepted the terms once (ever), otherwise
    # force them to read message...

    if settings.get('terms_ok'):
        return

    while 1:
        ch = await ux_show_story("""\
By using this product, you are accepting our Terms of Sale and Use.

Read the full document at:
  coldcard.com/legal

Press %s to accept terms and continue.""" % OK, escape='7')

        if ch == 'y':
            break

    await show_bag_number()

    # Note fact they accepted the terms. Annoying to do more than once.
    settings.set('terms_ok', 1)
    settings.save()

async def view_ident(*a):
    # show the XPUB, and other ident on screen
    import callgate, stash

    msg = ""
    if stash.bip39_passphrase:
        msg += 'BIP-39 passphrase is in effect.\n\n'
    elif pa.tmp_value:
        msg += 'Temporary seed is in effect.\n\n'

    tpl = '''\
Master Key Fingerprint:

  {xfp}

USB Serial Number:

  {serial}

Extended Master Key:

{xpub}
'''
    my_xfp = settings.get('xfp', 0)
    xpub = settings.get('xpub', None)
    msg += tpl.format(xpub=(xpub or '(none yet)'),
                      xfp=xfp2str(my_xfp),
                      serial=version.serial_number())

    bn = callgate.get_bag_number()
    if bn:
        msg += '\nShipping Bag:\n  %s\n' % bn

    if xpub:
        msg += '\nPress %s to show QR code of xpub.' % (
            KEY_QR if version.has_qwerty else "(3)"
        )

    ch = await ux_show_story(msg, escape=('3'+KEY_QR if xpub else None))

    if ch in '3'+KEY_QR:
        # show the QR
        from ux import show_qr_code
        await show_qr_code(xpub, False)


async def show_settings_space(*a):
    percentage_capacity = int(settings.get_capacity() * 100)
    if percentage_capacity < 10:
        percentage_capacity = 10
    await ux_show_story('Settings storage space in use:\n\n'
                        '       %d%%' % percentage_capacity)

async def show_mcu_keys_left(*a):
    import callgate
    avail, used, total = callgate.mcu_key_usage()
    await ux_show_story('MCU key slots remaining:\n\n    %d of %d' % (avail, total))


async def maybe_dev_menu(*a):
    from version import is_devmode

    if not is_devmode:
        ok = await ux_confirm('Developer features could be used to weaken security or release key material.\n\nDo not proceed unless you know what you are doing and why.')

        if not ok:
            return None

    from flow import DevelopersMenu
    return DevelopersMenu

async def dev_enable_vcp(*a):
    # Enable USB serial port emulation, for devs.
    # Mk3 and earlier only.
    #
    from usb import is_vcp_active

    if is_vcp_active():
        await ux_show_story("""The USB virtual serial port is already enabled.""")
        return

    was = pyb.usb_mode()
    pyb.usb_mode(None)
    if was and 'MSC' in was:
        pyb.usb_mode('VCP+MSC')
    else:
        pyb.usb_mode('VCP+HID')

    # allow REPL access
    ckcc.vcp_enabled(True)

    await ux_show_story("""\
The USB virtual serial port has now been enabled. Use a real computer to connect to it.""")

async def dev_enable_disk(*a):
    # Enable disk emulation, which allows them to change code.
    # Mk3 and earlier only.
    #
    cur = pyb.usb_mode()

    if cur and 'MSC' in cur:
        await ux_show_story("""The USB disk emulation is already enabled.""")
        return

    # serial port and disk (but no HID-based USB protocol)
    pyb.usb_mode(None)
    pyb.usb_mode('VCP+MSC')

    await ux_show_story("""\
The disk emulation has now been enabled. Your code can go into /lib. \
Keep tmp files and other junk out!""")


async def dev_enable_protocol(*a):
    # Turn off disk emulation. Keep VCP enabled, since they are still devs.
    # Mk3 and earlier
    cur = pyb.usb_mode()
    if cur and 'HID' in cur:
        await ux_show_story('Coldcard USB protocol is already enabled (HID mode)')
        return

    if settings.get('du', 0):
        await ux_show_story('USB disabled in settings.')
        return

    # might need to reset stuff?
    from usb import enable_usb

    # reset and re-enable
    pyb.usb_mode(None)
    enable_usb()

    # enable REPL
    ckcc.vcp_enabled(True)

    await ux_show_story('Back to normal USB mode.')

async def microsd_upgrade(menu, label, item):
    # Upgrade vis MicroSD card
    # - search for a particular file
    # - verify it lightly
    # - erase serial flash
    # - copy it over (slow)
    # - reboot into bootloader, which finishes install
    from glob import dis, PSRAM
    from files import dfu_parse
    from utils import check_firmware_hdr
    from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_MAX_LENGTH_MK4

    if version.has_battery:
        import battery
        lvl = battery.get_batt_threshold()
        if lvl is not None and lvl <= 1:
            await ux_show_story("Battery power is somewhat low right now. "
"Please install fresh batteries or connect USB power during upgrade process.", title="Low Battery")
            return

    force_vdisk = item.arg
    fn = await file_picker(suffix='.dfu', min_size=0x7800, max_size=FW_MAX_LENGTH_MK4,
                           force_vdisk=force_vdisk)

    if not fn: return

    failed = None
    with CardSlot(force_vdisk=force_vdisk) as card:
        with card.open(fn, 'rb') as fp:
            try:
                offset, size = dfu_parse(fp)

                # read just the signature header
                hdr = bytearray(FW_HEADER_SIZE)
                fp.seek(offset + FW_HEADER_OFFSET)
                rv = fp.readinto(hdr)
                assert rv == FW_HEADER_SIZE

                # check header values
                failed = check_firmware_hdr(hdr, size)
            except Exception as exc:
                # recover from garbage DFU files.
                failed = "Failed: " + str(exc)

            if not failed:
                # copy binary into PSRAM
                fp.seek(offset)

                dis.fullscreen("Loading...")

                buf = bytearray(0x20000)
                pos = 0
                while pos < size:
                    dis.progress_bar_show(pos/size)

                    here = fp.readinto(buf)
                    if not here: break

                    PSRAM.write(pos, buf)
                    pos += here

    if failed:
        await ux_show_story(failed, title='Sorry!')
        return

    # continue process...
    from auth import FirmwareUpgradeRequest
    m = FirmwareUpgradeRequest(hdr, size, psram_offset=0)
    the_ux.push(m)

async def start_dfu(*a):
    from callgate import enter_dfu
    enter_dfu(0)
    # NOT REACHED

async def reset_self(*a):
    import machine
    machine.soft_reset()
    # NOT REACHED

async def initial_pin_setup(*a):
    # First time they select a PIN of any type.
    from login import LoginUX
    lll = LoginUX()
    title = 'Choose PIN'

    ch = await ux_show_story('''\
Pick the main wallet's PIN code now. Be more clever, but an example:

123-4567

It has two parts: prefix (123-) and suffix (-4567). \
Each part must be between 2 to 6 digits long. Total length \
can be as long as 12 digits.

The prefix part determines the anti-phishing words you will \
see each time you login.

Your new PIN protects access to 
this Coldcard device and is not a factor in the wallet's \
seed words or private keys.

THERE IS ABSOLUTELY NO WAY TO RECOVER A FORGOTTEN PIN! Write it down.
''', title=title)
    if ch != 'y': return

    while 1:
        ch = await ux_show_story('''\
There is ABSOLUTELY NO WAY to 'reset the PIN' or 'factory reset' the Coldcard if you forget the PIN.

DO NOT FORGET THE PIN CODE.

Press (6) to prove you read to the end of this message.''', title='WARNING', escape='6')

        if ch == 'x': return
        if ch == '6': break

    # do the actual picking
    pin = await lll.get_new_pin()
    del lll

    if pin is None: return

    # A new pin is to be set!
    from glob import dis
    dis.fullscreen("Saving...")

    try:
        dis.busy_bar(True)
        assert pa.is_blank()

        pa.change(new_pin=pin)

        # check it? kinda, but also get object into normal "logged in" state
        pa.setup(pin)
        ok = pa.login()
        assert ok

        # must re-read settings after login, because they are encrypted
        # with a key derived from the main secret.
        settings.set_key()
        settings.load()
    except Exception as e:
        print("Exception: %s" % e)
    finally:
        dis.busy_bar(False)

    # Allow USB protocol, now that we are auth'ed
    from usb import enable_usb
    enable_usb()

    from flow import EmptyWallet
    return MenuSystem(EmptyWallet)

async def block_until_login():
    #
    # Force user to enter a valid PIN.
    # 
    from login import LoginUX
    from ux import AbortInteraction

    # maybe show a calculator rather than login screen
    try:
        if version.has_qwerty and settings.get('calc', 0):
            with imported('calc') as calc:
                await calc.login_repl()
            return 
    except:
        pass

    # do they want a randomized (shuffled) keypad?
    rnd_keypad = settings.get('rngk', 0)

    # single key that "kills" self if pressed on "words" screen
    kill_btn = settings.get('kbtn', None)

    rv = None       # might already be logged-in if _skip_pin used

    while not pa.is_successful():
        lll = LoginUX(rnd_keypad, kill_btn)

        try:
            rv = await lll.try_login(bypass_pin=None)
            if rv: break
        except AbortInteraction:
            # not allowed!
            pass

    return rv

async def show_nickname(nick):
    # Show a nickname for this coldcard (as a personalization)
    # - no keys here, just show it until they press anything
    from glob import dis
    from ux import ux_wait_keyup
    import callgate

    dis.clear()

    if dis.has_lcd:
        from lcd_display import CHARS_W, CHARS_H
        from utils import word_wrap

        lines = list(word_wrap(nick, CHARS_W))
        y = ((CHARS_H - len(lines) + 1) // 2) - 1
        for n, ln in enumerate(lines):
            dis.text(None, y+n, ln)
    else:
        from display import FontLarge, FontSmall

        if dis.width(nick, FontLarge) <= dis.WIDTH:
            dis.text(None, 21, nick, font=FontLarge)
        else:
            dis.text(None, 27, nick, font=FontSmall)

    dis.show()

    ch = await ux_wait_keyup(flush=True)

    if ch.upper() == settings.get('kbtn', None):
        # support kill btn on nick screen too
        callgate.fast_wipe(False)
        

async def pick_killkey(*a):
    # Setting: kill seed sometimes (requires mk4)
    if version.has_qwerty:
        msg = '''\
If you press this key at any point during login, your seed phrase will be immediately wiped.'''
    else:
        msg = '''\
If you press this key while the anti- phishing words are shown during login, \
your seed phrase will be immediately wiped.

Best if this does not match the first number of the second half of your PIN.'''

    if await ux_show_story(msg) != 'y':
        return

    from choosers import kill_key_chooser
    start_chooser(kill_key_chooser)

async def pick_scramble(*a):
    # Setting: scrambled keypad or normal
    if await ux_show_story("When entering PIN, randomize the order of the key numbers, "
            "so that cameras and shoulder-surfers are defeated.") != 'y':
        return

    from choosers import scramble_keypad_chooser
    start_chooser(scramble_keypad_chooser)

async def pick_nickname(*a):
    # from settings menu, enter a nickname
    from nvstore import SettingsObject

    # Value is not stored with normal settings, it's part of "prelogin" settings
    # which are encrypted with zero-key.
    s = SettingsObject.prelogin()
    nick = s.get('nick', '')

    if not nick:
        ch = await ux_show_story('''\
You can give this Coldcard a nickname and it will be shown before login.''')
        if ch != 'y': return

    nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname")

    from glob import dis
    dis.fullscreen("Saving...")
    dis.busy_bar(True)

    nn = nn.strip() if nn else None
    s.set('nick', nn)
    s.save()
    dis.busy_bar(False)
    del s


async def logout_now(*a):
    # wipe memory and lock up
    from utils import clean_shutdown
    clean_shutdown()

async def login_now(*a):
    # wipe memory and reboot
    from utils import clean_shutdown
    clean_shutdown(2)


async def virgin_help(*a):
    await ux_show_story("""\
8 = Down (do it!)
5 = Up
OK = Checkmark
X = Cancel/Back
0 = Go to top

More on our website:

 coldcardwallet
           .com
""")

async def start_seed_import(menu, label, item):
    import seed
    if version.has_qwerty:
        from ux_q1 import seed_word_entry
        await seed_word_entry('Enter Seed Words', item.arg,
                                            done_cb=seed.commit_new_words)
    else:
        return seed.WordNestMenu(item.arg)

async def pick_new_seed(menu, label, item):
    import seed
    return await seed.make_new_wallet(item.arg)

async def new_from_dice(menu, label, item):
    import seed
    return await seed.new_from_dice(item.arg)

async def any_active_duress_ux():
    from trick_pins import tp
    # if TPs are hidden this msg will not be shown
    if any(tp.get_duress_pins()):
        await ux_show_story('You have one or more duress wallets defined '
                            'under Trick PINs. Please empty them, and clear '
                            'associated Trick PINs before continuing.')
        return True
    return False

async def convert_ephemeral_to_master(*a):
    import seed
    from stash import bip39_passphrase

    words = settings.get("words", True)
    _type = 'BIP-39 passphrase' if bip39_passphrase else 'temporary seed'
    msg = 'Convert currently used %s to master seed. Old master seed' % _type
    if words or bip39_passphrase:
        msg += ' words themselves are erased forever, '
    else:
        msg += ' is erased forever, '

    msg += ('and its settings blanked. This action is destructive '
            'and may affect funds, if any, on old master seed. '
            'Make sure all duress wallets associated with previous '
            'seed are deleted, otherwise they will be carried forward '
            'without being properly generated from new master seed. '
            'Saved temporary seed settings and Seed Vault are lost. ')

    if bip39_passphrase:
        msg += ('BIP-39 passphrase '
                'is captured during this process and will be in effect '
                'going forward, but the passphrase itself is erased '
                'and unrecoverable. ')
    if not words:
        msg += 'The resulting wallet cannot be used with any other passphrase. '

    msg += 'A reboot is part of this process. '
    msg += 'PIN code, and %s funds are not affected.' % _type
    if not await ux_confirm(msg, confirm_key='4'):
        return await ux_aborted()

    # settings.save is part of re-building fs
    await seed.remember_ephemeral_seed()
    await login_now()

async def clear_seed(*a):
    # Erase the seed words, and private key from this wallet!
    # This is super dangerous for the customer's money.
    import seed

    # in hobble mode, they cannot reach duress wallets and/or maybe we don't
    # want to reveal them? So don't block them based on that.
    if not pa.hobbled_mode:
        if await any_active_duress_ux():
            return await ux_aborted()

    if not await ux_confirm('Wipe seed words and reset wallet. '
                            'All funds will be lost. '
                            'You better have a backup of the seed words. '
                            'All settings like multisig wallets are also wiped. '
                            'Saved temporary seed settings and Seed Vault are lost. '
                            'Trick PINs are also completely removed.'):
        return await ux_aborted()

    if not await ux_confirm('''Are you REALLY sure though???\n\n\
This action will certainly cause you to lose all funds associated with this wallet, \
unless you have a backup of the seed words and know how to import them into a \
new wallet.''', 'AGAIN...', confirm_key='4'):
        return await ux_aborted()

    # clear all trick PINs from SE2
    from trick_pins import tp
    tp.clear_all()

    # clear settings, address cache, settings from tmp seeds / seedvault seeds
    from files import wipe_flash_filesystem
    wipe_flash_filesystem(False)

    seed.clear_seed()
    # NOT REACHED -- reset happens


def render_master_secrets(mode, raw, node):
    # Render list of words, or XPRV / master secret to text.
    import stash, chains

    c = chains.current_chain()
    qr_alnum = False

    if mode == 'words':
        import bip39
        words = bip39.b2a_words(raw).split(' ')

        # This optimization make the QR very nice, and space for
        # all the words too
        qr = ' '.join(w[0:4] for w in words)
        qr_alnum = True

        title = 'Seed words (%d):' % len(words)
        msg = ""
        if not version.has_qwerty:
            msg += title + "\n"
            title = None

        msg += ux_render_words(words)

        if stash.bip39_passphrase:
            msg += '\n\nBIP-39 Passphrase:\n    *****'
            if node:
                msg += '\n\nSeed+Passphrase:\n%s' % c.serialize_private(node)


    elif mode == 'xprv':
        title = "Extended Private Key" if version.has_qwerty else None
        msg = c.serialize_private(node)
        qr = msg

    elif mode == 'master':
        title = "Master Secret" if version.has_qwerty else None
        msg = '%d bytes:\n\n' % len(raw)
        qr = str(b2a_hex(raw), 'ascii')
        msg += qr
    else:
        raise ValueError(mode)

    return title, msg, qr, qr_alnum

async def view_seed_words(*a):
    if not await ux_confirm('The next screen will show the seed words'
                            ' (and if defined, your BIP-39 passphrase).'
                            '\n\nAnyone with knowledge of those words '
                            'can control all funds in this wallet.'):
        return

    import stash
    from glob import dis, NFC

    dis.fullscreen("Wait...")
    dis.busy_bar(True)

    # preserve old UI where we show words + passphrase
    # instead of just calculated seed + passphrase = extended privkey
    # new: calculated xprv is now also shown for BIP39 passphrase wallet
    raw = mode = None
    if stash.bip39_passphrase:
        # get main secret - bypass tmp
        with stash.SensitiveValues(bypass_tmp=True, enforce_delta=True) as sv:
            assert sv.mode == "words"
            raw = sv.raw[:]
            mode = sv.mode

        stash.SensitiveValues.clear_cache()

    with stash.SensitiveValues(bypass_tmp=False, enforce_delta=True) as sv:
        dis.busy_bar(False)
        title, msg, qr, qr_alnum = render_master_secrets(mode or sv.mode,
                                                         raw or sv.raw,
                                                         sv.node)
        esc = "1"
        if not version.has_qwerty:
            msg += '\n\nPress (1) to view as QR Code'
            if NFC:
                msg += ", (3) to share via NFC"
                esc += "3"
            msg += "."

        while 1:
            ch = await ux_show_story(msg, title=title, sensitive=True, escape=esc,
                                     hint_icons=KEY_QR+(KEY_NFC if NFC else ''))
            if ch in '1'+KEY_QR:
                from ux import show_qr_code
                await show_qr_code(qr, qr_alnum, is_secret=True)
                continue
            elif NFC and (ch in '3'+KEY_NFC):
                await NFC.share_text(qr, is_secret=True)
                continue
            break

    stash.blank_object(qr)
    stash.blank_object(msg)
    stash.blank_object(raw)

async def export_seedqr(*a):
    # see standard: <https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md>
    import bip39, stash

    if not await ux_confirm('The next screen will show the seed words in a QR code.'
                            '\n\nAnyone with knowledge of those words '
                            'can control all funds in this wallet.'):
        return

    from glob import dis
    dis.fullscreen("Wait...")
    dis.busy_bar(True)

    # Note: cannot reach this menu item if no words. If they are tmp, that's cool.

    with stash.SensitiveValues(bypass_tmp=False, enforce_delta=True) as sv:
        if sv.mode != 'words':
            raise ValueError(sv.mode)

        words = bip39.b2a_words(sv.raw).split(' ')

        dis.busy_bar(False)
        qr = encode_seed_qr(words)

        del words

    from ux import show_qr_code
    await show_qr_code(qr, True, msg="SeedQR", is_secret=True)

    stash.blank_object(qr)

async def version_migration():
    # Handle changes between upgrades, and allow downgrades when possible.
    # - long term we generally cannot delete code from here, because we
    #   never know when a user might skip a bunch of intermediate versions

    # Data migration issue:
    # - "login countdown" feature now stored elsewhere [mk3]
    had_delay = settings.get('lgto', 0)
    if had_delay:
        from nvstore import SettingsObject
        settings.remove_key('lgto')
        s = SettingsObject.prelogin()
        s.set('lgto', had_delay)
        s.save()
        del s

    # Disable vdisk so it is off by default until re-enabled, after
    # version 5.0.6 is installed
    settings.remove_key('vdsk')

    # 6.4.0 multisig migration
    from wallet import do_640_multisig_migration
    await do_640_multisig_migration()

async def version_migration_prelogin():
    # same, but for setting before login
    # these have moved into SE2 for Mk4 and so can be removed
    for n in [ 'cd_lgto', 'cd_mode', 'cd_pin' ]:
        settings.remove_key(n)

async def start_login_sequence():
    # Boot up login sequence here.
    #
    # - easy to brick units here, so catch and ignore errors where possible/appropriate
    #
    from ux import idle_logout, ux_login_countdown
    from glob import dis
    from imptask import IMPT

    if pa.is_blank():
        # Blank devices, with no PIN set all, can continue w/o login
        goto_top_menu()
        return

    # data migration on settings that are used pre-login
    try:
        await version_migration_prelogin()
    except: pass

    if version.has_battery:
        from battery import batt_idle_logout
        IMPT.start_task('b-idle', batt_idle_logout())

    # maybe show a nickname before we do anything
    try:
        nickname = settings.get('nick', None)
        if nickname:
            await show_nickname(nickname)
    except: pass

    # Allow impatient devs and crazy people to skip the PIN
    guess = settings.get('_skip_pin', None)
    if guess is not None:
        try:
            dis.fullscreen("Skip PIN...")
            pa.setup(guess)
            pa.login()
        except: pass

    # If that didn't work, or no skip defined, force
    # them to login successfully.
    sp_unlock = False
    try:
        from trick_pins import tp

        # Get a PIN and try to use it to login
        # - does warnings about attempt usage counts
        await block_until_login()

        sp_unlock = tp.was_sp_unlock()
        if sp_unlock:
            # Trying to unlock spending policy: ask for main PIN next.
            await ux_show_story("Spending Policy Unlock: Please provide Main PIN next.")
            pa.reset()
            await block_until_login()

            # we don't really know if that was the Main PIN (could easily be the bypass
            # PIN again) and if it's a duress wallet, that's cool...

        # Do we need to do countdown delay? (real or otherwise)
        # - wiping has already occured if that was selected by trick details
        # - delay is variable, stored in tc_arg
        delay = tp.was_countdown_pin()

        # Maybe they do know the right PIN, but always do a delay anyway, because they wanted that
        if not delay:
            delay = settings.get('lgto', 0)

        if delay:
            # kill some time, with countdown, and get "the" PIN again for real login
            pa.reset()

            await ux_login_countdown(delay * (60 if not version.is_devmode else 1))

            # keep it simple for Mk4+: just challenge again for any PIN
            # - if it's the same countdown pin, it will be accepted and they
            #   get in (as most trick pins would do)
            await block_until_login()

    except BaseException as exc:
        # Robustness: any logic errors/bugs in above will brick the Coldcard
        # even for legit owner, since they can't login. So try to recover, when it's
        # safe to do so. Remember the bootrom checks PIN on every access to
        # the secret, so "letting" them past this point is harmless if they don't know
        # the true pin.
        # sys.print_exception(exc)
        if not pa.is_successful():
            raise

    # Successful login...

    # Must re-read settings after login
    dis.fullscreen("Startup...")
    settings.set_key()
    settings.load(dis)

    # handle upgrades/downgrade issues
    try:
        await version_migration()
    except: pass

    # Maybe insist on the "right" microSD being already installed?
    try:
        from pwsave import MicroSD2FA
        MicroSD2FA.enforce_policy()
    except: pass

    # apply the hobbling for the spending policy, if appropriate
    try:
        from ccc import sssp_spending_policy, sssp_word_challenge

        if sp_unlock and sssp_spending_policy('words'):
            # challenge them also for first and last seed word! (will reboot on fail)
            await sssp_word_challenge()
            dis.fullscreen("Startup...")

        if sp_unlock:
            # Disable spending policy going forward; user has to re-enable.
            pa.hobbled_mode = False
            sssp_spending_policy('en', set_value=False)
        else:
            # normal entry mode, but might have policy enabled, if so enable it now.
            pa.hobbled_mode = sssp_spending_policy('en')

    except: pass

    # implement idle timeout now that we are logged-in
    IMPT.start_task('idle', idle_logout())

    # Populate xfp/xpub values, if missing.
    # - can happen for first-time login of duress wallet
    # - may indicate lost settings, which we can easily recover from
    # - these values are important to USB protocol
    if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank():
        try:
            import stash

            # Recalculate xfp/xpub values (depends both on secret and chain)
            with stash.SensitiveValues() as sv:
                sv.capture_xpub()
        except Exception as exc:
            # just in case, keep going; we're not useless and this
            # is early in boot process
            print("XFP save failed: %s" % exc)

    # Version warning before HSM is offered
    if version.is_edge and not ckcc.is_simulator():
        await ux_show_story("This firmware version is qualified for use with wallets (such as"
                            " AnchorWatch, Liana, and Nunchuk) that keep redundant key schemas for recovery"
                            " independent of COLDCARD. We support the very latest Bitcoin innovations"
                            " in the Edge Version.", title="Edge Version")

    dis.draw_status(xfp=settings.get('xfp'))

    # If HSM policy file is available, offer to start that,
    # **before** the USB is even enabled.
    if version.supports_hsm:
        # do not offer HSM if wallet is blank -> HSM needs secret
        if not pa.is_secret_blank():
            try:
                import hsm, hsm_ux

                if hsm.hsm_policy_available():
                    settings.put("hsmcmd", True)
                    ar = await hsm_ux.start_hsm_approval(usb_mode=False, startup_mode=True)
                    if ar:
                        await ar.interact()
            except: pass

    if pa.is_deltamode():
        # pretend Secure Notes & Passwords is disabled
        # pretend SeedVault is disabled
        try:
            settings.remove_key("secnap")
            settings.master_set("seedvault", False)
        except: pass

    if version.has_nfc and settings.get('nfc', 0):
        # Maybe allow NFC now
        import nfc
        nfc.NFCHandler.startup()

    if settings.get('vidsk', 0):
        # Maybe start virtual disk
        import vdisk
        vdisk.VirtDisk()

    # Allow USB protocol, now that we are auth'ed
    if not settings.get('du', 0):
        from usb import enable_usb
        enable_usb()


async def restore_main_secret(*a):
    from glob import dis
    from seed import restore_to_main_secret, in_seed_vault

    escape = None
    msg = "Restore main wallet and its settings?\n\n"
    if not in_seed_vault(pa.tmp_value):
        msg += (
            "Press %s to forget current temporary seed "
            "settings, or press (1) to save & keep "
            "those settings if same seed is later restored." % OK
        )
        escape = "1"

    ch = await ux_show_story(msg, escape=escape)
    if ch == "x": return

    dis.fullscreen("Working...")

    ps = True
    if escape and (ch == "y"):
        ps = False

    await restore_to_main_secret(preserve_settings=ps)
    goto_top_menu()

def make_top_menu():
    from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu, HobbledTopMenu
    from glob import hsm_active, settings
    from pincodes import pa

    if hsm_active:
        from hsm_ux import hsm_ux_obj
        m = hsm_ux_obj
    elif version.is_factory_mode:
        m = MenuSystem(FactoryMenu)
    elif pa.is_blank():
        # let them play a little before picking a PIN first time
        m = MenuSystem(VirginSystem, should_cont=lambda: pa.is_blank())
    else:
        assert pa.is_successful(), "nonblank but wrong pin"

        if pa.has_secrets():
            # let them do a few things, but not all the things, when "hobbled"
            _cls = HobbledTopMenu[:] if pa.hobbled_mode else NormalSystem[:]

            if pa.tmp_value or settings.get("hmx", False):
                active_xfp = settings.get("xfp", 0)
                sl, sr = ("[", "]") if pa.tmp_value else ("<", ">")
                if active_xfp:
                    ui_xfp = sl + xfp2str(active_xfp) + sr
                    _cls.insert(0, MenuItem(ui_xfp, f=ready2sign))
                if pa.tmp_value:
                    _cls.append(MenuItem("Restore Master", f=restore_main_secret, shortcut='m'))
        else:
            _cls = EmptyWallet

        m = MenuSystem(_cls)
    return m

def goto_top_menu(first_time=False):
    # Start/restart menu system
    m = make_top_menu()
    the_ux.reset(m)

    if first_time and not pa.is_secret_blank():
        # guide new user thru some setup stuff
        from ftux import FirstTimeUX
        the_ux.push(FirstTimeUX())

    return m

SENSITIVE_NOT_SECRET = '''

The file created is sensitive--in terms of privacy--but should not \
compromise your funds directly.'''

PICK_ACCOUNT = '\n\nPress %s to continue. Press (1) to enter a non-zero account number.' % OK


async def dump_summary(*A):
    # save addresses, and some other public details into a file
    if not await ux_confirm('''\
Saves a text file with a summary of the *public* details \
of your wallet. For example, this gives the XPUB (extended public key) \
that you will need to import other wallet software to track balance.''' + SENSITIVE_NOT_SECRET):
        return

    # pick a semi-random file name, save it.
    await make_summary_file()

async def export_xpub(label, _2, item):
    # provide bare xpub in a QR/NFC for import into simple wallets.
    import chains, glob, stash
    from ux import show_qr_code

    chain = chains.current_chain()
    acct = 0
    slip132 = False  # non-slip is default from Oct 2024

    # decode menu code => standard derivation
    mode = item.arg
    if mode == -1:
        # XFP shortcut
        xfp = xfp2str(settings.get('xfp', 0))
        await show_qr_code(xfp, True)
        return

    elif mode == 0:
        path = "m"
        addr_fmt = AF_CLASSIC
    else:
        remap = {44:0, 49:1, 84:2,86:3}[mode]
        _, path, addr_fmt = chains.CommonDerivations[remap]
        path = path.format(account=acct, coin_type=chain.b44_cointype,
                           change=0, idx=0)[:-4]

    while 1:
        msg = 'Show QR of the XPUB for path:\n\n%s\n\n' % path
        esc = ""
        if path != "m":
            esc += "1"
            msg += "Press (1) to select account other than %s." % (acct or "zero")
            if addr_fmt not in (AF_CLASSIC, AF_P2TR):
                esc += "2"
                slp_af = addr_fmt
                if slip132:
                    slp_af = AF_CLASSIC

                slp = chain.slip132[slp_af].hint + "pub"
                msg += " Press (2) to show %s %s." % (
                    slp, "(BIP-32)" if slip132 else "(SLIP-132)"
                )
        if glob.NFC:
            if version.has_qwerty:
                esc += KEY_NFC
                key_hint = KEY_NFC
            else:
                esc += "3"
                key_hint = "(3)"
            msg += " Press %s to share via NFC. " % key_hint

        ch = await ux_show_story(msg, escape=esc)
        if ch == 'x': return
        if ch == "2":
            slip132 = not slip132
            continue
        if ch == '1':
            acct = await ux_enter_bip32_index('Account Number:') or 0
            pth_split = path.split("/")
            pth_split[-1] = ("%dh" % acct)
            path = "/".join(pth_split)
            continue

        # assume zero account if not picked
        path = path.format(acct=acct)

        from glob import dis
        dis.fullscreen('Wait...')

        # render xpub/ypub/zpub
        with stash.SensitiveValues() as sv:
            node = sv.derive_path(path) if path != 'm' else sv.node
            xpub = chain.serialize_public(node, addr_fmt if slip132 else AF_CLASSIC)

        from ownership import OWNERSHIP
        OWNERSHIP.note_wallet_used(addr_fmt, acct)

        if glob.NFC and ch in '3'+KEY_NFC:
            await glob.NFC.share_text(xpub)
        else:
            await show_qr_code(xpub, False)


def electrum_export_story(noun="Electrum", background=False):
    # saves memory being in a function
    return ('''\
This saves a skeleton %s wallet file. \
You can then open that file in the wallet without ever connecting this Coldcard to a computer.\n
''' % noun
        + (background or 'Choose an address type for the wallet on the next screen.'+PICK_ACCOUNT)
        + SENSITIVE_NOT_SECRET)

async def electrum_skeleton(a, b, item):
    # save xpub, and some other public details into a file: NOT MULTISIG
    title = item.arg
    fname_pat = "new-%s.json" % title.lower()

    ch = await ux_show_story(electrum_export_story(title), escape='1')

    account_num = 0
    if ch == '1':
        account_num = await ux_enter_bip32_index('Account Number:') or 0
    elif ch != 'y':
        return

    rv = [
        MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
                 arg=(af, account_num, title, fname_pat))
        for af in chains.SINGLESIG_AF
    ]
    the_ux.push(MenuSystem(rv))

def ss_descriptor_export_story(addition="", background="", acct=True):
    # saves memory being in a function
    return ("This saves a ranged xpub descriptor" + addition
            + background
            + (PICK_ACCOUNT if acct else "")
            + SENSITIVE_NOT_SECRET)

async def ss_descriptor_skeleton(_0, _1, item):
    # Export of descriptor data (wallet)
    addition, f_pattern = "", "descriptor.txt"
    int_ext = direct_way = None
    allowed_af = chains.SINGLESIG_AF
    if item.arg:
        int_ext, allowed_af, ll, f_pattern, direct_way = item.arg
        addition = " for " + ll

    account_num = 0
    if not direct_way:
        ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')

        if ch == '1':
            account_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
        elif ch != 'y':
            return

    if int_ext is None:
        ch = await ux_show_story(
            "To export receiving and change descriptors in one descriptor "
            "(<0;1> notation) press %s, press (1) to export "
            "receiving and change descriptors separately." % OK, escape='1')
        if ch == "x": return
        int_ext = False if ch == "1" else True

    if len(allowed_af) == 1:
        await make_descriptor_wallet_export(allowed_af[0], account_num, int_ext=int_ext,
                                            fname_pattern=f_pattern, direct_way=direct_way)
    else:
        rv = [
            MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
                     arg=(af, account_num, int_ext, f_pattern, direct_way))
            for af in allowed_af
        ]
        the_ux.push(MenuSystem(rv))


async def key_expression_skeleton_step2(_1, _2, item):
    # pick a semi-random file name, render and save it.
    orig_path, addr_fmt = item.arg
    await make_key_expression_export(orig_path, addr_fmt)

async def key_expression_skeleton(_0, _1, item):
    # Export key expression -> [xfp/d/e/r]xpub

    acct_num = 0
    ch = await ux_show_story("This saves a extended key expression."
                             + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
    if ch == '1':
        acct_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
    elif ch != 'y':
        return

    # element on 2nd index is address format for signed exports
    # if multisig key use p2pkh
    todo = [
        ("Segwit P2WPKH", "m/84h/%dh/%dh", AF_P2WPKH),
        ("Taproot P2TR", "m/86h/%dh/%dh", AF_P2TR),
        ("Classic P2PKH", "m/44h/%dh/%dh", AF_CLASSIC),
        ("P2SH-Segwit", "m/49h/%dh/%dh", AF_P2WPKH_P2SH),
        ("Multi P2WSH", "m/48h/%dh/%dh/2h", AF_CLASSIC),
        ("Multi P2TR", "m/48h/%dh/%dh/3h", AF_CLASSIC),
        ("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h", AF_CLASSIC),
    ]

    from address_explorer import KeypathMenu

    async def doit(*a):
        return KeypathMenu(ranged=False, done_fn=make_key_expression_export)

    ct = chains.current_chain().b44_cointype

    rv = [
        MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct_num), af))
        for label, orig_der, af in todo
    ]
    rv += [MenuItem("Custom Path", menu=doit)]

    the_ux.push(MenuSystem(rv))

async def samourai_post_mix_descriptor_export(*a):
    name = "POST-MIX"
    post_mix_acct_num = 2147483646
    await samourai_account_descriptor(name, post_mix_acct_num)

async def samourai_pre_mix_descriptor_export(*a):
    name = "PRE-MIX"
    pre_mix_acct_num = 2147483645
    await samourai_account_descriptor(name, pre_mix_acct_num)

# async def samourai_bad_bank_descriptor_export(*a):
#     name = "PRE-MIX"
#     pre_mix_acct_num = 2147483644
#     await samourai_account_descriptor(name, pre_mix_acct_num)

async def samourai_account_descriptor(name, account_num):
    ch = await ux_show_story(
        ss_descriptor_export_story(
            addition=" for Samourai %s account." % name,
            acct=False)
    )

    if ch != 'y':
        return
    fn_pattern = "samourai-%s.txt" % name.lower()
    await make_descriptor_wallet_export(AF_P2WPKH, account_num, fname_pattern=fn_pattern)

async def descriptor_skeleton_step2(_1, _2, item):
    # pick a semi-random file name, render and save it.
    addr_fmt, account_num, int_ext, f_pattern, dw = item.arg
    await make_descriptor_wallet_export(addr_fmt, account_num, int_ext=int_ext,
                                        fname_pattern=f_pattern, direct_way=dw)


async def bitcoin_core_skeleton(*A):
    # save output descriptors into a file
    # - user has no choice, it's going to be bech32 with  m/84'/{coin_type}'/0' path

    ch = await ux_show_story('''\
This saves commands and instructions into a file, including the public keys (xpub). \
You can then run the commands in Bitcoin Core's console window, \
without ever connecting this Coldcard to a computer.\
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')

    account_num = 0
    if ch == '1':
        account_num = await ux_enter_bip32_index('Account Number:') or 0
    elif ch != 'y':
        return

    # no choices to be made, just do it.
    await make_bitcoin_core_wallet(account_num)


async def electrum_skeleton_step2(_1, _2, item):
    # pick a semi-random file name, render and save it.
    addr_fmt, account_num, title, fname_pat = item.arg
    await export_contents(title + " wallet",
                          lambda: generate_electrum_wallet(addr_fmt, account_num),
                          fname_pat, is_json=True)

async def _generic_export(prompt, label, f_pattern):
    # like the Multisig export, make a single JSON file with
    # basically all useful XPUB's in it.
    ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
    account_num = 0
    if ch == '1':
        account_num = await ux_enter_bip32_index('Account Number:') or 0
    elif ch != 'y':
        return

    await export_contents(label, lambda: generate_generic_export(account_num),
                          f_pattern, is_json=True)

async def generic_skeleton(*A):
    # like the Multisig export, make a single JSON file with
    # basically all useful XPUB's in it.
    prompt = '''\
Saves JSON file, with XPUB values that are needed to watch typical \
single-signer UTXO associated with this Coldcard.'''

    await _generic_export(prompt, 'Generic Export', 'coldcard-export.json')


async def named_generic_skeleton(menu, label, item):
    name = item.arg
    # make a single JSON file with basically all useful XPUB's in it.
    # identical to generic_skeleton but with different story and filename.
    prompt = ('This saves a JSON file to use with %s Wallet. '
              'Works for both single signature and multisig wallets.') % name

    await _generic_export(prompt, '%s Wallet' % name,
                          '%s-export.json' % name.lower())


async def wasabi_skeleton(*A):
    # save xpub, and some other public details into a file
    # - user has no choice, it's going to be bech32 with  m/84'/0'/0' path

    ch = await ux_show_story('''\
This saves a skeleton Wasabi wallet file. \
You can then open that file in Wasabi without ever connecting this Coldcard to a computer.\
''' + SENSITIVE_NOT_SECRET)
    if ch != 'y':
        return

    # no choices to be made, just do it.
    await export_contents('Wasabi wallet', lambda: generate_wasabi_wallet(),
                          'new-wasabi.json', is_json=True)

async def unchained_capital_export(*a):
    # they were using our airgapped export, and the BIP-45 path from that
    #
    ch = await ux_show_story('''\
This saves multisig XPUB information required to setup on the Unchained platform. \
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
    account_num = 0
    if ch == '1':
        account_num = await ux_enter_bip32_index('Account Number:') or 0
    elif ch != 'y':
        return

    xfp = xfp2str(settings.get('xfp', 0))
    fname = 'unchained-%s.json' % xfp

    await export_contents('Unchained', lambda: generate_unchained_export(account_num),
                          fname, is_json=True)


async def backup_everything(*A):
    # save everything, using a password, into single encrypted file, typically on SD
    import backups

    await backups.make_complete_backup()

async def verify_backup(*A):
    # check most recent backup is "good"
    # read 7z header, and measure checksums
    import backups

    fn = await file_picker(suffix='.7z', max_size=backups.MAX_BACKUP_FILE_SIZE)

    if not fn:
        return

    # do a limited CRC-check over encrypted file
    await backups.verify_backup_file(fn)

async def import_extended_key_as_secret(extended_key, ephemeral, origin=None):
    try:
        import seed
        if ephemeral:
            await seed.set_ephemeral_seed_extended_key(extended_key, origin=origin)
        else:
            await seed.set_seed_extended_key(extended_key)
    except Exception as e:
        await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))

async def import_xprv(_1, _2, item):
    # read an XPRV from a text file and use it.
    from glob import NFC

    extended_key = None
    label = "extended private key"

    ephemeral = item.arg
    if not ephemeral:
        assert pa.is_secret_blank() # "must not have secret"

    def contains_xprv(fname):
        # just check if likely to be valid; not full check
        try:
            with open(fname, 'rt') as fd:
                for ln in fd:
                    # match tprv and xprv, plus y/zprv etc
                    if 'prv' in ln: return True
                return False
        except OSError:
            # directories?
            return False

    choice = await import_export_prompt("%s file" % label, is_import=True)

    if choice == KEY_CANCEL:
        return
    elif choice == KEY_NFC:
        extended_key = await NFC.read_extended_private_key()
        if not extended_key:
            # failed to get any data - exit
            # error already displayed in nfc.py
            return
    elif choice == KEY_QR:
        from ux_q1 import QRScannerInteraction
        extended_key = await QRScannerInteraction.scan('Scan XPRV from a QR code')
        if not extended_key:
            # press pressed CANCEL
            return
    else:
        # only get here if NFC was not chosen
        # pick a likely-looking file.
        fn = await file_picker(suffix='.txt', min_size=50, max_size=2000, taster=contains_xprv,
                               none_msg="Must contain " + label + ".", **choice)

        if not fn: return

        with CardSlot(readonly=True, **choice) as card:
            with open(fn, 'rt') as fd:
                for ln in fd.readlines():
                    if 'prv' in ln:
                        extended_key = ln
                        break

    await import_extended_key_as_secret(extended_key, ephemeral, origin='Imported XPRV')
    # not reached; will do reset.

async def need_clear_seed(*a):
    await ux_show_story('''\
You must clear the wallet seed before restoring a backup because it replaces \
the seed value and the old seed would be lost.\n\n\
Visit the advanced menu and choose 'Destroy Seed'.''')

async def restore_backup(a, b, item):
    # normal word based imports (tmp or master depending on item.arg)
    fn = await file_picker(suffix=".7z")
    if fn:
        import backups
        await backups.restore_complete(fn, item.arg, True)

async def restore_backup_dev(*a):
    # used ONLY for Restore Bkup in I Am Developer
    fn = await file_picker(suffix=[".7z", ".txt"])
    if fn:
        words = False if fn[-3:] == ".7z" else None
        import backups
        await backups.restore_complete(fn, not pa.is_secret_blank(), words)

async def bkpw_override(*A):
    # allows user to:
    #   1.) manually set bkpw
    #   2.) remove existing bkpw setting
    #   3.) view current active bkpw
    # - some truncation of titles here on Mk4,
    #   which is okay because re-using strings to save space.
    from backups import bkpw_min_len

    if pa.is_secret_blank():
        return

    wipe_if_deltamode()

    while True:
        pwd = settings.get("bkpw", None)

        msg = ("Password used to encrypt COLDCARD backup files."
               "\n\nPress (0) to change backup password")
        esc = "0"
        if pwd:
            esc += "12"
            msg += ", (1) to forget current password, (2) to show current active backup password."
        else:
            msg += "."

        ch = await ux_show_story(title="BKPW Override", msg=msg, escape=esc)
        if ch == "x": return
        elif ch == "1":
            if await ux_confirm("Delete current stored password?"):
                settings.remove_key("bkpw")
                settings.save()
                await ux_dramatic_pause("Deleted.", 2)

        elif ch == "2":
            if await ux_confirm('The next screen will show current active backup password.'
                                '\n\nAnyone with knowledge of the password will '
                                'be able to decrypt your backups.'):
                await ux_show_story(pwd, title="Your Backup Password")

        elif ch == "0":
            if version.has_qwerty:
                from notes import get_a_password
                npwd = await get_a_password(pwd, min_len=bkpw_min_len)
            else:
                npwd = await ux_input_text(pwd, prompt="Your Backup Password",
                                           min_len=bkpw_min_len, max_len=128)

            if (npwd is None) or (npwd == pwd): continue

            settings.set('bkpw', npwd)
            settings.save()
            await ux_dramatic_pause("Saved.", 2)


async def wipe_filesystem(*A):
    if not await ux_confirm('''\
Erase internal filesystem and rebuild it. Resets contents of internal flash area \
used for settings, address search cache, and HSM config file. Does not affect funds, \
or seed words but will reset settings used with other temporary seeds & BIP-39 passphrases. \
Does not affect MicroSD card, if any.''', confirm_key="4"):
        return

    from files import wipe_flash_filesystem
    wipe_flash_filesystem()

async def nuke_device(*a):
    if not await ux_confirm("Wipe Seed & Brick device? This will wipe the seed, purge"
                            " all related settings, and makes ewaste from this device."):
        return

    if not await ux_confirm("Brick device?\n\nBy design, there is no way to reset or recover"
                            " the secure element, and its contents become forever inaccessible.",
                            confirm_key="1"):
        return

    import callgate
    callgate.fast_brick()
    # NOT REACHED

async def wipe_vdisk(*A):
    if not await ux_confirm('''\
Erases and reformats shared RAM disk. This is a secure erase that blanks every byte.'''):
        return

    import glob
    await glob.VD.wipe_disk()

async def wipe_sd_card(*A):
    if not await ux_confirm('''\
Erases and reformats MicroSD card. This is not a secure erase but more of a quick format.'''):
        return

    from files import wipe_microsd_card
    wipe_microsd_card()


async def qr_share_file(_1, _2, item):
    # Pick file from SD card and share as (BB)Qr
    from files import CardSlot, CardMissingError, needs_microsd
    from export import export_by_qr

    force_bbqr = item.arg

    def is_suitable(fname):
        f = fname.lower()
        return f.endswith('.psbt') or f.endswith('.txn') \
            or f.endswith('.txt') or f.endswith(".json") or fname.endswith(".sig")

    try:
        while 1:
            txid = None
            fn = await file_picker(min_size=10, max_size=MAX_TXN_LEN, taster=is_suitable)
            if not fn: return

            basename = fn.split('/')[-1]
            ext = fn.split('.')[-1].lower()

            try:
                with CardSlot() as card:
                    with open(fn, 'rb') as fp:
                        data = fp.read()

            except CardMissingError:
                await needs_microsd()
                return

            if ext == "txn":
                tc = "T"
                txid = txid_from_fname(basename)
                if data[2:8] == b'000000':
                    # it's a txn, and we wrote as hex
                    data = data.decode()
                else:
                    assert data[2:8] == bytes(6)
                    data = b2a_hex(data).decode()
            elif data[0:5] == b'psbt\xff':
                tc = "P"
            elif data[0:6] in (b'cHNidP', b'707362'):
                tc = "U"
                data = data.decode().strip()
            elif ext in ('txt', 'json', 'sig'):
                tc = "U"
                if ext == "json":
                    tc = "J"
                data = data.decode()
            else:
                raise ValueError(ext)

            await export_by_qr(data, txid, tc, force_bbqr=force_bbqr)
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg="Failed to share file via QR.\n\n%s\n%s" % (e, problem_file_line(e))
        )

async def nfc_share_file(*A):
    # Share txt, txn and PSBT files over NFC.
    from glob import NFC
    try:
        await NFC.share_file()
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg="Failed to share file.\n\n%s\n%s" % (e, problem_file_line(e))
        )

async def nfc_pushtx_file(*A):
    # Share a signed txn over NFC using PushTx technology
    # - requires a signed txn, perhaps from another system on SD card
    from glob import NFC
    try:
        await NFC.push_tx_from_file()
    except Exception as e:
        await ux_show_story(title="ERROR", msg="Failed to share file. %s" % str(e))


async def nfc_show_address(*A):
    from glob import NFC
    try:
        await NFC.address_show_and_share()
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg="Failed to show address.\n\n%s\n%s" % (e, problem_file_line(e))
        )


async def nfc_sign_msg(*A):
    # Receive data over NFC (text - follow sign txt file format)
    #      User approval on device
    #      Send signature RFC armored format back over NFC
    from glob import NFC
    try:
        await NFC.start_msg_sign()
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg="Failed to sign message.\n\n%s\n%s" % (e, problem_file_line(e))
        )

async def nfc_sign_verify(*A):
    # Receive armored data over NFC
    from glob import NFC
    try:
        await NFC.verify_sig_nfc()
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg="Failed to verify signed message.\n\n%s\n%s" % (e, problem_file_line(e))
        )

async def nfc_address_verify(*A):
    # Receive any random address (just the address) and find out if we own it.
    from glob import NFC
    try:
        await NFC.verify_address_nfc()
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg='Address verification failed.\n\n%s\n%s' % (e, problem_file_line(e))
        )

async def nfc_recv_ephemeral(*A):
    # Mk4: Share txt, txn and PSBT files over NFC.
    from glob import NFC
    try:
        await NFC.import_ephemeral_seed_words_nfc()
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg="Failed to import temporary seed via NFC.\n\n%s\n%s" % (e, problem_file_line(e))
        )

async def nfc_sign_psbt(*A):
    from glob import NFC
    try:
        await NFC.start_psbt_rx()
    except Exception as e:
        await ux_show_story(
            title="ERROR",
            msg='Failed to sign PSBT.\n\n%s\n%s' % (e, problem_file_line(e))
        )

async def list_files(*A):
    fn = await file_picker(min_size=0)
    if not fn: return

    chk = sha256()

    try:
        with CardSlot() as card:
            with card.open(fn, 'rb') as fp:
                while 1:
                    data = fp.read(1024)
                    if not data: break
                    chk.update(data)
    except CardMissingError:
        await needs_microsd()
        return

    from pincodes import pa

    digest = chk.digest()
    path, basename = fn.rsplit('/', 1)
    msg_base = 'SHA256(%s)\n\n' + B2A(digest) + '\n\nPress (1) to rename file, '
    escape = "61"
    if pa.has_secrets():
        msg_base += '(4) to sign file digest and export detached signature, '
        escape += "4"
    msg_base += '(6) to delete.'

    while True:
        ch = await ux_show_story(msg_base % basename, escape=escape)
        if ch == "x": break
        if ch in '461':
            with CardSlot() as card:
                if ch == '6':
                    card.securely_blank_file(fn)
                    break
                elif ch == '1':
                    new_basename = await ux_input_text(basename, max_len=32, min_len=3)
                    if new_basename:
                        try:
                            # prohibit both slashes and space in filenames
                            for s in "\/ ":
                                assert s not in new_basename, "illegal char"
                            uos.rename(path + "/" + basename, path + "/" + new_basename)
                            basename = new_basename
                        except Exception as e:
                            await ux_show_story("Failed to rename the file. " + str(e),
                                                title="Failure")
                else:
                    from msgsign import write_sig_file

                    sig_nice = write_sig_file([(digest, fn)])
                    await ux_show_story("Signature file %s written." % sig_nice)
    return

async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
                      choices=None, none_msg=None, force_vdisk=False, slot_b=None,
                      allow_batch=False, ux=True):
    # present a menu w/ a list of files... to be read
    # - optionally, enforce a max size, and provide a "tasting" function
    # - if (not ux), don't prompt, just do the search and return list
    # - if choices is provided; skip search process
    # - escape: allow these chars to skip picking process
    # - slot_b: None=>pick slot w/ card in it, or A if both.
    # - allow_batch: adds an "all of the above" choice: ("menu label", menu_handler)
    # - suffix argument MUST contain the dot (.txt), if list of suffixes, all MUST

    if suffix:
        # actually make it a list of "suffixes"
        if not isinstance(suffix, list):
            suffix = [suffix]
        assert all(s[0] == '.' for s in suffix)

    if choices is None:
        choices = []
        try:
            with CardSlot(force_vdisk=force_vdisk, slot_b=slot_b, readonly=True) as card:
                sofar = set()

                for path in card.get_paths():
                    for fn, ftype, *var in uos.ilistdir(path):
                        if ftype == 0x4000:
                            # ignore subdirs
                            continue

                        if fn[0] == '.':
                            # unix-style hidden files
                            continue

                        if suffix and not any(fn.lower().endswith(s) for s in suffix):
                            # wrong suffix, skip
                            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 (min_size <= file_size <= max_size):
                            continue

                        if taster is not None:
                            try:
                                yummy = taster(full_fname)
                            except OSError:
                                #print("fail: %s" % full_fname)
                                yummy = False

                            if not yummy:
                                continue

                        label = fn
                        while label in sofar:
                            # just the file name isn't unique enough sometimes?
                            # - shouldn't happen anymore now that we don't support internal FS
                            # - unless we do muliple paths
                            label += path.split('/')[-1] + '/' + fn

                        sofar.add(label)
                        choices.append((label, path, fn))

        except CardMissingError:
            # don't show anything if we're just gathering data
            if ux is not False:
                await needs_microsd()
            return None

    if ux is False:
        return choices

    if not choices:
        msg = 'No suitable files found. '

        if none_msg:
            msg += none_msg
        if suffix:
            msg += '\n\nThe filename must end in: ' + ' OR '.join(suffix)

        msg += '\n\nMaybe insert (another) SD card and try again?'

        await ux_show_story(msg)
        return

    picked = []
    async def clicked(_1,_2,item):
        picked.append('/'.join(item.arg))
        the_ux.pop()

    choices.sort()

    items = [MenuItem(label, f=clicked, arg=(path, fn)) for label, path, fn in choices]
    if allow_batch and len(choices) > 1:
        # Allow an "all" selection
        label, funct = allow_batch
        items.insert(0, MenuItem(label, f=funct, arg=choices))

    menu = MenuSystem(items)
    the_ux.push(menu)

    await menu.interact()

    return picked[0] if picked else None

async def debug_assert(*a):
    assert False, "failed assertion"

async def debug_bbqr_test(*a):
    # Q only: test BBQr w/ lots of data
    from ux_q1 import show_bbqr_codes
    from gpu_binary import BINARY
    await show_bbqr_codes('B', BINARY*3, 'GPU binary')

async def debug_except(*a):
    print(34 / 0)

async def check_firewall_read(*a):
    import uctypes
    ps = uctypes.bytes_at(0x7800, 32)       # off the mark for mk4, but still valid test
    assert False        # should not be reached

async def bless_flash(*a):
    # make green LED turn on
    from glob import dis

    # impt: bugfix
    dis.bootrom_takeover()

    # do it
    pa.greenlight_firmware()

    # redraw our screen
    dis.busy_bar(False)  # includes dis.show()


def is_psbt(filename):
    if '-signed' in filename.lower():       # XXX problem: multi-signers?
        return False

    with open(filename, 'rb') as fd:
        taste = fd.read(10)
        if taste[0:5] == b'psbt\xff':
            return True
        if taste[0:10] == b'70736274ff':        # hex-encoded
            return True
        if taste[0:6] == b'cHNidP':             # base64-encoded
            return True
        return False

async def _batch_sign(choices=None):
    if not choices:
        picked = await import_export_prompt("PSBTs", is_import=True, no_nfc=True, no_qr=True)

        if picked == KEY_CANCEL:
            return
        assert isinstance(picked, dict)

        choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
                                    max_size=MAX_TXN_LEN, taster=is_psbt, **picked)

    if not choices:
        await ux_show_story("No PSBTs found. Need to have '.psbt' suffix.")
        return

    from auth import sign_psbt_file
    from ux import the_ux
    for label, path, fn in choices:
        ch = await ux_show_story("Sign %s ??\n\nPress %s to sign, (1) to skip this PSBT,"
                                 " %s to quit and exit." % (fn, OK, X), escape="1")
        if ch == "x": break
        elif ch == "y":
            input_psbt = path + '/' + fn
            await sign_psbt_file(input_psbt)
            await sleep_ms(100)
            await the_ux.top_of_stack().interact()

async def batch_sign(_1, _2, item):
    try:
        await _batch_sign(item.arg)
    except Exception as e:
        import sys
        await ux_show_story("FAILURE: batch sign failed\n\n" + problem_file_line(e))

async def _ready2sign(intro="", probe=True, miniscript_wallet=None):
    # - if probe=True -> check if any signable in SD card (A slot on Q), if so do it
    # - if probe=False -> offer all enabled import options via UX
    # - if no card, check virtual disk for PSBT
    from pincodes import pa
    from glob import NFC

    opt = {}
    choices = []
    sb_only = False

    if probe:
        # just check if we have candidates, no UI
        sb_only = True
        choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
                                    max_size=MAX_TXN_LEN, taster=is_psbt)

    if pa.tmp_value:
        title = '[%s]' % xfp2str(settings.get('xfp'))
    else:
        title = None

    if not choices:
        # if we have only one SD card inserted, at this point, we know no PSBTs on them
        # as above file_picker already checked
        # if we have both inserted, A was already checked - so only care about B
        footnotes = ("You will always be prompted to confirm the details "
                     "before any signature is performed.")
        picked = await import_export_prompt("PSBT", is_import=True, intro=intro,
                                            footnotes=footnotes, slot_b_only=sb_only,
                                            title=title)
        if isinstance(picked, dict):
            opt = picked  # reset options to what was chosen by user
            choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
                                        max_size=MAX_TXN_LEN, taster=is_psbt,
                                        **opt)
            if not choices:
                await ux_show_story('Unable to find any suitable files for this operation.'
                                    ' The filename must end in psbt.')
                return
        else:
            if NFC and picked == KEY_NFC:
                await NFC.start_psbt_rx(miniscript_wallet)
            if picked == KEY_QR:
                await _scan_any_qr(miniscript_wallet=miniscript_wallet)

            return

    if len(choices) == 1:
        # single - skip the menu
        label,path,fn = choices[0]
        input_psbt = path + '/' + fn
    else:
        # multiples - ask which, and offer batch to sign them all
        input_psbt = await file_picker(choices=choices, allow_batch=("[Sign All]", batch_sign))
        if not input_psbt:
            return

    # start the process
    from auth import sign_psbt_file
    opt["miniscript_wallet"] = miniscript_wallet
    await sign_psbt_file(input_psbt, **opt)


async def ready2sign(*a):
    # Top menu choice of top menu! Signing!
    # - check if any signable in SD card, if so do it
    # - if no card, check virtual disk for PSBT

    await _ready2sign('''Coldcard is ready to sign spending transactions!

Put the proposed transaction onto MicroSD card \
in PSBT format (Partially Signed Bitcoin Transaction) \
or upload a transaction to be signed \
from your desktop wallet software or command line tools.''')


async def sign_message_on_sd(*a):
    # Menu item: choose a file to be signed (as a short text message)
    #
    def is_signable(filename):
        if '-signed' in filename.lower():
            return False
        with open(filename, 'rt') as fd:
            lines = fd.readlines()
            # min 1 line max 3 lines
            return 1 <= len(lines) <= 3

    fn = await file_picker(suffix=['.txt', ".json"], min_size=2, max_size=500, taster=is_signable,
                           none_msg=('Must be txt file with one msg line, optionally '
                                     'followed by a subkey derivation path on a second line '
                                     'and/or address format on third line. JSON msg signing '
                                     'format also supported'))

    if not fn:
        return

    # start the process
    from auth import sign_txt_file
    await sign_txt_file(fn)


async def verify_sig_file(*a):
    def is_sig_file(filename):
        with open(filename, 'rt') as fd:
            line0 = fd.readline()
            if "SIGNED MESSAGE" in line0:
                return True
            return False

    fn = await file_picker(min_size=220, max_size=10000, taster=is_sig_file,
                           none_msg='Must be file with ascii armor.')

    if not fn:
        return

    # start the process
    from msgsign import verify_txt_sig_file
    await verify_txt_sig_file(fn)


async def main_pin_changer(*a):
    # Help them to change the main (true) PIN with appropriate warnings.
    # - the bootloader maybe lying to us about main vs trick pin
    # - what may look like just policy here, is in fact enforced by the bootrom code
    #
    from glob import dis
    from login import LoginUX
    from pincodes import BootloaderError, EPIN_OLD_AUTH_FAIL

    lll = LoginUX()
    title = 'Main PIN'
    msg = '''\
You will be changing the main PIN used to unlock your Coldcard.

THERE IS ABSOLUTELY NO WAY TO RECOVER A FORGOTTEN PIN!\n
Write it down.'''

    ch = await ux_show_story(msg, title=title)
    if ch != 'y': return


    async def incorrect_pin():
        await ux_show_story('You provided an incorrect value for the existing PIN.',
                                title='Wrong PIN')
        return

    args = {}

    # We need the existing pin, so prompt for that.
    lll.subtitle = 'Old ' + title
    old_pin = await lll.prompt_pin()
    if old_pin is None:
        return await ux_aborted()

    args['old_pin'] = old_pin.encode()

    # we can verify the main pin right away here. Be nice.
    if args['old_pin'] != pa.pin:
        return await incorrect_pin()

    while 1:
        lll.reset()
        lll.subtitle = "New " + title
        pin = await lll.get_new_pin()

        if pin is None:
            return await ux_aborted()

        from trick_pins import tp
        prob = tp.check_new_main_pin(pin)
        if prob:
            await ux_show_story(prob, title="Try Again")
            continue

        args['new_pin'] = pin.encode()
        break

    # install it.
    try:
        dis.fullscreen("Saving PIN...")
        dis.busy_bar(True)

        pa.change(**args)
        dis.busy_bar(False)
    except Exception as exc:
        dis.busy_bar(False)

        code = exc.args[1]

        if code == EPIN_OLD_AUTH_FAIL:
            # unlikely: but maybe we got tricked?
            return await incorrect_pin()
        else:
            return await ux_show_story("Unexpected low-level error: %s" % exc.args[0],
                                            title='Error')

    # Main pin is changed, and we use it lots, so update pa
    # - this step can be super slow with 608, unfortunately
    try:
        dis.fullscreen("Verify...")
        dis.busy_bar(True)

        pa.setup(args['new_pin'])

        if not pa.is_successful():
            # typical: do need login, but if we just cleared the main PIN,
            # we cannot/need not login again
            pa.login()

        # Deltamode trick pins need to track main pin
        from trick_pins import tp
        tp.main_pin_has_changed(pa.pin.decode())

    finally:
        dis.busy_bar(False)

async def show_version(*a):
    # show firmware, bootload versions.
    import callgate, version
    from glob import NFC

    built, rel, *_ = version.get_mpy_version()
    bl = callgate.get_bl_version()
    bl_dev = dict(bl[1]).get('DEV', 0)
    chk = str(b2a_hex(callgate.get_bl_checksum(0))[-8:], 'ascii')

    se = '\n  '.join(callgate.get_se_parts())

    # exposed over USB interface:
    serial = version.serial_number()

    # this UID is exposed over NFC interface, but only when enabled and in active use
    if NFC:
        serial += '\n\nNFC UID:\n  ' + NFC.get_uid().replace(':', '')

    hw = version.hw_label
    if not version.has_nfc:
        hw += ' (no NFC)'

    msg = '''\
Coldcard Firmware

  {rel}
  {built}

Bootloader:
  {bl}
  {chk}

Serial:
  {ser}

Hardware:
  {hw}

Secure Elements:
  {se}
'''

    msg = msg.format(rel=rel, built=built, bl=bl[0]+(' DEV' if bl_dev else''), chk=chk,
                     se=se, ser=serial, hw=hw)
    if version.has_qr:
        from glob import SCAN, dis
        msg += '\nQR Scanner:\n  %s\n' % (SCAN.version or 'missing')

        msg += '\nGPU:\n  %s\n' % dis.gpu.get_version()

    await ux_show_story(msg)

async def ship_wo_bag(*a):
    # Factory command: for dev and test units that have no bag number, and never will.
    ok = await ux_confirm('''Not recommended! DO NOT USE for units going to paying customers.''')
    if not ok: return

    import callgate
    from glob import dis
    from version import is_devmode

    failed = callgate.set_bag_number(b'NOT BAGGED')      # 32 chars max

    if failed:
        await ux_dramatic_pause('FAILED', 30)
    else:
        # lock the bootrom firmware forever
        callgate.set_rdp_level(2)

        # bag number affects green light status (as does RDP level)
        dis.bootrom_takeover()
        pa.greenlight_firmware()
        dis.fullscreen('No Bag. DONE')
        callgate.show_logout(1)

async def set_highwater(*a):
    # rarely? used command
    import callgate

    have = version.get_mpy_version()[0]
    ts = version.get_header_value('timestamp')

    hw = callgate.get_highwater()

    if hw == ts:
        await ux_show_story('''Current version (%s) already marked as high-water mark.''' % have)
        return

    ok = await ux_confirm('''Mark current version (%s) as the minimum, and prevent any downgrades below this version.

Rarely needed as critical security updates will set this automatically.''' % have)

    if not ok: return

    rv = callgate.set_highwater(ts)

    # add error display here? meh.

    assert rv == 0, "Failed: %r" % rv

async def start_hsm_menu_item(*a):
    from hsm_ux import start_hsm_approval
    await start_hsm_approval(sf_len=0, usb_mode=False)

async def wipe_hsm_policy(*A):
    # deep in danger zone menu; no background story, nor confirmation
    # - sends them back to top menu, so that dynamic contents are fixed
    from hsm import hsm_delete_policy
    hsm_delete_policy()
    goto_top_menu()

async def wipe_address_cache(*a):
    ok = await ux_confirm('''Clear cached addresses used in ownership search. Harmless to erase, just costs time.''')
    if not ok: return

    from ownership import OWNERSHIP
    OWNERSHIP.wipe_all()

    await ux_dramatic_pause("Cleared.", 3)

async def wipe_ovc(*a):
    ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \
This data protects you against specific attacks. Use this only if certain a false-positive \
has occured in the detection logic.''')
    if not ok: return

    import history
    history.OutptValueCache.clear()

    await ux_dramatic_pause("Cleared.", 3)

async def change_usb_disable(dis):
    # user has disabled USB port (or re-enabled)
    import pyb
    cur = pyb.usb_mode()

    from usb import enable_usb, disable_usb
    if cur and dis:
        # usb enabled, but should not be now
        disable_usb()
    elif not cur and not dis:
        # USB disabled, but now should be
        enable_usb()

async def usb_keyboard_emulation(enable):
    # just sets emu flag on and adds Entry Password into top menu
    # no USB switching at this point
    # - need to force reload of main menu, so it shows/hides
    new_top_menu = make_top_menu()
    the_ux.stack[0] = new_top_menu  # top menu is always element 0

async def change_nfc_enable(enable):
    # NFC enable / disable
    from glob import NFC
    import nfc

    if not enable:
        if NFC:
            NFC.shutdown()
    else:
        nfc.NFCHandler.startup()

async def change_virtdisk_enable(enable):
    # NOTE: enable can be 0,1,2
    import glob, vdisk

    if bool(enable) == bool(glob.VD):
        # not a change in state, do nothing
        return

    if enable:
        # just showing up as new media is enough (MacOS) to make it show up
        vdisk.VirtDisk()
        assert glob.VD
    else:
        assert glob.VD
        glob.VD.shutdown()
        assert not glob.VD

async def change_seed_vault(is_enabled):
    # user has changed seed vault enable/disable flag
    if (not is_enabled) and settings.master_get('seeds'):
        # problem: they still have some seeds... also this path blocks
        # disable from within a tmp seed
        settings.set('seedvault', 1)        # restore it
        await ux_show_story("Please remove all seeds from the vault before disabling.")
        return

    goto_top_menu()

async def change_which_chain(*a):
    # setting already changed, but reflect that value in other settings
    from glob import dis
    dis.fullscreen("Wait...")
    try:
        # update xpub stored in settings
        import stash
        with stash.SensitiveValues() as sv:
            sv.capture_xpub()
    except ValueError:
        # no secrets yet, not an error
        pass

async def microsd_2fa(*a):
    # Feature: enforce special MicroSD being inserted at login time (a 2FA)
    from pwsave import MicroSD2FA
    
    if not settings.get('sd2fa'):
        msg = ('When enabled, this feature requires a specially prepared MicroSD card '
               'to be inserted during login process. After correct PIN is provided, '
               'if card slot is empty or unknown card present, the seed is wiped.')

        if version.num_sd_slots > 1:
            msg += ("\n\nIf multiple SD cards are present during login, make sure that"
                    " authorized card is in the top slot (slot A).")

        ch = await ux_show_story(msg)
        if ch != 'y':
            return

    return MicroSD2FA.menu()


async def keyboard_test(*a):
    # to aid keyboard testing/dev
    if version.has_qwerty:
        await ux_input_text('', max_len=128, scan_ok=True, confirm_exit=False,
                            prompt='Keyboard Test', placeholder='(type whatever)')
    else:
        from ux_mk4 import ux_input_digits
        await ux_input_digits('')

async def quick_nfc_test(*a):
    from selftest import test_nfc
    await test_nfc()

async def clear_tested_flag(*a):
    # so can re-create first time power up in
    # factory case (direct to selftest)
    settings.remove_key('tested')
    settings.save()
    await reset_self()

#
# Q wrappers; these will be present, but are very short on mk4
#
async def reflash_gpu(*a):
    from glob import dis
    await dis.gpu.reflash_gpu_ux()

async def scan_any_qr(menu, label, item):
    expect_secret, tmp = item.arg
    await _scan_any_qr(expect_secret, tmp)

async def _scan_any_qr(expect_secret=False, tmp=False, miniscript_wallet=None):
    from ux_q1 import QRScannerInteraction
    x = QRScannerInteraction()
    await x.scan_anything(expect_secret=expect_secret, tmp=tmp,
                          miniscript_wallet=miniscript_wallet)


PUSHTX_SUPPLIERS = [
    # (label, URL)
    ('coldcard.com', 'https://coldcard.com/pushtx#'),
    # from https://github.com/mempool/mempool/pull/5132
    ('mempool.space', 'https://mempool.space/pushtx#'),
]

async def feature_requires_nfc():
    # prompt them that it's need (iff not already enabled)
    # - return F if they decline
    if settings.get('nfc'):
        return True

    # force on NFC, so it works... but they can still turn it off later, etc.
    if not await ux_confirm("This feature requires NFC to be enabled. %s to enable." % OK):
        return False

    settings.set("nfc", 1)
    await change_nfc_enable(1)

    return True

async def pushtx_setup_menu(*a):
    # let them pick a URL from menu to enable "pushtx" feature, and provide
    # some background, and even let them enter a custom URL.

    if not settings.get("ptxurl", False):
        # force a warning on them, unless they are already doing it.
        ch = await ux_show_story(
            "When this is enabled, immediately after transaction signing, you can "
            "tap any NFC-enabled phone on the COLDCARD and your newly-signed "
            "transaction will be immediately broadcast on the public network.\n\n"
            "You must choose a provider by URL here, or give your own URL. "
            "\n\nYour phone's IP address vs. transaction details could be linked by the service. "
            "Make sure your phone is not in airplane mode. Requires NFC.",
            title="PUSH TX",
        )
        if ch != "y":
            return

    if not await feature_requires_nfc():
        # they don't want to proceed
        return

    async def doit(menu, picked, xx_self):
        # using stock values, or Disable 
        val = xx_self.arg[0]

        if val:
            settings.set('ptxurl', val)
        else:
            settings.remove_key('ptxurl')

        menu.chosen = picked
        menu.show()
        await sleep_ms(100)     # visual feedback that we changed it
        the_ux.pop()

    async def edit_custom(menu, picked, xx_self):
        val = xx_self.arg[0]
        while 1:
            nv = await ux_input_text(val, confirm_exit=True, scan_ok=True, prompt="Enter URL")
            # cleanup? URL validation? 
            if nv:
                prob = None
                if (not nv.startswith('http://')) and (not nv.startswith('https://')):
                    prob = "Must start with http:// or https://."
                elif len(nv) < 12:
                    prob = "Too short."
                elif nv[-1] not in '#?&':
                    prob = "Final char must be # or ? or &."

                if prob:
                    await ux_show_story(prob + " Try again.")
                    val = nv
                    continue
            break

        if nv:
            settings.set('ptxurl', nv)
            # force menu redraw
            m = await pushtx_setup_menu()
            the_ux.replace(m)
        else:
            settings.remove_key('ptxurl')
            the_ux.pop()

    was = settings.get("ptxurl", None)
    try:
        cur = [n for n, (l, u) in enumerate(PUSHTX_SUPPLIERS) if u == was][0]
    except IndexError:
        cur = None

    choices = [MenuItem(l, f=doit, arg=(u,)) for l,u in PUSHTX_SUPPLIERS]

    if was and cur is None:
        # they have a non-standard choice
        label = was.split("/")[2]  # pull out domain (netloc)
        choices.append(MenuItem(label, f=edit_custom, arg=(was,)))
        cur = len(choices)-1
    else:
        choices.append(MenuItem("Custom URL...", f=edit_custom, arg=('http',)))

    choices.append(MenuItem("Disable", f=doit, arg=(None,)))

    return MenuSystem(choices, chosen=cur)

# EOF
