public/js/main.js

const {
  PDFDocument,
  StandardFonts,
  rgb
} = PDFLib
/**
 * @namespace document
 */

/**
 * @namespace mainjs
 */

/**
 * Click Event
 * @event document#click
 * @type {Object}
 * @property {HTMLElement} target
 * @property {number} which
 */

/**
 * Keyboard Event
 * @event document#keyboard
 * @type {Object}
 * @property {HTMLElement} target
 * @property {number} which
 * @property {number} keyCode
 */

/**
 * Focus Event
 * @event document#focus
 * @type {Object}
 * @property {HTMLElement} target
 */

/**
 * Change Event
 * @event document#change
 * @type {Object}
 * @property {HTMLElement} target
 */

/**
 * Window Resize
 * @event document#resize
 * @type {Object}
 * @property {window} target
 */

/**
 * Window Hash Change
 * @event document#hashchange
 * @type {Object}
 * @property {window} target
 */

/**
 * @typedef  {Object}   mainjs.WordArray
 * @memberof mainjs
 * @property {number[]} words Bytes array as signed integers
 * @property {number}   sigBytes
 */

/**
 * Default parameters
 * @typedef {{method: string, path: string, mnemonic: string, 
 * passphrase: string, client: string, mnemonicLabel: string}} mainjs.params
 * @memberof mainjs
 */

/** @type {mainjs.params} */
const DefaultParams = {
  method: 'slip10-ed25519',
  path: "m/44'/283'/0'/0/0",
  mnemonic: '',
  passphrase: '',
  client: 'coinomi',
  mnemonicLabel: 'Enter BIP39 Mnemonic (12-24 words)...'
}

/** @type {string[]} */
const clients = ['atomic', 'coinomi', 'exodus', 'ledger', 'trust', 'custom', 'search']

/**
 * @typedef {Object} Status
 * @property {number} done  Completed task
 * @property {number} wip   Work in progress
 * @property {number} err   There was an error
 */
const Status = {
  done: 31401,
  wip: 31402,
  err: 31403
}

const bk = {
  xs: 0,
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1200,
  xxl: 1400
}
const vw = () => Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = () => Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)

/**
 * Returns current viewport breakpoint
 * @returns {string} Abbreviated breakpoint (xs|sm|md|lg|xl|xxl)
 */
const D = () => [...Object.entries(bk)].reverse().reduce((p, [k, v], i) => {
  if (vw() >= v && p === "na") return k;
  else return p
}, "na")

// State global variable:
/** 
 * @type {{d: string, algo: Object, statusTimeoutId: number}} 
 * @memberof mainjs
 */
var S = {
  d: undefined,
  algo: undefined,
  statusTimeoutId: undefined
}

/** 
 * @type {number}
 * @memberof mainjs  
 */
var intervalId

function onDocumentReadyHandler() {
  clear('all')
  S.d = D()
  updateStatus('Ready!')
  tabSwitcher()
}
/**
 * Changes navigation tab based on window location hash
 * @memberof mainjs
 * @listens document#hashchange
 */
function tabSwitcher() {
  switch (window.location.hash) {
    case '#':
    case '':
    case undefined:
    case '#mnemoTab':
      showMnemonicTab()
      break;
    case '#addressTab':
      showAddressTab()
      break;
    case '#infoTab':
      showInfoTab()
      break;
    default:
      // do nothing
      break;
  }
}

/**
 * Generates array with numbers from 0 [+ offset] to N [+ offset]
 * @memberof mainjs  
 * @param {number} n     Size of array
 * @param {number} [o=0] Offset
 * @returns {Array} Array with N numbers
 */
const range = (n, o = 0) => Array.from(new Uint8Array(n).map((e, i) => i + o))

/**
 * Gets current parameters
 * @memberof mainjs  
 * @returns {mainjs.params} params
 */
