<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'

const acorn = require('acorn')
const moment = require('moment')
const Immutable = require('immutable')
const electron = require('electron')
const ipc = electron.ipcMain
const path = require('path')
const os = require('os')
const qr = require('qr-image')
const underscore = require('underscore')
const tldjs = require('tldjs')
const urlFormat = require('url').format
const levelUp = require('level')
const random = require('random-lib')
const uuid = require('uuid')
const BigNumber = require('bignumber.js')

// Actions
const appActions = require('../../../js/actions/appActions')

// State
const ledgerState = require('../../common/state/ledgerState')
const pageDataState = require('../../common/state/pageDataState')
const migrationState = require('../../common/state/migrationState')

// Constants
const settings = require('../../../js/constants/settings')
const messages = require('../../../js/constants/messages')

// Utils
const tabs = require('../../browser/tabs')
const locale = require('../../locale')
const appConfig = require('../../../js/constants/appConfig')
const getSetting = require('../../../js/settings').getSetting
const {fileUrl, getSourceAboutUrl, isSourceAboutUrl} = require('../../../js/lib/appUrlUtil')
const urlParse = require('../../common/urlParse')
const ruleSolver = require('../../extensions/brave/content/scripts/pageInformation')
const request = require('../../../js/lib/request')
const ledgerUtil = require('../../common/lib/ledgerUtil')
const tabState = require('../../common/state/tabState')
const pageDataUtil = require('../../common/lib/pageDataUtil')
const {getWebContents} = require('../../browser/webContentsCache')

// Caching
let locationDefault = 'NOOP'
let currentUrl = locationDefault
let currentTimestamp = new Date().getTime()
let visitsByPublisher = {}
let bootP
let quitP
const _internal = {
  verboseP: process.env.LEDGER_VERBOSE || false,
  debugP: process.env.LEDGER_DEBUG || false,
  ruleset: {
    raw: [],
    cooked: []
  }
}

// Libraries
let ledgerPublisher
let ledgerClient
let client
let synopsis

// Timers
let balanceTimeoutId = false
let runTimeoutId

// Database
let v2RulesetDB
const v2RulesetPath = 'ledger-rulesV2.leveldb'
let v2PublishersDB
const v2PublishersPath = 'ledger-publishersV2.leveldb'
const statePath = 'ledger-state.json'
const newClientPath = 'ledger-newstate.json'

