[BUG #2029] Properly handle gas details from payment requests

This commit is contained in:
Julien Eluard 2018-01-05 08:12:56 +01:00 committed by Oskar Thoren
parent 0a1c3bcd88
commit f90e1a4ed3
No known key found for this signature in database
GPG Key ID: 5128AB0637CD85AF
12 changed files with 134 additions and 66 deletions

View File

@ -3,7 +3,8 @@
[status-im.i18n :as i18n]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.eip681 :as eip681]
[status-im.utils.handlers :as handlers]))
[status-im.utils.handlers :as handlers]
[status-im.utils.money :as money]))
(handlers/register-handler-db
:wallet/toggle-flashlight
@ -12,37 +13,44 @@
toggled-state (if (= :on flashlight-state) :off :on)]
(assoc-in db [:wallet :send-transaction :camera-flashlight] toggled-state))))
(defn- fill-request-details [db address name amount]
(defn- fill-request-details [db {:keys [address name value symbol gas gasPrice] :as m}]
{:pre [(not (nil? address))]}
(update-in
db [:wallet :send-transaction]
#(cond-> (assoc % :to address :to-name name)
amount (assoc :amount amount))))
#(cond-> (assoc % :to address)
value (assoc :amount value)
name (assoc :to-name name)
symbol (assoc :symbol symbol)
gas (assoc :gas (money/bignumber gas))
gasPrice (assoc :gas-price (money/bignumber gasPrice))
(and symbol (not gasPrice))
(assoc :gas-price (ethereum/estimate-gas symbol)))))
(defn- extract-details
"First try to parse as EIP681 URI, if not assume this is an address directly.
Returns a map containing at least the `address`, `symbol` and `chain-id` keys"
Returns a map containing at least the `address` and `chain-id` keys"
[s chain-id]
(or (let [m (eip681/parse-uri s)]
(merge m (eip681/extract-request-details m)))
(when (ethereum/address? s)
{:address s :chain-id chain-id :symbol :ETH})))
{:address s :chain-id chain-id})))
(handlers/register-handler-fx
:wallet/fill-request-from-url
(fn [{{:keys [web3 network] :as db} :db} [_ data name]]
(let [{:keys [view-id]} db
current-chain-id (get-in constants/default-networks [network :raw-config :NetworkId])
{:keys [address chain-id value]} (extract-details data current-chain-id)
{:keys [address chain-id] :as details} (extract-details data current-chain-id)
valid-network? (boolean (= current-chain-id chain-id))]
(cond-> {:db db}
(and address (= :choose-recipient view-id)) (assoc :dispatch [:navigate-back])
(and address valid-network?) (update :db #(fill-request-details % address name value))
(and address valid-network?) (update :db #(fill-request-details % details))
(not address) (assoc :show-error (i18n/label :t/wallet-invalid-address {:data data}))
(and address (not valid-network?)) (assoc :show-error (i18n/label :t/wallet-invalid-chain-id {:data data :chain current-chain-id}))))))
(handlers/register-handler-fx
:wallet/fill-request-from-contact
(fn [{db :db} [_ {:keys [address name]}]]
{:db (fill-request-details db address name nil)
{:db (fill-request-details db {:address address :name name})
:dispatch-n [[:navigate-back]
[:navigate-back]]}))

View File

@ -62,17 +62,16 @@
[react/text
(name symbol)]]]))
(views/defview choose-currency [style]
(views/letsubs [visible-tokens [:wallet.settings/visible-tokens]
symbol [:wallet.send/symbol]]
(views/defview choose-currency [{:keys [style on-change value]}]
(views/letsubs [visible-tokens [:wallet.settings/visible-tokens]]
[react/view
[react/text {:style styles/label} (i18n/label :t/currency)]
[react/view (merge styles/currency-container
style)
[react/picker {:selected (name symbol)
[react/picker {:selected value
:style {:color "white"}
:item-style styles/wallet-name
:on-change #(re-frame/dispatch [:wallet.send/set-symbol (keyword %)])}
:on-change on-change}
(map (fn [s] {:value (name s) :color "white"}) (conj visible-tokens (:symbol tokens/ethereum)))]]]))
(defn choose-recipient-content [{:keys [address name on-press style]}]

View File

@ -5,6 +5,7 @@
(spec/def ::amount (spec/nilable money/valid?))
(spec/def ::amount-error (spec/nilable string?))
(spec/def ::symbol (spec/nilable keyword?))
(spec/def :wallet/request-transaction (allowed-keys
:opt-un [::amount ::amount-error]))
:opt-un [::amount ::amount-error ::symbol]))

View File

@ -32,3 +32,8 @@
{:db (-> db
(assoc-in [:wallet/request-transaction :amount] (money/ether->wei value))
(assoc-in [:wallet/request-transaction :amount-error] error))})))
(handlers/register-handler-fx
:wallet.request/set-symbol
(fn [{:keys [db]} [_ s]]
{:db (assoc-in db [:wallet/request-transaction :symbol] s)}))