function getParams() {
  params = {
    mnemonic: $('#mnemoTxt').val(),
    method: $('#method option:checked')[0].text,
    path: getPath(),
    passphrase: $('#mnemoPass').val(),
    client: $('#clients option:checked').val(),
    mnemonicLabel: $('#mnemoLbl').html(),
  }
  return params
}
/**
 * Gets current selected path
 * @memberof mainjs  
 * @returns {string} path
 */
function getPath() {
  path = "m"
  ip = $('.deriv-path')
  ip.map(i => {
    path += '/' + (parseInt(ip[i].value) ? parseInt(ip[i].value) : 0)
    if (i < 3) path += "'"
  })
  return path
}
/**
 * Clears form fields and reset to defaults
 * @memberof mainjs  
 * @param {string} include - Section to include while clearing
 */
function clear(include) {
  $('#bip39seed').val('')
  $('#algoAddress').val('')
  $('#algoKey').val('')
  $('#qrcode').html('')
  $('#qrcode2').html('')
  $('.algo-secret').hide()
  $('#algoMnemonic').html('')
  S.algo = undefined
  if (include == 'mnemonic' || include == 'all') $('#mnemoTxt').val('')
  if (include == 'lookup' || include == 'all') {
    $('#lookupWhat').val('')
    $('#lookupWhat').removeClass('is-valid')
    $('#lookupWhat').removeClass('is-invalid')
    $('#lookupFeedback').html('')
  }
  if (include == 'search' || include == 'all') {
    $('#searchAddress').val('')
    $('#searchAddress').removeClass('is-valid')
    $('#searchAddress').removeClass('is-invalid')
    $('#searchFeedback').html('')
  }
  if (include == 'all') {
    // $('#coinomi')[0].checked = true
    changeWalletClient(DefaultParams.client)
  }
}
/**
 * Switches navigation to the Mnemonic tab
 * @memberof mainjs  
 * @returns {void} Nothing
 */
function showMnemonicTab() {
  $('#mainTitle').html('BIP39 to Algorand Mnemonic')
  $('.mnemo-tab').show()
  $('.address-tab').hide()
  $('.info-tab').hide()
  $('.tools-tab').show()
  $('#mnemoTab').addClass('active')
  $('#addressTab').removeClass('active')
  $('#infoTab').removeClass('active')
  $('#searchSection').hide()
  clear()
  changeWalletClient(getParams().client)
}
/**
 * Switches navigation to the My Cool Address tab
 * @memberof mainjs  
 * @returns {void} Nothing
 */
function showAddressTab() {
  $('#mainTitle').html('Find Me a Cool Address')
  $('.mnemo-tab').hide()
  $('.address-tab').show()
  $('.info-tab').hide()
  $('.tools-tab').show()
  $('#mnemoTab').removeClass('active')
  $('#addressTab').addClass('active')
  $('#infoTab').removeClass('active')
  clear('lookup')
}
/**
 * Switches navigation to the More Info tab
 * @memberof mainjs
 * @returns {void} Nothing
 */
function showInfoTab() {
  $('#mainTitle').html('')
  $('.mnemo-tab').hide()
  $('.address-tab').hide()
  $('.info-tab').show()
  $('.tools-tab').hide()
  $('#mnemoTab').removeClass('active')
  $('#addressTab').removeClass('active')
  $('#infoTab').addClass('active')
  // clear('lookup')
}
/**
 * Displays a status message
 * @memberof mainjs  
 * @param {string} m - Message to display in status
 * @param {Status} s - Status of the task, if done it wil auto clear
 * @returns {void}
 */
function updateStatus(m, s = Status.done) {
  if (S.statusTimeoutId) clearTimeout(S.statusTimeoutId)
  clearStatus()
  if (s === Status.done) {
    S.statusTimeoutId = setTimeout(() => clearStatus(), 3000)
    $("#status").addClass("text-white bg-success")
  } else if (s === Status.wip) {
    $("#status").addClass("text-dark bg-warning")
  } else if (s === Status.err) {
    S.statusTimeoutId = setTimeout(() => clearStatus(), 3000)
    $("#status").addClass("text-white bg-danger")
  } else return
  $("#status").addClass('shadow-sm px-3 py-1')
  $("#status > small").html(m)
}
/**
 * Clears status message
 * @memberof mainjs
 * @returns {void}
 */
