decred.org/dcrdex@v1.0.3/client/webserver/site/src/js/init.ts (about)

     1  import Doc from './doc'
     2  import BasePage from './basepage'
     3  import { postJSON } from './http'
     4  import * as intl from './locales'
     5  import {
     6    bind as bindForm,
     7    slideSwap
     8  } from './forms'
     9  import { Wave } from './charts'
    10  import {
    11    app,
    12    PageElement,
    13    SupportedAsset,
    14    User,
    15    WalletInfo,
    16    WalletDefinition,
    17    ConfigOption,
    18    APIResponse
    19  } from './registry'
    20  
    21  interface InitResponse extends APIResponse {
    22    hosts: string[]
    23    mnemonic?: string
    24  }
    25  
    26  /*
    27   * InitPage is the page handler for the /init view. InitPage is essentially a
    28   * form handler. There are no non-form elements on /init. InitPage additionally
    29   * has a role caching the initialization password. A couple of notes about
    30   * InitPage.
    31   *   1) There is no going backwards. Once you set a password, you can't go back
    32   *      to the password form. If you refresh, you won't end up on /init, so
    33   *      won't have access to the QuickConfigForm or SeedBackupForm . Once you
    34   *      submit your auto-config choices, you can't change them. This has
    35   *      implications for coding and UI. There are no "go back" or "close form"
    36   *      elements.
    37   *   2) The user can preclude auto-config and seed backup by clicking an
    38   *      available header link after password init, e.g. Wallets, in the page
    39   *      header. NOTE: Regardless of what the user does after setting the app
    40   *      pass, they will receive a notification reminding them to back up their
    41   *      seed. Perhaps it would be better to somehow delay that message until
    42   *      they choose to ignore the seed backup dialog, but having more reminders
    43   *      is also okay.
    44   */
    45  export default class InitPage extends BasePage {
    46    body: HTMLElement
    47    page: Record<string, PageElement>
    48    initForm: AppInitForm
    49    quickConfigForm: QuickConfigForm
    50    seedBackupForm: SeedBackupForm
    51    mnemonic?: string
    52  
    53    constructor (body: HTMLElement) {
    54      super()
    55      this.body = body
    56      const page = this.page = Doc.idDescendants(body)
    57      this.initForm = new AppInitForm(page.appPWForm, (pw: string, hosts: string[], mnemonic?: string) => { this.appInited(pw, hosts, mnemonic) })
    58      this.quickConfigForm = new QuickConfigForm(page.quickConfigForm, () => this.quickConfigDone())
    59      this.seedBackupForm = new SeedBackupForm(page.seedBackupForm, () => this.seedBackedUp())
    60    }
    61  
    62    async appInited (pw: string, hosts: string[], mnemonic?: string) {
    63      this.mnemonic = mnemonic
    64      const page = this.page
    65      await this.quickConfigForm.update(pw, hosts)
    66      if (mnemonic) this.seedBackupForm.update(mnemonic)
    67      slideSwap(page.appPWForm, page.quickConfigForm)
    68    }
    69  
    70    quickConfigDone () {
    71      if (!this.mnemonic) app().loadPage('wallets')
    72      else slideSwap(this.page.quickConfigForm, this.page.seedBackupForm)
    73    }
    74  
    75    seedBackedUp () {
    76      app().loadPage('wallets')
    77    }
    78  }
    79  
    80  /*
    81   * The AppInitForm handles the form that sets the app password, accepts an
    82   * optional seed, and initializes the app.
    83   */
    84  class AppInitForm {
    85    form: PageElement
    86    page: Record<string, PageElement>
    87    success: (pw: string, hosts: string[], mnemonic?: string) => void
    88  
    89    constructor (form: PageElement, success: (pw: string, hosts: string[], mnemonic?: string) => void) {
    90      this.form = form
    91      this.success = success
    92      const page = this.page = Doc.idDescendants(form)
    93      bindForm(form, page.appPWSubmit, () => this.setAppPass())
    94      bindForm(form, page.toggleSeedInput, () => {
    95        if (Doc.isHidden(page.seedInputBox)) {
    96          page.toggleSeedInputIcon.classList.remove('ico-plus')
    97          page.toggleSeedInputIcon.classList.add('ico-minus')
    98          Doc.show(page.seedInputBox)
    99        } else {
   100          page.toggleSeedInputIcon.classList.remove('ico-minus')
   101          page.toggleSeedInputIcon.classList.add('ico-plus')
   102          Doc.hide(page.seedInputBox)
   103        }
   104      })
   105    }
   106  
   107    /* Set the application password. Attached to form submission. */
   108    async setAppPass () {
   109      const page = this.page
   110      Doc.hide(page.appPWErrMsg)
   111      const pw = page.appPW.value || ''
   112      const pwAgain = page.appPWAgain.value
   113      if (pw === '') {
   114        page.appPWErrMsg.textContent = intl.prep(intl.ID_NO_PASS_ERROR_MSG)
   115        Doc.show(page.appPWErrMsg)
   116        return
   117      }
   118      if (pw !== pwAgain) {
   119        page.appPWErrMsg.textContent = intl.prep(intl.ID_PASSWORD_NOT_MATCH)
   120        Doc.show(page.appPWErrMsg)
   121        return
   122      }
   123  
   124      page.appPW.value = ''
   125      page.appPWAgain.value = ''
   126      const loaded = app().loading(this.form)
   127      // const seed = page.seedInput.value?.replace(/\s+/g, '') // strip whitespace
   128      const seed = page.seedInput.value ?? ''
   129      const res: InitResponse = await postJSON('/api/init', {
   130        pass: pw,
   131        seed: seed
   132      })
   133      loaded()
   134      if (!app().checkResponse(res)) {
   135        page.appPWErrMsg.textContent = res.msg
   136        Doc.show(page.appPWErrMsg)
   137        return
   138      }
   139      this.success(pw, res.hosts, res.mnemonic)
   140    }
   141  }
   142  
   143  // HostConfigRow is used by the QuickConfigForm to track the user's choices.
   144  interface HostConfigRow {
   145    host: string
   146    checkbox: HTMLInputElement
   147  }
   148  
   149  // WalletConfigRow is used by the QuickConfigForm to track the user's choices.
   150  interface WalletConfigRow {
   151    asset: SupportedAsset
   152    type: string
   153    checkbox: HTMLInputElement
   154  }
   155  
   156  let rowIDCounter = 0
   157  
   158  /*
   159   * QuickConfigForm handles the form that allows users to quickly configure
   160   * view-only servers and native wallets (that don't require any configuration).
   161   */
   162  class QuickConfigForm {
   163    page: Record<string, PageElement>
   164    form: PageElement
   165    servers: HostConfigRow[]
   166    wallets: WalletConfigRow[]
   167    pw: string
   168    success: () => void
   169  
   170    constructor (form: PageElement, success: () => void) {
   171      this.form = form
   172      this.success = success
   173      const page = this.page = Doc.idDescendants(form)
   174      Doc.cleanTemplates(page.qcServerTmpl, page.qcWalletTmpl)
   175      bindForm(form, page.quickConfigSubmit, () => { this.submit() })
   176      bindForm(form, page.qcErrAck, () => { this.success() })
   177    }
   178  
   179    async update (pw: string, hosts: string[]) {
   180      this.pw = pw
   181      const page = this.page
   182  
   183      this.servers = []
   184      for (const host of hosts) {
   185        const row = page.qcServerTmpl.cloneNode(true) as PageElement
   186        page.qcServersBox.appendChild(row)
   187        const tmpl = Doc.parseTemplate(row)
   188        rowIDCounter++
   189        const rowID = `qcsrow${rowIDCounter}`
   190        row.htmlFor = rowID
   191        tmpl.checkbox.id = rowID
   192        tmpl.host.textContent = host
   193        this.servers.push({ host, checkbox: tmpl.checkbox as HTMLInputElement })
   194      }
   195  
   196      const u = await app().fetchUser() as User
   197      this.wallets = []
   198      for (const a of Object.values(u.assets)) {
   199        if (a.token) continue
   200        const winfo = a.info as WalletInfo
   201        let autoConfigurable: WalletDefinition | null = null
   202        for (const wDef of winfo.availablewallets) {
   203          if (!wDef.seeded) continue
   204          if (wDef.configopts && wDef.configopts.some((opt: ConfigOption) => opt.required)) continue
   205          autoConfigurable = wDef
   206          break
   207        }
   208        if (!autoConfigurable) continue
   209        const row = page.qcWalletTmpl.cloneNode(true) as PageElement
   210        page.qcWalletsBox.appendChild(row)
   211        const tmpl = Doc.parseTemplate(row)
   212        rowIDCounter++
   213        const rowID = `qcwrow${rowIDCounter}`
   214        row.htmlFor = rowID
   215        tmpl.checkbox.id = rowID
   216        tmpl.icon.src = Doc.logoPath(a.symbol)
   217        tmpl.name.textContent = a.name
   218        this.wallets.push({
   219          asset: a,
   220          type: autoConfigurable.type,
   221          checkbox: tmpl.checkbox as HTMLInputElement
   222        })
   223      }
   224    }
   225  
   226    async submit () {
   227      const [failedHosts, failedWallets]: [string[], string[]] = [[], []]
   228      const ani = new Wave(this.form, { backgroundColor: true, message: '...' })
   229      ani.opts.message = intl.prep(intl.ID_ADDING_SERVERS)
   230      const connectServer = async (srvRow: HostConfigRow) => {
   231        if (!srvRow.checkbox.checked) return
   232        const req = {
   233          addr: srvRow.host,
   234          appPW: this.pw
   235        }
   236        const res = await postJSON('/api/adddex', req) // DRAFT NOTE: ignore errors ok?
   237        if (!app().checkResponse(res)) failedHosts.push(srvRow.host)
   238      }
   239      await Promise.all(this.servers.map(connectServer))
   240  
   241      ani.opts.message = intl.prep(intl.ID_CREATING_WALLETS)
   242      const createWallet = async (walletRow: WalletConfigRow) => {
   243        const { asset: a, type, checkbox } = walletRow
   244        if (!checkbox.checked) return
   245        const config: Record<string, string> = {}
   246        const walletDef = app().walletDefinition(a.id, type)
   247        for (const opt of (walletDef.configopts ?? [])) {
   248          if (!opt.default) continue
   249          if (opt.isboolean) {
   250            config[opt.key] = opt.default ? '1' : '0'
   251            continue
   252          }
   253          if (opt.repeatable && config[opt.key]) config[opt.key] += opt.repeatable + opt.default
   254          else config[opt.key] = String(opt.default)
   255        }
   256        const createForm = {
   257          assetID: a.id,
   258          appPass: this.pw,
   259          config: config,
   260          walletType: type
   261        }
   262        const res = await postJSON('/api/newwallet', createForm)
   263        if (!app().checkResponse(res)) failedWallets.push(a.name)
   264      }
   265      await Promise.all(this.wallets.map(createWallet))
   266  
   267      ani.stop()
   268      await app().fetchUser() // Calls updateMenuItemsDisplay internally
   269      if (failedWallets.length + failedHosts.length === 0) return this.success()
   270  
   271      const page = this.page
   272      Doc.hide(page.qcChoices)
   273      Doc.show(page.qcErrors)
   274  
   275      if (failedHosts.length) {
   276        for (const host of failedHosts) {
   277          page.qcServerErrorList.appendChild(document.createTextNode(host))
   278          page.qcServerErrorList.appendChild(document.createElement('br'))
   279        }
   280      } else Doc.hide(page.qcServerErrors)
   281  
   282      if (failedWallets.length) {
   283        for (const name of failedWallets) {
   284          page.qcWalletErrorList.appendChild(document.createTextNode(name))
   285          page.qcWalletErrorList.appendChild(document.createElement('br'))
   286        }
   287      } else Doc.hide(page.qcWalletErrors)
   288    }
   289  }
   290  
   291  /*
   292   * SeedBackupForm handles the form that allows the user to back up their seed
   293   * during initialization.
   294   */
   295  class SeedBackupForm {
   296    form: PageElement
   297    page: Record<string, PageElement>
   298    mnemonic: string
   299  
   300    constructor (form: PageElement, success: () => void) {
   301      this.form = form
   302      const page = this.page = Doc.idDescendants(form)
   303      bindForm(form, page.seedAck, () => success())
   304      bindForm(form, page.showSeed, () => this.showSeed())
   305    }
   306  
   307    update (mnemonic: string) {
   308      this.mnemonic = mnemonic
   309    }
   310  
   311    showSeed () {
   312      const page = this.page
   313      page.mnemonic.textContent = this.mnemonic // `${words.slice(0, 5).join(' ')}\n${words.slice(5, 10).join(' ')}\n${words.slice(10,15).join(' ')}`
   314      Doc.hide(page.sbWanna)
   315      Doc.show(page.sbSeed)
   316    }
   317  }