// Definitions
const miliseconds = {
  year: 365 * 24 * 60 * 60 * 1000,
  week: 7 * 24 * 60 * 60 * 1000,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
}
const clientOptions = {
  debugP: process.env.LEDGER_DEBUG,
  loggingP: process.env.LEDGER_LOGGING,
  rulesTestP: process.env.LEDGER_RULES_TESTING,
  verboseP: process.env.LEDGER_VERBOSE,
  server: process.env.LEDGER_SERVER_URL,
  createWorker: electron.app.createWorker,
  version: 'v2'
}
const fileTypes = {
  bmp: new Buffer([0x42, 0x4d]),
  gif: new Buffer([0x47, 0x49, 0x46, 0x38, [0x37, 0x39], 0x61]),
  ico: new Buffer([0x00, 0x00, 0x01, 0x00]),
  jpeg: new Buffer([0xff, 0xd8, 0xff]),
  png: new Buffer([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
}
const minimumVisitTimeDefault = 8 * 1000
const nextAddFundsTime = 3 * miliseconds.day
let signatureMax = 0
underscore.keys(fileTypes).forEach((fileType) => {
  if (signatureMax < fileTypes[fileType].length) signatureMax = fileTypes[fileType].length
})
signatureMax = Math.ceil(signatureMax * 1.5)

// Notifications
const sufficientBalanceToReconcile = (state) => {
  const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0)
  const unconfirmed = Number(ledgerState.getInfoProp(state, 'unconfirmed') || 0)
  const bat = ledgerState.getInfoProp(state, 'bat')
  return bat && (balance + unconfirmed > 0.9 * Number(bat))
}
const hasFunds = (state) => {
  const balance = getSetting(settings.PAYMENTS_ENABLED)
    ? Number(ledgerState.getInfoProp(state, 'balance') || 0)
    : 0
  return balance > 0
}
const shouldShowNotificationReviewPublishers = () => {
  const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP)
  return !nextTime || (new Date().getTime() > nextTime)
}
const shouldShowNotificationAddFunds = () => {
  const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP)
  return !nextTime || (new Date().getTime() > nextTime)
}
const notifications = {
  text: {
    hello: locale.translation('updateHello'),
    paymentDone: undefined,
    addFunds: locale.translation('addFundsNotification'),
    tryPayments: locale.translation('notificationTryPayments'),
    reconciliation: locale.translation('reconciliationNotification'),
    walletConvertedToBat: locale.translation('walletConvertedToBat')
  },
  pollingInterval: 15 * miliseconds.minutes,
  timeout: undefined,
  displayOptions: {
    style: 'greetingStyle',
    persist: false
  },
  init: (state) => {
    // Check if relevant browser notifications should be shown every 15 minutes
    if (notifications.timeout) {
      clearInterval(notifications.timeout)
    }
    notifications.timeout = setInterval((state) => {
      notifications.onInterval(state)
    }, notifications.pollingInterval, state)
  },
  onLaunch: (state) => {
    const enabled = getSetting(settings.PAYMENTS_ENABLED)
    if (!enabled) {
      return state
    }

    state = checkBtcBatMigrated(state, enabled)

    if (hasFunds(state)) {
      // Don't bother processing the rest, which are only notifications.
      if (!getSetting(settings.PAYMENTS_NOTIFICATIONS)) {
        return state
      }

      // Show one-time BAT conversion message:
      // - if payments are enabled
      // - user has a positive balance
      // - this is an existing profile (new profiles will have firstRunTimestamp matching batMercuryTimestamp)
      // - wallet has been transitioned
      // - notification has not already been shown yet
      // (see https://github.com/brave/browser-laptop/issues/11021)
      const isNewInstall = migrationState.isNewInstall(state)
      const hasUpgradedWallet = migrationState.hasUpgradedWallet(state)
      const hasBeenNotified = migrationState.hasBeenNotified(state)
      if (!isNewInstall && hasUpgradedWallet && !hasBeenNotified) {
        notifications.showBraveWalletUpdated()
      }
    }

    return state
  },
  onInterval: (state) => {
    if (getSetting(settings.PAYMENTS_ENABLED)) {
      if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) {
        notifications.showEnabledNotifications(state)
      }
    } else {
      notifications.showDisabledNotifications(state)
    }
  },
  onResponse: (message, buttonIndex, activeWindow) => {
    switch (message) {
      case notifications.text.addFunds:
        // See showNotificationAddFunds() for buttons.
        // buttonIndex === 1 is "Later"; the timestamp until which to delay is set
        // in showNotificationAddFunds() when triggering this notification.
        if (buttonIndex === 0) {
          appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false)
        } else if (buttonIndex === 2 && activeWindow) {
          // Add funds: Open payments panel
          appActions.createTabRequested({
            url: 'about:preferences#payments',
            windowId: activeWindow.id
          })
        }
        break

      case notifications.text.reconciliation:
        // buttonIndex === 1 is Dismiss
        if (buttonIndex === 0) {
          appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false)
        } else if (buttonIndex === 2 && activeWindow) {
          appActions.createTabRequested({
            url: 'about:preferences#payments',
            windowId: activeWindow.id
          })
        }
        break

      case notifications.text.paymentDone:
        if (buttonIndex === 0) {
          appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false)
        }
        break

      case notifications.text.tryPayments:
        if (buttonIndex === 1 && activeWindow) {
          appActions.createTabRequested({
            url: 'about:preferences#payments',
            windowId: activeWindow.id
          })
        }
        appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true)
        break

      case notifications.text.walletConvertedToBat:
        if (buttonIndex === 0) {
          // Open backup modal
          appActions.createTabRequested({
            url: 'about:preferences#payments?ledgerBackupOverlayVisible',
            windowId: activeWindow.id
          })
        }
        break

      default:
        return
    }

    appActions.hideNotification(message)
  },
  /**
   * Show message that it's time to add funds if reconciliation is less than
   * a day in the future and balance is too low.
   * 24 hours prior to reconciliation, show message asking user to review
   * their votes.
   */
  showEnabledNotifications: (state) => {
    const reconcileStamp = ledgerState.getInfoProp(state, 'reconcileStamp')
    if (!reconcileStamp) {
      return
    }

    if (reconcileStamp - new Date().getTime() < miliseconds.day) {
      if (sufficientBalanceToReconcile(state)) {
        if (shouldShowNotificationReviewPublishers()) {
          const reconcileFrequency = ledgerState.getInfoProp(state, 'reconcileFrequency')
          notifications.showReviewPublishers(reconcileStamp + ((reconcileFrequency - 2) * miliseconds.day))
        }
      } else if (shouldShowNotificationAddFunds()) {
        notifications.showAddFunds()
      }
    } else if (reconcileStamp - new Date().getTime() < 2 * miliseconds.day) {
      if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) {
        notifications.showReviewPublishers(new Date().getTime() + miliseconds.day)
      }
    }
  },
  showDisabledNotifications: (state) => {
    if (!getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) {
      const firstRunTimestamp = state.get('firstRunTimestamp')
      if (new Date().getTime() - firstRunTimestamp < appConfig.payments.delayNotificationTryPayments) {
        return
      }

      appActions.showNotification({
        greeting: locale.translation('updateHello'),
        message: notifications.text.tryPayments,
        buttons: [
          {text: locale.translation('noThanks')},
          {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'}
        ],
        options: {
          style: 'greetingStyle',
          persist: false
        }
      })
    }
  },
  showReviewPublishers: (nextTime) => {
    appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime)

    appActions.showNotification({
      greeting: notifications.text.hello,
      message: notifications.text.reconciliation,
      buttons: [
        {text: locale.translation('turnOffNotifications')},
        {text: locale.translation('dismiss')},
        {text: locale.translation('reviewSites'), className: 'primaryButton'}
      ],
      options: notifications.displayOptions
    })
  },
  showAddFunds: () => {
    const nextTime = new Date().getTime() + nextAddFundsTime
    appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime)

    appActions.showNotification({
      greeting: notifications.text.hello,
      message: notifications.text.addFunds,
      buttons: [
        {text: locale.translation('turnOffNotifications')},
        {text: locale.translation('updateLater')},
        {text: locale.translation('addFunds'), className: 'primaryButton'}
      ],
      options: notifications.displayOptions
    })
  },
  // Called from observeTransactions() when we see a new payment (transaction).
  showPaymentDone: (transactionContributionFiat) => {
    notifications.text.paymentDone = locale.translation('notificationPaymentDone')
      .replace(/{{\s*amount\s*}}/, transactionContributionFiat.amount)
      .replace(/{{\s*currency\s*}}/, transactionContributionFiat.currency)
    // Hide the 'waiting for deposit' message box if it exists
    appActions.hideNotification(notifications.text.addFunds)
    appActions.showNotification({
      greeting: locale.translation('updateHello'),
      message: notifications.text.paymentDone,
      buttons: [
        {text: locale.translation('turnOffNotifications')},
        {text: locale.translation('Ok'), className: 'primaryButton'}
      ],
      options: notifications.displayOptions
    })
  },
  showBraveWalletUpdated: () => {
    appActions.onBitcoinToBatNotified()

    appActions.showNotification({
      greeting: notifications.text.hello,
      message: notifications.text.walletConvertedToBat,
      // Learn More.
      buttons: [
        {text: locale.translation('walletConvertedBackup')},
        {text: locale.translation('walletConvertedDismiss')}
      ],
      options: {
        style: 'greetingStyle',
        persist: false,
        advancedLink: 'https://brave.com/faq-payments/#brave-payments',
        advancedText: locale.translation('walletConvertedLearnMore')
      }
    })
  }
}

