Metamask address support (#20844)

* wip, small renamings

* Added metamask regex

* Added tests for metamask conversion

* Add metamask address to shell scanner and wallet address scanner

* unify address functions usage

* address-related functions moved to a dedicated namespace

* minor fixes

* fix lint and tests

* review notes fixes

* return back function

* lint fix

* Support metamask addresses without suffix, prefixed `:ethereum`
This commit is contained in:
Volodymyr Kozieiev 2024-08-29 15:48:41 +01:00 committed by GitHub
parent 96b88a427d
commit bf7b86ccd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 132 additions and 58 deletions

View File

@ -24,7 +24,7 @@
(def web-prefixes ["https://" "http://" "https://www." "http://www."]) (def web-prefixes ["https://" "http://" "https://www." "http://www."])
(def web2-domain "status.app") (def status-web2-domain "status.app")
(def user-path "u#") (def user-path "u#")
(def user-with-data-path "u/") (def user-with-data-path "u/")
@ -32,13 +32,14 @@
(def community-with-data-path "c/") (def community-with-data-path "c/")
(def channel-path "cc/") (def channel-path "cc/")
(def web-urls (map #(str % web2-domain "/") web-prefixes)) (def status-web-urls (map #(str % status-web2-domain "/") web-prefixes))
(defn path-urls (defn prepend-status-urls
[path] [path]
(map #(str % path) web-urls)) (map #(str % path) status-web-urls))
(def handled-schemes (set (into uri-schemes web-urls)))
(def handled-schemes (set (into uri-schemes status-web-urls)))
(def group-chat-extractor (def group-chat-extractor
{[#"(.*)" :params] {"" :group-chat {[#"(.*)" :params] {"" :group-chat

View File

@ -260,9 +260,7 @@
(def regx-community-universal-link #"((^https?://status.app/)|(^status-app://))c/([\x00-\x7F]+)$") (def regx-community-universal-link #"((^https?://status.app/)|(^status-app://))c/([\x00-\x7F]+)$")
(def regx-deep-link #"((^ethereum:.*)|(^status-app://[\x00-\x7F]+$))") (def regx-deep-link #"((^ethereum:.*)|(^status-app://[\x00-\x7F]+$))")
(def regx-ens #"^(?=.{5,255}$)([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$") (def regx-ens #"^(?=.{5,255}$)([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$")
(def regx-multichain-address #"^(?:(?:eth:|arb1:|oeth:)(?=:|))*0x[0-9a-fA-F]{40}$")
(def regx-address-contains #"(?i)0x[a-fA-F0-9]{40}")
(def regx-starts-with-uuid #"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") (def regx-starts-with-uuid #"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
(def regx-full-or-partial-address #"^0x[a-fA-F0-9]{1,40}$") (def regx-full-or-partial-address #"^0x[a-fA-F0-9]{1,40}$")

View File

@ -9,6 +9,7 @@
[status-im.contexts.settings.wallet.saved-addresses.add-address-to-save.style :as style] [status-im.contexts.settings.wallet.saved-addresses.add-address-to-save.style :as style]
[status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.common.utils :as utils]
[status-im.contexts.wallet.common.validation :as validation] [status-im.contexts.wallet.common.validation :as validation]
[utils.address :as utils-address]
[utils.debounce :as debounce] [utils.debounce :as debounce]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
@ -31,7 +32,7 @@
:own-account :own-account
(not (not
(or (validation/eth-address? user-input) (or (utils-address/eip-3770-address? user-input)
(validation/ens-name? user-input))) (validation/ens-name? user-input)))
:invalid-address-or-ens))) :invalid-address-or-ens)))

View File

@ -6,9 +6,9 @@
[status-im.common.scan-qr-code.view :as scan-qr-code] [status-im.common.scan-qr-code.view :as scan-qr-code]
[status-im.common.validation.general :as validators] [status-im.common.validation.general :as validators]
[status-im.contexts.communities.events] [status-im.contexts.communities.events]
[status-im.contexts.wallet.common.validation :as wallet-validation]
[status-im.contexts.wallet.wallet-connect.utils :as wc-utils] [status-im.contexts.wallet.wallet-connect.utils :as wc-utils]
[status-im.feature-flags :as ff] [status-im.feature-flags :as ff]
[utils.address :as utils-address]
[utils.debounce :as debounce] [utils.debounce :as debounce]
[utils.ethereum.eip.eip681 :as eip681] [utils.ethereum.eip.eip681 :as eip681]
[utils.i18n :as i18n] [utils.i18n :as i18n]
@ -19,19 +19,15 @@
:theme :dark :theme :dark
:text (i18n/label :t/invalid-qr)}) :text (i18n/label :t/invalid-qr)})
(defn- text-for-url-path? (defn- text-a-status-url-for-path?
[text path] [text path]
(some #(string/starts-with? text %) (router/path-urls path))) (some #(string/starts-with? text %) (router/prepend-status-urls path)))
(defn- extract-id (defn- extract-id
[scanned-text] [scanned-text]
(let [index (string/index-of scanned-text "#")] (let [index (string/index-of scanned-text "#")]
(subs scanned-text (inc index)))) (subs scanned-text (inc index))))
(defn eth-address?
[scanned-text]
(wallet-validation/eth-address? scanned-text))
(defn eip681-address? (defn eip681-address?
[scanned-text] [scanned-text]
(-> scanned-text (-> scanned-text
@ -71,15 +67,15 @@
[:wallet-connect/on-scan-connection scanned-text] [:wallet-connect/on-scan-connection scanned-text]
300)) 300))
(defn on-qr-code-scanned (defn- on-qr-code-scanned
[scanned-text] [scanned-text]
(cond (cond
(or (or
(text-for-url-path? scanned-text router/community-with-data-path) (text-a-status-url-for-path? scanned-text router/community-with-data-path)
(text-for-url-path? scanned-text router/channel-path)) (text-a-status-url-for-path? scanned-text router/channel-path))
(debounce/debounce-and-dispatch [:universal-links/handle-url scanned-text] 300) (debounce/debounce-and-dispatch [:universal-links/handle-url scanned-text] 300)
(text-for-url-path? scanned-text router/user-with-data-path) (text-a-status-url-for-path? scanned-text router/user-with-data-path)
(let [address (extract-id scanned-text)] (let [address (extract-id scanned-text)]
(load-and-show-profile address)) (load-and-show-profile address))
@ -87,9 +83,9 @@
(validators/valid-compressed-key? scanned-text)) (validators/valid-compressed-key? scanned-text))
(load-and-show-profile scanned-text) (load-and-show-profile scanned-text)
(eth-address? scanned-text) (utils-address/supported-address? scanned-text)
(do (when-let [address (utils-address/supported-address->status-address scanned-text)]
(debounce/debounce-and-dispatch [:generic-scanner/scan-success scanned-text] 300) (debounce/debounce-and-dispatch [:generic-scanner/scan-success address] 300)
(debounce/debounce-and-dispatch [:navigate-change-tab :wallet-stack] 300)) (debounce/debounce-and-dispatch [:navigate-change-tab :wallet-stack] 300))
(eip681-address? scanned-text) (eip681-address? scanned-text)

View File

@ -6,10 +6,10 @@
[react-native.core :as rn] [react-native.core :as rn]
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im.common.floating-button-page.view :as floating-button-page] [status-im.common.floating-button-page.view :as floating-button-page]
[status-im.constants :as constants]
[status-im.contexts.wallet.add-account.add-address-to-watch.style :as style] [status-im.contexts.wallet.add-account.add-address-to-watch.style :as style]
[status-im.contexts.wallet.common.validation :as validation] [status-im.contexts.wallet.common.validation :as validation]
[status-im.subs.wallet.add-account.address-to-watch] [status-im.subs.wallet.add-account.address-to-watch]
[utils.address :as utils-address]
[utils.debounce :as debounce] [utils.debounce :as debounce]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
@ -20,13 +20,9 @@
(or (nil? user-input) (= user-input "")) nil (or (nil? user-input) (= user-input "")) nil
(contains? known-addresses user-input) (i18n/label :t/address-already-in-use) (contains? known-addresses user-input) (i18n/label :t/address-already-in-use)
(not (not
(or (validation/eth-address? user-input) (or (utils-address/supported-address? user-input)
(validation/ens-name? user-input))) (i18n/label :t/invalid-address))) (validation/ens-name? user-input))) (i18n/label :t/invalid-address)))
(defn- extract-address
[scanned-text]
(re-find constants/regx-address-contains scanned-text))
(defn- address-input (defn- address-input
[{:keys [input-value validation-msg validate clear-input]}] [{:keys [input-value validation-msg validate clear-input]}]
(let [scanned-address (rf/sub [:wallet/scanned-address]) (let [scanned-address (rf/sub [:wallet/scanned-address])
@ -141,10 +137,12 @@
(= activity-state :scanning) (= activity-state :scanning)
(not validated-address)) (not validated-address))
:on-press (fn [] :on-press (fn []
(rf/dispatch [:navigate-to (rf/dispatch
:screen/wallet.confirm-address-to-watch [:navigate-to
{:address (extract-address :screen/wallet.confirm-address-to-watch
validated-address)}]) {:address
(utils-address/extract-address-without-chains-info
validated-address)}])
(clear-input)) (clear-input))
:container-style {:z-index 2}} :container-style {:z-index 2}}
(i18n/label :t/continue)]} (i18n/label :t/continue)]}

View File

@ -1,28 +1,11 @@
(ns status-im.contexts.wallet.common.scan-account.view (ns status-im.contexts.wallet.common.scan-account.view
(:require (:require
[clojure.string :as string]
[status-im.common.scan-qr-code.view :as scan-qr-code] [status-im.common.scan-qr-code.view :as scan-qr-code]
[status-im.constants :as constants] [utils.address :as utils-address]
[utils.debounce :as debounce] [utils.debounce :as debounce]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(def ^:private supported-networks #{:eth :arb1 :oeth})
(defn- contains-supported-address?
[s]
(let [address? (boolean (re-find constants/regx-address-contains s))
networks (when address?
(as-> s $
(string/split $ ":")
(butlast $)))
supported? (every? supported-networks (map keyword networks))]
(and address? supported?)))
(defn- extract-address
[scanned-text]
(first (re-seq constants/regx-multichain-address scanned-text)))
(defn view (defn view
[] []
(let [{:keys [on-result]} (rf/sub [:get-screen-params])] (let [{:keys [on-result]} (rf/sub [:get-screen-params])]
@ -30,9 +13,9 @@
{:title (i18n/label :t/scan-qr) {:title (i18n/label :t/scan-qr)
:subtitle (i18n/label :t/scan-an-address-qr-code) :subtitle (i18n/label :t/scan-an-address-qr-code)
:error-message (i18n/label :t/oops-this-qr-does-not-contain-an-address) :error-message (i18n/label :t/oops-this-qr-does-not-contain-an-address)
:validate-fn #(contains-supported-address? %) :validate-fn #(utils-address/supported-address? %)
:on-success-scan (fn [result] :on-success-scan (fn [result]
(let [address (extract-address result)] (let [address (utils-address/supported-address->status-address result)]
(when on-result (on-result address)) (when on-result (on-result address))
(debounce/debounce-and-dispatch (debounce/debounce-and-dispatch
[:wallet/scan-address-success address] [:wallet/scan-address-success address]

View File

@ -2,7 +2,6 @@
(:require [status-im.constants :as constants])) (:require [status-im.constants :as constants]))
(defn ens-name? [s] (boolean (re-find constants/regx-ens s))) (defn ens-name? [s] (boolean (re-find constants/regx-ens s)))
(defn eth-address? [s] (re-find constants/regx-multichain-address s))
(defn private-key? (defn private-key?
[s] [s]
(or (re-find constants/regx-private-key-hex s) (or (re-find constants/regx-private-key-hex s)

View File

@ -12,12 +12,12 @@
[status-im.contexts.wallet.common.account-switcher.view :as account-switcher] [status-im.contexts.wallet.common.account-switcher.view :as account-switcher]
[status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.common.utils :as utils]
[status-im.contexts.wallet.common.utils.networks :as network-utils] [status-im.contexts.wallet.common.utils.networks :as network-utils]
[status-im.contexts.wallet.common.validation :as validation]
[status-im.contexts.wallet.item-types :as types] [status-im.contexts.wallet.item-types :as types]
[status-im.contexts.wallet.send.select-address.style :as style] [status-im.contexts.wallet.send.select-address.style :as style]
[status-im.contexts.wallet.send.select-address.tabs.view :as tabs] [status-im.contexts.wallet.send.select-address.tabs.view :as tabs]
[status-im.feature-flags :as ff] [status-im.feature-flags :as ff]
[status-im.setup.hot-reload :as hot-reload] [status-im.setup.hot-reload :as hot-reload]
[utils.address :as utils-address]
[utils.debounce :as debounce] [utils.debounce :as debounce]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
@ -33,9 +33,9 @@
[address] [address]
(debounce/debounce-and-dispatch (debounce/debounce-and-dispatch
(cond (cond
(<= (count address) 0) [:wallet/address-validation-failed address] (<= (count address) 0) [:wallet/address-validation-failed address]
(validation/eth-address? address) [:wallet/address-validation-success address] (utils-address/eip-3770-address? address) [:wallet/address-validation-success address]
:else [:wallet/address-validation-failed address]) :else [:wallet/address-validation-failed address])
300)) 300))
(defn- address-input (defn- address-input
@ -58,7 +58,7 @@
{:on-result on-result}])) {:on-result on-result}]))
:ens-regex constants/regx-ens :ens-regex constants/regx-ens
:scanned-value (or (when recipient-plain-address? send-address) scanned-address) :scanned-value (or (when recipient-plain-address? send-address) scanned-address)
:address-regex constants/regx-multichain-address :address-regex utils-address/regx-eip-3770-address
:on-detect-address (fn [address] :on-detect-address (fn [address]
(when (or (= current-screen-id :screen/wallet.select-address) (when (or (= current-screen-id :screen/wallet.select-address)
(= current-screen-id :screen/wallet.scan-address)) (= current-screen-id :screen/wallet.scan-address))

View File

@ -4,7 +4,12 @@
[native-module.core :as native-module] [native-module.core :as native-module]
[utils.ethereum.eip.eip55 :as eip55])) [utils.ethereum.eip.eip55 :as eip55]))
(def hex-prefix "0x") (def hex-prefix "0x")
;; EIP-3770 is a format used by Status and described here: https://eips.ethereum.org/EIPS/eip-3770
(def regx-eip-3770-address #"^(?:(?:eth:|arb1:|oeth:)(?=:|))*0x[0-9a-fA-F]{40}$")
(def regx-metamask-address #"^ethereum:(0x[0-9a-fA-F]{40})(?:@(0x1|0xa|0xa4b1))?$")
(def regx-address-contains #"(?i)0x[a-fA-F0-9]{40}")
(defn normalized-hex (defn normalized-hex
[hex] [hex]
@ -77,3 +82,49 @@
[value] [value]
(when value (when value
(str (subs value 0 5) "..." (subs value (- (count value) 3) (count value))))) (str (subs value 0 5) "..." (subs value (- (count value) 3) (count value)))))
(defn eip-155-suffix->eip-3770-prefix
[eip-155-suffix]
(case eip-155-suffix
"0x1" "eth:"
"0xa4b1" "arb1:"
"0xa" "oeth:"
nil))
(defn split-metamask-address
[address]
(re-find regx-metamask-address address))
(defn metamask-address?
[address]
(boolean (split-metamask-address address)))
(defn eip-3770-address?
"Checks if address follows EIP-3770 format which is default for Status"
[s]
(re-find regx-eip-3770-address s))
(defn supported-address?
[s]
(boolean (or (eip-3770-address? s)
(metamask-address? s))))
(defn metamask-address->status-address
[metamask-address]
(when-let [[_ address metamask-network-suffix] (split-metamask-address metamask-address)]
(if-let [status-network-prefix (eip-155-suffix->eip-3770-prefix metamask-network-suffix)]
(str status-network-prefix address)
address)))
(defn supported-address->status-address
[address]
(cond
(eip-3770-address? address)
address
(metamask-address? address)
(metamask-address->status-address address)))
(defn extract-address-without-chains-info
[address]
(re-find regx-address-contains address))

View File

@ -39,3 +39,50 @@
(testing "Ensure the function returns nil when given nil" (testing "Ensure the function returns nil when given nil"
(is (nil? (utils.address/get-abbreviated-profile-url nil))))) (is (nil? (utils.address/get-abbreviated-profile-url nil)))))
(def valid-metamask-addresses
["ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1"
"ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1"
"ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa"
"ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"])
(def invalid-metamask-addresses
["ethe:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1"
":0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1"
"0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa"
"ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1d"
"ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd20xa4b1"
"ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2:0xa"])
(def metamask-to-status
[{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1"
:status "eth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"}
{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1"
:status "arb1:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"}
{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa"
:status "oeth:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"}
{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"
:status "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2"}
{:metamask "ethe:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1" :status nil}
{:metamask ":0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa4b1" :status nil}
{:metamask "0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0xa" :status nil}
{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2@0x1d" :status nil}
{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd20xa4b1" :status nil}
{:metamask "ethereum:0x38cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd2:0xa" :status nil}])
(deftest metamask-address?-test
(testing "Check valid metamask addresses"
(dorun
(for [address valid-metamask-addresses]
(is (utils.address/metamask-address? address)))))
(testing "Check invalid metamask addresses"
(dorun
(for [address invalid-metamask-addresses]
(is (not (utils.address/metamask-address? address)))))))
(deftest metamask-address->status-address-test
(testing "Check metamask to status address conversion is valid"
(dorun
(for [{metamask-address :metamask
status-address :status} metamask-to-status]
(is (= status-address (utils.address/metamask-address->status-address metamask-address)))))))