From 2fda266f07845cbc1ef532a9574acec54e9119f3 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 2 Dec 2019 07:12:35 -0500 Subject: [PATCH] Add ENS name resolution to EIP681 support Signed-off-by: Andrey Shovkoplyas --- src/status_im/ethereum/eip681.cljs | 33 +++++-- src/status_im/utils/universal_links/core.cljs | 14 ++- .../wallet/choose_recipient/core.cljs | 90 ++++++++++++++----- test/cljs/status_im/test/ethereum/eip681.cljs | 11 ++- 4 files changed, 113 insertions(+), 35 deletions(-) diff --git a/src/status_im/ethereum/eip681.cljs b/src/status_im/ethereum/eip681.cljs index 2f6cf95b10..24f3fc81da 100644 --- a/src/status_im/ethereum/eip681.cljs +++ b/src/status_im/ethereum/eip681.cljs @@ -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])) @@ -41,21 +43,34 @@ {: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-eth-value [s] "Takes a map as returned by `parse-uri` and returns value as BigNumber" diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs index 336c8b1dd5..f9056ad2ca 100644 --- a/src/status_im/utils/universal_links/core.cljs +++ b/src/status_im/utils/universal_links/core.cljs @@ -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] @@ -16,7 +17,8 @@ [status-im.utils.config :as config] [status-im.utils.fx :as fx] [status-im.utils.platform :as platform] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [status-im.wallet.choose-recipient.core :as choose-recipient])) ;; TODO(yenda) investigate why `handle-universal-link` event is ;; dispatched 7 times for the same link @@ -43,6 +45,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))) @@ -81,8 +86,9 @@ (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] - [:wallet/fill-request-from-url url]]}) + (fx/merge cofx + (choose-recipient/resolve-ens-addresses url) + (navigation/navigate-to-cofx :wallet nil))) (defn handle-not-found [full-url] (log/info "universal-links: no handler for " full-url)) @@ -107,7 +113,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))) diff --git a/src/status_im/wallet/choose_recipient/core.cljs b/src/status_im/wallet/choose_recipient/core.cljs index fb9f902220..4bda3a9454 100644 --- a/src/status_im/wallet/choose_recipient/core.cljs +++ b/src/status_im/wallet/choose_recipient/core.cljs @@ -49,11 +49,10 @@ (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] @@ -72,18 +71,33 @@ 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 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) @@ -94,9 +108,9 @@ {: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]} - [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} data] +(fx/defn request-uri-parsed + {:events [:wallet/request-uri-parsed]} + [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} data uri] (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) valid-network? (boolean (= current-chain-id chain-id)) @@ -116,10 +130,49 @@ (ethereum/get-default-account (get-in db [:multiaccount :accounts]))) (not old-symbol) (update :db assoc-in [:wallet/prepare-transaction :symbol] (or new-symbol :ETH)) - (not address) (assoc :ui/show-error (i18n/label :t/wallet-invalid-address {:data data})) + (not address) (assoc :ui/show-error (i18n/label :t/wallet-invalid-address {:data uri})) (and address (not valid-network?)) (assoc :ui/show-error (i18n/label :t/wallet-invalid-chain-id - {:data data :chain current-chain-id}))))) + {:data uri :chain current-chain-id}))))) + +(fx/defn qr-scanner-cancel + {:events [:wallet.send/qr-scanner-cancel]} + [{db :db} _] + {:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)}) + +(fx/defn resolve-ens-addresses + {:events [:wallet.send/resolve-ens-addresses :wallet.send/qr-code-request-scanned]} + [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db :as cofx} uri] + (if-let [message (eip681/parse-uri uri)] + ;; 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 + (request-uri-parsed cofx message uri) + {::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)) uri]))}})) + {:ui/show-error (i18n/label :t/wallet-invalid-address {:data uri})})) (fx/defn qr-scanner-result {:events [:wallet.send/qr-scanner-result]} @@ -127,9 +180,4 @@ (fx/merge cofx {:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)} (navigation/navigate-back) - (fill-request-from-url data))) - -(fx/defn qr-scanner-cancel - {:events [:wallet.send/qr-scanner-cancel]} - [{db :db} _] - {:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)}) \ No newline at end of file + (resolve-ens-addresses data))) diff --git a/test/cljs/status_im/test/ethereum/eip681.cljs b/test/cljs/status_im/test/ethereum/eip681.cljs index 2103251237..74a8a571ee 100644 --- a/test/cljs/status_im/test/ethereum/eip681.cljs +++ b/test/cljs/status_im/test/ethereum/eip681.cljs @@ -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"))) @@ -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"