Skip to content

Commit

Permalink
Add ENS validation check to EIP681 parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
acolytec3 committed Nov 21, 2019
1 parent 5ecc759 commit 920a855
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 36 deletions.
39 changes: 30 additions & 9 deletions src/status_im/ethereum/eip681.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
e.g. ethereum:0x1234@1/transfer?to=0x5678&value=1e18&gas=5000"
(:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.ens :as ens]
[status-im.ethereum.tokens :as tokens]
[status-im.utils.money :as money]))

Expand Down Expand Up @@ -41,21 +43,40 @@
{:function-arguments (apply dissoc m valid-native-arguments)}))
arguments)))

;; TODO add ENS support

(defn parse-uri
"Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address`.
Note that values are not decoded and you might need to rely on specific methods for some fields (parse-value, parse-number).
"Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address`
which will be either a valid ENS or Ethereum address.
Note that values are not decoded and you might need to rely on specific methods for some fields
(parse-value, parse-number).
Invalid URI will be parsed as `nil`."
[s]
(when (string? s)
(let [[_ authority-path query] (re-find uri-pattern s)]
(when authority-path
(let [[_ address chain-id function-name] (re-find authority-path-pattern authority-path)]
(when (ethereum/address? address)
(when-let [arguments (parse-arguments function-name query)]
(merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-keyword->chain-id :mainnet))}
arguments))))))))
(let [[_ raw-address chain-id function-name] (re-find authority-path-pattern authority-path)]
(when (or (ethereum/address? raw-address)
(if (string/starts-with? raw-address "pay-")
(let [pay-address (string/replace-first raw-address "pay-" "")]
(or (ens/is-valid-eth-name? pay-address)
(ethereum/address? pay-address)))))
(let [address (if (string/starts-with? raw-address "pay-")
(string/replace-first raw-address "pay-" "")
raw-address)]
(when-let [arguments (parse-arguments function-name query)]
(let [contract-address (get-in arguments [:function-arguments :address])]
(if-not (or (not contract-address) (or (ens/is-valid-eth-name? contract-address) (ethereum/address? contract-address)))
nil
(merge {:address address
:chain-id (if chain-id
(js/parseInt chain-id)
(ethereum/chain-keyword->chain-id :mainnet))}
arguments)))))))))))

(defn parse-uri-handler
[s origin]
(when-let [parsed-message (parse-uri s)]
(re-frame/dispatch [:wallet.send/resolve-ens-addresses parsed-message origin nil])
nil))

(defn parse-eth-value [s]
"Takes a map as returned by `parse-uri` and returns value as BigNumber"
Expand Down
5 changes: 3 additions & 2 deletions src/status_im/ui/screens/wallet/choose_recipient/views.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
[status-im.ui.components.toolbar.actions :as actions]
[status-im.ui.components.toolbar.view :as topbar]
[status-im.ui.screens.wallet.choose-recipient.styles :as styles]
[status-im.utils.platform :as platform]))
[status-im.utils.platform :as platform]
[status-im.ethereum.eip681 :as eip681]))

(defn- topbar [camera-flashlight]
[topbar/toolbar
Expand Down Expand Up @@ -61,7 +62,7 @@
;:torchMode (camera/set-torch camera-flashlight)
:onBarCodeRead #(when-not @read-once?
(reset! read-once? true)
(re-frame/dispatch [:wallet/fill-request-from-url (camera/get-qr-code-data %) :qr]))}]]
(eip681/parse-uri-handler (camera/get-qr-code-data %) :qr))}]]
[viewfinder dimensions (size dimensions)]]
[toolbar/toolbar
{:center {:type :secondary
Expand Down
11 changes: 7 additions & 4 deletions src/status_im/ui/screens/wallet/components/views.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
[status-im.ui.screens.wallet.choose-recipient.views
:as
choose-recipient]
[status-im.ethereum.ens :as ens]
[status-im.ui.screens.wallet.components.styles :as styles]
[status-im.wallet.utils :as wallet.utils]
[status-im.utils.core :as utils.core]
Expand Down Expand Up @@ -162,10 +163,12 @@
(defn- recipient-address [address modal?]
[react/text {:style (merge styles/recipient-address (when-not address styles/recipient-no-address))
:accessibility-label :recipient-address-text}
(or (eip55/address->checksum (ethereum/normalized-address address))
(if modal?
(i18n/label :t/new-contract)
(i18n/label :t/specify-recipient)))])
(if (ens/is-valid-eth-name? address)
address
(or (eip55/address->checksum (ethereum/normalized-address address))
(if modal?
(i18n/label :t/new-contract)
(i18n/label :t/specify-recipient))))])

(views/defview recipient-contact [address name request?]
(views/letsubs [contact [:contacts/contact-by-address address]]
Expand Down
10 changes: 7 additions & 3 deletions src/status_im/utils/universal_links/core.cljs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns status-im.utils.universal-links.core
(:require [cljs.spec.alpha :as spec]
[clojure.string :as string]
[goog.string :as gstring]
[re-frame.core :as re-frame]
[status-im.multiaccounts.model :as multiaccounts.model]
Expand Down Expand Up @@ -43,6 +44,9 @@
(re-matches regex)
peek))

(defn is-request-url? [url]
(string/starts-with? url "ethereum:"))

(defn universal-link? [url]
(boolean
(re-matches constants/regx-universal-link url)))
Expand Down Expand Up @@ -81,8 +85,8 @@
(navigation/navigate-to-cofx (assoc-in cofx [:db :contacts/identity] public-key) :profile nil))))

