Added ERC20 listing support

This commit is contained in:
Julien Eluard 2017-11-22 11:37:20 +01:00 committed by Julien Eluard
parent f7e63e9bea
commit 3df4a7f1e4
28 changed files with 334 additions and 179 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,5 +1,6 @@
(ns status-im.constants (ns status-im.constants
(:require [status-im.i18n :as i18n] (:require [status-im.i18n :as i18n]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.types :as types] [status-im.utils.types :as types]
[status-im.utils.config :as config])) [status-im.utils.config :as config]))
@ -43,24 +44,14 @@
:raw-config config)])) :raw-config config)]))
(into {}))) (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 (def mainnet-networks
{"mainnet" {:id "mainnet", {"mainnet" {:id "mainnet",
:name "Mainnet", :name "Mainnet",
:config {:NetworkId mainnet-id :config {:NetworkId (ethereum/chain-id :mainnet)
:DataDir "/ethereum/mainnet"}} :DataDir "/ethereum/mainnet"}}
"mainnet_rpc" {:id "mainnet_rpc", "mainnet_rpc" {:id "mainnet_rpc",
:name "Mainnet with upstream RPC", :name "Mainnet with upstream RPC",
:config {:NetworkId mainnet-id :config {:NetworkId (ethereum/chain-id :mainnet)
:DataDir "/ethereum/mainnet_rpc" :DataDir "/ethereum/mainnet_rpc"
:UpstreamConfig {:Enabled true :UpstreamConfig {:Enabled true
:URL "https://mainnet.infura.io/z6GCTmjdP3FETEJmMBI4"}}}}) :URL "https://mainnet.infura.io/z6GCTmjdP3FETEJmMBI4"}}}})
@ -68,21 +59,21 @@
(def testnet-networks (def testnet-networks
{"testnet" {:id "testnet", {"testnet" {:id "testnet",
:name "Ropsten", :name "Ropsten",
:config {:NetworkId ropsten-id :config {:NetworkId (ethereum/chain-id :ropsten)
:DataDir "/ethereum/testnet"}} :DataDir "/ethereum/testnet"}}
"testnet_rpc" {:id "testnet_rpc", "testnet_rpc" {:id "testnet_rpc",
:name "Ropsten with upstream RPC", :name "Ropsten with upstream RPC",
:config {:NetworkId ropsten-id :config {:NetworkId (ethereum/chain-id :ropsten)
:DataDir "/ethereum/testnet_rpc" :DataDir "/ethereum/testnet_rpc"
:UpstreamConfig {:Enabled true :UpstreamConfig {:Enabled true
:URL "https://ropsten.infura.io/z6GCTmjdP3FETEJmMBI4"}}} :URL "https://ropsten.infura.io/z6GCTmjdP3FETEJmMBI4"}}}
"rinkeby" {:id "rinkeby", "rinkeby" {:id "rinkeby",
:name "Rinkeby", :name "Rinkeby",
:config {:NetworkId rinkeby-id :config {:NetworkId (ethereum/chain-id :rinkeby)
:DataDir "/ethereum/rinkeby"}} :DataDir "/ethereum/rinkeby"}}
"rinkeby_rpc" {:id "rinkeby_rpc", "rinkeby_rpc" {:id "rinkeby_rpc",
:name "Rinkeby with upstream RPC", :name "Rinkeby with upstream RPC",
:config {:NetworkId rinkeby-id :config {:NetworkId (ethereum/chain-id :rinkeby)
:DataDir "/ethereum/rinkeby_rpc" :DataDir "/ethereum/rinkeby_rpc"
:UpstreamConfig {:Enabled true :UpstreamConfig {:Enabled true
:URL "https://rinkeby.infura.io/z6GCTmjdP3FETEJmMBI4"}}}}) :URL "https://rinkeby.infura.io/z6GCTmjdP3FETEJmMBI4"}}}})

View File

