diff --git a/src/react_native/wallet_connect.cljs b/src/react_native/wallet_connect.cljs index ee1e6ff68b..e7809efb2b 100644 --- a/src/react_native/wallet_connect.cljs +++ b/src/react_native/wallet_connect.cljs @@ -1,7 +1,8 @@ (ns react-native.wallet-connect (:require ["@walletconnect/core" :refer [Core]] - ["@walletconnect/utils" :refer [buildApprovedNamespaces getSdkError]] + ["@walletconnect/utils" :refer + [buildApprovedNamespaces getSdkError parseUri]] ["@walletconnect/web3wallet" :refer [Web3Wallet]])) (defn- wallet-connect-core @@ -24,3 +25,9 @@ (defn get-sdk-error [error-key] (getSdkError error-key)) + +(defn parse-uri + [uri] + (-> uri + parseUri + (js->clj :keywordize-keys true))) diff --git a/src/status_im/config.cljs b/src/status_im/config.cljs index 472a48a248..89e0022297 100644 --- a/src/status_im/config.cljs +++ b/src/status_im/config.cljs @@ -24,7 +24,7 @@ (goog-define ALCHEMY_OPTIMISM_MAINNET_TOKEN "") (goog-define ALCHEMY_OPTIMISM_GOERLI_TOKEN "") (goog-define ALCHEMY_OPTIMISM_SEPOLIA_TOKEN "") -(goog-define WALLET_CONNECT_PROJECT_ID "") +(goog-define WALLET_CONNECT_PROJECT_ID "87815d72a81d739d2a7ce15c2cfdefb3") (def mainnet-rpc-url (str "https://eth-archival.rpc.grove.city/v1/" POKT_TOKEN)) (def goerli-rpc-url (str "https://goerli-archival.gateway.pokt.network/v1/lb/" POKT_TOKEN)) diff --git a/src/status_im/contexts/shell/qr_reader/view.cljs b/src/status_im/contexts/shell/qr_reader/view.cljs index a30c1fad86..3868e1b5cb 100644 --- a/src/status_im/contexts/shell/qr_reader/view.cljs +++ b/src/status_im/contexts/shell/qr_reader/view.cljs @@ -1,17 +1,18 @@ (ns status-im.contexts.shell.qr-reader.view - (:require - [clojure.string :as string] - [react-native.core :as rn] - [react-native.hooks :as hooks] - [status-im.common.router :as router] - [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] - [utils.debounce :as debounce] - [utils.ethereum.eip.eip681 :as eip681] - [utils.i18n :as i18n] - [utils.url :as url])) + (:require [clojure.string :as string] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [status-im.common.router :as router] + [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.debounce :as debounce] + [utils.ethereum.eip.eip681 :as eip681] + [utils.i18n :as i18n] + [utils.url :as url])) (def invalid-qr-toast {:type :negative @@ -42,10 +43,6 @@ [_] false) -(defn wallet-connect-code? - [scanned-text] - (string/starts-with? scanned-text "wc:")) - (defn url? [scanned-text] (url/url? scanned-text)) @@ -68,6 +65,12 @@ [:toasts/upsert invalid-qr-toast] 300)) +(defn- handle-wallet-connect + [scanned-text] + (debounce/debounce-and-dispatch + [:wallet-connect/on-scan-connection scanned-text] + 300)) + (defn on-qr-code-scanned [scanned-text] (cond @@ -100,9 +103,10 @@ ;; TODO: https://github.com/status-im/status-mobile/issues/18744 nil - (wallet-connect-code? scanned-text) - ;; WalletConnect is not working yet, this flow should be updated once WalletConnect is ready - nil + (and + (wc-utils/valid-uri? scanned-text) + (ff/enabled? ::ff/wallet.wallet-connect)) + (handle-wallet-connect scanned-text) (url? scanned-text) (debounce/debounce-and-dispatch [:browser.ui/open-url scanned-text] 300) 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 585fab6e42..6a35b53b80 100644 --- a/src/status_im/contexts/wallet/common/scan_account/view.cljs +++ b/src/status_im/contexts/wallet/common/scan_account/view.cljs @@ -1,10 +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.debounce :as debounce] - [utils.i18n :as i18n] - [utils.re-frame :as rf])) + (:require + [clojure.string :as string] + [status-im.common.scan-qr-code.view :as scan-qr-code] + [status-im.constants :as constants] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) (def ^:private supported-networks #{:eth :arb1 :oeth}) diff --git a/src/status_im/contexts/wallet/wallet_connect/events.cljs b/src/status_im/contexts/wallet/wallet_connect/events.cljs index 08177376f4..f98ebabd65 100644 --- a/src/status_im/contexts/wallet/wallet_connect/events.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/events.cljs @@ -1,12 +1,15 @@ (ns status-im.contexts.wallet.wallet-connect.events (:require [re-frame.core :as rf] + [react-native.wallet-connect :as wallet-connect] [status-im.constants :as constants] [status-im.contexts.wallet.wallet-connect.core :as wallet-connect-core] status-im.contexts.wallet.wallet-connect.effects status-im.contexts.wallet.wallet-connect.processing-events status-im.contexts.wallet.wallet-connect.responding-events + [status-im.contexts.wallet.wallet-connect.utils :as wc-utils] [taoensso.timbre :as log] - [utils.ethereum.chain :as chain])) + [utils.ethereum.chain :as chain] + [utils.i18n :as i18n])) (rf/reg-event-fx :wallet-connect/init @@ -51,7 +54,9 @@ :wallet-connect/on-session-proposal (fn [{:keys [db]} [proposal]] (log/info "Received Wallet Connect session proposal: " {:id (:id proposal)}) - {:db (assoc db :wallet-connect/current-proposal proposal)})) + {:db (assoc db :wallet-connect/current-proposal proposal) + :fx [[:dispatch + [:open-modal :screen/wallet.wallet-connect-session-proposal]]]})) (rf/reg-event-fx :wallet-connect/on-session-request @@ -153,3 +158,32 @@ :event :wallet-connect/approve-session}) (rf/dispatch [:wallet-connect/reset-current-session-proposal]))}]]}))) + +(rf/reg-event-fx + :wallet-connect/on-scan-connection + (fn [_ [scanned-text]] + (let [parsed-uri (wallet-connect/parse-uri scanned-text) + version (:version parsed-uri) + expired? (-> parsed-uri + :expiryTimestamp + wc-utils/timestamp-expired?) + version-supported? (wc-utils/version-supported? version)] + (cond + expired? + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text (i18n/label :t/wallet-connect-qr-expired)}]]]} + + (not version-supported?) + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text (i18n/label :t/wallet-connect-version-not-supported + {:version version})}]]]} + + :else + {:fx [[:dispatch [:wallet-connect/pair scanned-text]] + [:dispatch [:dismiss-modal :screen/wallet.wallet-connect-session-proposal]]]})))) diff --git a/src/status_im/contexts/wallet/wallet_connect/session_proposal/style.cljs b/src/status_im/contexts/wallet/wallet_connect/session_proposal/style.cljs new file mode 100644 index 0000000000..b99de1ef5d --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/session_proposal/style.cljs @@ -0,0 +1,31 @@ +(ns status-im.contexts.wallet.wallet-connect.session-proposal.style + (:require [quo.foundations.colors :as colors])) + +(def dapp-avatar + {:padding-horizontal 20 + :padding-top 12}) + +(def approval-note-container + {:margin-horizontal 20 + :padding 12 + :border-radius 16 + :border-width 1 + :border-color colors/neutral-10 + :background-color colors/neutral-2_5}) + +(def approval-note-title + {:color colors/neutral-50 + :margin-bottom 8}) + +(def approval-note-li + {:flex 1 + :flex-direction :row + :align-items :center}) + +(def approval-li-spacer + {:width 8}) + +(def detail-item + {:margin-bottom 20 + :margin-horizontal 20 + :padding 12}) diff --git a/src/status_im/contexts/wallet/wallet_connect/session_proposal/view.cljs b/src/status_im/contexts/wallet/wallet_connect/session_proposal/view.cljs new file mode 100644 index 0000000000..c8f9c43971 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/session_proposal/view.cljs @@ -0,0 +1,119 @@ +(ns status-im.contexts.wallet.wallet-connect.session-proposal.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [quo.theme] + [react-native.core :as rn] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.wallet-connect.session-proposal.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- dapp-metadata + [] + (let [proposer (rf/sub [:wallet-connect/session-proposer]) + {:keys [icons name url]} (:metadata proposer)] + [:<> + [rn/view {:style style/dapp-avatar} + [quo/user-avatar + {:profile-picture (first icons) + :size :big}]] + [quo/page-top + {:title name + :description :context-tag + :context-tag {:type :icon + :size 32 + :icon :i/link + :context url}}]])) + +(defn- approval-note + [] + (let [dapp-name (rf/sub [:wallet-connect/session-proposer-name]) + labels [(i18n/label :t/check-your-account-balance-and-activity) + (i18n/label :t/request-txns-and-message-signing)]] + [rn/view {:style style/approval-note-container} + [quo/text {:style style/approval-note-title} + (i18n/label :t/dapp-will-be-able-to {:dapp-name dapp-name})] + (map-indexed + (fn [idx label] + ^{:key (str idx label)} + [rn/view {:style style/approval-note-li} + [quo/icon :i/bullet + {:color colors/neutral-50}] + [rn/view {:style style/approval-li-spacer}] + [quo/text label]]) + labels)])) + +(defn- accounts-data-item + [] + ;; TODO. This account is currently hard coded in + ;; `status-im.contexts.wallet.wallet-connect.events`. Should be selectable and changeable + (let [accounts (rf/sub [:wallet/accounts-without-watched-accounts]) + name (-> accounts first :name)] + [quo/data-item + {:container-style style/detail-item + :blur? false + :description :default + :icon-right? true + :right-icon :i/chevron-right + :icon-color colors/neutral-10 + :card? false + :label :preview + ;; TODO. The quo component for data item doesn't support showing accounts yet + :status :default + :size :small + :title (i18n/label :t/account-title) + :subtitle name}])) + +(defn- networks-data-item + [] + [quo/data-item + {:container-style style/detail-item + :blur? false + :description :default + :icon-right? true + :card? true + :label :none + :status :default + :size :small + :title (i18n/label :t/networks) + ;; TODO. The quo component for data-item does not support showing networks yet + :subtitle "Networks will show up here"}]) + +(defn- footer + [] + (let [customization-color (rf/sub [:profile/customization-color])] + [quo/bottom-actions + {:actions :two-actions + :button-two-label (i18n/label :t/decline) + :button-two-props {:type :grey + :accessibility-label :wc-deny-connection + :on-press #(do (rf/dispatch [:navigate-back]) + (rf/dispatch + [:wallet-connect/reset-current-session]))} + :button-one-label (i18n/label :t/connect) + :button-one-props {:customization-color customization-color + :type :primary + :accessibility-label :wc-connect + :on-press #(rf/dispatch [:wallet-connect/approve-session])}}])) + +(defn- header + [] + [quo/page-nav + {:type :no-title + :background :blur + :icon-name :i/close + :on-press (rn/use-callback #(rf/dispatch [:navigate-back])) + :accessibility-label :wc-session-proposal-top-bar}]) + +(defn view + [] + [floating-button-page/view + {:footer-container-padding 0 + :header [header] + :footer [footer]} + [rn/view + [dapp-metadata] + [accounts-data-item] + [networks-data-item] + [approval-note]]]) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils.cljs b/src/status_im/contexts/wallet/wallet_connect/utils.cljs new file mode 100644 index 0000000000..736bab1d88 --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils.cljs @@ -0,0 +1,29 @@ +(ns status-im.contexts.wallet.wallet-connect.utils + (:require [react-native.wallet-connect :as wallet-connect])) + +(defn version-supported? + [version] + (= version 2)) + +(defn- current-timestamp + [] + (quot (.getTime (js/Date.)) 1000)) + +(defn timestamp-expired? + [expiry-timestamp] + (> (current-timestamp) expiry-timestamp)) + +(defn valid-wc-uri? + [parsed-uri] + (let [{:keys [topic version expiryTimestamp]} parsed-uri] + (and (seq topic) + (number? version) + (number? expiryTimestamp)))) + +(defn valid-uri? + "Check if the uri is in the wallet-connect format. + At this stage, the uri might be expired or from an unsupported version" + [s] + (-> s + wallet-connect/parse-uri + valid-wc-uri?)) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index 1e6668ce88..3967cb5d7d 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -109,6 +109,7 @@ [status-im.contexts.wallet.send.transaction-confirmation.view :as wallet-transaction-confirmation] [status-im.contexts.wallet.send.transaction-progress.view :as wallet-transaction-progress] [status-im.contexts.wallet.swap.select-asset-to-pay.view :as wallet-swap-select-asset-to-pay] + [status-im.contexts.wallet.wallet-connect.session-proposal.view :as wallet-connect-session-proposal] [status-im.contexts.wallet.wallet-connect.sign-message.view :as wallet-connect-sign-message] [status-im.navigation.options :as options] [status-im.navigation.transitions :as transitions])) @@ -392,6 +393,10 @@ :options {:insets {:top? true}} :component wallet-connected-dapps/view} + {:name :screen/wallet.wallet-connect-session-proposal + :options {:sheet? true} + :component wallet-connect-session-proposal/view} + {:name :screen/wallet.edit-account :component wallet-edit-account/view} diff --git a/src/status_im/subs/wallet/wallet_connect.cljs b/src/status_im/subs/wallet/wallet_connect.cljs index e0694af70c..d523d144b0 100644 --- a/src/status_im/subs/wallet/wallet_connect.cljs +++ b/src/status_im/subs/wallet/wallet_connect.cljs @@ -30,3 +30,15 @@ {:customization-color customization-color :name name :emoji emoji}))) + +(rf/reg-sub + :wallet-connect/session-proposer + :<- [:wallet-connect/current-proposal] + (fn [proposal] + (-> proposal :params :proposer))) + +(rf/reg-sub + :wallet-connect/session-proposer-name + :<- [:wallet-connect/session-proposer] + (fn [proposer] + (-> proposer :metadata :name))) diff --git a/src/status_im/subs/wallet/wallet_connect_test.cljs b/src/status_im/subs/wallet/wallet_connect_test.cljs new file mode 100644 index 0000000000..097335ed4c --- /dev/null +++ b/src/status_im/subs/wallet/wallet_connect_test.cljs @@ -0,0 +1,68 @@ +(ns status-im.subs.wallet.wallet-connect-test + (:require + [cljs.test :refer [is testing]] + [re-frame.db :as rf-db] + status-im.subs.root + status-im.subs.wallet.wallet-connect + [test-helpers.unit :as h] + [utils.re-frame :as rf])) + +(def sample-session + {:id 1716798889093634 + :params + {:id 1716798889093634 + :pairingTopic "9b18e1348817a548bbc97f9b4a09278f4fdf7c984e4a61ddf461bd1f57710d33" + :expiryTimestamp 1716799189 + :requiredNamespaces {} + :optionalNamespaces {:eip155 + {:chains ["eip155:1" "eip155:42161" "eip155:137" "eip155:43114" "eip155:56" + "eip155:10" "eip155:100" + "eip155:324" "eip155:7777777" "eip155:8453" "eip155:42220" + "eip155:1313161554" "eip155:11155111" "eip155:11155420"] + :methods ["personal_sign" "eth_accounts" "eth_requestAccounts" + "eth_sendRawTransaction" "eth_sendTransaction" + "eth_sign" "eth_signTransaction" "eth_signTypedData" + "eth_signTypedData_v3" "eth_signTypedData_v4" + "wallet_addEthereumChain" "wallet_getCallsStatus" + "wallet_getCapabilities" "wallet_getPermissions" + "wallet_registerOnboarding" "wallet_requestPermissions" + "wallet_scanQRCode" "wallet_sendCalls" + "wallet_showCallsStatus" "wallet_switchEthereumChain" + "wallet_watchAsset"] + :events ["chainChanged" "accountsChanged"]}} + :relays [{:protocol "irn"}] + :proposer {:publicKey "cddea055b8974d93380e6c7e72110145506c06524047866f8034f3db0990137a" + :metadata {:name "Web3Modal" + :description "Web3Modal Laboratory" + :url "https://lab.web3modal.com" + :icons ["https://avatars.githubusercontent.com/u/37784886"]}}} + :verifyContext {:verified {:verifyUrl "https://verify.walletconnect.com" + :validation "VALID" + :origin "https://lab.web3modal.com" + :isScam false}}}) + +(h/deftest-sub :wallet-connect/session-proposer + [sub-name] + (testing "Return the session proposer public key and metadata" + (swap! rf-db/app-db + assoc + :wallet-connect/current-proposal + sample-session) + + (let [proposer (rf/sub [sub-name])] + (is (= (-> proposer :publicKey) + (-> sample-session :params :proposer :publicKey))) + + (is (= (-> proposer :metadata :url) + (-> sample-session :params :proposer :metadata :url)))))) + +(h/deftest-sub :wallet-connect/session-proposer-name + [sub-name] + (testing "Return only the name of the session proposer" + (swap! rf-db/app-db + assoc + :wallet-connect/current-proposal + sample-session) + + (is (= (-> sample-session :params :proposer :metadata :name) + (rf/sub [sub-name]))))) diff --git a/translations/en.json b/translations/en.json index 0ca35c7e1d..cb7bedb71e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1049,6 +1049,7 @@ "language-and-currency": "Language and currency", "opening-buy-crypto": "Opening {{site}}...", "network": "Network", + "networks": "Networks", "network-chain": "Network chain", "network-fee": "Network fee", "network-id": "Network ID", @@ -2682,5 +2683,10 @@ "you-cannot-add-your-own-account-as-a-saved-address": "You cannot add your own account as a saved address", "this-address-is-already-saved": "This address is already saved", "this-ens-name-is-not-registered-yet": "This ENS name is not registered yet", - "address-saved": "Address saved" + "address-saved": "Address saved", + "dapp-will-be-able-to": "{{dapp-name}} will be able to:", + "check-your-account-balance-and-activity": "Check your account balance and activity", + "request-txns-and-message-signing": "Request transactions and message signing", + "wallet-connect-qr-expired": "WalletConnect QR has expired", + "wallet-connect-version-not-supported": "WalletConnect version {{version}} is not supported" }