From bf7b86ccd5ec8ed3859b6cf1ec7d6cf8591092fb Mon Sep 17 00:00:00 2001 From: Volodymyr Kozieiev Date: Thu, 29 Aug 2024 15:48:41 +0100 Subject: [PATCH] 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` --- src/status_im/common/router.cljs | 11 ++-- src/status_im/constants.cljs | 2 - .../add_address_to_save/view.cljs | 3 +- .../contexts/shell/qr_reader/view.cljs | 24 ++++----- .../add_address_to_watch/view.cljs | 18 +++---- .../wallet/common/scan_account/view.cljs | 23 ++------- .../contexts/wallet/common/validation.cljs | 1 - .../wallet/send/select_address/view.cljs | 10 ++-- src/utils/address.cljs | 51 +++++++++++++++++++ src/utils/address_test.cljs | 47 +++++++++++++++++ 10 files changed, 132 insertions(+), 58 deletions(-) diff --git a/src/status_im/common/router.cljs b/src/status_im/common/router.cljs index 06e8cbaaae..05892374b8 100644 --- a/src/status_im/common/router.cljs +++ b/src/status_im/common/router.cljs @@ -24,7 +24,7 @@ (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-with-data-path "u/") @@ -32,13 +32,14 @@ (def community-with-data-path "c/") (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] - (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 {[#"(.*)" :params] {"" :group-chat diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 22c45ff76d..23cd9911f2 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -260,9 +260,7 @@ (def regx-community-universal-link #"((^https?://status.app/)|(^status-app://))c/([\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-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-full-or-partial-address #"^0x[a-fA-F0-9]{1,40}$") diff --git a/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/view.cljs b/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/view.cljs index 4631ac7052..fd6f79192e 100644 --- a/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/view.cljs +++ b/src/status_im/contexts/settings/wallet/saved_addresses/add_address_to_save/view.cljs @@ -9,6 +9,7 @@ [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.validation :as validation] + [utils.address :as utils-address] [utils.debounce :as debounce] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -31,7 +32,7 @@ :own-account (not - (or (validation/eth-address? user-input) + (or (utils-address/eip-3770-address? user-input) (validation/ens-name? user-input))) :invalid-address-or-ens))) diff --git a/src/status_im/contexts/shell/qr_reader/view.cljs b/src/status_im/contexts/shell/qr_reader/view.cljs index cf967d0b19..63ef09dd20 100644 --- a/src/status_im/contexts/shell/qr_reader/view.cljs +++ b/src/status_im/contexts/shell/qr_reader/view.cljs @@ -6,9 +6,9 @@ [status-im.common.scan-qr-code.view :as scan-qr-code] [status-im.common.validation.general :as validators] [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.feature-flags :as ff] + [utils.address :as utils-address] [utils.debounce :as debounce] [utils.ethereum.eip.eip681 :as eip681] [utils.i18n :as i18n] @@ -19,19 +19,15 @@ :theme :dark :text (i18n/label :t/invalid-qr)}) -(defn- text-for-url-path? +(defn- text-a-status-url-for-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 [scanned-text] (let [index (string/index-of scanned-text "#")] (subs scanned-text (inc index)))) -(defn eth-address? - [scanned-text] - (wallet-validation/eth-address? scanned-text)) - (defn eip681-address? [scanned-text] (-> scanned-text @@ -71,15 +67,15 @@ [:wallet-connect/on-scan-connection scanned-text] 300)) -(defn on-qr-code-scanned +(defn- on-qr-code-scanned [scanned-text] (cond (or - (text-for-url-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/community-with-data-path) + (text-a-status-url-for-path? scanned-text router/channel-path)) (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)] (load-and-show-profile address)) @@ -87,9 +83,9 @@ (validators/valid-compressed-key? scanned-text)) (load-and-show-profile scanned-text) - (eth-address? scanned-text) - (do - (debounce/debounce-and-dispatch [:generic-scanner/scan-success scanned-text] 300) + (utils-address/supported-address? scanned-text) + (when-let [address (utils-address/supported-address->status-address scanned-text)] + (debounce/debounce-and-dispatch [:generic-scanner/scan-success address] 300) (debounce/debounce-and-dispatch [:navigate-change-tab :wallet-stack] 300)) (eip681-address? scanned-text) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/view.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/view.cljs index ca464a0ffd..ff9b67bfb1 100644 --- a/src/status_im/contexts/wallet/add_account/add_address_to_watch/view.cljs +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/view.cljs @@ -6,10 +6,10 @@ [react-native.core :as rn] [reagent.core :as reagent] [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.common.validation :as validation] [status-im.subs.wallet.add-account.address-to-watch] + [utils.address :as utils-address] [utils.debounce :as debounce] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -20,13 +20,9 @@ (or (nil? user-input) (= user-input "")) nil (contains? known-addresses user-input) (i18n/label :t/address-already-in-use) (not - (or (validation/eth-address? user-input) + (or (utils-address/supported-address? user-input) (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 [{:keys [input-value validation-msg validate clear-input]}] (let [scanned-address (rf/sub [:wallet/scanned-address]) @@ -141,10 +137,12 @@ (= activity-state :scanning) (not validated-address)) :on-press (fn [] - (rf/dispatch [:navigate-to - :screen/wallet.confirm-address-to-watch - {:address (extract-address - validated-address)}]) + (rf/dispatch + [:navigate-to + :screen/wallet.confirm-address-to-watch + {:address + (utils-address/extract-address-without-chains-info + validated-address)}]) (clear-input)) :container-style {:z-index 2}} (i18n/label :t/continue)]} diff --git a/src/status_im/contexts/wallet/common/scan_account/view.cljs b/src/status_im/contexts/wallet/common/scan_account/view.cljs index 966c6d0ade..907850a28e 100644 --- a/src/status_im/contexts/wallet/common/scan_account/view.cljs +++ b/src/status_im/contexts/wallet/common/scan_account/view.cljs @@ -1,28 +1,11 @@ (ns status-im.contexts.wallet.common.scan-account.view (:require - [clojure.string :as string] [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.i18n :as i18n] [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 [] (let [{:keys [on-result]} (rf/sub [:get-screen-params])] @@ -30,9 +13,9 @@ {:title (i18n/label :t/scan-qr) :subtitle (i18n/label :t/scan-an-address-qr-code) :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] - (let [address (extract-address result)] + (let [address (utils-address/supported-address->status-address result)] (when on-result (on-result address)) (debounce/debounce-and-dispatch [:wallet/scan-address-success address] diff --git a/src/status_im/contexts/wallet/common/validation.cljs b/src/status_im/contexts/wallet/common/validation.cljs index 532c2d61ba..3e640f4f52 100644 --- a/src/status_im/contexts/wallet/common/validation.cljs +++ b/src/status_im/contexts/wallet/common/validation.cljs @@ -2,7 +2,6 @@ (:require [status-im.constants :as constants])) (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? [s] (or (re-find constants/regx-private-key-hex s) diff --git a/src/status_im/contexts/wallet/send/select_address/view.cljs b/src/status_im/contexts/wallet/send/select_address/view.cljs index b16770b34e..4c2e91579f 100644 --- a/src/status_im/contexts/wallet/send/select_address/view.cljs +++ b/src/status_im/contexts/wallet/send/select_address/view.cljs @@ -12,12 +12,12 @@ [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.networks :as network-utils] - [status-im.contexts.wallet.common.validation :as validation] [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.tabs.view :as tabs] [status-im.feature-flags :as ff] [status-im.setup.hot-reload :as hot-reload] + [utils.address :as utils-address] [utils.debounce :as debounce] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -33,9 +33,9 @@ [address] (debounce/debounce-and-dispatch (cond - (<= (count address) 0) [:wallet/address-validation-failed address] - (validation/eth-address? address) [:wallet/address-validation-success address] - :else [:wallet/address-validation-failed address]) + (<= (count address) 0) [:wallet/address-validation-failed address] + (utils-address/eip-3770-address? address) [:wallet/address-validation-success address] + :else [:wallet/address-validation-failed address]) 300)) (defn- address-input @@ -58,7 +58,7 @@ {:on-result on-result}])) :ens-regex constants/regx-ens :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] (when (or (= current-screen-id :screen/wallet.select-address) (= current-screen-id :screen/wallet.scan-address)) diff --git a/src/utils/address.cljs b/src/utils/address.cljs index f19f412b2f..1b964751fc 100644 --- a/src/utils/address.cljs +++ b/src/utils/address.cljs @@ -4,7 +4,12 @@ [native-module.core :as native-module] [utils.ethereum.eip.eip55 :as eip55])) + (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 [hex] @@ -77,3 +82,49 @@ [value] (when 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)) diff --git a/src/utils/address_test.cljs b/src/utils/address_test.cljs index 56725c03de..3dd434747a 100644 --- a/src/utils/address_test.cljs +++ b/src/utils/address_test.cljs @@ -39,3 +39,50 @@ (testing "Ensure the function returns nil when given 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)))))))