@ -3,8 +3,9 @@
(:require [status-im.ui.components.react :as react] (:require [status-im.ui.components.react :as react]
[status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.context-menu :refer [context-menu]] [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.ui.components.common.styles :as styles]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.platform :as platform]
[status-im.i18n :as i18n])) [status-im.i18n :as i18n]))
(defn top-shadow [] (defn top-shadow []
@ -62,13 +63,12 @@
[top-shadow]]) [top-shadow]])
(defview network-info [{:keys [text-color]}] (defview network-info [{:keys [text-color]}]
(letsubs [testnet? [:testnet?] (letsubs [network-id [:get-network-id]]
testnet-name [:testnet-name]]
[react/view [react/view
[react/view styles/network-container [react/view styles/network-container
[react/view styles/network-icon [react/view styles/network-icon
[vector-icons/icon :icons/network {:color :white}]] [vector-icons/icon :icons/network {:color :white}]]
[react/text {:style (styles/network-text text-color)} [react/text {:style (styles/network-text text-color)}
(if testnet? (if (ethereum/testnet? network-id)
(i18n/label :t/testnet-text {:testnet testnet-name}) (i18n/label :t/testnet-text {:testnet (get-in ethereum/chain-ids [network-id :name] "Unknown")})
(i18n/label :t/mainnet-text))]]])) (i18n/label :t/mainnet-text))]]]))

View File