// TODO is it ok to have IPC here or is there better place
if (ipc) {
  ipc.on(messages.LEDGER_PUBLISHER, (event, location) => {
    if (!synopsis || event.sender.session === electron.session.fromPartition('default') || !tldjs.isValid(location)) {
      event.returnValue = {}
      return
    }

    let ctx = urlParse(location, true)
    ctx.TLD = tldjs.getPublicSuffix(ctx.host)
    if (!ctx.TLD) {
      if (_internal.verboseP) console.log('\nno TLD for:' + ctx.host)
      event.returnValue = {}
      return
    }

    ctx = underscore.mapObject(ctx, function (value) {
      if (!underscore.isFunction(value)) return value
    })
    ctx.URL = location
    ctx.SLD = tldjs.getDomain(ctx.host)
    ctx.RLD = tldjs.getSubdomain(ctx.host)
    ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : ''

    if (!event.sender.isDestroyed()) {
      event.sender.send(messages.LEDGER_PUBLISHER_RESPONSE + '-' + location, {
        context: ctx,
        rules: _internal.ruleset.cooked
      })
    }
  })

  ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex) => {
    notifications.onResponse(
      message,
      buttonIndex,
      electron.BrowserWindow.getActiveWindow()
    )
  })
}

let ledgerPaymentsPresent = {}
const paymentPresent = (state, tabId, present) => {
  if (present) {
    ledgerPaymentsPresent[tabId] = present
  } else {
    delete ledgerPaymentsPresent[tabId]
  }

  if (Object.keys(ledgerPaymentsPresent).length > 0 && getSetting(settings.PAYMENTS_ENABLED)) {
    if (!balanceTimeoutId) {
      getBalance(state)
    }
  } else if (balanceTimeoutId) {
    clearTimeout(balanceTimeoutId)
    balanceTimeoutId = false
  }
}

