<commit_before>(ns status-im.ethereum.transactions.core
  (:require [re-frame.core :as re-frame]
            [status-im.ethereum.decode :as decode]
            [status-im.ethereum.eip55 :as eip55]
            [status-im.ethereum.encode :as encode]
            [status-im.ethereum.json-rpc :as json-rpc]
            [status-im.ens.core :as ens]
            [status-im.utils.fx :as fx]
            [status-im.wallet.core :as wallet]
            [taoensso.timbre :as log]
            [cljs.spec.alpha :as spec]))

(def confirmations-count-threshold 12)

(def etherscan-supported? #{:testnet :mainnet :rinkeby})

(let [network->subdomain {:testnet "ropsten" :rinkeby "rinkeby"}]
  (defn get-transaction-details-url [chain hash]
    {:pre [(keyword? chain) (string? hash)]
     :post [(or (nil? %) (string? %))]}
    (when (etherscan-supported? chain)
      (let [network-subdomain (when-let [subdomain (network->subdomain chain)]
                                (str subdomain "."))]
        (str "https://" network-subdomain "etherscan.io/tx/" hash)))))

(def default-erc20-token
  {:symbol   :ERC20
   :decimals 18
   :name     "ERC20"})

(defn direction
  [address to]
  (if (= address to)
    :inbound
    :outbound))

(defn- parse-token-transfer
  [chain-tokens contract]
  (let [{:keys [symbol] :as token} (get chain-tokens contract
                                        default-erc20-token)]
    {:symbol        symbol
     :token         token
     ;; NOTE(goranjovic) - just a flag we need when we merge this entry
     ;; with the existing entry in the app, e.g. transaction info with
     ;; gas details, or a previous transfer entry with old confirmations
     ;; count.
     :transfer      true}))

(defn enrich-transfer
  [chain-tokens
   {:keys [address blockNumber timestamp from txStatus txHash gasPrice
           gasUsed contract value gasLimit input nonce to type id]}]
  (let [erc20?  (= type "erc20")
        failed? (= txStatus "0x0")]
    (merge {:address   (eip55/address->checksum address)
            :id        id
            :block     (str (decode/uint blockNumber))
            :timestamp (* (decode/uint timestamp) 1000)
            :gas-used  (str (decode/uint gasUsed))
            :gas-price (str (decode/uint gasPrice))
            :gas-limit (str (decode/uint gasLimit))
            :nonce     (str (decode/uint nonce))
            :hash      txHash
            :data      input
            :from      from
            :to        to
            :type      (if failed?
                         :failed
                         (direction address to))
            :value     (str (decode/uint value))}
           (if erc20?
             (parse-token-transfer chain-tokens contract)
             ;; this is not a ERC20 token transaction
             {:symbol :ETH}))))

(defn enrich-transfers
  [chain-tokens transfers]
  (mapv (fn [transfer]
          (enrich-transfer chain-tokens transfer))
        transfers))

;; -----------------------------------------------
;; transactions api
;; -----------------------------------------------

(def default-transfers-limit 20)

(fx/defn watch-transaction
  "Set a watch for the given transaction
   `watch-params` needs to contain a `trigger-fn` and `on-trigger` functions
   `trigger-fn` is a function that returns true if the watch has been triggered
   `on-trigger` is a function that returns the effects to apply when the
   transaction has been triggered"
  [{:keys [db]} transaction-id {:keys [trigger-fn on-trigger] :as watch-params}]
  (when (and (fn? trigger-fn)
             (fn? on-trigger))
    {:db (assoc-in db [:ethereum/watched-transactions transaction-id]
                   watch-params)}))

(fx/defn check-transaction
  "Check if the transaction has been triggered and applies the effects returned
   by `on-trigger` if that is the case"
  [{:keys [db] :as cofx} {:keys [hash] :as transaction}]
  (when-let [watch-params
             (get-in db [:ethereum/watched-transactions hash])]
    (let [{:keys [trigger-fn on-trigger]} watch-params]
      (when (trigger-fn db transaction)
        (fx/merge cofx
                  {:db (update db :ethereum/watched-transactions
                               dissoc hash)}
                  (on-trigger transaction))))))

(fx/defn check-watched-transactions
  [{:keys [db] :as cofx}]
  (let [watched-transactions
        (reduce-kv (fn [acc _ {:keys [transactions]}]
                     (merge acc
                            (select-keys transactions
                                         (keys (get db :ethereum/watched-transactions)))))
                   {}
                   (get-in db [:wallet :accounts]))]
    (apply fx/merge
           cofx
           (map (fn [[_ transaction]]
                  (check-transaction transaction))
                watched-transactions))))

(fx/defn add-transfer
  "We determine a unique id for the transfer before adding it because some
   transaction can contain multiple transfers and they would overwrite each other
   in the transfer map if identified by hash"
  [{:keys [db] :as cofx} {:keys [hash id address] :as transfer}]
  (let [transfer-by-hash (get-in db [:wallet :accounts address :transactions hash])]
    (when-let [unique-id (when-not (= transfer transfer-by-hash)
                           (if (and transfer-by-hash
                                    (not (= :pending
                                            (:type transfer-by-hash))))
                             id
                             hash))]
      (fx/merge cofx
                {:db (assoc-in db [:wallet :accounts address :transactions unique-id]
                               (assoc transfer :hash unique-id))}
                (check-transaction transfer)))))

(defn get-min-known-block [db address]
  (get-in db [:wallet :accounts (eip55/address->checksum address) :min-block]))

(defn min-block-transfers-count [db address]
  (get-in db [:wallet :accounts
              (eip55/address->checksum address)
              :min-block-transfers-count]))

(fx/defn set-lowest-fetched-block
  [{:keys [db]} address transfers]
  (let [checksum (eip55/address->checksum address)
        {:keys [min-block min-block-transfers-count]}
        (reduce
         (fn [{:keys [min-block] :as acc}
              {:keys [block hash]}]
           (cond
             (or (nil? min-block) (> min-block block))
             {:min-block                 block
              :min-block-transfers-count 1}

             (and (= min-block block)
                  (nil? (get-in db [:wallet :accounts checksum :transactions hash])))
             (update acc :min-block-transfers-count inc)

             :else acc))
         {:min-block
          (get-min-known-block db address)

          :min-block-transfers-count
          (min-block-transfers-count db address)}
         transfers)]
    (log/debug "[transactions] set-lowest-fetched-block"
               "address" address
               "min-block" min-block
               "min-block-transfers-count" min-block-transfers-count)
    {:db (update-in db [:wallet :accounts checksum] assoc
                    :min-block min-block
                    :min-block-transfers-count min-block-transfers-count)}))

(defn update-fetching-status
  [db addresses fetching-type state]
  (update-in
   db [:wallet :fetching]<commit_msg>[#10771] Fix loading of tx history before 10000000th block<commit_after>(ns status-im.ethereum.transactions.core
  (:require [re-frame.core :as re-frame]
            [status-im.ethereum.decode :as decode]
            [status-im.ethereum.eip55 :as eip55]
            [status-im.ethereum.encode :as encode]
            [status-im.ethereum.json-rpc :as json-rpc]
            [status-im.ens.core :as ens]
            [status-im.utils.fx :as fx]
            [status-im.wallet.core :as wallet]
            [taoensso.timbre :as log]
            [cljs.spec.alpha :as spec]))

(def confirmations-count-threshold 12)

(def etherscan-supported? #{:testnet :mainnet :rinkeby})

(let [network->subdomain {:testnet "ropsten" :rinkeby "rinkeby"}]
  (defn get-transaction-details-url [chain hash]
    {:pre [(keyword? chain) (string? hash)]
     :post [(or (nil? %) (string? %))]}
    (when (etherscan-supported? chain)
      (let [network-subdomain (when-let [subdomain (network->subdomain chain)]
                                (str subdomain "."))]
        (str "https://" network-subdomain "etherscan.io/tx/" hash)))))

(def default-erc20-token
  {:symbol   :ERC20
   :decimals 18
   :name     "ERC20"})

(defn direction
  [address to]
  (if (= address to)
    :inbound
    :outbound))

(defn- parse-token-transfer
  [chain-tokens contract]
  (let [{:keys [symbol] :as token} (get chain-tokens contract
                                        default-erc20-token)]
    {:symbol        symbol
     :token         token
     ;; NOTE(goranjovic) - just a flag we need when we merge this entry
     ;; with the existing entry in the app, e.g. transaction info with
     ;; gas details, or a previous transfer entry with old confirmations
     ;; count.
     :transfer      true}))

(defn enrich-transfer
  [chain-tokens
   {:keys [address blockNumber timestamp from txStatus txHash gasPrice
           gasUsed contract value gasLimit input nonce to type id]}]
  (let [erc20?  (= type "erc20")
        failed? (= txStatus "0x0")]
    (merge {:address   (eip55/address->checksum address)
            :id        id
            :block     (str (decode/uint blockNumber))
            :timestamp (* (decode/uint timestamp) 1000)
            :gas-used  (str (decode/uint gasUsed))
            :gas-price (str (decode/uint gasPrice))
            :gas-limit (str (decode/uint gasLimit))
            :nonce     (str (decode/uint nonce))
            :hash      txHash
            :data      input
            :from      from
            :to        to
            :type      (if failed?
                         :failed
                         (direction address to))
            :value     (str (decode/uint value))}
           (if erc20?
             (parse-token-transfer chain-tokens contract)
             ;; this is not a ERC20 token transaction
             {:symbol :ETH}))))

(defn enrich-transfers
  [chain-tokens transfers]
  (mapv (fn [transfer]
          (enrich-transfer chain-tokens transfer))
        transfers))

;; -----------------------------------------------
;; transactions api
;; -----------------------------------------------

(def default-transfers-limit 20)

(fx/defn watch-transaction
  "Set a watch for the given transaction
   `watch-params` needs to contain a `trigger-fn` and `on-trigger` functions
   `trigger-fn` is a function that returns true if the watch has been triggered
   `on-trigger` is a function that returns the effects to apply when the
   transaction has been triggered"
  [{:keys [db]} transaction-id {:keys [trigger-fn on-trigger] :as watch-params}]
  (when (and (fn? trigger-fn)
             (fn? on-trigger))
    {:db (assoc-in db [:ethereum/watched-transactions transaction-id]
                   watch-params)}))

(fx/defn check-transaction
  "Check if the transaction has been triggered and applies the effects returned
   by `on-trigger` if that is the case"
  [{:keys [db] :as cofx} {:keys [hash] :as transaction}]
  (when-let [watch-params
             (get-in db [:ethereum/watched-transactions hash])]
    (let [{:keys [trigger-fn on-trigger]} watch-params]
      (when (trigger-fn db transaction)
        (fx/merge cofx
                  {:db (update db :ethereum/watched-transactions
                               dissoc hash)}
                  (on-trigger transaction))))))

(fx/defn check-watched-transactions
  [{:keys [db] :as cofx}]
  (let [watched-transactions
        (reduce-kv (fn [acc _ {:keys [transactions]}]
                     (merge acc
                            (select-keys transactions
                                         (keys (get db :ethereum/watched-transactions)))))
                   {}
                   (get-in db [:wallet :accounts]))]
    (apply fx/merge
           cofx
           (map (fn [[_ transaction]]
                  (check-transaction transaction))
                watched-transactions))))

(fx/defn add-transfer
  "We determine a unique id for the transfer before adding it because some
   transaction can contain multiple transfers and they would overwrite each other
   in the transfer map if identified by hash"
  [{:keys [db] :as cofx} {:keys [hash id address] :as transfer}]
  (let [transfer-by-hash (get-in db [:wallet :accounts address :transactions hash])]
    (when-let [unique-id (when-not (= transfer transfer-by-hash)
                           (if (and transfer-by-hash
                                    (not (= :pending
                                            (:type transfer-by-hash))))
                             id
                             hash))]
      (fx/merge cofx
                {:db (assoc-in db [:wallet :accounts address :transactions unique-id]
                               (assoc transfer :hash unique-id))}
                (check-transaction transfer)))))

(defn get-min-known-block [db address]
  (get-in db [:wallet :accounts (eip55/address->checksum address) :min-block]))

(defn min-block-transfers-count [db address]
  (get-in db [:wallet :accounts
              (eip55/address->checksum address)
              :min-block-transfers-count]))

(fx/defn set-lowest-fetched-block
  [{:keys [db]} address transfers]
  (let [checksum (eip55/address->checksum address)
        {:keys [min-block min-block-transfers-count]}
        (reduce
         (fn [{:keys [min-block] :as acc}
              {:keys [block hash]}]
           (cond
             (or (nil? min-block) (> min-block (js/parseInt block)))
             {:min-block                 (js/parseInt block)
              :min-block-transfers-count 1}

             (and (= min-block block)
                  (nil? (get-in db [:wallet :accounts checksum :transactions hash])))
             (update acc :min-block-transfers-count inc)

             :else acc))
         {:min-block
          (when-let [min-block-string (get-min-known-block db address)]
            (js/parseInt min-block-string))

          :min-block-transfers-count
          (min-block-transfers-count db address)}
         transfers)]
    (log/debug "[transactions] set-lowest-fetched-block"
               "address" address
               "min-block" min-block
               "min-block-transfers-count" min-block-transfers-count)
    {:db (update-in db [:wallet :accounts checksum] assoc
                    :min-block min-block
                    :min-block-transfers-count min-block-transfers-count)}))

(defn update-fetching-status
  [db addresses fetching-type state]
  (update-in
   db [:wallet :fetching]
{'fraction_non_alphanumeric': 0.13106664864134107, 'fraction_numerical': 0.006421522238745437, 'mean_word_length': 1.8545244067142581, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 17, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5098940', 'n_tokens_mistral': 4381, 'n_tokens_neox': 4228, 'n_words': 1211}