@ -49,7 +49,8 @@
:margin-right 16}) :margin-right 16})
(def right-item-wrapper (def right-item-wrapper
{:margin-right 16}) {:margin-right 16
:justify-content :center})
(def base-separator (def base-separator
{:height 1 {:height 1

View File

@ -1,6 +1,5 @@
(ns status-im.ui.screens.network-settings.subs (ns status-im.ui.screens.network-settings.subs
(:require [re-frame.core :refer [reg-sub subscribe]] (:require [re-frame.core :refer [reg-sub subscribe]]))
[status-im.constants :as constants]))
(reg-sub (reg-sub
:get-current-account-network :get-current-account-network
@ -15,15 +14,3 @@
:<- [:get :network] :<- [:get :network]
(fn [[networks network]] (fn [[networks network]]
(get-in networks [network :raw-config :NetworkId]))) (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)))

View File

@ -8,7 +8,7 @@
[status-im.i18n :refer [label]] [status-im.i18n :refer [label]]
[status-im.ui.screens.profile.qr-code.styles :as styles] [status-im.ui.screens.profile.qr-code.styles :as styles]
[status-im.utils.money :as money] [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]])) (:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defview qr-code-view [] (defview qr-code-view []

View File

@ -5,7 +5,7 @@
[status-im.utils.handlers :as u :refer [register-handler]] [status-im.utils.handlers :as u :refer [register-handler]]
[status-im.utils.utils :as utils] [status-im.utils.utils :as utils]
[status-im.i18n :as i18n] [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 (defmethod nav/preload-data! :qr-scanner
[db [_ _ identifier]] [db [_ _ identifier]]

View File

@ -1,7 +1,7 @@
(ns status-im.ui.screens.wallet.choose-recipient.events (ns status-im.ui.screens.wallet.choose-recipient.events
(:require [status-im.constants :as constants] (:require [status-im.constants :as constants]
[status-im.i18n :as i18n] [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])) [status-im.utils.handlers :as handlers]))
(handlers/register-handler-db (handlers/register-handler-db

View File

@ -4,6 +4,9 @@
[status-im.native-module.core :as status] [status-im.native-module.core :as status]
[status-im.ui.screens.wallet.db :as wallet.db] [status-im.ui.screens.wallet.db :as wallet.db]
[status-im.ui.screens.wallet.navigation] [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.handlers :as handlers]
[status-im.utils.prices :as prices] [status-im.utils.prices :as prices]
[status-im.utils.transactions :as transactions] [status-im.utils.transactions :as transactions]
@ -22,6 +25,18 @@
(on-error err)))) (on-error err))))
(on-error "web3 or account-id not available"))) (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] (defn assoc-error-message [db error-type err]
(assoc-in db [:wallet :errors error-type] (or (when err (str err)) (assoc-in db [:wallet :errors error-type] (or (when err (str err))
:unknown-error))) :unknown-error)))
@ -39,6 +54,17 @@
:on-success #(dispatch [success-event %]) :on-success #(dispatch [success-event %])
:on-error #(dispatch [error-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 (reg-fx
:get-transactions :get-transactions
(fn [{:keys [network account-id success-event error-event]}] (fn [{:keys [network account-id success-event error-event]}]
@ -60,11 +86,17 @@
(handlers/register-handler-fx (handlers/register-handler-fx
:update-wallet :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 {:get-balance {:web3 web3
:account-id current-account-id :account-id current-account-id
:success-event :update-balance-success :success-event :update-balance-success
:error-event :update-balance-fail} :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" :get-prices {:from "ETH"
:to "USD" :to "USD"
:success-event :update-prices-success :success-event :update-prices-success
@ -105,7 +137,7 @@
:update-balance-success :update-balance-success
(fn [db [_ balance]] (fn [db [_ balance]]
(-> db (-> db
(assoc-in [:wallet :balance] balance) (assoc-in [:wallet :balance :ETH] balance)
(assoc-in [:wallet :balance-loading?] false)))) (assoc-in [:wallet :balance-loading?] false))))
(handlers/register-handler-db (handlers/register-handler-db
@ -116,6 +148,21 @@
(assoc-error-message :balance-update err) (assoc-error-message :balance-update err)
(assoc-in [:wallet :balance-loading?] false)))) (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 (handlers/register-handler-db
:update-prices-success :update-prices-success
(fn [db [_ prices]] (fn [db [_ prices]]

View File

@ -109,5 +109,14 @@
:color styles/color-gray4 :color styles/color-gray4
:margin-left 6}) :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] (defn asset-border [color]
{:border-color color :border-width 1 :border-radius 32}) {:border-color color :border-width 1 :border-radius 32})

View File

@ -12,6 +12,8 @@
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.react-native.resources :as resources] [status-im.react-native.resources :as resources]
[status-im.utils.config :as config] [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.money :as money]
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
[status-im.utils.utils :as utils] [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} [btn/button {:disabled? true :style (button.styles/button-bar :last) :text-style styles/main-button-text}
(i18n/label :t/wallet-exchange)]]]]) (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 [] (defn add-asset []
[list/touchable-item show-not-implemented! [list/touchable-item show-not-implemented!
[react/view [react/view
@ -93,54 +88,42 @@
[react/text {:style styles/add-asset-text} [react/text {:style styles/add-asset-text}
(i18n/label :t/wallet-add-asset)]]]]]) (i18n/label :t/wallet-add-asset)]]]]])
(defn render-asset [{:keys [id currency amount]}] (defn render-asset [{:keys [name symbol icon decimals amount]}]
;; TODO(jeluard) Navigate to asset details screen (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"))
[list/touchable-item show-not-implemented!
[react/view [react/view
[list/item [list/item
[list/item-image {:uri :launch_logo}] (let [{:keys [source style]} icon]
[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]) [list/item-image source style])
[react/view {:style styles/asset-item-value-container} [react/view {:style styles/asset-item-value-container}
[react/text {:style styles/asset-item-value [react/text {:style styles/asset-item-value
:number-of-lines 1 :number-of-lines 1
:ellipsize-mode :tail} :ellipsize-mode :tail}
(money/to-fixed (money/wei-> :eth amount))] (money/to-fixed (money/token->unit (or amount 0) decimals))]
[react/text {:style styles/asset-item-currency [react/text {:style styles/asset-item-currency
:uppercase? true :uppercase? true
:number-of-lines 1} :number-of-lines 1}
id]]]] symbol]]
[list/item-icon {:icon :icons/forward}]]]]
[add-asset])) [add-asset]))
(defn asset-section [balance prices-loading? balance-loading?] (defn tokens-for [network]
(let [assets (concat [{:id "eth" :currency :eth :amount balance}] (get tokens/all (ethereum/network network)))
(if config/erc20-enabled?
[{:id "snt" :currency :snt :amount 5000000000000000000000}]))] (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/view {:style styles/asset-section}
[react/text {:style styles/asset-section-title} (i18n/label :t/wallet-assets)] [react/text {:style styles/asset-section-title} (i18n/label :t/wallet-assets)]
[list/flat-list [list/flat-list
{:data (concat assets [{}]) ;; Extra map triggers rendering for add-asset {:data assets ;; TODO(jeluard) Reenable once we `add-an-asset` story is flecthed out ;; (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.
:render-fn render-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?))}]])) :refreshing (boolean (or prices-loading? balance-loading?))}]]))
(defview wallet [] (defview wallet []
(letsubs [balance [:balance] (letsubs [network [:network]
balance [:balance]
portfolio-value [:portfolio-value] portfolio-value [:portfolio-value]
portfolio-change [:portfolio-change] portfolio-change [:portfolio-change]
prices-loading? [:prices-loading?] prices-loading? [:prices-loading?]
@ -151,4 +134,4 @@
[toolbar-view] [toolbar-view]
[react/view components.styles/flex [react/view components.styles/flex
[main-section portfolio-value portfolio-change syncing? error-message] [main-section portfolio-value portfolio-change syncing? error-message]
[asset-section balance prices-loading? balance-loading?]]])) [asset-section network balance prices-loading? balance-loading?]]]))

