Skip to content

Commit

Permalink
Add ENS name resolution to EIP681 support
Browse files Browse the repository at this point in the history
  • Loading branch information
acolytec3 committed Dec 4, 2019
1 parent 6db7495 commit a853229
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 34 deletions.
33 changes: 24 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,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"
Expand Down
14 changes: 10 additions & 4 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 All @@ -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
Expand All @@ -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)))
Expand Down Expand Up @@ -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))
Expand All @@ -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)))
Expand Down
88 changes: 68 additions & 20 deletions src/status_im/wallet/choose_recipient/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)
Expand All @@ -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))
Expand All @@ -119,17 +133,51 @@
(not address) (assoc :ui/show-error (i18n/label :t/wallet-invalid-address {:data data}))
(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]}
[{db :db :as cofx} data opts]
(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)))
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 a853229

Please sign in to comment.