diff --git a/resources/images/tokens/default.png b/resources/images/tokens/default.png new file mode 100644 index 0000000000..40d1d3eff1 Binary files /dev/null and b/resources/images/tokens/default.png differ diff --git a/src/status_im/chat/views/input/animations/expandable.cljs b/src/status_im/chat/views/input/animations/expandable.cljs index a75b4e415d..7a0edf1518 100644 --- a/src/status_im/chat/views/input/animations/expandable.cljs +++ b/src/status_im/chat/views/input/animations/expandable.cljs @@ -6,7 +6,7 @@ [status-im.ui.components.animation :as anim] [status-im.ui.components.drag-drop :as drag] [status-im.ui.components.react :refer [view - animated-view]] + animated-view]] [status-im.chat.views.input.animations.responder :as resp] [status-im.chat.views.input.utils :as input-utils] [status-im.chat.styles.animations :as style] diff --git a/src/status_im/chat/views/input/emoji.cljs b/src/status_im/chat/views/input/emoji.cljs index 0e8a1b3dad..e0abdb5149 100644 --- a/src/status_im/chat/views/input/emoji.cljs +++ b/src/status_im/chat/views/input/emoji.cljs @@ -2,9 +2,9 @@ (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch]] [status-im.ui.components.react :refer [view - text - icon - emoji-picker]] + text + icon + emoji-picker]] [status-im.chat.styles.input.emoji :as style] [status-im.i18n :refer [label]])) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index f80888dd2e..8cc2fade37 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -1,5 +1,6 @@ (ns status-im.constants (:require [status-im.i18n :as i18n] + [status-im.utils.ethereum.core :as ethereum] [status-im.utils.types :as types] [status-im.utils.config :as config])) @@ -43,24 +44,14 @@ :raw-config config)])) (into {}))) -(def mainnet-id 1) -(def ropsten-id 3) -(def rinkeby-id 4) - -(defn get-testnet-name [testnet-id] - (cond - (= testnet-id ropsten-id) "Ropsten" - (= testnet-id rinkeby-id) "Rinkeby" - :else "Unknown")) - (def mainnet-networks {"mainnet" {:id "mainnet", :name "Mainnet", - :config {:NetworkId mainnet-id + :config {:NetworkId (ethereum/chain-id :mainnet) :DataDir "/ethereum/mainnet"}} "mainnet_rpc" {:id "mainnet_rpc", :name "Mainnet with upstream RPC", - :config {:NetworkId mainnet-id + :config {:NetworkId (ethereum/chain-id :mainnet) :DataDir "/ethereum/mainnet_rpc" :UpstreamConfig {:Enabled true :URL "https://mainnet.infura.io/z6GCTmjdP3FETEJmMBI4"}}}}) @@ -68,21 +59,21 @@ (def testnet-networks {"testnet" {:id "testnet", :name "Ropsten", - :config {:NetworkId ropsten-id + :config {:NetworkId (ethereum/chain-id :ropsten) :DataDir "/ethereum/testnet"}} "testnet_rpc" {:id "testnet_rpc", :name "Ropsten with upstream RPC", - :config {:NetworkId ropsten-id + :config {:NetworkId (ethereum/chain-id :ropsten) :DataDir "/ethereum/testnet_rpc" :UpstreamConfig {:Enabled true :URL "https://ropsten.infura.io/z6GCTmjdP3FETEJmMBI4"}}} "rinkeby" {:id "rinkeby", :name "Rinkeby", - :config {:NetworkId rinkeby-id + :config {:NetworkId (ethereum/chain-id :rinkeby) :DataDir "/ethereum/rinkeby"}} "rinkeby_rpc" {:id "rinkeby_rpc", :name "Rinkeby with upstream RPC", - :config {:NetworkId rinkeby-id + :config {:NetworkId (ethereum/chain-id :rinkeby) :DataDir "/ethereum/rinkeby_rpc" :UpstreamConfig {:Enabled true :URL "https://rinkeby.infura.io/z6GCTmjdP3FETEJmMBI4"}}}}) diff --git a/src/status_im/ui/components/common/common.cljs b/src/status_im/ui/components/common/common.cljs index c0e829bf81..fd399ebd4d 100644 --- a/src/status_im/ui/components/common/common.cljs +++ b/src/status_im/ui/components/common/common.cljs @@ -3,8 +3,9 @@ (:require [status-im.ui.components.react :as react] [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.context-menu :refer [context-menu]] - [status-im.utils.platform :as platform] [status-im.ui.components.common.styles :as styles] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.platform :as platform] [status-im.i18n :as i18n])) (defn top-shadow [] @@ -62,13 +63,12 @@ [top-shadow]]) (defview network-info [{:keys [text-color]}] - (letsubs [testnet? [:testnet?] - testnet-name [:testnet-name]] + (letsubs [network-id [:get-network-id]] [react/view [react/view styles/network-container [react/view styles/network-icon [vector-icons/icon :icons/network {:color :white}]] [react/text {:style (styles/network-text text-color)} - (if testnet? - (i18n/label :t/testnet-text {:testnet testnet-name}) + (if (ethereum/testnet? network-id) + (i18n/label :t/testnet-text {:testnet (get-in ethereum/chain-ids [network-id :name] "Unknown")}) (i18n/label :t/mainnet-text))]]])) diff --git a/src/status_im/ui/components/list/styles.cljs b/src/status_im/ui/components/list/styles.cljs index 78f450fe20..f6d83f67df 100644 --- a/src/status_im/ui/components/list/styles.cljs +++ b/src/status_im/ui/components/list/styles.cljs @@ -49,7 +49,8 @@ :margin-right 16}) (def right-item-wrapper - {:margin-right 16}) + {:margin-right 16 + :justify-content :center}) (def base-separator {:height 1 diff --git a/src/status_im/ui/components/sync_state/gradient.cljs b/src/status_im/ui/components/sync_state/gradient.cljs index 8e6072bf9d..dc82caa4ae 100644 --- a/src/status_im/ui/components/sync_state/gradient.cljs +++ b/src/status_im/ui/components/sync_state/gradient.cljs @@ -2,10 +2,10 @@ (:require [re-frame.core :refer [subscribe dispatch]] [reagent.core :as r] [status-im.ui.components.react :refer [view - text - animated-view - linear-gradient - get-dimensions]] + text + animated-view + linear-gradient + get-dimensions]] [status-im.ui.components.sync-state.styles :as st] [status-im.ui.components.animation :as anim] [taoensso.timbre :as log])) diff --git a/src/status_im/ui/screens/network_settings/subs.cljs b/src/status_im/ui/screens/network_settings/subs.cljs index 02c79b10cd..ae56e552f5 100644 --- a/src/status_im/ui/screens/network_settings/subs.cljs +++ b/src/status_im/ui/screens/network_settings/subs.cljs @@ -1,6 +1,5 @@ (ns status-im.ui.screens.network-settings.subs - (:require [re-frame.core :refer [reg-sub subscribe]] - [status-im.constants :as constants])) + (:require [re-frame.core :refer [reg-sub subscribe]])) (reg-sub :get-current-account-network @@ -14,16 +13,4 @@ :<- [:get :networks/networks] :<- [:get :network] (fn [[networks network]] - (get-in networks [network :raw-config :NetworkId]))) - -(reg-sub - :testnet? - :<- [:get-network-id] - (fn [network-id] - (contains? #{constants/rinkeby-id constants/ropsten-id} network-id))) - -(reg-sub - :testnet-name - :<- [:get-network-id] - (fn [network-id] - (constants/get-testnet-name network-id))) \ No newline at end of file + (get-in networks [network :raw-config :NetworkId]))) \ No newline at end of file diff --git a/src/status_im/ui/screens/profile/qr_code/views.cljs b/src/status_im/ui/screens/profile/qr_code/views.cljs index dececc1124..17e02347ce 100644 --- a/src/status_im/ui/screens/profile/qr_code/views.cljs +++ b/src/status_im/ui/screens/profile/qr_code/views.cljs @@ -8,7 +8,7 @@ [status-im.i18n :refer [label]] [status-im.ui.screens.profile.qr-code.styles :as styles] [status-im.utils.money :as money] - [status-im.utils.eip.eip681 :as eip681]) + [status-im.utils.ethereum.eip681 :as eip681]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) (defview qr-code-view [] diff --git a/src/status_im/ui/screens/qr_scanner/events.cljs b/src/status_im/ui/screens/qr_scanner/events.cljs index 16ca87eb55..854e921962 100644 --- a/src/status_im/ui/screens/qr_scanner/events.cljs +++ b/src/status_im/ui/screens/qr_scanner/events.cljs @@ -5,7 +5,7 @@ [status-im.utils.handlers :as u :refer [register-handler]] [status-im.utils.utils :as utils] [status-im.i18n :as i18n] - [status-im.utils.eip.eip681 :as eip681])) + [status-im.utils.ethereum.eip681 :as eip681])) (defmethod nav/preload-data! :qr-scanner [db [_ _ identifier]] diff --git a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs b/src/status_im/ui/screens/wallet/choose_recipient/events.cljs index f7cb45ef3e..9f3b72826a 100644 --- a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs +++ b/src/status_im/ui/screens/wallet/choose_recipient/events.cljs @@ -1,7 +1,7 @@ (ns status-im.ui.screens.wallet.choose-recipient.events (:require [status-im.constants :as constants] [status-im.i18n :as i18n] - [status-im.utils.eip.eip681 :as eip681] + [status-im.utils.ethereum.eip681 :as eip681] [status-im.utils.handlers :as handlers])) (handlers/register-handler-db diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs index 5423b1495d..2f7ec577c3 100644 --- a/src/status_im/ui/screens/wallet/events.cljs +++ b/src/status_im/ui/screens/wallet/events.cljs @@ -4,6 +4,9 @@ [status-im.native-module.core :as status] [status-im.ui.screens.wallet.db :as wallet.db] [status-im.ui.screens.wallet.navigation] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.erc20 :as erc20] + [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.handlers :as handlers] [status-im.utils.prices :as prices] [status-im.utils.transactions :as transactions] @@ -22,6 +25,18 @@ (on-error err)))) (on-error "web3 or account-id not available"))) +(defn get-token-balance [{:keys [web3 contract account-id on-success on-error]}] + (if (and web3 contract account-id) + (erc20/balance-of + web3 + contract + (ethereum/normalized-address account-id) + (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))) + (on-error "web3, contract or account-id not available"))) + (defn assoc-error-message [db error-type err] (assoc-in db [:wallet :errors error-type] (or (when err (str err)) :unknown-error))) @@ -39,6 +54,17 @@ :on-success #(dispatch [success-event %]) :on-error #(dispatch [error-event %])}))) +(reg-fx + :get-tokens-balance + (fn [{:keys [web3 symbols network account-id success-event error-event]}] + (doseq [symbol symbols] + (let [contract (:address (tokens/token-for (ethereum/network network) symbol))] + (get-token-balance {:web3 web3 + :contract contract + :account-id account-id + :on-success #(dispatch [success-event symbol %]) + :on-error #(dispatch [error-event %])}))))) + (reg-fx :get-transactions (fn [{:keys [network account-id success-event error-event]}] @@ -60,11 +86,17 @@ (handlers/register-handler-fx :update-wallet - (fn [{{:keys [web3 accounts/current-account-id network] :as db} :db} [_ a]] + (fn [{{:keys [web3 accounts/current-account-id network] :as db} :db} [_ symbols]] {:get-balance {:web3 web3 :account-id current-account-id :success-event :update-balance-success :error-event :update-balance-fail} + :get-tokens-balance {:web3 web3 + :account-id current-account-id + :symbols symbols + :network network + :success-event :update-token-balance-success + :error-event :update-token-balance-fail} :get-prices {:from "ETH" :to "USD" :success-event :update-prices-success @@ -105,7 +137,7 @@ :update-balance-success (fn [db [_ balance]] (-> db - (assoc-in [:wallet :balance] balance) + (assoc-in [:wallet :balance :ETH] balance) (assoc-in [:wallet :balance-loading?] false)))) (handlers/register-handler-db @@ -116,6 +148,21 @@ (assoc-error-message :balance-update err) (assoc-in [:wallet :balance-loading?] false)))) +(handlers/register-handler-db + :update-token-balance-success + (fn [db [_ symbol balance]] + (-> db + (assoc-in [:wallet :balance symbol] balance) + (assoc-in [:wallet :balance-loading?] false)))) + +(handlers/register-handler-db + :update-token-balance-fail + (fn [db [_ err]] + (log/debug "Unable to get token balance: " err) + (-> db + (assoc-error-message :balance-update err) + (assoc-in [:wallet :balance-loading?] false)))) + (handlers/register-handler-db :update-prices-success (fn [db [_ prices]] diff --git a/src/status_im/ui/screens/wallet/main/styles.cljs b/src/status_im/ui/screens/wallet/main/styles.cljs index 47af26613a..d6f8a04dbd 100644 --- a/src/status_im/ui/screens/wallet/main/styles.cljs +++ b/src/status_im/ui/screens/wallet/main/styles.cljs @@ -109,5 +109,14 @@ :color styles/color-gray4 :margin-left 6}) +(def corner-dot + {:position :absolute + :top 12 + :right 6 + :width 4 + :height 4 + :border-radius 2 + :background-color styles/color-cyan}) + (defn asset-border [color] {:border-color color :border-width 1 :border-radius 32}) diff --git a/src/status_im/ui/screens/wallet/main/views.cljs b/src/status_im/ui/screens/wallet/main/views.cljs index a1d5ec38a8..e2e32ed8ce 100644 --- a/src/status_im/ui/screens/wallet/main/views.cljs +++ b/src/status_im/ui/screens/wallet/main/views.cljs @@ -12,6 +12,8 @@ [status-im.i18n :as i18n] [status-im.react-native.resources :as resources] [status-im.utils.config :as config] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.money :as money] [status-im.utils.platform :as platform] [status-im.utils.utils :as utils] @@ -77,13 +79,6 @@ [btn/button {:disabled? true :style (button.styles/button-bar :last) :text-style styles/main-button-text} (i18n/label :t/wallet-exchange)]]]]) -;; TODO(goranjovic): snt is temporarily given the same logo that eth uses to minimize the changes -;; while ERC20 is mocked and hidden behind a flag. -(defn- token->image [id] - (case id - "eth" {:source (:ethereum resources/assets) :style (styles/asset-border components.styles/color-gray-transparent-light)} - "snt" {:source (:ethereum resources/assets) :style (styles/asset-border components.styles/color-blue)})) - (defn add-asset [] [list/touchable-item show-not-implemented! [react/view @@ -93,54 +88,42 @@ [react/text {:style styles/add-asset-text} (i18n/label :t/wallet-add-asset)]]]]]) -(defn render-asset [{:keys [id currency amount]}] - ;; TODO(jeluard) Navigate to asset details screen - #_ - [list/touchable-item show-not-implemented! - [react/view - [list/item - [list/item-image {:uri :launch_logo}] - [react/view {:style styles/asset-item-value-container} - [react/text {:style styles/asset-item-value} (str amount)] - [react/text {:style styles/asset-item-currency - :uppercase? true} - id]] - [list/item-icon {:icon :icons/forward}]]]] - (if id - [react/view - [list/item - (let [{:keys [source style]} (token->image id)] - [list/item-image source style]) - [react/view {:style styles/asset-item-value-container} - [react/text {:style styles/asset-item-value - :number-of-lines 1 - :ellipsize-mode :tail} - (money/to-fixed (money/wei-> :eth amount))] - [react/text {:style styles/asset-item-currency - :uppercase? true - :number-of-lines 1} - id]]]] +(defn render-asset [{:keys [name symbol icon decimals amount]}] + (if name ;; If no 'name' then this the dummy value used to render `add-asset` + [list/touchable-item #(utils/show-popup "TODO" (str "Details about " symbol " here")) + [react/view + [list/item + (let [{:keys [source style]} icon] + [list/item-image source style]) + [react/view {:style styles/asset-item-value-container} + [react/text {:style styles/asset-item-value + :number-of-lines 1 + :ellipsize-mode :tail} + (money/to-fixed (money/token->unit (or amount 0) decimals))] + [react/text {:style styles/asset-item-currency + :uppercase? true + :number-of-lines 1} + symbol]] + [list/item-icon {:icon :icons/forward}]]]] [add-asset])) -(defn asset-section [balance prices-loading? balance-loading?] - (let [assets (concat [{:id "eth" :currency :eth :amount balance}] - (if config/erc20-enabled? - [{:id "snt" :currency :snt :amount 5000000000000000000000}]))] +(defn tokens-for [network] + (get tokens/all (ethereum/network network))) + +(defn asset-section [network balance prices-loading? balance-loading?] + (let [tokens (tokens-for network) + assets (map #(assoc % :amount (get balance (:symbol %))) (concat [tokens/ethereum] (when config/erc20-enabled? tokens)))] [react/view {:style styles/asset-section} [react/text {:style styles/asset-section-title} (i18n/label :t/wallet-assets)] [list/flat-list - {:data (concat assets [{}]) ;; Extra map triggers rendering for add-asset - ;; TODO(goranjovic): Refactor - ;; the order where new element is inserted - ;; with `conj` depends on the underlying collection type. - ;; whereas `concat` like here guarantees that empty element - ;; will be inserted in the end. + {:data assets ;; TODO(jeluard) Reenable once we `add-an-asset` story is flecthed out ;; (concat assets [{}]) ;; Extra map triggers rendering for add-asset :render-fn render-asset - :on-refresh #(rf/dispatch [:update-wallet]) + :on-refresh #(rf/dispatch [:update-wallet (when config/erc20-enabled? (map :symbol tokens))]) :refreshing (boolean (or prices-loading? balance-loading?))}]])) (defview wallet [] - (letsubs [balance [:balance] + (letsubs [network [:network] + balance [:balance] portfolio-value [:portfolio-value] portfolio-change [:portfolio-change] prices-loading? [:prices-loading?] @@ -151,4 +134,4 @@ [toolbar-view] [react/view components.styles/flex [main-section portfolio-value portfolio-change syncing? error-message] - [asset-section balance prices-loading? balance-loading?]]])) + [asset-section network balance prices-loading? balance-loading?]]])) diff --git a/src/status_im/ui/screens/wallet/navigation.cljs b/src/status_im/ui/screens/wallet/navigation.cljs index 14c65b359d..513e746c57 100644 --- a/src/status_im/ui/screens/wallet/navigation.cljs +++ b/src/status_im/ui/screens/wallet/navigation.cljs @@ -1,10 +1,11 @@ (ns status-im.ui.screens.wallet.navigation (:require [re-frame.core :as re-frame] - [status-im.ui.screens.navigation :as navigation])) + [status-im.ui.screens.navigation :as navigation] + [status-im.ui.screens.wallet.main.views :as main])) (defmethod navigation/preload-data! :wallet [db _] - (re-frame/dispatch [:update-wallet]) + (re-frame/dispatch [:update-wallet (map :symbol (main/tokens-for (:network db)))]) (assoc-in db [:wallet :current-tab] 0)) (defmethod navigation/preload-data! :transactions-history diff --git a/src/status_im/ui/screens/wallet/request/views.cljs b/src/status_im/ui/screens/wallet/request/views.cljs index 2c6fc5abac..d789d3c4ba 100644 --- a/src/status_im/ui/screens/wallet/request/views.cljs +++ b/src/status_im/ui/screens/wallet/request/views.cljs @@ -15,7 +15,7 @@ [status-im.ui.components.styles :as components.styles] [status-im.i18n :as i18n] [status-im.utils.platform :as platform] - [status-im.utils.eip.eip681 :as eip681] + [status-im.utils.ethereum.eip681 :as eip681] [status-im.utils.money :as money])) (defn toolbar-view [] diff --git a/src/status_im/ui/screens/wallet/subs.cljs b/src/status_im/ui/screens/wallet/subs.cljs index 556e9f6a32..07b5ffb5d3 100644 --- a/src/status_im/ui/screens/wallet/subs.cljs +++ b/src/status_im/ui/screens/wallet/subs.cljs @@ -30,7 +30,7 @@ :<- [:price] (fn [[balance price]] (if (and balance price) - (-> (money/wei->ether balance) + (-> (money/wei->ether (get balance :ETH)) ;; TODO(jeluard) Modify to consider tokens (money/eth->usd price) (money/with-precision 2) str) @@ -42,7 +42,7 @@ :<- [:balance] (fn [[price last-day balance]] (when (and price last-day) - (if (> balance 0) + (if (> (get balance :ETH) 0) ;; TODO(jeluard) Modify to consider tokens (-> (money/percent-change price last-day) (money/with-precision 2) .toNumber) diff --git a/src/status_im/utils/erc20.cljs b/src/status_im/utils/erc20.cljs deleted file mode 100644 index 1682fcf526..0000000000 --- a/src/status_im/utils/erc20.cljs +++ /dev/null @@ -1,51 +0,0 @@ -(ns status-im.utils.erc20 - (:require [clojure.string :as string] - [status-im.js-dependencies :as dependencies])) - -;; Example -;; -;; Contract: https://ropsten.etherscan.io/address/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5#readContract -;; Owner: https://ropsten.etherscan.io/token/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5?a=0xa7cfd581060ec66414790691681732db249502bd -;; -;; With a running node on Ropsten: -;; (let [web3 (:web3 @re-frame.db/app-db) -;; contract "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" -;; address "0xa7cfd581060ec66414790691681732db249502bd"] -;; (erc20/balance-of web3 contract address println)) -;; -;; => 0x0000000000000000000000000000000000000000000000000000000001bd0c4a -;; (hex->int "0x0000000000000000000000000000000000000000000000000000000001bd0c4a") ;; => 29166666 (note token decimals) - -(defn sha3 [s] - (.sha3 dependencies/Web3.prototype (str s))) - -(defn hex->int [s] - (js/parseInt s 16)) - -(defn zero-pad-64 [s] - (str (apply str (drop (count s) (repeat 64 "0"))) s)) - -(defn format-param [param] - (if (number? param) - (zero-pad-64 (hex->int param)) - (zero-pad-64 (subs param 2)))) - -(defn format-call-params [method-id & params] - (let [params (string/join (map format-param params))] - (str method-id params))) - -(defn get-call-params [contract method-id & params] - (let [data (apply format-call-params method-id params)] - {:to contract :data data})) - -(defn sig->method-id [signature] - (apply str (take 10 (sha3 signature)))) - -(defn balance-of-params [token of] - (let [method-id (sig->method-id "balanceOf(address)")] - (get-call-params token method-id of))) - -(defn balance-of [web3 token of cb] - (.call (.-eth web3) - (clj->js (balance-of-params token of)) - cb)) diff --git a/src/status_im/utils/ethereum/core.cljs b/src/status_im/utils/ethereum/core.cljs new file mode 100644 index 0000000000..f988dae255 --- /dev/null +++ b/src/status_im/utils/ethereum/core.cljs @@ -0,0 +1,65 @@ +(ns status-im.utils.ethereum.core + (:require [clojure.string :as string] + [status-im.js-dependencies :as dependencies])) + +;; IDs standardized in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids + +(def chain-ids + {:mainnet {:id 1 :name "Mainnet"} + :ropsten {:id 3 :name "Ropsten"} + :rinkeby {:id 4 :name "Rinkeby"}}) + +(defn chain-id [k] + (get-in chain-ids [k :id])) + +(defn testnet? [id] + (contains? #{(chain-id :ropsten) (chain-id :rinkeby)} id)) + +(def hex-prefix "0x") + +(defn normalized-address [address] + (when address + (if (string/starts-with? address hex-prefix) + address + (str hex-prefix address)))) + +(defn network [network] + (when network + (keyword (string/replace network "_rpc" "")))) + +(defn sha3 [s] + (.sha3 dependencies/Web3.prototype (str s))) + +(defn hex->boolean [s] + (= s "0x0")) + +(defn boolean->hex [b] + (if b "0x0" "0x1")) + +(defn hex->int [s] + (js/parseInt s 16)) + +(defn int->hex [i] + (.toHex dependencies/Web3.prototype i)) + +(defn zero-pad-64 [s] + (str (apply str (drop (count s) (repeat 64 "0"))) s)) + +(defn format-param [param] + (if (number? param) + (zero-pad-64 (hex->int param)) + (zero-pad-64 (subs param 2)))) + +(defn format-call-params [method-id & params] + (let [params (string/join (map format-param params))] + (str method-id params))) + +(defn- sig->method-id [signature] + (apply str (take 10 (sha3 signature)))) + +(defn call [web3 params cb] + (.call (.-eth web3) (clj->js params) cb)) + +(defn call-params [contract method-sig & params] + (let [data (apply format-call-params (sig->method-id method-sig) params)] + {:to contract :data data})) \ No newline at end of file diff --git a/src/status_im/utils/eip/eip681.cljs b/src/status_im/utils/ethereum/eip681.cljs similarity index 93% rename from src/status_im/utils/eip/eip681.cljs rename to src/status_im/utils/ethereum/eip681.cljs index 12c65fa0f3..d3af2b2552 100644 --- a/src/status_im/utils/eip/eip681.cljs +++ b/src/status_im/utils/ethereum/eip681.cljs @@ -1,11 +1,11 @@ -(ns status-im.utils.eip.eip681 +(ns status-im.utils.ethereum.eip681 "Utility function related to [EIP681](https://github.com/ethereum/EIPs/issues/681) This EIP standardize how ethereum payment request can be represented as URI (say to embed them in a QR code). e.g. ethereum:0x1234@1/transfer?to=0x5678&value=1e18&gas=5000" (:require [clojure.string :as string] - [status-im.constants :as constants] + [status-im.utils.ethereum.core :as ethereum] [status-im.utils.money :as money])) (def scheme "ethereum") @@ -43,7 +43,7 @@ (when authority-path (let [[_ address chain-id function-name] (re-find authority-path-pattern authority-path)] (when-not (or (string/blank? address) function-name) ;; Native token support only TODO(jeluard) Add ERC20 support - (merge {:address address :chain-id (if chain-id (js/parseInt chain-id) constants/mainnet-id)} + (merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-id :mainnet))} (parse-query query)))))))) @@ -59,7 +59,7 @@ (when (and address (not function-name)) ;; Native token support only TODO(jeluard) Add ERC20 support (let [parameters (dissoc (into {} (filter second m)) :chain-id)] ;; filter nil values (str scheme scheme-separator address - (when (and chain-id (not= chain-id constants/mainnet-id)) + (when (and chain-id (not= chain-id (ethereum/chain-id :mainnet))) ;; Add chain-id if specified and is not main-net (str chain-id-separator chain-id)) (when-not (empty? parameters) diff --git a/src/status_im/utils/ethereum/erc20.cljs b/src/status_im/utils/ethereum/erc20.cljs new file mode 100644 index 0000000000..eef214c75e --- /dev/null +++ b/src/status_im/utils/ethereum/erc20.cljs @@ -0,0 +1,58 @@ +(ns status-im.utils.ethereum.erc20 + " + Helper functions to interact with [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md) smart contract + + Example + + Contract: https://ropsten.etherscan.io/address/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5#readContract + Owner: https://ropsten.etherscan.io/token/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5?a=0xa7cfd581060ec66414790691681732db249502bd + + With a running node on Ropsten: + (let [web3 (:web3 @re-frame.db/app-db) + contract \"0x29b5f6efad2ad701952dfde9f29c960b5d6199c5\" + address \"0xa7cfd581060ec66414790691681732db249502bd\"] + (erc20/balance-of web3 contract address println)) + + => 29166666 + " + (:require [status-im.utils.ethereum.core :as ethereum]) + (:refer-clojure :exclude [name symbol])) + +(defn name [web3 contract cb] + (ethereum/call web3 (ethereum/call-params contract "name()") cb)) + +(defn symbol [web3 contract cb] + (ethereum/call web3 (ethereum/call-params contract "symbol()") cb)) + +(defn decimals [web3 contract cb] + (ethereum/call web3 (ethereum/call-params contract "decimals()") cb)) + +(defn total-supply [web3 contract cb] + (ethereum/call web3 + (ethereum/call-params contract "totalSupply()") + #(cb %1 (ethereum/hex->int %2)))) + +(defn balance-of [web3 contract address cb] + (ethereum/call web3 + (ethereum/call-params contract "balanceOf(address)" address) + #(cb %1 (ethereum/hex->int %2)))) + +(defn transfer [web3 contract address value cb] + (ethereum/call web3 + (ethereum/call-params contract "transfer(address, uint256)" address (ethereum/int->hex value)) + #(cb %1 (ethereum/hex->boolean %2)))) + +(defn transfer-from [web3 contract from-address to-address value cb] + (ethereum/call web3 + (ethereum/call-params contract "transferFrom(address, address, uint256)" from-address to-address (ethereum/int->hex value)) + #(cb %1 (ethereum/hex->boolean %2)))) + +(defn approve [web3 contract address value cb] + (ethereum/call web3 + (ethereum/call-params contract "approve(address, uint256)" address (ethereum/int->hex value)) + #(cb %1 (ethereum/hex->boolean %2)))) + +(defn allowance [web3 contract owner-address spender-address cb] + (ethereum/call web3 + (ethereum/call-params contract "allowance(address, address)" owner-address spender-address) + #(cb %1 (ethereum/hex->int %2)))) \ No newline at end of file diff --git a/src/status_im/utils/ethereum/macros.clj b/src/status_im/utils/ethereum/macros.clj new file mode 100644 index 0000000000..31bcdfca37 --- /dev/null +++ b/src/status_im/utils/ethereum/macros.clj @@ -0,0 +1,25 @@ +(ns status-im.utils.ethereum.macros + (:require [clojure.string :as string] + [clojure.java.io :as io])) + +(def tokens-folder "./resources/images/tokens/") + +(def default-icon-path (str tokens-folder "default.png")) + +(defn icon-path + [symbol] + (let [s (str "./resources/images/tokens/" (string/lower-case (name symbol)) ".png")] + (if (.exists (io/file s)) + `(js/require ~s) + `(js/require "./resources/images/tokens/default.png")))) + +(defn- token->icon [{:keys [icon symbol]}] + ;; Tokens can define their own icons. + ;; If not try to make one using a local image as resource, if it does not exist fallback to default. + (or icon (icon-path symbol))) + +(defmacro resolve-icons + "In react-native arguments to require must be static strings. + Resolve all icons at compilation time so no variable is used." + [tokens] + (mapv #(assoc-in % [:icon :source] (token->icon %)) tokens)) \ No newline at end of file diff --git a/src/status_im/utils/ethereum/tokens.cljs b/src/status_im/utils/ethereum/tokens.cljs new file mode 100644 index 0000000000..0557a25783 --- /dev/null +++ b/src/status_im/utils/ethereum/tokens.cljs @@ -0,0 +1,27 @@ +(ns status-im.utils.ethereum.tokens + (:require [status-im.ui.components.styles :as styles]) + (:require-macros [status-im.utils.ethereum.macros :refer [resolve-icons]])) + +(defn- asset-border [color] + {:border-color color :border-width 1 :border-radius 32}) + +(def ethereum {:name "Ethereum" :symbol :ETH :decimals 18 + :icon {:source (js/require "./resources/images/assets/ethereum.png") + :style (asset-border styles/color-light-blue-transparent)}}) + +(def all + {:mainnet + (resolve-icons + [{:name "Status Network Token" + :symbol :SNT + :decimals 18 + :address "0x744d70FDBE2Ba4CF95131626614a1763DF805B9E"}]) + :testnet + (resolve-icons + [{:name "Status Test Token" + :symbol :STT + :decimals 18 + :address "0xc55cf4b03948d7ebc8b9e8bad92643703811d162"}])}) + +(defn token-for [chain-id symbol] + (some #(if (= symbol (:symbol %)) %) (get all chain-id))) \ No newline at end of file diff --git a/src/status_im/utils/money.cljs b/src/status_im/utils/money.cljs index c404a26804..1fe551dc6e 100644 --- a/src/status_im/utils/money.cljs +++ b/src/status_im/utils/money.cljs @@ -49,18 +49,20 @@ (dependencies/Web3.prototype.toDecimal (normalize s)) (catch :default err nil)))) +(defn from-decimal [n] (str "1" (string/join (repeat n "0")))) + (def eth-units {:wei (bignumber "1") - :kwei (bignumber "1000") - :mwei (bignumber "1000000") - :gwei (bignumber "1000000000") - :szabo (bignumber "1000000000000") - :finney (bignumber "1000000000000000") - :eth (bignumber "1000000000000000000") - :keth (bignumber "1000000000000000000000") - :meth (bignumber "1000000000000000000000000") - :geth (bignumber "1000000000000000000000000000") - :teth (bignumber "1000000000000000000000000000000")}) + :kwei (bignumber (from-decimal 3)) + :mwei (bignumber (from-decimal 6)) + :gwei (bignumber (from-decimal 9)) + :szabo (bignumber (from-decimal 12)) + :finney (bignumber (from-decimal 15)) + :eth (bignumber (from-decimal 18)) + :keth (bignumber (from-decimal 21)) + :meth (bignumber (from-decimal 24)) + :geth (bignumber (from-decimal 27)) + :teth (bignumber (from-decimal 30))}) (defn wei-> [unit n] (when-let [bn (bignumber n)] @@ -80,11 +82,16 @@ (when bn (.times bn (bignumber 1e18)))) +(defn token->unit [n decimals] + (when-let [bn (bignumber n)] + (.dividedBy bn (bignumber (from-decimal decimals))))) + (defn fee-value [gas gas-price] (.times (bignumber gas) (bignumber gas-price))) (defn eth->usd [eth usd-price] - (.times (bignumber eth) (bignumber usd-price))) + (when-let [bn (bignumber eth)] + (.times bn (bignumber usd-price)))) (defn percent-change [from to] (let [bnf (bignumber from) @@ -99,5 +106,5 @@ (.round bn decimals))) (defn sufficient-funds? [amount balance] - (when amount + (when balance (.greaterThanOrEqualTo balance amount))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index d282666208..8c108b8a5b 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -13,8 +13,8 @@ [status-im.test.utils.utils] [status-im.test.utils.money] [status-im.test.utils.clocks] - [status-im.test.utils.eip.eip681] - [status-im.test.utils.erc20] + [status-im.test.utils.ethereum.eip681] + [status-im.test.utils.ethereum.core] [status-im.test.utils.random] [status-im.test.utils.gfycat.core] [status-im.test.utils.signing-phrase.core] @@ -44,8 +44,8 @@ 'status-im.test.utils.utils 'status-im.test.utils.money 'status-im.test.utils.clocks - 'status-im.test.utils.eip.eip681 - 'status-im.test.utils.erc20 + 'status-im.test.utils.ethereum.eip681 + 'status-im.test.utils.ethereum.core 'status-im.test.utils.random 'status-im.test.utils.gfycat.core 'status-im.test.utils.signing-phrase.core diff --git a/test/cljs/status_im/test/utils/erc20.cljs b/test/cljs/status_im/test/utils/ethereum/core.cljs similarity index 67% rename from test/cljs/status_im/test/utils/erc20.cljs rename to test/cljs/status_im/test/utils/ethereum/core.cljs index 7259092c65..0228287a17 100644 --- a/test/cljs/status_im/test/utils/erc20.cljs +++ b/test/cljs/status_im/test/utils/ethereum/core.cljs @@ -1,11 +1,11 @@ -(ns status-im.test.utils.erc20 +(ns status-im.test.utils.ethereum.core (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.erc20 :as erc20])) + [status-im.utils.ethereum.core :as core])) -(deftest erc20 +(deftest call-params (testing "ERC20 balance-of params" (let [contract "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" address "0xa7cfd581060ec66414790691681732db249502bd"] - (is (= (erc20/balance-of-params contract address) + (is (= (core/call-params contract "balanceOf(address)" address) {:to "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" :data "0x70a08231000000000000000000000000a7cfd581060ec66414790691681732db249502bd"}))))) diff --git a/test/cljs/status_im/test/utils/eip/eip681.cljs b/test/cljs/status_im/test/utils/ethereum/eip681.cljs similarity index 97% rename from test/cljs/status_im/test/utils/eip/eip681.cljs rename to test/cljs/status_im/test/utils/ethereum/eip681.cljs index a1b186b3a0..f5591aaa79 100644 --- a/test/cljs/status_im/test/utils/eip/eip681.cljs +++ b/test/cljs/status_im/test/utils/ethereum/eip681.cljs @@ -1,6 +1,6 @@ -(ns status-im.test.utils.eip.eip681 +(ns status-im.test.utils.ethereum.eip681 (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.utils.eip.eip681 :as eip681] + [status-im.utils.ethereum.eip681 :as eip681] [status-im.utils.money :as money])) (deftest parse-uri diff --git a/test/cljs/status_im/utils/ethereum/tokens.cljs b/test/cljs/status_im/utils/ethereum/tokens.cljs new file mode 100644 index 0000000000..adf37ccf50 --- /dev/null +++ b/test/cljs/status_im/utils/ethereum/tokens.cljs @@ -0,0 +1,5 @@ +(ns status-im.utils.ethereum.tokens) + +(def ethereum {}) + +(def all {}) \ No newline at end of file