View File

@ -1,10 +1,11 @@
(ns status-im.ui.screens.wallet.navigation (ns status-im.ui.screens.wallet.navigation
(:require [re-frame.core :as re-frame] (: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 (defmethod navigation/preload-data! :wallet
[db _] [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)) (assoc-in db [:wallet :current-tab] 0))
(defmethod navigation/preload-data! :transactions-history (defmethod navigation/preload-data! :transactions-history

View File

@ -15,7 +15,7 @@
[status-im.ui.components.styles :as components.styles] [status-im.ui.components.styles :as components.styles]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.utils.platform :as platform] [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])) [status-im.utils.money :as money]))
(defn toolbar-view [] (defn toolbar-view []

View File

@ -30,7 +30,7 @@
:<- [:price] :<- [:price]
(fn [[balance price]] (fn [[balance price]]
(if (and 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/eth->usd price)
(money/with-precision 2) (money/with-precision 2)
str) str)
@ -42,7 +42,7 @@
:<- [:balance] :<- [:balance]
(fn [[price last-day balance]] (fn [[price last-day balance]]
(when (and price last-day) (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/percent-change price last-day)
(money/with-precision 2) (money/with-precision 2)
.toNumber) .toNumber)

View File

@ -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))

View File

@ -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}))

View File

@ -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) "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). 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" e.g. ethereum:0x1234@1/transfer?to=0x5678&value=1e18&gas=5000"
(:require [clojure.string :as string] (:require [clojure.string :as string]
[status-im.constants :as constants] [status-im.utils.ethereum.core :as ethereum]
[status-im.utils.money :as money])) [status-im.utils.money :as money]))
(def scheme "ethereum") (def scheme "ethereum")
@ -43,7 +43,7 @@
(when authority-path (when authority-path
(let [[_ address chain-id function-name] (re-find authority-path-pattern 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 (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)))))))) (parse-query query))))))))
@ -59,7 +59,7 @@
(when (and address (not function-name)) ;; Native token support only TODO(jeluard) Add ERC20 support (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 (let [parameters (dissoc (into {} (filter second m)) :chain-id)] ;; filter nil values
(str scheme scheme-separator address (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 ;; Add chain-id if specified and is not main-net
(str chain-id-separator chain-id)) (str chain-id-separator chain-id))
(when-not (empty? parameters) (when-not (empty? parameters)

View File

@ -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))))

View File

@ -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))

View File

@ -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)))

View File