function clearStatus() {
  c = ['shadow-sm', 'px-3', 'py-1', 'text-white', 'text-dark',
    'bg-success', 'bg-warning', 'bg-danger'
  ]
  $("#status").removeClass(c)
  $("#status > small").html('')
  S.statusTimeoutId = undefined
}
/**
 * Generates random BIP39 mnemonic
 * @memberof mainjs
 * @returns {void}
 */
function generateMnemonic() {
  w = parseInt($('#mnemoRandSize option:selected').val())
  s = (w * 11 - (w * 11) % 32) / 8
  bip39toalgo.randomWords(s).then(random => {
    $('#mnemoTxt').val(random)
    checkMnemonic()
  })
}
/**
 * Clears mnemonic and (in)valid feedback
 * @memberof mainjs
 * @returns {void}
 */
function clearMnemonic() {
  clear('all')
  $('#mnemoTxt').removeClass('is-valid')
  $('#mnemoTxt').removeClass('is-invalid')
}
/**
 * Verifies if BIP39 mnemonic is valid
 * @memberof mainjs
 * @returns {void}
 */
function checkMnemonic() {
  sel = getParams()
  parsedMnemonic = bip39toalgo.parseMnemonic(sel.mnemonic)
  if (!sel.mnemonic || sel.mnemonic == '') {
    $('#mnemoTxt').removeClass('is-valid')
    $('#mnemoTxt').removeClass('is-invalid')
  } else if (parsedMnemonic.valid) {
    $('#mnemoTxt').addClass('is-valid')
    $('#mnemoTxt').removeClass('is-invalid')
    // $('#mnemoLbl').html('invalid mnemonic')
  } else {
    $('#mnemoTxt').addClass('is-invalid')
    $('#mnemoTxt').removeClass('is-valid')
  }
}
/**
 * Updates path fields in form
 * @memberof mainjs
 * @param {string} path Full derivation path
 * @param {number} disableIndex Disable input for all path index less than this
 * @returns {void}
 */
function printPath(path, disableIndex = -1) {
  ip = $('.deriv-path')
  ip.map(i => {
    if (path) ip[i].value = parseInt(path.split('/')[i + 1])
    else ip[i].value = ''
    if (i < disableIndex) ip[i].disabled = true
    else ip[i].disabled = false
  })
}
/**
 * Changes selected derivation method in dropdown
 * @memberof mainjs
 * @param {string} method Derivation method
 * @returns {void}
 */
function changeMethod(method) {
  mt = $('#method option')
  mt.map(i => {
    if (mt[i].text == method) mt[i].selected = true
  })
}
/**
 * Changes selected wallet client in dropdown
 * @memberof mainjs
 * @param {string} client Wallet client
 * @returns {void}
 */
function changeWalletClient(client) {
  idx = clients.indexOf(client)
  if (idx === -1) throw new Error(`Client "${client}" not supported!`)
  $('#clients')[0].selectedIndex = idx
  if (client != 'custom' && client != 'search') {
    wallet = bip39toalgo.wallets[client]
    $('#searchSection').hide()
    $('#method').prop('disabled', true)
    if (client == 'atomic') printPath(wallet.path, 999)
    else printPath(wallet.path, 2)
    changeMethod(wallet.method)
  } else if (client == 'custom') {
    printPath(DefaultParams.path)
    $('#method').prop('disabled', false)
    $('#searchSection').hide()
  } else if (client == 'search') {
    printPath(DefaultParams.path, 999)
    $('#method').prop('disabled', true)
    $('#searchSection').show()
    $('#startBtn').prop('disabled', true)
  }
  if(client != "search") $('#startBtn').prop('disabled', false)
}
/**
 * Handles change wallet client event and calls {@link mainjs.changeWalletClient}
 * @memberof mainjs
 * @param {document#event:change} e Change event
 * @listens document#change
 */