const addFoundClosed = (state) => {
  if (balanceTimeoutId) {
    clearTimeout(balanceTimeoutId)
  }
  const balanceFn = getBalance.bind(null, state)
  balanceTimeoutId = setTimeout(balanceFn, 5 * miliseconds.second)
}

const boot = () => {
  if (bootP || client) {
    return
  }

  bootP = true
  const fs = require('fs')
  fs.access(pathName(statePath), fs.FF_OK, (err) => {
    if (!err) return

    if (err.code !== 'ENOENT') console.error('statePath read error: ' + err.toString())

    appActions.onBootStateFile()
  })
}

const onBootStateFile = (state) => {
  state = ledgerState.setInfoProp(state, 'creating', true)

  try {
    clientprep()
    client = ledgerClient(null, underscore.extend({roundtrip: roundtrip}, clientOptions), null)
  } catch (ex) {
    state = ledgerState.resetInfo(state)
    bootP = false
    console.error('ledger client boot error: ', ex)
    return state
  }

  if (client.sync(callback) === true) {
    run(random.randomInt({min: miliseconds.minute, max: 10 * miliseconds.minute}))
  }

  getBalance(state)

  bootP = false

  return state
}

const promptForRecoveryKeyFile = () => {
  const defaultRecoveryKeyFilePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt')
  let files
  if (process.env.SPECTRON) {
    // skip the dialog for tests
    console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`)
    files = [defaultRecoveryKeyFilePath]
  } else {
    const dialog = electron.dialog
    files = dialog.showOpenDialog({
      properties: ['openFile'],
      defaultPath: defaultRecoveryKeyFilePath,
      filters: [{
        name: 'TXT files',
        extensions: ['txt']
      }]
    })
  }

  return (files && files.length ? files[0] : null)
}

const logError = (state, err, caller) => {
  if (err) {
    console.error('Error in %j: %j', caller, err)
    state = ledgerState.setLedgerError(state, err, caller)
  } else {
    state = ledgerState.setLedgerError(state)
  }

  return state
}

const loadKeysFromBackupFile = (state, filePath) => {
  let recoveryKey = null
  const fs = require('fs')
  let data = fs.readFileSync(filePath)

  if (!data || !data.length || !(data.toString())) {
    state = logError(state, 'No data in backup file', 'recoveryWallet')
  } else {
    try {
      const recoveryFileContents = data.toString()

      let messageLines = recoveryFileContents.split(os.EOL)

      let passphraseLine = '' || messageLines[3]

      const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' '))
      recoveryKey = (passphraseLine.match(passphrasePattern) || [])[1]
    } catch (exc) {
      state = logError(state, exc, 'recoveryWallet')
    }
  }

  return {
    state,
    recoveryKey
  }
}

const getPublisherData = (result, scorekeeper) => {
  let duration = result.duration

  let data = {
    verified: result.options.verified || false,
    exclude: result.options.exclude || false,
    site: result.publisherKey,
    views: result.visits,
    duration: duration,
    daysSpent: 0,
    hoursSpent: 0,
    minutesSpent: 0,
    secondsSpent: 0,
    faviconURL: result.faviconURL,
    score: result.scores[scorekeeper],
    pinPercentage: result.pinPercentage,
    weight: result.pinPercentage
  }
  // HACK: Protocol is sometimes blank here, so default to http:// so we can
  // still generate publisherURL.
  data.publisherURL = (result.protocol || 'http:') + '//' + result.publisherKey

  if (duration >= miliseconds.day) {
    data.daysSpent = Math.max(Math.round(duration / miliseconds.day), 1)
  } else if (duration >= miliseconds.hour) {
    data.hoursSpent = Math.max(Math.floor(duration / miliseconds.hour), 1)
    data.minutesSpent = Math.round((duration % miliseconds.hour) / miliseconds.minute)
  } else if (duration >= miliseconds.minute) {
    data.minutesSpent = Math.max(Math.round(duration / miliseconds.minute), 1)
    data.secondsSpent = Math.round((duration % miliseconds.minute) / miliseconds.second)<commit_msg>Fixes minutes in ledger table<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'

const acorn = require('acorn')
const moment = require('moment')
const Immutable = require('immutable')
const electron = require('electron')
const ipc = electron.ipcMain
const path = require('path')
const os = require('os')
const qr = require('qr-image')
const underscore = require('underscore')
const tldjs = require('tldjs')
const urlFormat = require('url').format
const levelUp = require('level')
const random = require('random-lib')
const uuid = require('uuid')
const BigNumber = require('bignumber.js')

// Actions
const appActions = require('../../../js/actions/appActions')

// State
const ledgerState = require('../../common/state/ledgerState')
const pageDataState = require('../../common/state/pageDataState')
const migrationState = require('../../common/state/migrationState')

// Constants
const settings = require('../../../js/constants/settings')
const messages = require('../../../js/constants/messages')

// Utils
const tabs = require('../../browser/tabs')
const locale = require('../../locale')
const appConfig = require('../../../js/constants/appConfig')
const getSetting = require('../../../js/settings').getSetting
const {fileUrl, getSourceAboutUrl, isSourceAboutUrl} = require('../../../js/lib/appUrlUtil')
const urlParse = require('../../common/urlParse')
const ruleSolver = require('../../extensions/brave/content/scripts/pageInformation')
const request = require('../../../js/lib/request')
const ledgerUtil = require('../../common/lib/ledgerUtil')
const tabState = require('../../common/state/tabState')
const pageDataUtil = require('../../common/lib/pageDataUtil')
const {getWebContents} = require('../../browser/webContentsCache')

// Caching
let locationDefault = 'NOOP'
let currentUrl = locationDefault
let currentTimestamp = new Date().getTime()
let visitsByPublisher = {}
let bootP
let quitP
const _internal = {
  verboseP: process.env.LEDGER_VERBOSE || false,
  debugP: process.env.LEDGER_DEBUG || false,
  ruleset: {
    raw: [],
    cooked: []
  }
}

// Libraries
let ledgerPublisher
let ledgerClient
let client
let synopsis

// Timers
let balanceTimeoutId = false
let runTimeoutId

// Database
let v2RulesetDB
const v2RulesetPath = 'ledger-rulesV2.leveldb'
let v2PublishersDB
const v2PublishersPath = 'ledger-publishersV2.leveldb'
const statePath = 'ledger-state.json'
const newClientPath = 'ledger-newstate.json'

// Definitions
const miliseconds = {
  year: 365 * 24 * 60 * 60 * 1000,
  week: 7 * 24 * 60 * 60 * 1000,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
}
const clientOptions = {
  debugP: process.env.LEDGER_DEBUG,
  loggingP: process.env.LEDGER_LOGGING,
  rulesTestP: process.env.LEDGER_RULES_TESTING,
  verboseP: process.env.LEDGER_VERBOSE,
  server: process.env.LEDGER_SERVER_URL,
  createWorker: electron.app.createWorker,
  version: 'v2'
}
const fileTypes = {
  bmp: new Buffer([0x42, 0x4d]),
  gif: new Buffer([0x47, 0x49, 0x46, 0x38, [0x37, 0x39], 0x61]),
  ico: new Buffer([0x00, 0x00, 0x01, 0x00]),
  jpeg: new Buffer([0xff, 0xd8, 0xff]),
  png: new Buffer([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
}
const minimumVisitTimeDefault = 8 * 1000
const nextAddFundsTime = 3 * miliseconds.day
let signatureMax = 0
underscore.keys(fileTypes).forEach((fileType) => {
  if (signatureMax < fileTypes[fileType].length) signatureMax = fileTypes[fileType].length
})
signatureMax = Math.ceil(signatureMax * 1.5)

// Notifications
const sufficientBalanceToReconcile = (state) => {
  const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0)
  const unconfirmed = Number(ledgerState.getInfoProp(state, 'unconfirmed') || 0)
  const bat = ledgerState.getInfoProp(state, 'bat')
  return bat && (balance + unconfirmed > 0.9 * Number(bat))
}
const hasFunds = (state) => {
  const balance = getSetting(settings.PAYMENTS_ENABLED)
    ? Number(ledgerState.getInfoProp(state, 'balance') || 0)
    : 0
  return balance > 0
}
const shouldShowNotificationReviewPublishers = () => {
  const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP)
  return !nextTime || (new Date().getTime() > nextTime)
}
const shouldShowNotificationAddFunds = () => {
  const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP)
  return !nextTime || (new Date().getTime() > nextTime)
}
const notifications = {
  text: {
    hello: locale.translation('updateHello'),
    paymentDone: undefined,
    addFunds: locale.translation('addFundsNotification'),
    tryPayments: locale.translation('notificationTryPayments'),
    reconciliation: locale.translation('reconciliationNotification'),
    walletConvertedToBat: locale.translation('walletConvertedToBat')
  },
  pollingInterval: 15 * miliseconds.minutes,
  timeout: undefined,
  displayOptions: {
    style: 'greetingStyle',
    persist: false
  },
  init: (state) => {
    // Check if relevant browser notifications should be shown every 15 minutes
    if (notifications.timeout) {
      clearInterval(notifications.timeout)
    }
    notifications.timeout = setInterval((state) => {
      notifications.onInterval(state)
    }, notifications.pollingInterval, state)
  },
  onLaunch: (state) => {
    const enabled = getSetting(settings.PAYMENTS_ENABLED)
    if (!enabled) {
      return state
    }

    state = checkBtcBatMigrated(state, enabled)

    if (hasFunds(state)) {
      // Don't bother processing the rest, which are only notifications.
      if (!getSetting(settings.PAYMENTS_NOTIFICATIONS)) {
        return state
      }

      // Show one-time BAT conversion message:
      // - if payments are enabled
      // - user has a positive balance
      // - this is an existing profile (new profiles will have firstRunTimestamp matching batMercuryTimestamp)
      // - wallet has been transitioned
      // - notification has not already been shown yet
      // (see https://github.com/brave/browser-laptop/issues/11021)
      const isNewInstall = migrationState.isNewInstall(state)
      const hasUpgradedWallet = migrationState.hasUpgradedWallet(state)
      const hasBeenNotified = migrationState.hasBeenNotified(state)
      if (!isNewInstall && hasUpgradedWallet && !hasBeenNotified) {
        notifications.showBraveWalletUpdated()
      }
    }

    return state
  },
  onInterval: (state) => {
    if (getSetting(settings.PAYMENTS_ENABLED)) {
      if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) {
        notifications.showEnabledNotifications(state)
      }
    } else {
      notifications.showDisabledNotifications(state)
    }
  },
  onResponse: (message, buttonIndex, activeWindow) => {
    switch (message) {
      case notifications.text.addFunds:
        // See showNotificationAddFunds() for buttons.
        // buttonIndex === 1 is "Later"; the timestamp until which to delay is set
        // in showNotificationAddFunds() when triggering this notification.
        if (buttonIndex === 0) {
          appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false)
        } else if (buttonIndex === 2 && activeWindow) {
          // Add funds: Open payments panel
          appActions.createTabRequested({
            url: 'about:preferences#payments',
            windowId: activeWindow.id
          })
        }
        break

      case notifications.text.reconciliation:
        // buttonIndex === 1 is Dismiss
        if (buttonIndex === 0) {
          appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false)
        } else if (buttonIndex === 2 && activeWindow) {
          appActions.createTabRequested({
            url: 'about:preferences#payments',
            windowId: activeWindow.id
          })
        }
        break

      case notifications.text.paymentDone:
        if (buttonIndex === 0) {
          appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false)
        }
        break

      case notifications.text.tryPayments:
        if (buttonIndex === 1 && activeWindow) {
          appActions.createTabRequested({
            url: 'about:preferences#payments',
            windowId: activeWindow.id
          })
        }
        appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true)
        break

      case notifications.text.walletConvertedToBat:
        if (buttonIndex === 0) {
          // Open backup modal
          appActions.createTabRequested({
            url: 'about:preferences#payments?ledgerBackupOverlayVisible',
            windowId: activeWindow.id
          })
        }
        break

      default:
        return
    }

    appActions.hideNotification(message)
  },
  /**
   * Show message that it's time to add funds if reconciliation is less than
   * a day in the future and balance is too low.
   * 24 hours prior to reconciliation, show message asking user to review
   * their votes.
   */
  showEnabledNotifications: (state) => {
    const reconcileStamp = ledgerState.getInfoProp(state, 'reconcileStamp')
    if (!reconcileStamp) {
      return
    }

    if (reconcileStamp - new Date().getTime() < miliseconds.day) {
      if (sufficientBalanceToReconcile(state)) {
        if (shouldShowNotificationReviewPublishers()) {
          const reconcileFrequency = ledgerState.getInfoProp(state, 'reconcileFrequency')
          notifications.showReviewPublishers(reconcileStamp + ((reconcileFrequency - 2) * miliseconds.day))
        }
      } else if (shouldShowNotificationAddFunds()) {
        notifications.showAddFunds()
      }
    } else if (reconcileStamp - new Date().getTime() < 2 * miliseconds.day) {
      if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) {
        notifications.showReviewPublishers(new Date().getTime() + miliseconds.day)
      }
    }
  },
  showDisabledNotifications: (state) => {
    if (!getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) {
      const firstRunTimestamp = state.get('firstRunTimestamp')
      if (new Date().getTime() - firstRunTimestamp < appConfig.payments.delayNotificationTryPayments) {
        return
      }

      appActions.showNotification({
        greeting: locale.translation('updateHello'),
        message: notifications.text.tryPayments,
        buttons: [
          {text: locale.translation('noThanks')},
          {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'}
        ],
        options: {
          style: 'greetingStyle',
          persist: false
        }
      })
    }
  },
  showReviewPublishers: (nextTime) => {
    appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime)

    appActions.showNotification({
      greeting: notifications.text.hello,
      message: notifications.text.reconciliation,
      buttons: [
        {text: locale.translation('turnOffNotifications')},
        {text: locale.translation('dismiss')},
        {text: locale.translation('reviewSites'), className: 'primaryButton'}
      ],
      options: notifications.displayOptions
    })
  },
  showAddFunds: () => {
    const nextTime = new Date().getTime() + nextAddFundsTime
    appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime)

    appActions.showNotification({
      greeting: notifications.text.hello,
      message: notifications.text.addFunds,
      buttons: [
        {text: locale.translation('turnOffNotifications')},
        {text: locale.translation('updateLater')},
        {text: locale.translation('addFunds'), className: 'primaryButton'}
      ],
      options: notifications.displayOptions
    })
  },
  // Called from observeTransactions() when we see a new payment (transaction).
  showPaymentDone: (transactionContributionFiat) => {
    notifications.text.paymentDone = locale.translation('notificationPaymentDone')
      .replace(/{{\s*amount\s*}}/, transactionContributionFiat.amount)
      .replace(/{{\s*currency\s*}}/, transactionContributionFiat.currency)
    // Hide the 'waiting for deposit' message box if it exists
    appActions.hideNotification(notifications.text.addFunds)
    appActions.showNotification({
      greeting: locale.translation('updateHello'),
      message: notifications.text.paymentDone,
      buttons: [
        {text: locale.translation('turnOffNotifications')},
        {text: locale.translation('Ok'), className: 'primaryButton'}
      ],
      options: notifications.displayOptions
    })
  },
  showBraveWalletUpdated: () => {
    appActions.onBitcoinToBatNotified()

    appActions.showNotification({
      greeting: notifications.text.hello,
      message: notifications.text.walletConvertedToBat,
      // Learn More.
      buttons: [
        {text: locale.translation('walletConvertedBackup')},
        {text: locale.translation('walletConvertedDismiss')}
      ],
      options: {
        style: 'greetingStyle',
        persist: false,
        advancedLink: 'https://brave.com/faq-payments/#brave-payments',
        advancedText: locale.translation('walletConvertedLearnMore')
      }
    })
  }
}

// TODO is it ok to have IPC here or is there better place
if (ipc) {
  ipc.on(messages.LEDGER_PUBLISHER, (event, location) => {
    if (!synopsis || event.sender.session === electron.session.fromPartition('default') || !tldjs.isValid(location)) {
      event.returnValue = {}
      return
    }

    let ctx = urlParse(location, true)
    ctx.TLD = tldjs.getPublicSuffix(ctx.host)
    if (!ctx.TLD) {
      if (_internal.verboseP) console.log('\nno TLD for:' + ctx.host)
      event.returnValue = {}
      return
    }

    ctx = underscore.mapObject(ctx, function (value) {
      if (!underscore.isFunction(value)) return value
    })
    ctx.URL = location
    ctx.SLD = tldjs.getDomain(ctx.host)
    ctx.RLD = tldjs.getSubdomain(ctx.host)
    ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : ''

    if (!event.sender.isDestroyed()) {
      event.sender.send(messages.LEDGER_PUBLISHER_RESPONSE + '-' + location, {
        context: ctx,
        rules: _internal.ruleset.cooked
      })
    }
  })

  ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex) => {
    notifications.onResponse(
      message,
      buttonIndex,
      electron.BrowserWindow.getActiveWindow()
    )
  })
}

let ledgerPaymentsPresent = {}
const paymentPresent = (state, tabId, present) => {
  if (present) {
    ledgerPaymentsPresent[tabId] = present
  } else {
    delete ledgerPaymentsPresent[tabId]
  }

  if (Object.keys(ledgerPaymentsPresent).length > 0 && getSetting(settings.PAYMENTS_ENABLED)) {
    if (!balanceTimeoutId) {
      getBalance(state)
    }
  } else if (balanceTimeoutId) {
    clearTimeout(balanceTimeoutId)
    balanceTimeoutId = false
  }
}

const addFoundClosed = (state) => {
  if (balanceTimeoutId) {
    clearTimeout(balanceTimeoutId)
  }
  const balanceFn = getBalance.bind(null, state)
  balanceTimeoutId = setTimeout(balanceFn, 5 * miliseconds.second)
}

const boot = () => {
  if (bootP || client) {
    return
  }

  bootP = true
  const fs = require('fs')
  fs.access(pathName(statePath), fs.FF_OK, (err) => {
    if (!err) return

    if (err.code !== 'ENOENT') console.error('statePath read error: ' + err.toString())

    appActions.onBootStateFile()
  })
}

const onBootStateFile = (state) => {
  state = ledgerState.setInfoProp(state, 'creating', true)

  try {
    clientprep()
    client = ledgerClient(null, underscore.extend({roundtrip: roundtrip}, clientOptions), null)
  } catch (ex) {
    state = ledgerState.resetInfo(state)
    bootP = false
    console.error('ledger client boot error: ', ex)
    return state
  }

  if (client.sync(callback) === true) {
    run(random.randomInt({min: miliseconds.minute, max: 10 * miliseconds.minute}))
  }

  getBalance(state)

  bootP = false

  return state
}

const promptForRecoveryKeyFile = () => {
  const defaultRecoveryKeyFilePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt')
  let files
  if (process.env.SPECTRON) {
    // skip the dialog for tests
    console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`)
    files = [defaultRecoveryKeyFilePath]
  } else {
    const dialog = electron.dialog
    files = dialog.showOpenDialog({
      properties: ['openFile'],
      defaultPath: defaultRecoveryKeyFilePath,
      filters: [{
        name: 'TXT files',
        extensions: ['txt']
      }]
    })
  }

  return (files && files.length ? files[0] : null)
}