@ -49,18 +49,20 @@
(dependencies/Web3.prototype.toDecimal (normalize s)) (dependencies/Web3.prototype.toDecimal (normalize s))
(catch :default err nil)))) (catch :default err nil))))
(defn from-decimal [n] (str "1" (string/join (repeat n "0"))))
(def eth-units (def eth-units
{:wei (bignumber "1") {:wei (bignumber "1")
:kwei (bignumber "1000") :kwei (bignumber (from-decimal 3))
:mwei (bignumber "1000000") :mwei (bignumber (from-decimal 6))
:gwei (bignumber "1000000000") :gwei (bignumber (from-decimal 9))
:szabo (bignumber "1000000000000") :szabo (bignumber (from-decimal 12))
:finney (bignumber "1000000000000000") :finney (bignumber (from-decimal 15))
:eth (bignumber "1000000000000000000") :eth (bignumber (from-decimal 18))
:keth (bignumber "1000000000000000000000") :keth (bignumber (from-decimal 21))
:meth (bignumber "1000000000000000000000000") :meth (bignumber (from-decimal 24))
:geth (bignumber "1000000000000000000000000000") :geth (bignumber (from-decimal 27))
:teth (bignumber "1000000000000000000000000000000")}) :teth (bignumber (from-decimal 30))})
(defn wei-> [unit n] (defn wei-> [unit n]
(when-let [bn (bignumber n)] (when-let [bn (bignumber n)]
@ -80,11 +82,16 @@
(when bn (when bn
(.times bn (bignumber 1e18)))) (.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] (defn fee-value [gas gas-price]
(.times (bignumber gas) (bignumber gas-price))) (.times (bignumber gas) (bignumber gas-price)))
(defn eth->usd [eth usd-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] (defn percent-change [from to]
(let [bnf (bignumber from) (let [bnf (bignumber from)
@ -99,5 +106,5 @@
(.round bn decimals))) (.round bn decimals)))
(defn sufficient-funds? [amount balance] (defn sufficient-funds? [amount balance]
(when amount (when balance
(.greaterThanOrEqualTo balance amount))) (.greaterThanOrEqualTo balance amount)))

View File

@ -13,8 +13,8 @@
[status-im.test.utils.utils] [status-im.test.utils.utils]
[status-im.test.utils.money] [status-im.test.utils.money]
[status-im.test.utils.clocks] [status-im.test.utils.clocks]
[status-im.test.utils.eip.eip681] [status-im.test.utils.ethereum.eip681]
[status-im.test.utils.erc20] [status-im.test.utils.ethereum.core]
[status-im.test.utils.random] [status-im.test.utils.random]
[status-im.test.utils.gfycat.core] [status-im.test.utils.gfycat.core]
[status-im.test.utils.signing-phrase.core] [status-im.test.utils.signing-phrase.core]
@ -44,8 +44,8 @@
'status-im.test.utils.utils 'status-im.test.utils.utils
'status-im.test.utils.money 'status-im.test.utils.money
'status-im.test.utils.clocks 'status-im.test.utils.clocks
'status-im.test.utils.eip.eip681 'status-im.test.utils.ethereum.eip681
'status-im.test.utils.erc20 'status-im.test.utils.ethereum.core
'status-im.test.utils.random 'status-im.test.utils.random
'status-im.test.utils.gfycat.core 'status-im.test.utils.gfycat.core
'status-im.test.utils.signing-phrase.core 'status-im.test.utils.signing-phrase.core

View File

@ -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]] (: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" (testing "ERC20 balance-of params"
(let [contract "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" (let [contract "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5"
address "0xa7cfd581060ec66414790691681732db249502bd"] address "0xa7cfd581060ec66414790691681732db249502bd"]
(is (= (erc20/balance-of-params contract address) (is (= (core/call-params contract "balanceOf(address)" address)
{:to "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" {:to "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5"
:data "0x70a08231000000000000000000000000a7cfd581060ec66414790691681732db249502bd"}))))) :data "0x70a08231000000000000000000000000a7cfd581060ec66414790691681732db249502bd"})))))

View File

@ -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]] (: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])) [status-im.utils.money :as money]))
(deftest parse-uri (deftest parse-uri

View File

@ -0,0 +1,5 @@
(ns status-im.utils.ethereum.tokens)
(def ethereum {})
(def all {})