function handleChangeWalletClient(e) {
  client = e.target.selectedOptions[0].value
  changeWalletClient(client)
}
/**
 * Derives BIP39 mnemonic with selected params and computes related Algorand's mnenomic
 * @memberof mainjs
 * @listens document#click
 * @returns {void}
 */
function startMnemonicDerivation() {
  sel = getParams()
  parsedMnemonic = bip39toalgo.parseMnemonic(sel.mnemonic)
  if (parsedMnemonic.valid) {
    $('#mnemoTxt').removeClass('is-invalid')
    $('#mnemoTxt').addClass('is-valid')
    bip39toalgo.deriveMnemonic(sel.mnemonic, sel.method, sel.path, sel.passphrase)
      .then(node => {
        // console.log(node)
        $('#bip39seed').val(node.bip39seed)
        $('#algoAddress').val(node.algo.address)
        $('#algoKey').val(node.algo.key)
        $('#mnemoTxt').val(parsedMnemonic.mnemonic)
        printWords(node.algo.words)
        printQR(node.algo.words)
        S.algo = node.algo
        updateStatus('done!')
      })
  } else {
    $('#mnemoTxt').removeClass('is-valid')
    $('#mnemoTxt').addClass('is-invalid')
  }
}
/**
 * Displays Alorand secret words section
 * @memberof mainjs
 * @param {string[]} words
 * @returns {void}
 */
function printWords(words) {
  $('.algo-secret').show()
  o = $('#algoMnemonic')
  o.html('')
  s = (D() === 'xs' || D() === 'sm') ? 2 : 5
  cols = range(s).map(i => {
    let div = document.createElement('div')
    div.classList.add('col')
    return div
  })
  idx = 0
  words.map((w, i) => {
    let div = document.createElement('div')
    let pre = document.createElement('pre')
    let span = document.createElement('span')
    div.classList.add('row')
    pre.innerHTML = `${(i+1).toString().padStart(2,' ')}. `
    span.innerHTML = w
    span.style.color = '#d63384' //pink
    pre.appendChild(span)
    div.appendChild(pre)
    cols[idx].appendChild(div)
    if (s == 2 && i == 12) idx++
    if (s == 5 && i % 5 == 4) idx++
  })
  cols.forEach(c => o.append(c))
}

/**
 * Re-print words if viewport crosses one of the defined breakpoints
 * @memberof mainjs
 * @listens document#resize
 * @returns {void}
 */
function rePrintWords() {
  if (D() !== S.d) {
    S.d = D()
    if (S.algo) printWords(S.algo.words)
  }
}
/**
 * Generates and prints QR code with words data
 * @memberof mainjs
 * @param {string[]} words Algorand secret words
 * @returns {void}
 */
function printQR(words) {
  algoMnemonic = {
    version: '1.0',
    mnemonic: words.join(' ')
  }
  $('#qrcode').html('')
  $('#qrcode').qrcode({
    text: JSON.stringify(algoMnemonic),
    width: 296,
    height: 296,
  })
}
/**
 * Generates and prints QR code witl wallet address
 * @memberof mainjs
 * @param {string} address Algorand wallet address
 * @returns {void}
 */
function printAddressQR(address) {
  // txt = {version:'1.0',address: address}
  $('#qrcode2').html('')
  $('#qrcode2').qrcode({
    text: address,
    width: 296,
    height: 296,
  })
}
/**
 * Copies to clipboard the value of clicked element
 * @param {document#event:click} e Click event
 * @listens document#click
 * @returns {void}
 */