(fx/defn handle-eip681 [cofx url]
{:dispatch-n [[:navigate-to :wallet-send-transaction]
[:wallet/fill-request-from-url url :deep-link]]})
(eip681/parse-uri-handler url :deep-link)
{:dispatch [:navigate-to :wallet-send-transaction]})

(defn handle-not-found [full-url]
(log/info "universal-links: no handler for " full-url))
Expand All @@ -107,7 +111,7 @@
(match-url url browse-regex)
(handle-browse cofx (match-url url browse-regex))

(some? (eip681/parse-uri url))
(is-request-url? url)
(handle-eip681 cofx url)

:else (handle-not-found url)))
Expand Down
80 changes: 63 additions & 17 deletions src/status_im/wallet/choose_recipient/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
[status-im.ethereum.ens :as ens]
[status-im.i18n :as i18n]
[status-im.utils.money :as money]
[status-im.utils.fx :as fx]))
[status-im.utils.fx :as fx]
[clojure.string :as string]))

(fx/defn toggle-flashlight
{:events [:wallet/toggle-flashlight]}
Expand Down Expand Up @@ -42,18 +43,14 @@
(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` and `chain-id` keys"
[s chain-id all-tokens]
(or (let [m (eip681/parse-uri s)]
(merge m (eip681/extract-request-details m all-tokens)))
(when (ethereum/address? s)
{:address s :chain-id chain-id})))
[m chain-id all-tokens]
(or (merge m (eip681/extract-request-details m all-tokens))
(when (ethereum/address? m)
{:address m :chain-id chain-id})))

;; NOTE(janherich) - whenever changing assets, we want to clear the previusly set amount/amount-text
(defn changed-asset [{:keys [db] :as fx} old-symbol new-symbol]
(-> fx
(merge {:wallet/update-gas-price
{:success-event :wallet/update-gas-price-success
:edit? false}})
(assoc-in [:db :wallet :send-transaction :amount] nil)
(assoc-in [:db :wallet :send-transaction :amount-text] nil)
(assoc-in [:db :wallet :send-transaction :asset-error]
Expand All @@ -68,18 +65,67 @@
ethereum/default-transaction-gas))

(re-frame/reg-fx
:resolve-address
::resolve-address
(fn [{:keys [registry ens-name cb]}]
(ens/get-addr registry ens-name cb)))

(re-frame/reg-fx
::resolve-addresses
(fn [{:keys [registry ens-names callback]}]
;; resolve all addresses then call the callback function with the array of
;;addresses as parameter
(-> (js/Promise.all
(clj->js (mapv (fn [ens-name]
(js/Promise.
(fn [resolve reject]
(ens/get-addr registry ens-name resolve))))
ens-names)))
(.then callback)
(.catch (fn [error]
(js/console.log error))))))

(fx/defn resolve-ens-addresses
{:events [:wallet.send/resolve-ens-addresses]}
[{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db}
message origin address]
;; first we get a vector of ens-names to resolve and a vector of paths of
;; these names
(let [{:keys [paths ens-names]}
(reduce (fn [acc path]
(let [address (get-in message path)]
(if (ens/is-valid-eth-name? address)
(-> acc
(update :paths conj path)
(update :ens-names conj address))
acc)))
{:paths [] :ens-names []}
[[:address] [:function-arguments :address]])]
(if (empty? ens-names)
;; if there is no ens-names, we dispatch request-uri-parsed immediately
(re-frame/dispatch [:wallet/request-uri-parsed message origin])
{::resolve-addresses
{:registry (get ens/ens-registries (ethereum/chain-keyword db))
:ens-names ens-names
:callback
(fn [addresses]
(re-frame/dispatch
[:wallet/request-uri-parsed
;; we replace the ens-names at their path in the message by their
;; actual address
(reduce (fn [message [path address]]
(assoc-in message path address))
message
(map vector paths addresses))
origin]))}})))

(fx/defn set-recipient
{:events [:wallet.send/set-recipient]}
{:events [:wallet.send/set-recipient ::recipient-address-resolved]}
[{:keys [db]} recipient]
(let [chain (ethereum/chain-keyword db)]
(if (ens/is-valid-eth-name? recipient)
{:resolve-address {:registry (get ens/ens-registries chain)
:ens-name recipient
:cb #(re-frame/dispatch [:wallet.send/set-recipient %])}}
{::resolve-address {:registry (get ens/ens-registries chain)
:ens-name recipient
:cb #(re-frame/dispatch [::recipient-address-resolved %])}}
(if (ethereum/address? recipient)
(let [checksum (eip55/address->checksum recipient)]
(if (eip55/valid-address-checksum? checksum)
Expand All @@ -90,8 +136,8 @@
{:ui/show-error (i18n/label :t/wallet-invalid-address-checksum {:data recipient})}))
{:ui/show-error (i18n/label :t/wallet-invalid-address {:data recipient})}))))

(fx/defn fill-request-from-url
{:events [:wallet/fill-request-from-url]}
(fx/defn request-uri-parsed
{:events [:wallet/request-uri-parsed]}
[{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} data origin]
(let [current-chain-id (get-in constants/default-networks [current-network :config :NetworkId])
{:keys [address chain-id] :as details} (extract-details data current-chain-id all-tokens)
Expand All @@ -105,8 +151,8 @@
symbol-changed? (and old-symbol new-symbol (not= old-symbol new-symbol))]
(cond-> {:db db}
(not= :deep-link origin) (assoc :dispatch [:navigate-back]) ;; Only navigate-back when called from within wallet
(and address valid-network?) (update :db #(fill-request-details % details false))
symbol-changed? (changed-asset old-symbol new-symbol)
(and address valid-network?) (update :db #(fill-request-details % details false))
(and old-amount new-amount (not= old-amount new-amount)) (changed-amount-warning old-amount new-amount)
;; NOTE(goranjovic) - the next line is there is because QR code scanning switches the amount to ETH
;; automatically, so we need to update the gas limit accordingly. The check for origin screen is there
Expand Down
11 changes: 10 additions & 1 deletion test/cljs/status_im/test/ethereum/eip681.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
(is (= nil (eip681/parse-uri "ethereum:?value=1")))
(is (= nil (eip681/parse-uri "bitcoin:0x1234")))
(is (= nil (eip681/parse-uri "ethereum:0x1234")))
(is (= nil (eip681/parse-uri "ethereum:gimme.eth?value=1e18")))
(is (= nil (eip681/parse-uri "ethereum:gimme.ether?value=1e18")))
(is (= nil (eip681/parse-uri "ethereum:pay-gimme.ether?value=1e18")))
(is (= nil (eip681/parse-uri "ethereum:pay-snt.thetoken.ether/transfer?address=gimme.eth&uint256=1&gas=100")))
(is (= nil (eip681/parse-uri "ethereum:pay-snt.thetoken.eth/transfer?address=gimme.ether&uint256=1&gas=100")))
(is (= {:address "gimme.eth" :value "1e18" :chain-id 1} (eip681/parse-uri "ethereum:pay-gimme.eth?value=1e18")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :chain-id 1} (eip681/parse-uri "ethereum:pay-0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1e18")))
(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 (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7", :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?unknown=1")))
Expand All @@ -27,7 +34,9 @@
(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"))))
(eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100")))
(is (= {:address "snt.thetoken.eth" :chain-id 1 :function-name "transfer" :gas "100" :function-arguments {:address "gimme.eth" :uint256 "1"}}
(eip681/parse-uri "ethereum:pay-snt.thetoken.eth/transfer?address=gimme.eth&uint256=1&gas=100"))))

(def all-tokens
{:mainnet {"0x744d70fdbe2ba4cf95131626614a1763df805b9e" {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e"
Expand Down

0 comments on commit 920a855

Please sign in to comment.