View File

@ -1,11 +1,14 @@
(ns status-im.ui.screens.wallet.request.subs
(:require [re-frame.core :as re-frame]))
(:require [re-frame.core :as re-frame]
[status-im.utils.ethereum.tokens :as tokens]))
(re-frame/reg-sub
:wallet.request/request-enabled?
:<- [:get-in [:wallet/request-transaction :amount]]
:<- [:get-in [:wallet/request-transaction :amount-error]]
(fn [[amount amount-error]]
:<- [:get-in [:wallet/request-transaction :symbol]]
(fn [[amount amount-error symbol]]
(and
(or (nil? symbol) (tokens/ethereum? symbol))
(nil? amount-error)
(not (nil? amount)))))

View File

@ -1,7 +1,6 @@
(ns status-im.ui.screens.wallet.request.views
(:require-macros [status-im.utils.views :as views])
(:require
[re-frame.core :as re-frame]
(:require [re-frame.core :as re-frame]
[status-im.ui.components.react :as react]
[status-im.ui.components.qr-code :as components.qr-code]
[status-im.ui.components.toolbar.actions :as actions]
@ -17,6 +16,7 @@
[status-im.utils.platform :as platform]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.eip681 :as eip681]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.money :as money]))
(defn toolbar-view []
@ -31,17 +31,25 @@
:action :request
:params {:hide-actions? true}}]))
(views/defview qr-code [amount]
(defn- generate-value [address {:keys [symbol chain-id] :as m}]
(if (tokens/ethereum? symbol)
(eip681/generate-uri address (dissoc m :symbol))
(eip681/generate-erc20-uri address m)))
(views/defview qr-code [amount symbol]
(views/letsubs [account [:get-current-account]
chain-id [:get-network-id]]
[components.qr-code/qr-code
{:value (eip681/generate-uri (ethereum/normalized-address (:address account)) (merge {:chain-id chain-id} (when amount {:value amount})))
:size 256}]))
(let [address (ethereum/normalized-address (:address account))
params {:chain-id chain-id :value amount :symbol (or symbol :ETH)}]
{:value (generate-value address params)
:size 256})]))
(views/defview request-transaction []
;;Because input field is in the end of view we will scroll to the end on input focus event
(views/letsubs [amount [:get-in [:wallet/request-transaction :amount]]
amount-error [:get-in [:wallet/request-transaction :amount-error]]
symbol [:get-in [:wallet/request-transaction :symbol]]
request-enabled? [:wallet.request/request-enabled?]
scroll (atom nil)]
[react/keyboard-avoiding-view wallet.styles/wallet-modal-container
@ -52,7 +60,7 @@
[react/view components.styles/flex
[react/view styles/network-container
[react/view styles/qr-container
[qr-code amount]]]
[qr-code amount symbol]]]
[react/view wallet.styles/choose-wallet-container
[components/choose-wallet]]
[react/view wallet.styles/amount-container
@ -61,7 +69,9 @@
:input-options {:on-focus (fn [] (when @scroll (js/setTimeout #(.scrollToEnd @scroll) 100)))
:on-change-text #(re-frame/dispatch [:wallet.request/set-and-validate-amount %])}}]
[react/view wallet.styles/choose-currency-container
[components/choose-currency wallet.styles/choose-currency]]]]]
[components/choose-currency {:style wallet.styles/choose-currency
:on-change #(re-frame/dispatch [:wallet.request/set-symbol (keyword %)])
:value (name (or symbol :ETH))}]]]]]
[components/separator]
[react/view wallet.styles/buttons-container
[react/touchable-highlight {:style wallet.styles/button :disabled true}

View File

@ -70,17 +70,11 @@
(assoc-in [:wallet :send-transaction :amount] (money/ether->wei value))
(assoc-in [:wallet :send-transaction :amount-error] error))})))
(defn- estimated-gas [symbol]
(if (tokens/ethereum? symbol)
ethereum/default-transaction-gas
;; TODO(jeluard) Rely on estimateGas call
(.times ethereum/default-transaction-gas 5)))
(handlers/register-handler-fx
:wallet.send/set-symbol
(fn [{:keys [db]} [_ symbol]]
{:db (-> (assoc-in db [:wallet :send-transaction :symbol] symbol)
(assoc-in [:wallet :send-transaction :gas] (estimated-gas symbol)))}))
(assoc-in [:wallet :send-transaction :gas] (ethereum/estimate-gas symbol)))}))
(handlers/register-handler-fx
:wallet.send/toggle-advanced

View File

@ -189,7 +189,7 @@
(when on-press
[vector-icons/icon :icons/forward {:color :white}])]]]])
(defn- send-transaction-panel [{:keys [modal? transaction scroll advanced?] :as transaction}]
(defn- send-transaction-panel [{:keys [modal? transaction scroll advanced? symbol]}]
(let [{:keys [amount amount-error signing? to to-name sufficient-funds? in-progress? from-chat?]} transaction]
[react/keyboard-avoiding-view wallet.styles/wallet-modal-container
[react/view components.styles/flex
@ -219,7 +219,9 @@
[react/view wallet.styles/choose-currency-container
[components/view-currency wallet.styles/choose-currency]]
[react/view wallet.styles/choose-currency-container
[components/choose-currency wallet.styles/choose-currency]])]
[components/choose-currency {:style wallet.styles/choose-currency
:on-change #(re-frame/dispatch [:wallet.send/set-symbol (keyword %)])
:value (name symbol)}]])]
[react/view {:style send.styles/advanced-wrapper}
[react/touchable-highlight {:on-press #(re-frame/dispatch [:wallet.send/toggle-advanced (not advanced?)])}
[react/view {:style send.styles/advanced-button-wrapper}
@ -245,15 +247,17 @@
(defview send-transaction []
(letsubs [transaction [:wallet.send/transaction]
symbol [:wallet.send/symbol]
advanced? [:wallet.send/advanced?]
scroll (atom nil)]
[send-transaction-panel {:modal? false :transaction transaction :scroll scroll :advanced? advanced?}]))
[send-transaction-panel {:modal? false :transaction transaction :scroll scroll :advanced? advanced? :symbol symbol}]))
(defview send-transaction-modal []
(letsubs [transaction [:wallet.send/unsigned-transaction]
symbol [:wallet.send/symbol]
advanced? [:wallet.send/advanced?]]
(if transaction
[send-transaction-panel {:modal? true :transaction transaction :advanced? advanced?}]
[send-transaction-panel {:modal? true :transaction transaction :advanced? advanced? :symbol symbol}]
[react/view wallet.styles/wallet-modal-container
[react/view components.styles/flex
[status-bar/status-bar {:type :modal-wallet}]

View File

@ -1,6 +1,7 @@
(ns status-im.utils.ethereum.core
(:require [clojure.string :as string]
[status-im.js-dependencies :as dependencies]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.money :as money]))
;; IDs standardized in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids
@ -83,3 +84,9 @@
(def default-transaction-gas (money/bignumber 21000))
(def default-gas-price (money/->wei :gwei 21))
(defn estimate-gas [symbol]
(if (tokens/ethereum? symbol)
default-transaction-gas
;; TODO(jeluard) Rely on estimateGas call
(.times default-transaction-gas 5)))

View File

@ -23,21 +23,23 @@
(def key-value-format (str "([^" parameter-separator key-value-separator "]+)"))
(def query-pattern (re-pattern (str key-value-format key-value-separator key-value-format)))
(def valid-native-arguments #{:value :gas})
(def valid-native-arguments #{:value :gas :gasPrice})
(defn- parse-query [s]
(into {} (for [[_ k v] (re-seq query-pattern (or s ""))]
[(keyword k) v])))
(defn- parse-native-arguments [m]
(when (set/superset? valid-native-arguments (set (keys m)))
m))
(select-keys m valid-native-arguments))
(defn- parse-arguments [function-name s]
(let [m (parse-query s)]
(let [m (parse-query s)
arguments (parse-native-arguments m)]
(if function-name
(merge {:function-name function-name} (when-not (empty? m) {:function-arguments m}))
(parse-native-arguments m))))
(merge arguments {:function-name function-name}
(when (seq m)
{:function-arguments (apply dissoc m valid-native-arguments)}))
arguments)))
;; TODO add ENS support
@ -94,5 +96,16 @@
(str chain-id-separator chain-id))
(when-not (empty? parameters)
(if function-name
(str function-name-separator function-name query-separator (generate-query-string function-arguments))
(str function-name-separator function-name query-separator
(let [native-parameters (dissoc parameters :function-name :function-arguments)]
(generate-query-string (merge function-arguments native-parameters))))
(str query-separator (generate-query-string parameters))))))))
(defn generate-erc20-uri
"Generate a EIP 681 URI encapsulating ERC20 token transfer"
[address {:keys [symbol value chain-id] :as m}]
(when-let [token (tokens/symbol->token (or (ethereum/chain-id->chain-keyword chain-id) :mainnet) symbol)]
(generate-uri (:address token)
(merge (dissoc m :value :symbol)
{:function-name "transfer"
:function-arguments {:uint256 value :address address}}))))