function copyTargetValue(e) {
  if (e.target.value || e.target.value !== '') {
    e.target.select()
    e.target.setSelectionRange(0, 99999) /* For mobile devices */
    document.execCommand('copy')
    updateStatus('Copied')
  }
}
/**
 * Searches for Algorand wallet address that starts with given prefix
 * @memberof mainjs
 * @param {string} prefix Beginning sequence of characters to search
 * @param {boolean} alive Active process indicator
 * @param {number} id Interval Id
 * @param {number} tick Initial timestampt
 */
function findMyCoolAddress(prefix, alive, id, tick) {
  bip39toalgo.randomAlgoAddress()
    .then(algo => {
      alive = !algo.address.startsWith(prefix.toUpperCase())
      if (algo) $('#algoAddress').val(algo.address)
      if (!alive) {
        clearInterval(id)
        tock = Date.now()
        $('#lookupFeedback').html('<br>Address found in ' + (tock - tick) / 1000 + ' seconds')
        $('#lookupBtn').prop('disabled', false)
        $('#lookupBtnSpinner').hide()
        $('#algoKey').val(algo.key)
        bip39toalgo.algoWords(algo.key).then(algo => {
          printWords(algo.words)
          printQR(algo.words)
          S.algo = algo
          updateStatus('done!')
        })
      }
    })
}
/**
 * Starts search for wallet address that begins with prefix
 * @memberof mainjs
 * @param {string} prefix Beginning sequence of characters to search
 * @param {boolean} alive Active process indicator
 * @returns {number} Interval Id
 */
function startMyCoolAddressLookup(prefix, alive) {
  clear()
  tick = Date.now()
  updateStatus('Working...', Status.wip)
  id = setInterval(() => {
    findMyCoolAddress(prefix, alive, id, tick)
  }, 10)
  return id
}
/**
 * Handles click address lookup and calls
 * {@link mainjs.startMyCoolAddressLookup}
 * @memberof mainjs
 * @listens document#click
 * @returns {void}
 */
function handleMyCoolAddressLookup() {
  where = 'prefix'
  what = $('#lookupWhat').val()
  check = checkBase32Input('#lookupWhat', '#lookupInvalid', 6)
  if (check > 0) {
    $('#lookupFeedback').html('')
    $('#lookupBtn').prop('disabled', true)
    $('#lookupBtnSpinner').show()
    intervalId = startMyCoolAddressLookup(what, true)
  }
}
/**
 * Kills address lookup process
 * @memberof mainjs
 * @listens document#click
 */
function killMyCoolAddressLookup() {
  clearInterval(intervalId)
  updateStatus('Canceled', Status.err)
  $('#lookupBtn').prop('disabled', false)
  $('#lookupBtnSpinner').hide()
}
/**
 * Derives mnemonic for given field and evaluates if 
 * derived Algorand address matches given address
 * @memberof mainjs
 * @param {string} address Algorand wallet address to search
 * @param {mainjs.WordArray} seed BIP39 seed WordArray
 * @param {string[]} field Search field with derivation method/path combinations
 * @param {number} id Interval Id that started the process
 * @returns {void}
 */
function derivationSearch(address, seed, field, id) {
  if (field.length == 0) {
    // done searching entire derivation method/path field
    return bip39toalgo.deriveBip39Seed(seed, 'bip39-seed', "m/0'/0'/0'/0/0")
      .then(node => {
        $('#bip39seed').val(node.bip39seed)
        $('#algoAddress').val('Not found')
        $('#algoKey').val('Not found')
        updateStatus('Not found!', Status.err)
        return true
      })
      .then(done => endDerivationSearch(done, id))
  }
  method = field[0][0]
  path = field[0][1]
  field.shift()
  ip.map(i => {
    ip[i].value = parseInt(path.split('/')[i + 1])
  })
  mt = $('#method option')
  mt.map(i => {
    if (mt[i].text == method) mt[i].selected = true
  })

  return bip39toalgo.deriveBip39Seed(seed, method, path)
    .then(node => {
      $('#bip39seed').val(node.bip39seed)
      $('#algoAddress').val(node.algo.address)
      $('#algoKey').val(node.algo.key)
      ip = $('.deriv-path')
      if (address == node.algo.address) {
        printWords(node.algo.words)
        printQR(node.algo.words)
        S.algo = node.algo
        updateStatus('done!')
        return true
      } else return false
    })
    .then(done => endDerivationSearch(done, id))
}
/**
 * Starts derivation search process
 * @memberof mainjs
 * @param {string} address Algorand wallet address to search
 * @returns {void}
 */
