🤝 🔁 [19834] Add dapp connection flow with a basic design (#20204) (#20325)

* 🔳 QR on success not being called

- IDK why, trying things out

* 🙃 Stupid of me

- My handler was not being called because I wrote the code in a
different QR scanner

*  Approval screen taking shape

* 🧹 Lint fix

* 🎛️ Wallet connect session screen shows up

- Hard coded the first account
- The data item component doesn't support networks or accounts yet
- The quo category component cannot show a data-item yet
- Connected accept and decline button

* 🧰 Fix review issues

* 🔨 Fix lint

* 🔧 Rename event and move dispatch

* 🔧 Fix lint

* 🎏 Add ff for wc scanner

- Bring back missing event
This commit is contained in:
Shivek Khurana 2024-06-05 16:17:19 +05:30 committed by GitHub
parent 901dddc603
commit 379ba87c16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 347 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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