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"
(: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)
(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)]
(merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-keyword->chain-id :mainnet))}
arguments))))))))
(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"

View File

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

View File

@ -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)
{::resolve-address {:registry (get ens/ens-registries chain)
:ens-name recipient
:cb #(re-frame/dispatch [:wallet.send/set-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)})
(resolve-ens-addresses data)))

View File

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