View File

@ -1,6 +1,5 @@
(ns status-im.utils.ethereum.tokens
(:require [status-im.ui.components.styles :as styles]
[status-im.utils.ethereum.core :as ethereum])
(:require [status-im.ui.components.styles :as styles])
(:require-macros [status-im.utils.ethereum.macros :refer [resolve-icons]]))
(defn- asset-border [color]

View File

@ -13,8 +13,8 @@
(is (= nil(eip681/parse-uri "ethereum:0x1234")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1")))
(is (= nil (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?unknown=1")))
(is (= nil (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7", :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?unknown=1")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7", :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "2.014e18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=2.014e18")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "-1e18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=-1e18")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "+1E18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=+1E18")))
@ -24,7 +24,21 @@
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@1?value=1e18&gas=5000")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 3} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1e18&gas=5000")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer"} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1"))))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}}
(eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :gas "100" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}}
(eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100"))))
(deftest generate-erc20-uri
(is (= nil (eip681/generate-erc20-uri nil nil)))
(is (= "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"
(eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :SNT :value 5})))
(is (= "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7&gas=10000&gasPrice=10000"
(eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :SNT :value 5 :gas 10000 :gasPrice 10000})))
(is (= "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"
(eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :SNT :chain-id 1 :value 5})))
(is (= "ethereum:0xc55cf4b03948d7ebc8b9e8bad92643703811d162@3/transfer?uint256=5&address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"
(eip681/generate-erc20-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:symbol :STT :chain-id 3 :value 5}))))
(deftest generate-uri
(is (= nil (eip681/generate-uri nil nil)))
@ -35,7 +49,18 @@
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1000000000000000000" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1e18)})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1&gas=100" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1&gas=100" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 3})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 1}}))))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100"
(eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"
{:gas (money/bignumber 100) :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 1}}))))
(deftest round-trip
(let [uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1&gas=100"
{:keys [address] :as params} (eip681/parse-uri uri)]
(is (= uri (eip681/generate-uri address (dissoc params :address)))))
(let [uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3/transfer?uint256=5&address=0xc55cf4b03948d7ebc8b9e8bad92643703811d162"
{:keys [address] :as params} (eip681/parse-uri uri)]
(is (= uri (eip681/generate-uri address (dissoc params :address))))))
(deftest parse-eth-value
(is (= nil (eip681/parse-eth-value nil)))