Add ENS name resolution to EIP681 support

Signed-off-by: Andrey Shovkoplyas <motor4ik@gmail.com>
This commit is contained in:
acolytec3 2019-12-02 07:12:35 -05:00 committed by Andrey Shovkoplyas
parent 72dfb5189e
commit 2fda266f07
No known key found for this signature in database
GPG Key ID: EAAB7C8622D860A4
4 changed files with 113 additions and 35 deletions

View File

@ -5,7 +5,9 @@
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]
[re-frame.core :as re-frame]
[status-im.ethereum.core :as ethereum] [status-im.ethereum.core :as ethereum]
[status-im.ethereum.ens :as ens]
[status-im.ethereum.tokens :as tokens] [status-im.ethereum.tokens :as tokens]
[status-im.utils.money :as money])) [status-im.utils.money :as money]))
@ -41,21 +43,34 @@
{:function-arguments (apply dissoc m valid-native-arguments)})) {:function-arguments (apply dissoc m valid-native-arguments)}))
arguments))) arguments)))
;; TODO add ENS support
(defn parse-uri (defn parse-uri
"Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address`. "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). 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`." Invalid URI will be parsed as `nil`."
[s] [s]
(when (string? s) (when (string? s)
(let [[_ authority-path query] (re-find uri-pattern s)] (let [[_ authority-path query] (re-find uri-pattern s)]
(when authority-path (when authority-path
(let [[_ address chain-id function-name] (re-find authority-path-pattern authority-path)] (let [[_ raw-address chain-id function-name] (re-find authority-path-pattern authority-path)]
(when (ethereum/address? address) (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)] (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))} (let [contract-address (get-in arguments [:function-arguments :address])]
arguments)))))))) (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] (defn parse-eth-value [s]
"Takes a map as returned by `parse-uri` and returns value as BigNumber" "Takes a map as returned by `parse-uri` and returns value as BigNumber"

View File

@ -1,5 +1,6 @@
(ns status-im.utils.universal-links.core (ns status-im.utils.universal-links.core
(:require [cljs.spec.alpha :as spec] (:require [cljs.spec.alpha :as spec]
[clojure.string :as string]
[goog.string :as gstring] [goog.string :as gstring]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.multiaccounts.model :as multiaccounts.model] [status-im.multiaccounts.model :as multiaccounts.model]
@ -16,7 +17,8 @@
[status-im.utils.config :as config] [status-im.utils.config :as config]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.utils.platform :as platform] [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 ;; TODO(yenda) investigate why `handle-universal-link` event is
;; dispatched 7 times for the same link ;; dispatched 7 times for the same link
@ -43,6 +45,9 @@
(re-matches regex) (re-matches regex)
peek)) peek))
(defn is-request-url? [url]
(string/starts-with? url "ethereum:"))
(defn universal-link? [url] (defn universal-link? [url]
(boolean (boolean
(re-matches constants/regx-universal-link url))) (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)))) (navigation/navigate-to-cofx (assoc-in cofx [:db :contacts/identity] public-key) :profile nil))))
(fx/defn handle-eip681 [cofx url] (fx/defn handle-eip681 [cofx url]
{:dispatch-n [[:navigate-to :wallet] (fx/merge cofx
[:wallet/fill-request-from-url url]]}) (choose-recipient/resolve-ens-addresses url)
(navigation/navigate-to-cofx :wallet nil)))
(defn handle-not-found [full-url] (defn handle-not-found [full-url]
(log/info "universal-links: no handler for " full-url)) (log/info "universal-links: no handler for " full-url))
@ -107,7 +113,7 @@
(match-url url browse-regex) (match-url url browse-regex)
(handle-browse cofx (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) (handle-eip681 cofx url)
:else (handle-not-found url))) :else (handle-not-found url)))

View File

@ -49,11 +49,10 @@
(defn- extract-details (defn- extract-details
"First try to parse as EIP681 URI, if not assume this is an address directly. "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" Returns a map containing at least the `address` and `chain-id` keys"
[s chain-id all-tokens] [m chain-id all-tokens]
(or (let [m (eip681/parse-uri s)] (or (merge m (eip681/extract-request-details m all-tokens))
(merge m (eip681/extract-request-details m all-tokens))) (when (ethereum/address? m)
(when (ethereum/address? s) {:address m :chain-id chain-id})))
{:address s :chain-id chain-id})))
;; NOTE(janherich) - whenever changing assets, we want to clear the previusly set amount/amount-text ;; 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] (defn changed-asset [{:keys [db] :as fx} old-symbol new-symbol]
@ -72,18 +71,33 @@
ethereum/default-transaction-gas)) ethereum/default-transaction-gas))
(re-frame/reg-fx (re-frame/reg-fx
:resolve-address ::resolve-address
(fn [{:keys [registry ens-name cb]}] (fn [{:keys [registry ens-name cb]}]
(ens/get-addr 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 (fx/defn set-recipient
{:events [:wallet.send/set-recipient]} {:events [:wallet.send/set-recipient ::recipient-address-resolved]}
[{:keys [db]} recipient] [{:keys [db]} recipient]
(let [chain (ethereum/chain-keyword db)] (let [chain (ethereum/chain-keyword db)]
(if (ens/is-valid-eth-name? recipient) (if (ens/is-valid-eth-name? recipient)
{:resolve-address {:registry (get ens/ens-registries chain) {::resolve-address {:registry (get ens/ens-registries chain)
:ens-name recipient :ens-name recipient
:cb #(re-frame/dispatch [:wallet.send/set-recipient %])}} :cb #(re-frame/dispatch [::recipient-address-resolved %])}}
(if (ethereum/address? recipient) (if (ethereum/address? recipient)
(let [checksum (eip55/address->checksum recipient)] (let [checksum (eip55/address->checksum recipient)]
(if (eip55/valid-address-checksum? checksum) (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-checksum {:data recipient})}))
{:ui/show-error (i18n/label :t/wallet-invalid-address {:data recipient})})))) {:ui/show-error (i18n/label :t/wallet-invalid-address {:data recipient})}))))
(fx/defn fill-request-from-url (fx/defn request-uri-parsed
{:events [:wallet/fill-request-from-url]} {:events [:wallet/request-uri-parsed]}
[{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} data] [{{: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]) (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) {:keys [address chain-id] :as details} (extract-details data current-chain-id all-tokens)
valid-network? (boolean (= current-chain-id chain-id)) valid-network? (boolean (= current-chain-id chain-id))
@ -116,10 +130,49 @@
(ethereum/get-default-account (get-in db [:multiaccount :accounts]))) (ethereum/get-default-account (get-in db [:multiaccount :accounts])))
(not old-symbol) (not old-symbol)
(update :db assoc-in [:wallet/prepare-transaction :symbol] (or new-symbol :ETH)) (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?)) (and address (not valid-network?))
(assoc :ui/show-error (i18n/label :t/wallet-invalid-chain-id (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 (fx/defn qr-scanner-result
{:events [:wallet.send/qr-scanner-result]} {:events [:wallet.send/qr-scanner-result]}
@ -127,9 +180,4 @@
(fx/merge cofx (fx/merge cofx
{:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)} {:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)}
(navigation/navigate-back) (navigation/navigate-back)
(fill-request-from-url data))) (resolve-ens-addresses data)))
(fx/defn qr-scanner-cancel
{:events [:wallet.send/qr-scanner-cancel]}
[{db :db} _]
{:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)})

View File

@ -11,6 +11,13 @@
(is (= nil (eip681/parse-uri "ethereum:?value=1"))) (is (= nil (eip681/parse-uri "ethereum:?value=1")))
(is (= nil (eip681/parse-uri "bitcoin:0x1234"))) (is (= nil (eip681/parse-uri "bitcoin:0x1234")))
(is (= nil (eip681/parse-uri "ethereum: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" :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" :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"))) (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"}} (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"))) (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"}} (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 (def all-tokens
{:mainnet {"0x744d70fdbe2ba4cf95131626614a1763df805b9e" {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e" {:mainnet {"0x744d70fdbe2ba4cf95131626614a1763df805b9e" {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e"