<commit_before>class ledger.tasks.WalletLayoutRecoveryTask extends ledger.tasks.Task

  BatchSize: 20

  constructor: -> super 'recovery-global-instance'

  @instance: new @()

  onStart: () ->
    @_performRecovery()

  _performRecovery: ->
    syncToken = null
    savedState = {}
    @_loadSynchronizationData().then (data) =>
      savedState = data
      @_requestSynchronizationToken()
    .then (token) =>
      syncToken = token
      iterate = (index) =>
        @_recoverAccount(index, savedState, syncToken).then (isEmpty) =>
          unless isEmpty
            iterate(index + 1)
      iterate(0)
    .fail (er) ->
      # Handle reorgs
      @emit 'fatal_error'
      e er
    .then () =>
      l "Recovery completed"
      @emit 'done'
    .fin =>
      # Delete sync token and stop
      @_deleteSynchronizationToken(syncToken) if syncToken?
      @stopIfNeccessary()

  _recoverAccount: (accountIndex, savedState, syncToken) ->
    savedAccountState = savedState["account_#{accountIndex}"] or {}
    batches = savedAccountState["batches"] or []
    l "Recover account #{accountIndex}"
    iterate = (index) =>
      batch = batches[index]
      unless batch?
        batch =
          index: index
          blockHash: null
        batches.push batch
      l "Recover batch #{index}"
      @_recoverBatch(batch, accountIndex, syncToken).then ({hasNext, block}) ->
        if block? and (!batch['blockHeight']? or block.height > batch['blockHeight'])
          batch['blockHash'] = block.hash
          batch['blockHeight'] = block.height
        if !block? and !batch['blockHash']?
          batches.splice(-1, 1)
        if hasNext
          iterate(index)
        else if !hasNext and block?
          iterate(index + 1)

    iterate(0).then () =>
      d = ledger.defer()
      l "Account #{accountIndex} restored!"
      callback = =>
        l "Saving account #{accountIndex} state"
        savedAccountState["batches"] = batches
        savedState["account_#{accountIndex}"] = savedAccountState
        @_saveSynchronizationData(savedState).then =>
          l "Saved account #{accountIndex} state"
          d.resolve(batches.length == 0)
      ledger.tasks.TransactionConsumerTask.instance.pushCallback(callback)
      d.promise

  _recoverBatch: (batch, accountIndex, syncToken) ->
    d = ledger.defer()
    wallet = ledger.wallet.Wallet.instance
    account = wallet.getOrCreateAccount(accountIndex)
    blockHash = batch['blockHash']
    from = batch.index * @BatchSize
    to = from + @BatchSize
    hasNext = no
    @_recoverAddresses(account.getRootDerivationPath(), from, to, blockHash, syncToken).then (result) =>
      hasNext = result["truncated"]
      block = @_findHighestBlock(result.txs)
      ledger.tasks.TransactionConsumerTask.instance.pushTransactions(result['txs'])
      ledger.tasks.TransactionConsumerTask.instance.pushCallback =>
        d.resolve({hasNext, block})
    d.promise

  _recoverAddresses: (root, from, to, blockHash, syncToken) ->
    paths = _.map [from...to], (i) -> "#{root}/#{0}/#{i}"
    paths = paths.concat(_.map [from...to], (i) -> "#{root}/#{1}/#{i}")
    d = ledger.defer()
    l "Recovering ", paths
    callback = (response, error) =>
      return d.reject(error) if error?
      d.resolve(response)
    ledger.wallet.pathsToAddresses paths, (addresses) =>
      ledger.api.TransactionsRestClient.instance.getPaginatedTransactions(_.values(addresses), blockHash, syncToken, callback)
    d.promise

  _findHighestBlock: (txs) ->
    bestBlock = null
    for tx in txs
      if !bestBlock? or (tx.block?.height > bestBlock.height)
        bestBlock = tx.block
    bestBlock

  _requestSynchronizationToken: () ->
    d = ledger.defer()
    ledger.api.TransactionsRestClient.instance.getSyncToken (token, error) ->
      if (error?)
        d.reject(error)
      else
        d.resolve(token)
    d.promise

  _deleteSynchronizationToken: (token) ->
    d = ledger.defer()
    ledger.api.TransactionsRestClient.instance.deleteSyncToken token, ->
      d.resolve()
    d.promise


  _loadSynchronizationData: ->
    d = ledger.defer()
    ledger.storage.local.get 'ledger.tasks.WalletLayoutRecoveryTask', (data) =>
      l "Synchronization saved state ", data
      unless data['ledger.tasks.WalletLayoutRecoveryTask']?
        d.resolve({})
      else
        d.resolve(data['ledger.tasks.WalletLayoutRecoveryTask'])
    d.promise

  _saveSynchronizationData: (data) ->
    d = ledger.defer()
    l "Saving state", data
    save = {}
    save['ledger.tasks.WalletLayoutRecoveryTask'] = data
    ledger.storage.local.set save, =>
      d.resolve()
    d.promise

  _handlerReorgs: ->

  ###<commit_msg>Better promise chain for handling reorg<commit_after>class ledger.tasks.WalletLayoutRecoveryTask extends ledger.tasks.Task

  BatchSize: 20

  constructor: -> super 'recovery-global-instance'

  @instance: new @()

  onStart: () ->
    @_performRecovery().then () =>
      l "Recovery completed"
      @emit 'done'
    .fail (er) =>
      e "Serious error during synchro", er
      @emit 'fatal_error'
    .fin =>
      # Delete sync token and stop
      @_deleteSynchronizationToken(syncToken) if syncToken?
      @stopIfNeccessary()

  _performRecovery: () ->
    syncToken = null
    savedState = {}
    @_loadSynchronizationData().then (data) =>
      savedState = data
      @_requestSynchronizationToken()
    .then (token) =>
      syncToken = token
      iterate = (index) =>
        @_recoverAccount(index, savedState, syncToken).then (isEmpty) =>
          unless isEmpty
            iterate(index + 1)
      iterate(0)
    .fail (er) ->
      # Handle reorgs
      e "Failure during synchro", er
      if er?.getStatusCode?() is 404
        @_handlerReorgs(savedState, er.block).then () =>
          @_performRecovery()
      else
        throw er

  _recoverAccount: (accountIndex, savedState, syncToken) ->
    savedAccountState = savedState["account_#{accountIndex}"] or {}
    batches = savedAccountState["batches"] or []
    l "Recover account #{accountIndex}"
    iterate = (index) =>
      batch = batches[index]
      unless batch?
        batch =
          index: index
          blockHash: null
        batches.push batch
      l "Recover batch #{index}"
      @_recoverBatch(batch, accountIndex, syncToken).then ({hasNext, block}) ->
        if block? and (!batch['blockHeight']? or block.height > batch['blockHeight'])
          batch['blockHash'] = block.hash
          batch['blockHeight'] = block.height
        if !block? and !batch['blockHash']?
          batches.splice(-1, 1)
        if hasNext
          iterate(index)
        else if !hasNext and block?
          iterate(index + 1)

    iterate(0).then () =>
      d = ledger.defer()
      l "Account #{accountIndex} restored!"
      callback = =>
        l "Saving account #{accountIndex} state"
        savedAccountState["batches"] = batches
        savedState["account_#{accountIndex}"] = savedAccountState
        @_saveSynchronizationData(savedState).then =>
          l "Saved account #{accountIndex} state"
          d.resolve(batches.length == 0)
      ledger.tasks.TransactionConsumerTask.instance.pushCallback(callback)
      d.promise

  _recoverBatch: (batch, accountIndex, syncToken) ->
    d = ledger.defer()
    wallet = ledger.wallet.Wallet.instance
    account = wallet.getOrCreateAccount(accountIndex)
    blockHash = batch['blockHash']
    from = batch.index * @BatchSize
    to = from + @BatchSize
    hasNext = no
    @_recoverAddresses(account.getRootDerivationPath(), from, to, blockHash, syncToken).then (result) =>
      hasNext = result["truncated"]
      block = @_findHighestBlock(result.txs)
      ledger.tasks.TransactionConsumerTask.instance.pushTransactions(result['txs'])
      ledger.tasks.TransactionConsumerTask.instance.pushCallback =>
        d.resolve({hasNext, block})
    .fail (er) ->
      er.block = block
      throw er
    d.promise

  _recoverAddresses: (root, from, to, blockHash, syncToken) ->
    paths = _.map [from...to], (i) -> "#{root}/#{0}/#{i}"
    paths = paths.concat(_.map [from...to], (i) -> "#{root}/#{1}/#{i}")
    d = ledger.defer()
    l "Recovering ", paths
    callback = (response, error) =>
      return d.reject(error) if error?
      d.resolve(response)
    ledger.wallet.pathsToAddresses paths, (addresses) =>
      ledger.api.TransactionsRestClient.instance.getPaginatedTransactions(_.values(addresses), blockHash, syncToken, callback)
    d.promise

  _findHighestBlock: (txs) ->
    bestBlock = null
    for tx in txs
      if !bestBlock? or (tx.block?.height > bestBlock.height)
        bestBlock = tx.block
    bestBlock

  _requestSynchronizationToken: () ->
    d = ledger.defer()
    ledger.api.TransactionsRestClient.instance.getSyncToken (token, error) ->
      if (error?)
        d.reject(error)
      else
        d.resolve(token)
    d.promise

  _deleteSynchronizationToken: (token) ->
    d = ledger.defer()
    ledger.api.TransactionsRestClient.instance.deleteSyncToken token, ->
      d.resolve()
    d.promise


  _loadSynchronizationData: ->
    d = ledger.defer()
    ledger.storage.local.get 'ledger.tasks.WalletLayoutRecoveryTask', (data) =>
      l "Synchronization saved state ", data
      unless data['ledger.tasks.WalletLayoutRecoveryTask']?
        d.resolve({})
      else
        d.resolve(data['ledger.tasks.WalletLayoutRecoveryTask'])
    d.promise

  _saveSynchronizationData: (data) ->
    d = ledger.defer()
    l "Saving state", data
    save = {}
    save['ledger.tasks.WalletLayoutRecoveryTask'] = data
    ledger.storage.local.set save, =>
      d.resolve()
    d.promise

  _handlerReorgs: (state, failedBlock) ->
    # Iterate through the state and delete any block heigher or equal to block.height
    # Remove from the database all orphan transaction and blocks
    # Save the new state

  ###
{'fraction_non_alphanumeric': 0.1263936752483276, 'fraction_numerical': 0.002533954996959254, 'mean_word_length': 2.8170212765957445, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 78, 'https://': 0, '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': '6338133', 'n_tokens_mistral': 3052, 'n_tokens_neox': 3003, 'n_words': 643}