diff --git a/deps.edn b/deps.edn index bcb6213491..e43ff6cd50 100644 --- a/deps.edn +++ b/deps.edn @@ -11,7 +11,8 @@ com.taoensso/timbre {:mvn/version "4.10.0"} hickory {:mvn/version "0.7.1"} com.cognitect/transit-cljs {:mvn/version "0.8.248"} - status-im/pluto {:mvn/version "iteration-2-SNAPSHOT"}} + status-im/pluto {:mvn/version "iteration-2-SNAPSHOT"} + mvxcvi/alphabase {:mvn/version "1.0.0"}} :aliases {:dev {:extra-deps diff --git a/project.clj b/project.clj index fedfcdc3c5..832ca3058e 100644 --- a/project.clj +++ b/project.clj @@ -11,7 +11,8 @@ [com.taoensso/timbre "4.10.0"] [hickory "0.7.1"] [com.cognitect/transit-cljs "0.8.248"] - [status-im/pluto "iteration-2-SNAPSHOT"]] + [status-im/pluto "iteration-2-SNAPSHOT"] + [mvxcvi/alphabase "1.0.0"]] :plugins [[lein-cljsbuild "1.1.7"] [lein-re-frisk "0.5.8"] [lein-cljfmt "0.5.7"] diff --git a/src/status_im/models/browser.cljs b/src/status_im/models/browser.cljs index b1cccb69ba..78b99829e6 100644 --- a/src/status_im/models/browser.cljs +++ b/src/status_im/models/browser.cljs @@ -5,7 +5,13 @@ [status-im.data-store.dapp-permissions :as dapp-permissions] [status-im.i18n :as i18n] [status-im.ui.screens.browser.default-dapps :as default-dapps] - [status-im.utils.http :as http])) + [status-im.utils.http :as http] + [clojure.string :as string] + [status-im.utils.ethereum.resolver :as resolver] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.ens :as ens] + [status-im.utils.multihash :as multihash] + [status-im.utils.handlers-macro :as handlers-macro])) (defn get-current-url [{:keys [history history-index]}] (when (and history-index history) @@ -24,13 +30,13 @@ (assoc browser :dapp? true :name (:name dapp)) (assoc browser :dapp? false :name (i18n/label :t/browser))))) -(defn update-browser-fx [{:keys [db now]} browser] +(defn update-browser-fx [browser {:keys [db now]}] (let [updated-browser (check-if-dapp-in-list (assoc browser :timestamp now))] {:db (update-in db [:browser/browsers (:browser-id updated-browser)] merge updated-browser) :data-store/tx [(browser-store/save-browser-tx updated-browser)]})) -(defn update-browser-history-fx [cofx browser url loading?] +(defn update-browser-history-fx [browser url loading? cofx] (when-not loading? (let [history-index (:history-index browser) history (:history browser) @@ -43,12 +49,41 @@ new-index (if slash? history-index (dec (count new-history)))] - (update-browser-fx cofx - (assoc browser :history new-history :history-index new-index))))))) + (update-browser-fx (assoc browser :history new-history :history-index new-index) + cofx)))))) -(defn update-browser-and-navigate [cofx browser] - (merge (update-browser-fx cofx browser) - {:dispatch [:navigate-to :browser (:browser-id browser)]})) +(defn ens? [host] + (string/ends-with? host ".eth")) + +(defn ens-multihash-callback [hex] + (let [hash (when hex (multihash/base58 (multihash/create :sha2-256 (subs hex 2))))] + (if (and hash (not= hash resolver/default-hash)) + (re-frame/dispatch [:ens-multihash-resolved hash]) + (re-frame/dispatch [:update-browser-options {:resolving? false}])))) + +(defn resolve-multihash-fx [host loading error? {{:keys [web3 network] :as db} :db}] + (let [network (get-in db [:account/account :networks network]) + chain (ethereum/network->chain-keyword network)] + (if (and (not loading) (not error?) (ens? host)) + {:db (assoc-in db [:browser/options :resolving?] true) + :resolve-ens-multihash {:web3 web3 + :registry (get ens/ens-registries + chain) + :ens-name host + :cb ens-multihash-callback}} + {}))) + +(defn update-new-browser-and-navigate [host browser cofx] + (handlers-macro/merge-fx + cofx + {:dispatch [:navigate-to :browser {:browser-id (:browser-id browser) + :resolving? (ens? host)}]} + (update-browser-fx browser) + (resolve-multihash-fx host false false))) + +(defn update-browser-and-navigate [browser cofx] + (merge (update-browser-fx browser cofx) + {:dispatch [:navigate-to :browser {:browser-id (:browser-id browser)}]})) (def permissions {constants/dapp-permission-contact-code {:title (i18n/label :t/wants-to-access-profile) :description (i18n/label :t/your-contact-code) diff --git a/src/status_im/ui/screens/browser/db.cljs b/src/status_im/ui/screens/browser/db.cljs index 64010a44dc..75fa96797d 100644 --- a/src/status_im/ui/screens/browser/db.cljs +++ b/src/status_im/ui/screens/browser/db.cljs @@ -10,6 +10,7 @@ (spec/def :browser/history (spec/nilable vector?)) (spec/def :browser/history-index (spec/nilable int?)) (spec/def :browser/loading? (spec/nilable boolean?)) +(spec/def :browser/resolving? (spec/nilable boolean?)) (spec/def :browser/url-editing? (spec/nilable boolean?)) (spec/def :browser/show-tooltip (spec/nilable keyword?)) (spec/def :browser/show-permission (spec/nilable map?)) @@ -19,6 +20,7 @@ (allowed-keys :opt-un [:browser/browser-id :browser/loading? + :browser/resolving? :browser/url-editing? :browser/show-tooltip :browser/show-permission diff --git a/src/status_im/ui/screens/browser/events.cljs b/src/status_im/ui/screens/browser/events.cljs index a4b1b15c73..9ce89bd158 100644 --- a/src/status_im/ui/screens/browser/events.cljs +++ b/src/status_im/ui/screens/browser/events.cljs @@ -13,7 +13,8 @@ [status-im.utils.random :as random] [status-im.utils.types :as types] [status-im.utils.universal-links.core :as utils.universal-links] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [status-im.utils.ethereum.resolver :as resolver])) (re-frame/reg-fx :browse @@ -39,72 +40,89 @@ (fn [[message webview]] (.sendToBridge webview (types/clj->json message)))) +(re-frame/reg-fx + :resolve-ens-multihash + (fn [{:keys [web3 registry ens-name cb]}] + (resolver/content web3 registry ens-name cb))) + (handlers/register-handler-fx :browse-link-from-message (fn [_ [_ link]] {:browse link})) +(handlers/register-handler-fx + :ens-multihash-resolved + (fn [{:keys [db] :as cofx} [_ hash]] + (let [options (:browser/options db) + browsers (:browser/browsers db) + browser (get browsers (:browser-id options)) + history-index (:history-index browser)] + (handlers-macro/merge-fx + cofx + {:db (assoc-in db [:browser/options :resolving?] false)} + (model/update-browser-fx + (assoc-in browser [:history history-index] (str "https://ipfs.infura.io/ipfs/" hash))))))) + (handlers/register-handler-fx :open-url-in-browser - [re-frame/trim-v] - (fn [cofx [url]] - (let [normalized-url (http/normalize-and-decode-url url)] - (model/update-browser-and-navigate cofx {:browser-id (or (http/url-host normalized-url) (random/id)) - :history-index 0 - :history [normalized-url]})))) + (fn [cofx [_ url]] + (let [normalized-url (http/normalize-and-decode-url url) + host (http/url-host normalized-url)] + (model/update-new-browser-and-navigate + host + {:browser-id (or host (random/id)) + :history-index 0 + :history [normalized-url]} + cofx)))) (handlers/register-handler-fx :send-to-bridge - [re-frame/trim-v] - (fn [cofx [message]] + (fn [cofx [_ message]] {:send-to-bridge-fx [message (get-in cofx [:db :webview-bridge])]})) (handlers/register-handler-fx :open-browser - [re-frame/trim-v] - (fn [cofx [browser]] - (model/update-browser-and-navigate cofx browser))) + (fn [cofx [_ browser]] + (model/update-browser-and-navigate browser cofx))) (handlers/register-handler-fx :update-browser-on-nav-change - [re-frame/trim-v] - (fn [cofx [browser url loading]] - (model/update-browser-history-fx cofx browser url loading))) + (fn [cofx [_ browser url loading error?]] + (let [host (http/url-host url)] + (handlers-macro/merge-fx + cofx + (model/resolve-multihash-fx host loading error?) + (model/update-browser-history-fx browser url loading))))) (handlers/register-handler-fx :update-browser-options - [re-frame/trim-v] - (fn [{:keys [db]} [options]] + (fn [{:keys [db]} [_ options]] {:db (update db :browser/options merge options)})) (handlers/register-handler-fx :remove-browser - [re-frame/trim-v] - (fn [{:keys [db]} [browser-id]] + (fn [{:keys [db]} [_ browser-id]] {:db (update-in db [:browser/browsers] dissoc browser-id) :data-store/tx [(browser-store/remove-browser-tx browser-id)]})) (defn nav-update-browser [cofx browser history-index] - (model/update-browser-fx cofx (assoc browser :history-index history-index))) + (model/update-browser-fx (assoc browser :history-index history-index) cofx)) (handlers/register-handler-fx :browser-nav-back - [re-frame/trim-v] - (fn [cofx [{:keys [history-index] :as browser}]] + (fn [cofx [_ {:keys [history-index] :as browser}]] (when (pos? history-index) (nav-update-browser cofx browser (dec history-index))))) (handlers/register-handler-fx :browser-nav-forward - [re-frame/trim-v] - (fn [cofx [{:keys [history-index] :as browser}]] + (fn [cofx [_ {:keys [history-index] :as browser}]] (when (< history-index (dec (count (:history browser)))) (nav-update-browser cofx browser (inc history-index))))) (handlers/register-handler-fx :on-bridge-message - [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [message]] + (fn [{:keys [db] :as cofx} [_ message]] (let [{:browser/keys [options browsers]} db {:keys [browser-id]} options browser (get browsers browser-id) @@ -113,7 +131,7 @@ (cond (and (= type constants/history-state-changed) platform/ios? (not= "about:blank" url)) - (model/update-browser-history-fx cofx browser url false) + (model/update-browser-history-fx browser url false cofx) (= type constants/web3-send-async) (model/web3-send-async payload messageId cofx) @@ -127,7 +145,6 @@ (handlers/register-handler-fx :check-permissions-queue - [re-frame/trim-v] (fn [{:keys [db] :as cofx} _] (let [{:keys [show-permission permissions-queue]} (:browser/options db)] (when (and (nil? show-permission) (last permissions-queue)) @@ -145,9 +162,8 @@ (handlers/register-handler-fx :next-dapp-permission - [re-frame/trim-v] - (fn [cofx [params permission permissions-data]] + (fn [cofx [_ params permission permissions-data]] (model/next-permission {:params params :permission permission :permissions-data permissions-data} - cofx))) + cofx))) \ No newline at end of file diff --git a/src/status_im/ui/screens/browser/navigation.cljs b/src/status_im/ui/screens/browser/navigation.cljs index 2105b794cb..7d4befd608 100644 --- a/src/status_im/ui/screens/browser/navigation.cljs +++ b/src/status_im/ui/screens/browser/navigation.cljs @@ -2,5 +2,5 @@ (:require [status-im.ui.screens.navigation :as navigation])) (defmethod navigation/preload-data! :browser - [db [_ _ browser-id]] - (assoc db :browser/options {:browser-id browser-id})) + [db [_ _ options]] + (assoc db :browser/options options)) diff --git a/src/status_im/ui/screens/browser/views.cljs b/src/status_im/ui/screens/browser/views.cljs index de3b796bf9..8c42eecb6f 100644 --- a/src/status_im/ui/screens/browser/views.cljs +++ b/src/status_im/ui/screens/browser/views.cljs @@ -46,6 +46,7 @@ (re-frame/dispatch [:update-browser-on-nav-change browser (http/normalize-and-decode-url @url-text) + false false])) :placeholder (i18n/label :t/enter-url) :auto-capitalize :none @@ -81,12 +82,12 @@ [react/text {:style styles/web-view-error-text} (str desc)]])) -(defn on-navigation-change [event browser] +(defn on-navigation-change [event browser error?] (let [{:strs [url loading]} (js->clj event)] (when platform/ios? (re-frame/dispatch [:update-browser-options {:loading? loading}])) (when (not= "about:blank" url) - (re-frame/dispatch [:update-browser-on-nav-change browser url loading])))) + (re-frame/dispatch [:update-browser-on-nav-change browser url loading error?])))) (defn get-inject-js [url] (let [domain-name (nth (re-find #"^\w+://(www\.)?([^/:]+)" url) 2)] @@ -115,7 +116,7 @@ (views/letsubs [webview (atom nil) {:keys [address]} [:get-current-account] {:keys [browser-id dapp? name] :as browser} [:get-current-browser] - {:keys [error? loading? url-editing? show-tooltip show-permission]} [:get :browser/options] + {:keys [error? loading? url-editing? show-tooltip show-permission resolving?]} [:get :browser/options] rpc-url [:get :rpc-url] network-id [:get-network-id]] (let [can-go-back? (model/can-go-back? browser) @@ -131,12 +132,12 @@ :ref #(do (reset! webview %) (re-frame/dispatch [:set :webview-bridge %])) - :source {:uri url} + :source (when-not resolving? {:uri url}) :java-script-enabled true :bounces false :local-storage-enabled true :render-error web-view-error - :on-navigation-state-change #(on-navigation-change % browser) + :on-navigation-state-change #(on-navigation-change % browser error?) :on-bridge-message #(re-frame/dispatch [:on-bridge-message %]) :on-load #(re-frame/dispatch [:update-browser-options {:error? false}]) :on-error #(re-frame/dispatch [:update-browser-options {:error? true @@ -148,7 +149,7 @@ (ethereum/normalized-address address) (str network-id))) :injected-java-script js-res/webview-js}] - (when loading? + (when (or loading? resolving?) [react/view styles/web-view-loading [components/activity-indicator {:animating true}]])] [navigation webview browser can-go-back? can-go-forward?] diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index d7d6cb1b7c..bf5c1b1eb5 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -101,7 +101,7 @@ (spec/def :navigation/prev-view-id (spec/nilable keyword?)) ;; navigation screen params (spec/def :navigation.screen-params/network-details (allowed-keys :req [:networks/selected-network])) -(spec/def :navigation.screen-params/browser (spec/nilable string?)) +(spec/def :navigation.screen-params/browser (spec/nilable map?)) (spec/def :navigation.screen-params.profile-qr-viewer/contact (spec/nilable map?)) (spec/def :navigation.screen-params.profile-qr-viewer/source (spec/nilable keyword?)) (spec/def :navigation.screen-params.profile-qr-viewer/value (spec/nilable string?)) diff --git a/src/status_im/utils/ethereum/ens.cljs b/src/status_im/utils/ethereum/ens.cljs index 05b7a75e45..0d98b650d7 100644 --- a/src/status_im/utils/ethereum/ens.cljs +++ b/src/status_im/utils/ethereum/ens.cljs @@ -70,6 +70,11 @@ (namehash ens-name)) (fn [_ address] (cb (ethereum/hex->address address))))) +(defn content [web3 resolver ens-name cb] + (ethereum/call web3 + (ethereum/call-params resolver "content(bytes32)" (namehash ens-name)) + (fn [_ hash] (cb hash)))) + (def ABI-hash "0x2203ab56") (def pubkey-hash "0xc8690233") diff --git a/src/status_im/utils/ethereum/resolver.cljs b/src/status_im/utils/ethereum/resolver.cljs new file mode 100644 index 0000000000..6dab8a08aa --- /dev/null +++ b/src/status_im/utils/ethereum/resolver.cljs @@ -0,0 +1,14 @@ +(ns status-im.utils.ethereum.resolver + (:require [status-im.utils.ethereum.ens :as ens])) + +(def default-address "0x0000000000000000000000000000000000000000") +(def default-hash "0x0000000000000000000000000000000000000000000000000000000000000000") + +(defn content [web3 registry ens-name cb] + (ens/resolver web3 + registry + ens-name + (fn [address] + (if (and address (not= address default-address)) + (ens/content web3 address ens-name cb) + (cb nil))))) \ No newline at end of file diff --git a/src/status_im/utils/multihash.cljs b/src/status_im/utils/multihash.cljs new file mode 100644 index 0000000000..8cbc2a280f --- /dev/null +++ b/src/status_im/utils/multihash.cljs @@ -0,0 +1,185 @@ +(ns status-im.utils.multihash + "Core multihash type definition and helper methods." + (:require + [alphabase.base58 :as b58] + [alphabase.bytes :as bytes] + [alphabase.hex :as hex])) + +(def ^:const algorithm-codes + "Map of information about the available content hashing algorithms." + {:sha1 0x11 + :sha2-256 0x12 + :sha2-512 0x13 + :sha3 0x14 + :blake2b 0x40 + :blake2s 0x41}) + +(defn app-code? + "True if the given code number is assigned to the application-specfic range. + Returns nil if the argument is not an integer." + [code] + (when (integer? code) + (< 0 code 0x10))) + +(defn get-algorithm + "Looks up an algorithm by keyword name or code number. Returns `nil` if the + value does not map to any valid algorithm." + [value] + (cond + (keyword? value) + (when-let [code (get algorithm-codes value)] + {:code code, :name value}) + + (not (integer? value)) + nil + + (app-code? value) + {:code value, :name (keyword (str "app-" value))} + + :else + (some #(when (= value (val %)) + {:code value, :name (key %)}) + algorithm-codes))) + +;; ## Multihash Type + +;; Multihash identifiers have two properties: +;; +;; - `code` is a numeric code for an algorithm entry in `algorithm-codes` or an +;; application-specific algorithm code. +;; - `hex-digest` is a string holding the hex-encoded algorithm output. +;; +;; Multihash values also support metadata. +(deftype Multihash [code hex-digest _meta] + + Object + + (toString + [this] + (str "hash:" (name (:name (get-algorithm code))) \: hex-digest)) + + (-equiv + [this that] + (cond + (identical? this that) true + (instance? Multihash that) + (and (= code (:code that)) + (= hex-digest (:hex-digest that))) + :else false)) + + IHash + + (-hash + [this] + (hash-combine code hex-digest)) + + IComparable + + (-compare + [this that] + (cond + (= this that) 0 + (< code (:code that)) -1 + (> code (:code that)) 1 + :else (compare hex-digest (:hex-digest that)))) + + ILookup + + (-lookup + [this k] + (-lookup this k nil)) + + (-lookup + [this k not-found] + (case k + :code code + :algorithm (:name (get-algorithm code)) + :length (/ (count hex-digest) 2) + :digest (hex/decode hex-digest) + :hex-digest hex-digest + not-found)) + + IMeta + + (-meta + [this] + _meta) + + IWithMeta + + (-with-meta + [this meta-map] + (Multihash. code hex-digest meta-map))) + +(defn create + "Constructs a new Multihash identifier. Accepts either a numeric algorithm + code or a keyword name as the first argument. The digest may either by a byte + array or a hex string." + [algorithm digest] + (let [algo (get-algorithm algorithm)] + (when-not (integer? (:code algo)) + (throw (ex-info + (str "Argument " (pr-str algorithm) " does not " + "represent a valid hash algorithm.") + {:algorithm algorithm}))) + (let [hex-digest (if (string? digest) digest (hex/encode digest)) + byte-len (/ (count hex-digest) 2)] + (when (< 127 byte-len) + (throw (ex-info (str "Digest length must be less than 128 bytes: " + byte-len) + {:length byte-len}))) + (when-let [err (hex/validate hex-digest)] + (throw (ex-info err {:hex-digest hex-digest}))) + (->Multihash (:code algo) hex-digest nil)))) + +;; ## Encoding and Decoding + +(defn encode + "Encodes a multihash into a binary representation." + ^bytes + [mhash] + (let [length (:length mhash) + encoded (bytes/byte-array (+ length 2))] + (bytes/set-byte encoded 0 (:code mhash)) + (bytes/set-byte encoded 1 length) + (bytes/copy (:digest mhash) 0 encoded 2 length) + encoded)) + +(defn hex + "Encodes a multihash into a hexadecimal string." + [mhash] + (when mhash + (hex/encode (encode mhash)))) + +(defn base58 + "Encodes a multihash into a Base-58 string." + [mhash] + (when mhash + (b58/encode (encode mhash)))) + +(defn decode-array + "Decodes a byte array directly into multihash. Throws `ex-info` with a `:type` + of `:multihash/bad-input` if the data is malformed or invalid." + [^bytes encoded] + (let [encoded-size (alength encoded) + min-size 3] + (when (< encoded-size min-size) + (throw (ex-info + (str "Cannot read multihash from byte array: " encoded-size + " is less than the minimum of " min-size) + {:type :multihash/bad-input})))) + (let [code (bytes/get-byte encoded 0) + length (bytes/get-byte encoded 1) + payload (- (alength encoded) 2)] + (when-not (pos? length) + (throw (ex-info + (str "Encoded length " length " is invalid") + {:type :multihash/bad-input}))) + (when (< payload length) + (throw (ex-info + (str "Encoded digest length " length " exceeds actual " + "remaining payload of " payload " bytes") + {:type :multihash/bad-input}))) + (let [digest (bytes/byte-array length)] + (bytes/copy encoded 2 digest 0 length) + (create code digest)))) \ No newline at end of file