Add ENS name resolution to EIP681 support
Signed-off-by: Andrey Shovkoplyas <motor4ik@gmail.com>
This commit is contained in:
parent
72dfb5189e
commit
2fda266f07
|
@ -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)
|
||||||
(when-let [arguments (parse-arguments function-name query)]
|
(if (string/starts-with? raw-address "pay-")
|
||||||
(merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-keyword->chain-id :mainnet))}
|
(let [pay-address (string/replace-first raw-address "pay-" "")]
|
||||||
arguments))))))))
|
(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]
|
(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"
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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)})
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue