Dapps Typed Data request improvements (#21333)

* feat: added type data flattening + tests

* feat: added typed data flattened ui

* ref: moved typed data utils into their own ns

* ref: small changes & formatting

* feat: show typed data view only for typed data requests

* ref: moved typed-data? into sub

* fix: tests and styling
This commit is contained in:
Lungu Cristian 2024-10-02 13:50:38 +03:00 committed by GitHub
parent 0c3e836272
commit 97aff27980
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 235 additions and 53 deletions

View File

@ -7,6 +7,7 @@
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.contexts.wallet.wallet-connect.utils.signing :as signing] [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.transactions :as transactions]
[status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.security.core :as security])) [utils.security.core :as security]))
@ -123,11 +124,11 @@
(rf/reg-fx (rf/reg-fx
:effects.wallet-connect/sign-typed-data :effects.wallet-connect/sign-typed-data
(fn [{:keys [password address data version chain-id on-success on-error]}] (fn [{:keys [password address data version chain-id on-success on-error]}]
(-> (signing/eth-sign-typed-data (security/safe-unmask-data password) (-> (typed-data/sign (security/safe-unmask-data password)
address address
data data
chain-id chain-id
version) version)
(promesa/then on-success) (promesa/then on-success)
(promesa/catch on-error)))) (promesa/catch on-error))))

View File

@ -6,8 +6,8 @@
[status-im.contexts.wallet.wallet-connect.utils.data-store :as [status-im.contexts.wallet.wallet-connect.utils.data-store :as
data-store] data-store]
[status-im.contexts.wallet.wallet-connect.utils.networks :as networks] [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.transactions :as transactions]
[status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf] [utils.re-frame :as rf]
@ -135,16 +135,13 @@
(fn [{:keys [db]}] (fn [{:keys [db]}]
(try (try
(let [[address raw-data] (data-store/get-db-current-request-params db) (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) session-chain-id (-> (data-store/get-db-current-request-event db)
(get-in [:params :chainId]) (get-in [:params :chainId])
networks/eip155->chain-id) networks/eip155->chain-id)
data-chain-id (-> parsed-raw-data typed-data (-> raw-data
transforms/js->clj transforms/js-parse
signing/typed-data-chain-id) transforms/js->clj)
parsed-data (-> parsed-raw-data data-chain-id (typed-data/get-chain-id typed-data)]
(transforms/js-dissoc :types :primaryType)
(transforms/js-stringify 2))]
(if (and data-chain-id (if (and data-chain-id
(not= session-chain-id data-chain-id)) (not= session-chain-id data-chain-id))
{:fx [[:dispatch {:fx [[:dispatch
@ -155,7 +152,7 @@
[:wallet-connect/current-request] [:wallet-connect/current-request]
assoc assoc
:address (string/lower-case address) :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) :raw-data raw-data)
:fx [[:dispatch [:wallet-connect/show-request-modal]]]})) :fx [[:dispatch [:wallet-connect/show-request-modal]]]}))
(catch js/Error err (catch js/Error err

View File

@ -21,9 +21,19 @@
(def data-item (def data-item
{:flex 1 {:flex 1
:background-color :transparent :background-color :transparent})
:margin-bottom 8
:margin-top 2}) (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 (def data-item-container
{:padding 10 {:padding 10

View File

@ -1,8 +1,9 @@
(ns status-im.contexts.wallet.wallet-connect.modals.sign-message.view (ns status-im.contexts.wallet.wallet-connect.modals.sign-message.view
(:require [quo.core :as quo] (:require [quo.core :as quo]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.gesture :as gesture]
[react-native.safe-area :as safe-area] [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 [status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as
fees-data-item] fees-data-item]
[status-im.contexts.wallet.wallet-connect.modals.common.footer.view :as footer] [status-im.contexts.wallet.wallet-connect.modals.common.footer.view :as footer]
@ -12,6 +13,37 @@
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [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 (defn view
[] []
(let [bottom (safe-area/get-bottom) (let [bottom (safe-area/get-bottom)
@ -29,7 +61,7 @@
{:label (i18n/label :t/wallet-connect-sign-message-header) {:label (i18n/label :t/wallet-connect-sign-message-header)
:dapp dapp :dapp dapp
:account account}] :account account}]
[data-block/view]] [display-data-view]]
[footer/view [footer/view
{:warning-label (i18n/label :t/wallet-connect-sign-warning) {:warning-label (i18n/label :t/wallet-connect-sign-warning)
:slide-button-text (i18n/label :t/slide-to-sign)} :slide-button-text (i18n/label :t/slide-to-sign)}

View File

@ -1,28 +1,12 @@
(ns status-im.contexts.wallet.wallet-connect.utils.signing (ns status-im.contexts.wallet.wallet-connect.utils.signing
(:require [native-module.core :as native-module] (:require
[promesa.core :as promesa] [native-module.core :as native-module]
[status-im.contexts.wallet.wallet-connect.utils.data-store :as [promesa.core :as promesa]
data-store] [status-im.contexts.wallet.wallet-connect.utils.data-store :as
[status-im.contexts.wallet.wallet-connect.utils.networks :as networks] data-store]
[status-im.contexts.wallet.wallet-connect.utils.rpc :as rpc] [status-im.contexts.wallet.wallet-connect.utils.rpc :as rpc]
[utils.hex :as hex] [utils.hex :as hex]
[utils.number :as number] [utils.transforms :as transforms]))
[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)))
(defn eth-sign (defn eth-sign
[password address data] [password address data]
@ -38,13 +22,3 @@
(-> (rpc/wallet-hash-message-eip-191 data) (-> (rpc/wallet-hash-message-eip-191 data)
(promesa/then #(rpc/wallet-sign-message % address password)) (promesa/then #(rpc/wallet-sign-message % address password))
(promesa/then hex/prefix-hex))) (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?)))

View File

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

View File

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

View File

@ -4,6 +4,7 @@
[status-im.contexts.wallet.wallet-connect.utils.data-store :as [status-im.contexts.wallet.wallet-connect.utils.data-store :as
data-store] data-store]
[status-im.contexts.wallet.wallet-connect.utils.networks :as networks] [status-im.contexts.wallet.wallet-connect.utils.networks :as networks]
[status-im.contexts.wallet.wallet-connect.utils.typed-data :as typed-data]
[utils.string])) [utils.string]))
(rf/reg-sub (rf/reg-sub
@ -16,6 +17,14 @@
:<- [:wallet-connect/current-request] :<- [:wallet-connect/current-request]
:-> :display-data) :-> :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 (rf/reg-sub
:wallet-connect/current-request-account-details :wallet-connect/current-request-account-details
:<- [:wallet-connect/current-request-address] :<- [:wallet-connect/current-request-address]
@ -45,3 +54,8 @@
:wallet-connect/current-request-network :wallet-connect/current-request-network
:<- [:wallet-connect/chain-id] :<- [:wallet-connect/chain-id]
networks/chain-id->network-details) networks/chain-id->network-details)
(rf/reg-sub
:wallet-connect/typed-data-request?
:<- [:wallet-connect/current-request-method]
typed-data/typed-data-request?)