function startDerivationSearch(address) {
  clear()
  tick = Date.now()
  sel = getParams()
  updateStatus('Working...', Status.wip)
  bip39toalgo.bip39seed(sel.mnemonic, sel.passphrase, )
    .then(seed => {
      methods = ['slip10-ed25519', 'slip10-secp256k1', 'kholaw-ed25519']
      paths = []
      field = [
        ['bip39-seed', "m/0'/0'/0'/0/0"]
      ]
      range(10).map(i => range(10).map(j => paths.push("m/44'/283'/" + i + "'/0/" + j)))
      methods.forEach(m => paths.forEach(p => field.push([m, p])))
      i = 0
      id = setInterval(() => {
        derivationSearch(address, seed, field, id)
      }, 10)
      intervalId = id
    })
}
/**
 * Handles derivation search click event
 * and calls {@link mainjs.startDerivationSearch}
 * @memberof mainjs
 * @listens document#click
 */
function handleDerivationSearch() {
  check = checkBase32Input('#searchAddress', '#searchInvalid', 58, false)
  if (check > 0) {
    sel = getParams()
    parsedMnemonic = bip39toalgo.parseMnemonic(sel.mnemonic)
    if (parsedMnemonic.valid) {
      $('#searchBtn').prop('disabled', true)
      $('#searchBtnSpinner').show()
      address = $('#searchAddress').val()
      startDerivationSearch(address)
    } else {
      $('#mnemoTxt').removeClass('is-valid')
      $('#mnemoTxt').addClass('is-invalid')
    }
  }
}
/**
 * Ends derivation search
 * @memberof mainjs
 * @param {boolean} done Complete indicator
 * @param {number} id Interval Id to clear
 * @returns {void}
 */
function endDerivationSearch(done, id) {
  if (done) {
    clearInterval(id)
    $('#searchBtn').prop('disabled', false)
    $('#searchBtnSpinner').hide()
  }
}
/**
 * Handles cancel derivation search click event
 * @memberof mainjs
 * @listens document#click
 */
function killDerivationSearch(){
  updateStatus('Canceled', Status.err)
  endDerivationSearch(true, intervalId)
}
/**
 * Handles mnemonic input keyboard event,
 * provides auto-complete suggestions
 * and validates word to BIP39 wordlist
 * @memberof mainjs
 * @param {document#event:keyboard} e Keyboard event
 * @listens document#keyboard
 */
function mnemonicKeyboardEventHandler(e) {
  keyCode = e.keyCode || e.which
  m = e.target.value.trim().toLowerCase().normalize('NFKD').split(' ')
  w = m.slice(-1)[0]
  // bw = undefined
  if (e.type == 'keyup' && keyCode != 9) {
    bw = bip39toalgo.findBip39Word(w)
    if (bw) {
      $('#mnemoLbl').html(bw)
      $('#mnemoTxt').removeClass('is-invalid')
    } else if (w.length > 2) {
      w = m.slice(-1)[0]
      $('#mnemoLbl').html('invalid word "' + w + '"')
      $('#mnemoTxt').addClass('is-invalid')
      $('#mnemoTxt').removeClass('is-valid')
    }
  } else if (keyCode == 9) {
    e.preventDefault()
    w = m.slice(-1)[0]
    bw = bip39toalgo.findBip39Word(w)
    if (bw) e.target.value = (m.slice(0, -1).join(' ') + ' ' + bw).trim()
  }
}
/**
 * Handles mnemonic input focusout event
 * @memberof mainjs
 * @param {document#event:focus} e Focusout event
 * @listens document#focus
 * @returns {void}
 */
