diff --git a/src/status_im/contexts/wallet/wallet_connect/events/effects.cljs b/src/status_im/contexts/wallet/wallet_connect/events/effects.cljs index 320b500329..a4ae4639aa 100644 --- a/src/status_im/contexts/wallet/wallet_connect/events/effects.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/events/effects.cljs @@ -7,6 +7,7 @@ [status-im.constants :as constants] [status-im.contexts.wallet.wallet-connect.utils.signing :as signing] [status-im.contexts.wallet.wallet-connect.utils.transactions :as transactions] + [status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data] [utils.i18n :as i18n] [utils.security.core :as security])) @@ -123,11 +124,11 @@ (rf/reg-fx :effects.wallet-connect/sign-typed-data (fn [{:keys [password address data version chain-id on-success on-error]}] - (-> (signing/eth-sign-typed-data (security/safe-unmask-data password) - address - data - chain-id - version) + (-> (typed-data/sign (security/safe-unmask-data password) + address + data + chain-id + version) (promesa/then on-success) (promesa/catch on-error)))) diff --git a/src/status_im/contexts/wallet/wallet_connect/events/session_requests.cljs b/src/status_im/contexts/wallet/wallet_connect/events/session_requests.cljs index d248016748..c04c8c8fa7 100644 --- a/src/status_im/contexts/wallet/wallet_connect/events/session_requests.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/events/session_requests.cljs @@ -6,8 +6,8 @@ [status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store] [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] - [status-im.contexts.wallet.wallet-connect.utils.signing :as signing] [status-im.contexts.wallet.wallet-connect.utils.transactions :as transactions] + [status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data] [taoensso.timbre :as log] [utils.i18n :as i18n] [utils.re-frame :as rf] @@ -135,16 +135,13 @@ (fn [{:keys [db]}] (try (let [[address raw-data] (data-store/get-db-current-request-params db) - parsed-raw-data (transforms/js-parse raw-data) session-chain-id (-> (data-store/get-db-current-request-event db) (get-in [:params :chainId]) networks/eip155->chain-id) - data-chain-id (-> parsed-raw-data - transforms/js->clj - signing/typed-data-chain-id) - parsed-data (-> parsed-raw-data - (transforms/js-dissoc :types :primaryType) - (transforms/js-stringify 2))] + typed-data (-> raw-data + transforms/js-parse + transforms/js->clj) + data-chain-id (typed-data/get-chain-id typed-data)] (if (and data-chain-id (not= session-chain-id data-chain-id)) {:fx [[:dispatch @@ -155,7 +152,7 @@ [:wallet-connect/current-request] assoc :address (string/lower-case address) - :display-data (or parsed-data raw-data) + :display-data (typed-data/flatten-typed-data typed-data) :raw-data raw-data) :fx [[:dispatch [:wallet-connect/show-request-modal]]]})) (catch js/Error err diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/common/style.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/common/style.cljs index 329569e912..64dc38b3b0 100644 --- a/src/status_im/contexts/wallet/wallet_connect/modals/common/style.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/modals/common/style.cljs @@ -21,9 +21,19 @@ (def data-item {:flex 1 - :background-color :transparent - :margin-bottom 8 - :margin-top 2}) + :background-color :transparent}) + +(def typed-data-item + (assoc data-item :margin-bottom 4)) + +(def data-border-container + {:padding-horizontal 12 + :padding-top 8 + :margin-top 10.5 + :margin-bottom 0 + :border-width 1 + :border-color colors/neutral-10 + :border-radius 16}) (def data-item-container {:padding 10 diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/sign_message/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/sign_message/view.cljs index b3a3a4a93a..4d5edee83e 100644 --- a/src/status_im/contexts/wallet/wallet_connect/modals/sign_message/view.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/modals/sign_message/view.cljs @@ -1,8 +1,9 @@ (ns status-im.contexts.wallet.wallet-connect.modals.sign-message.view (:require [quo.core :as quo] [react-native.core :as rn] + [react-native.gesture :as gesture] [react-native.safe-area :as safe-area] - [status-im.contexts.wallet.wallet-connect.modals.common.data-block.view :as data-block] + [status-im.common.raw-data-block.view :as data-block] [status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as fees-data-item] [status-im.contexts.wallet.wallet-connect.modals.common.footer.view :as footer] @@ -12,6 +13,37 @@ [utils.i18n :as i18n] [utils.re-frame :as rf])) +(defn typed-data-field + [{:keys [label value]}] + [quo/data-item + {:card? false + :container-style style/typed-data-item + :title label + :subtitle value}]) + +(defn typed-data-view + [] + (let [display-data (rf/sub [:wallet-connect/current-request-display-data])] + [gesture/flat-list + {:data display-data + :style style/data-border-container + :over-scroll-mode :never + :content-container-style {:padding-bottom 12} + :render-fn typed-data-field + :shows-vertical-scroll-indicator false}])) + +(defn message-data-view + [] + (let [display-data (rf/sub [:wallet-connect/current-request-display-data])] + [data-block/view display-data])) + +(defn display-data-view + [] + (let [typed-data? (rf/sub [:wallet-connect/typed-data-request?])] + (if typed-data? + [typed-data-view] + [message-data-view]))) + (defn view [] (let [bottom (safe-area/get-bottom) @@ -29,7 +61,7 @@ {:label (i18n/label :t/wallet-connect-sign-message-header) :dapp dapp :account account}] - [data-block/view]] + [display-data-view]] [footer/view {:warning-label (i18n/label :t/wallet-connect-sign-warning) :slide-button-text (i18n/label :t/slide-to-sign)} diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/signing.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/signing.cljs index 2c18298c89..52b6c98e66 100644 --- a/src/status_im/contexts/wallet/wallet_connect/utils/signing.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/utils/signing.cljs @@ -1,28 +1,12 @@ (ns status-im.contexts.wallet.wallet-connect.utils.signing - (:require [native-module.core :as native-module] - [promesa.core :as promesa] - [status-im.contexts.wallet.wallet-connect.utils.data-store :as - data-store] - [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] - [status-im.contexts.wallet.wallet-connect.utils.rpc :as rpc] - [utils.hex :as hex] - [utils.number :as number] - [utils.transforms :as transforms])) - -(defn typed-data-chain-id - "Returns the `:chain-id` from typed data if it's present and if the EIP712 domain defines it. Without - the `:chain-id` in the domain type, it will not be signed as part of the typed-data." - [typed-data] - (let [chain-id-type? (->> typed-data - :types - :EIP712Domain - (some #(= "chainId" (:name %)))) - data-chain-id (-> typed-data - :domain - :chainId - number/parse-int)] - (when chain-id-type? - data-chain-id))) + (:require + [native-module.core :as native-module] + [promesa.core :as promesa] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as + data-store] + [status-im.contexts.wallet.wallet-connect.utils.rpc :as rpc] + [utils.hex :as hex] + [utils.transforms :as transforms])) (defn eth-sign [password address data] @@ -38,13 +22,3 @@ (-> (rpc/wallet-hash-message-eip-191 data) (promesa/then #(rpc/wallet-sign-message % address password)) (promesa/then hex/prefix-hex))) - -(defn eth-sign-typed-data - [password address data chain-id-eip155 version] - (let [legacy? (= version :v1) - chain-id (networks/eip155->chain-id chain-id-eip155)] - (rpc/wallet-safe-sign-typed-data data - address - password - chain-id - legacy?))) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/typed_data.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data.cljs new file mode 100644 index 0000000000..4d499032dd --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data.cljs @@ -0,0 +1,102 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.typed-data + (:require [clojure.string :as string] + [status-im.constants :as constants] + [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [status-im.contexts.wallet.wallet-connect.utils.rpc :as rpc] + [utils.number :as number])) + +(declare flatten-data) + +(defn- format-flattened-key + [k] + (cond + (keyword? k) (name k) + (number? k) (str k) + (string? k) k + :else "unsupported-key")) + +(defn- flatten-map + [data path] + (reduce-kv (fn [acc k v] + (->> (format-flattened-key k) + (conj path) + (flatten-data v acc))) + [] + data)) + +(defn- flatten-vec + [data path] + (->> data + (map-indexed vector) + (reduce (fn [acc [idx v]] + (->> (str idx) + (conj path) + (flatten-data v acc))) + []))) + +(defn flatten-data + "Recursively flatten a map or vector into a flat vector. + + e.g. `[[[\"person\" \"first-name\"] \"Rich\"] + [[[\"person\" \"last-name\"] \"Hickey\"]]]`" + ([value] + (flatten-data value [] [])) + ([value acc path] + (cond + (map? value) (into acc (flatten-map value path)) + (vector? value) (into acc (flatten-vec value path)) + :else (conj acc [path value])))) + +(defn format-fields + "Format the fields into maps with `:label` & `:value`, where the label + is the flattened keys joined with a separator + + e.g. `{:label \"person: first-name:\" :value \"Rich\"}`" + [data separator] + (mapv (fn [[kv v]] + {:label (-> separator + (string/join kv) + (str separator) + string/trim) + :value v}) + data)) + +(defn flatten-typed-data + "Flatten typed data and prepare it for UI" + [typed-data] + (-> typed-data + (select-keys [:domain :message]) + flatten-data + (format-fields ": "))) + +(defn get-chain-id + "Returns the `:chain-id` from typed data if it's present and if the EIP712 domain defines it. Without + the `:chain-id` in the domain type, it will not be signed as part of the typed-data." + [typed-data] + (let [chain-id-type? (->> typed-data + :types + :EIP712Domain + (some #(= "chainId" (:name %)))) + data-chain-id (-> typed-data + :domain + :chainId + number/parse-int)] + (when chain-id-type? + data-chain-id))) + +(defn sign + [password address data chain-id-eip155 version] + (let [legacy? (= version :v1) + chain-id (networks/eip155->chain-id chain-id-eip155)] + (rpc/wallet-safe-sign-typed-data data + address + password + chain-id + legacy?))) + +(defn typed-data-request? + [method] + (contains? + #{constants/wallet-connect-eth-sign-typed-v4-method + constants/wallet-connect-eth-sign-typed-method} + method)) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/typed_data_test.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data_test.cljs new file mode 100644 index 0000000000..c1f72bcfb4 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/typed_data_test.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.typed-data-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.wallet.wallet-connect.utils.typed-data :as sut])) + +(deftest flatten-data-test + (testing "correctly returns the fallback when key is not string/keyword/number" + (is (match? [[["unsupported-key"] "value"]] + (sut/flatten-data {[1 2] "value"})))) + + (testing "correctly flattens a simple map" + (is (match? [[["zero"] 0] + [["one"] 1] + [["2"] 2]] + (sut/flatten-data {:zero 0 + "one" 1 + 2 2})))) + + (testing "correctly flattens a simple vec" + (is (match? [[["0"] "zero"] [["1"] "one"]] + (sut/flatten-data ["zero" "one"])))) + + (testing "correctly flattens a nested map" + (is (match? [[["nested" "child"] "child value"] + [["nested" "nested" "0"] "child one"] + [["nested" "nested" "1"] "child two"] + [["flat"] "child value"]] + (sut/flatten-data {:nested {:child "child value" + :nested ["child one" "child two"]} + :flat "child value"})))) + + (testing "correctly flattens a nested vector" + (is (match? [[["0" "flat"] 1] + [["0" "nested-vector" "0"] 1] + [["0" "nested-vector" "1"] 2] + [["0" "nested-map" "one"] 1]] + (sut/flatten-data [{:flat 1 + :nested-vector [1 2] + :nested-map {:one 1}}]))))) + +(deftest flatten-typed-data-test + (testing "successfully extracts, flattens and formats the typed data" + (is (match? [{:label "domain: chain-id:" :value 1} + {:label "message: to: address:" :value "0x"} + {:label "message: from: address:" :value "0x"} + {:label "message: amount:" :value "0x"}] + (sut/flatten-typed-data {:domain {:chain-id 1} + :types {:Tx [{:name "to" + :type "address"}]} + :message {:to {:address "0x"} + :from {:address "0x"} + :amount "0x"}}))))) diff --git a/src/status_im/subs/wallet/dapps/requests.cljs b/src/status_im/subs/wallet/dapps/requests.cljs index d78ed28d7f..12f8272cc3 100644 --- a/src/status_im/subs/wallet/dapps/requests.cljs +++ b/src/status_im/subs/wallet/dapps/requests.cljs @@ -4,6 +4,7 @@ [status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store] [status-im.contexts.wallet.wallet-connect.utils.networks :as networks] + [status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data] [utils.string])) (rf/reg-sub @@ -16,6 +17,14 @@ :<- [:wallet-connect/current-request] :-> :display-data) +(rf/reg-sub + :wallet-connect/current-request-method + :<- [:wallet-connect/current-request] + (fn [request] + (-> request + :event + data-store/get-request-method))) + (rf/reg-sub :wallet-connect/current-request-account-details :<- [:wallet-connect/current-request-address] @@ -45,3 +54,8 @@ :wallet-connect/current-request-network :<- [:wallet-connect/chain-id] networks/chain-id->network-details) + +(rf/reg-sub + :wallet-connect/typed-data-request? + :<- [:wallet-connect/current-request-method] + typed-data/typed-data-request?)