const logError = (state, err, caller) => {
  if (err) {
    console.error('Error in %j: %j', caller, err)
    state = ledgerState.setLedgerError(state, err, caller)
  } else {
    state = ledgerState.setLedgerError(state)
  }

  return state
}

const loadKeysFromBackupFile = (state, filePath) => {
  let recoveryKey = null
  const fs = require('fs')
  let data = fs.readFileSync(filePath)

  if (!data || !data.length || !(data.toString())) {
    state = logError(state, 'No data in backup file', 'recoveryWallet')
  } else {
    try {
      const recoveryFileContents = data.toString()

      let messageLines = recoveryFileContents.split(os.EOL)

      let passphraseLine = '' || messageLines[3]

      const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' '))
      recoveryKey = (passphraseLine.match(passphrasePattern) || [])[1]
    } catch (exc) {
      state = logError(state, exc, 'recoveryWallet')
    }
  }

  return {
    state,
    recoveryKey
  }
}

const getPublisherData = (result, scorekeeper) => {
  let duration = result.duration

  let data = {
    verified: result.options.verified || false,
    exclude: result.options.exclude || false,
    site: result.publisherKey,
    views: result.visits,
    duration: duration,
    daysSpent: 0,
    hoursSpent: 0,
    minutesSpent: 0,
    secondsSpent: 0,
    faviconURL: result.faviconURL,
    score: result.scores[scorekeeper],
    pinPercentage: result.pinPercentage,
    weight: result.pinPercentage
  }
  // HACK: Protocol is sometimes blank here, so default to http:// so we can
  // still generate publisherURL.
  data.publisherURL = (result.protocol || 'http:') + '//' + result.publisherKey

  if (duration >= miliseconds.day) {
    data.daysSpent = Math.max(Math.round(duration / miliseconds.day), 1)
  } else if (duration >= miliseconds.hour) {
    data.hoursSpent = Math.max(Math.floor(duration / miliseconds.hour), 1)
    data.minutesSpent = Math.round((duration % miliseconds.hour) / miliseconds.minute)
  } else if (duration >= miliseconds.minute) {
    data.minutesSpent = Math.max(Math.floor(duration / miliseconds.minute), 1)
    data.secondsSpent = Math.round((duration % miliseconds.minute) / miliseconds.second)
{'fraction_non_alphanumeric': 0.12631690401821166, 'fraction_numerical': 0.009370533114511092, 'mean_word_length': 3.348912167606769, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 73, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3678660', 'n_tokens_mistral': 11849, 'n_tokens_neox': 11347, 'n_words': 2441}