function mnemonicFocusoutHangler(e) {
  $('#mnemoLbl').html(DefaultParams.mnemonicLabel)
  checkMnemonic()
}
/**
 * Checks if input is a valid base32 string,
 * meets given length/range criteria
 * and is a valid Algorand address
 * @memberof mainjs
 * @param {string} inputFieldId 
 * @param {string} invalidFeedbackId 
 * @param {number} maxLength 
 * @param {boolean} [range=true]
 * @returns {number}
 */
function checkBase32Input(inputFieldId, invalidFeedbackId, maxLength, range = true) {
  b32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
  inputField = $(inputFieldId)
  invalidFeedback = $(invalidFeedbackId)
  m = inputField.val().trim().toUpperCase().normalize('NFKD').split('')
  isValid = true
  if (!range && m.length != maxLength) {
    inputField.removeClass('is-valid')
    inputField.addClass('is-invalid')
    invalidFeedback.html('Invalid search: please input ' + maxLength + ' characters (A-Z and 2-7)')
    return -1
  }
  // out of bounds
  if (m.length == 0 || m.length > maxLength) {
    inputField.removeClass('is-valid')
    inputField.addClass('is-invalid')
    invalidFeedback.html('Invalid search: please input 1 to ' + maxLength + ' characters (A-Z and 2-7)')
    return -1
  }
  // Algorand addresses have length of 58 chars
  if (maxLength == 58 && m.length == 58) {
    b32endings = b32.split('').reduce((p, c, i) => {
      if (i % 4 == 0) p.push(c);
      return p
    }, [])
    validEnding = b32endings.indexOf(m.splice(-1)[0]) >= 0
    if (!validEnding) {
      inputField.removeClass('is-valid')
      inputField.addClass('is-invalid')
      invalidFeedback.html('Invalid Algorand address, last character should be A,E,I,M,Q,U,Y or 4')
      return -1
    }
  }

  m.map((e, i, a) => {
    isValid &= b32.search(e) >= 0
  })

  if (!isValid) {
    inputField.removeClass('is-valid')
    inputField.addClass('is-invalid')
    invalidFeedback.html('Invalid search: valid characters are A-Z and 2-7')
    return -1
  } else {
    inputField.removeClass('is-invalid')
    inputField.addClass('is-valid')
    return 1
  }
}

/**
 * Formats list of 25 words in a 5x5 grid, indexed Top-to-Bottom
 * @memberof mainjs
 * @param {string[]} words - List of Algorand's 25 secret words
 * @returns {string} Formatted string
 */
function prettifyWords(words) {
  prettyWords = words.map((w, i) => {
    w = ((i + 1).toString().padStart(2) + '. ' + w).padEnd(15)
    if (i >= 20) w += '\n'
    return w
  }).map((w, i, a) => {
    return a[5 * (i % 5) + Math.floor(i / 5)]
  })
  return prettyWords.join('')
}
/**
 * Creates PDF file on-the-fly (i.e. offline, 
 * all done in the browser) with Algorand Wallet and Secret
 * @memberof mainjs
 * @listens document#click
 */
async function createPdf() {
  const pdfDoc = await PDFDocument.create()
  const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)
  const courierFont = await pdfDoc.embedFont(StandardFonts.Courier)
  const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)

  const page = pdfDoc.addPage()

  printAddressQR(S.algo.address)
  const addressCanvas = $("#qrcode2 > canvas")[0]
  const addressCanvasImageData = addressCanvas ? addressCanvas.toDataURL() : undefined
  const addressCanvasImage = await pdfDoc.embedPng(addressCanvasImageData)
  $('#qrcode2').html('')

  const secretCanvas = $("#qrcode > canvas")[0]
  const secretCanvasImageData = secretCanvas ? secretCanvas.toDataURL() : undefined
  const secretCanvasImage = await pdfDoc.embedPng(secretCanvasImageData)

  const addressCanvasDims = addressCanvasImage.scale(0.5)
  const secretCanvasDims = secretCanvasImage.scale(0.7)

  const fontSize = 11

  height = page.getHeight() - 4 * fontSize
  txt = `ALGORAND OFFLINE PAPER WALLET
Use this wallet as a cold storage option. Import the wallet address as a view-only account in
the official Algorand app. Your secret will be completely offline. Store in a secure place!`

  page.drawText(txt, {
    x: 50,
    y: height,
    size: fontSize,
    font: helveticaFont,
    color: rgb(0, 0, 0)
  })

  txt = `Wallet Address\n`
  height -= 8 * fontSize
  page.drawText(txt, {
    x: 50,
    y: height,
    size: fontSize,
    font: helveticaFont,
    color: rgb(0, 0, 0)
  })

  txt = `${S.algo.address}\n`
  height -= 2 * fontSize
  page.drawText(txt, {
    x: 50,
    y: height,
    size: fontSize,
    font: courierFont,
    color: rgb(214 / 255, 51 / 255, 132 / 255) // magenta
  })

  height -= 2 * fontSize + addressCanvasDims.height
  page.drawImage(addressCanvasImage, {
    x: page.getWidth() / 2 - addressCanvasDims.width / 2, // center horizontally
    y: height,
    width: addressCanvasDims.width,
    height: addressCanvasDims.height,
  })

  txt = `Wallet Secret\n`
  height -= 4 * fontSize
  page.drawText(txt, {
    x: 50,
    y: height,
    size: fontSize,
    font: helveticaFont,
    color: rgb(0, 0, 0)
  })

  txt = prettifyWords(S.algo.words)
  height -= 2 * fontSize
  page.drawText(txt, {
    x: 50,
    y: height,
    size: fontSize,
    font: courierFont,
    color: rgb(214 / 255, 51 / 255, 132 / 255) // magenta
  })

  height -= 11 * fontSize + secretCanvasDims.height
  page.drawImage(secretCanvasImage, {
    x: page.getWidth() / 2 - secretCanvasDims.width / 2, // center horizontally
    y: height,
    width: secretCanvasDims.width,
    height: secretCanvasDims.height,
  })

  // Serialize the PDFDocument to bytes (a Uint8Array)
  const pdfBytes = await pdfDoc.save()

  // Trigger the browser to download the PDF document
  download(pdfBytes, `${S.algo.address}.pdf`, "application/pdf");
}

$(document).ready(onDocumentReadyHandler)
$(window).on('hashchange', tabSwitcher)
$(window).resize(rePrintWords)

// $('#mnemoTab').on('click', showMnemonicTab)
// $('#addressTab').on('click', showAddressTab)
// $('#infoTab').on('click', showInfoTab)

$('#mnemoClear').on('click', clearMnemonic)
$('#mnemoRand').on('click', generateMnemonic)
$('#clearResultsBtn').on('click', clear)
$('#bip39seed, #algoAddress, #algoKey').on('click', copyTargetValue)

$('#startBtn').on('click', startMnemonicDerivation)
$('#clients').on('change', handleChangeWalletClient)

$('#searchBtn').on('click', handleDerivationSearch)
$('#searchKill').on('click', killDerivationSearch)
$('#searchClear').on('click', () => clear('search'))

$('#lookupBtn').on('click', handleMyCoolAddressLookup)
$('#lookupKill').on('click', killMyCoolAddressLookup)
$('#lookupClear').on('click', () => clear('lookup'))

$('#mnemoTxt').on('keyup keydown', mnemonicKeyboardEventHandler)
$('#mnemoTxt').on('focusout', mnemonicFocusoutHangler)

$('#searchAddress').on('keyup', function (e) {
  checkBase32Input('#searchAddress', '#searchInvalid', 58)
})
$('#lookupWhat').on('keyup', function (e) {
  checkBase32Input('#lookupWhat', '#lookupInvalid', 6)
})

$('#btnPDF').on('click', () => createPdf())