Wallet Connect transactions (#20755)

* feat: updated signing endpoints and refactor

6e056348...e8aec741

* fix: using the generic warning at the bottom

* fix: show parsed transaction parameters

* feat: adding fees to transactions

6e056348...b2e5e7a8

* feat: added fees

* feat: added eip-1559 fee estimation & tx priority

* feat: added fees and failed processing handling

* fix: show testnet name in the request

* fix: address review comments

* feat: added max-fee color when not enough balance

* ref: broke down tx fees subscription

* fix: handle gas estimation on status-go

1ef2434b...5389f281

* fix: don't overwrite dynamic fees if already there

* fix: malli schema and review comments

* fix: addressed review comments 1

* fix: addressed comments 2

* fix: removed unused require

* fix: addressed QA review

484b8aca...d07f9b5b

* fix: requests being shown simultaneously

* fix: removed support for eth_signTransaction
This commit is contained in:
Lungu Cristian 2024-07-19 14:16:51 +03:00 committed by GitHub
parent 84b8943fd4
commit 02e24208db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 393 additions and 169 deletions

View File

@ -121,6 +121,7 @@
[:card? {:optional true} [:maybe :boolean]] [:card? {:optional true} [:maybe :boolean]]
[:right-icon {:optional true} [:maybe :keyword]] [:right-icon {:optional true} [:maybe :keyword]]
[:right-content {:optional true} [:maybe :map]] [:right-content {:optional true} [:maybe :map]]
[:icon-color {:optional true} [:maybe :schema.common/customization-color]]
[:status {:optional true} [:maybe [:enum :default :loading]]] [:status {:optional true} [:maybe [:enum :default :loading]]]
[:subtitle-type {:optional true} [:maybe [:enum :default :icon :network :account :editable]]] [:subtitle-type {:optional true} [:maybe [:enum :default :icon :network :account :editable]]]
[:size {:optional true} [:maybe [:enum :default :small :large]]] [:size {:optional true} [:maybe [:enum :default :small :large]]]

View File

@ -281,7 +281,8 @@
#{wallet-connect-personal-sign-method #{wallet-connect-personal-sign-method
wallet-connect-eth-sign-method wallet-connect-eth-sign-method
wallet-connect-eth-send-transaction-method wallet-connect-eth-send-transaction-method
wallet-connect-eth-sign-transaction-method ;; NOTE: disabled, as we have no clear use cases for it and other wallets don't support it
;; wallet-connect-eth-sign-transaction-method
wallet-connect-eth-sign-typed-method wallet-connect-eth-sign-typed-method
wallet-connect-eth-sign-typed-v4-method}) wallet-connect-eth-sign-typed-v4-method})
(def ^:const wallet-connect-supported-events #{"accountsChanged" "chainChanged"}) (def ^:const wallet-connect-supported-events #{"accountsChanged" "chainChanged"})
@ -512,6 +513,9 @@
(def ^:const optimism-full-name "Optimism") (def ^:const optimism-full-name "Optimism")
(def ^:const arbitrum-full-name "Arbitrum") (def ^:const arbitrum-full-name "Arbitrum")
(def ^:const sepolia-full-name "Sepolia")
(def ^:const goerli-full-name "Goerli")
(def ^:const mainnet-network-name :mainnet) (def ^:const mainnet-network-name :mainnet)
(def ^:const ethereum-network-name :ethereum) (def ^:const ethereum-network-name :ethereum)
(def ^:const optimism-network-name :optimism) (def ^:const optimism-network-name :optimism)

View File

@ -172,17 +172,20 @@
(defn get-network-details (defn get-network-details
[chain-id] [chain-id]
(condp contains? chain-id (as-> chain-id $
#{constants/ethereum-mainnet-chain-id constants/ethereum-goerli-chain-id (condp contains? $
constants/ethereum-sepolia-chain-id} #{constants/ethereum-mainnet-chain-id constants/ethereum-goerli-chain-id
mainnet-network-details constants/ethereum-sepolia-chain-id}
mainnet-network-details
#{constants/arbitrum-mainnet-chain-id constants/arbitrum-goerli-chain-id #{constants/arbitrum-mainnet-chain-id constants/arbitrum-goerli-chain-id
constants/arbitrum-sepolia-chain-id} constants/arbitrum-sepolia-chain-id}
arbitrum-network-details arbitrum-network-details
#{constants/optimism-mainnet-chain-id constants/optimism-goerli-chain-id #{constants/optimism-mainnet-chain-id constants/optimism-goerli-chain-id
constants/optimism-sepolia-chain-id} constants/optimism-sepolia-chain-id}
optimism-network-details optimism-network-details
nil)) nil)
(when $
(assoc $ :chain-id chain-id))))

View File

@ -120,7 +120,7 @@
[] []
[quo/alert-banner [quo/alert-banner
{:action? true {:action? true
:text (i18n/label :t/not-enough-assets) :text (i18n/label :t/not-enough-assets-to-pay-gas-fees)
:button-text (i18n/label :t/buy-eth) :button-text (i18n/label :t/buy-eth)
:on-button-press #(rf/dispatch [:show-bottom-sheet :on-button-press #(rf/dispatch [:show-bottom-sheet
{:content buy-token/view}])}]) {:content buy-token/view}])}])

View File

@ -100,6 +100,17 @@
networks (get-in db [:wallet :networks (if test-mode? :test :prod)])] networks (get-in db [:wallet :networks (if test-mode? :test :prod)])]
(mapv #(-> % :chain-id) networks))) (mapv #(-> % :chain-id) networks)))
(defn add-full-testnet-name
"Updates the `:full-name` key with the full testnet name if using testnet `:chain-id`.\n
e.g. `{:full-name \"Mainnet\"}` -> `{:full-name \"Mainnet Sepolia\"`}`"
[network]
(let [add-testnet-name (fn [testnet-name]
(update network :full-name #(str % " " testnet-name)))]
(condp #(contains? %1 %2) (:chain-id network)
constants/sepolia-chain-ids (add-testnet-name constants/sepolia-full-name)
constants/goerli-chain-ids (add-testnet-name constants/goerli-full-name)
network)))
(defn event-should-be-handled? (defn event-should-be-handled?
[db {:keys [topic]}] [db {:keys [topic]}]
(some #(= topic %) (some #(= topic %)

View File

@ -83,22 +83,33 @@
(promesa/then on-success) (promesa/then on-success)
(promesa/catch on-error))))) (promesa/catch on-error)))))
(rf/reg-fx
:effects.wallet-connect/prepare-transaction
(fn [{:keys [tx chain-id on-success on-error]}]
(-> (transactions/prepare-transaction tx
chain-id
transactions/default-tx-priority)
(promesa/then on-success)
(promesa/catch on-error))))
(rf/reg-fx (rf/reg-fx
:effects.wallet-connect/sign-transaction :effects.wallet-connect/sign-transaction
(fn [{:keys [password address chain-id tx on-success on-error]}] (fn [{:keys [password address chain-id tx-hash tx-args on-success on-error]}]
(-> (transactions/sign-transaction (security/safe-unmask-data password) (-> (transactions/sign-transaction (security/safe-unmask-data password)
address address
tx tx-hash
tx-args
chain-id) chain-id)
(promesa/then on-success) (promesa/then on-success)
(promesa/catch on-error)))) (promesa/catch on-error))))
(rf/reg-fx (rf/reg-fx
:effects.wallet-connect/send-transaction :effects.wallet-connect/send-transaction
(fn [{:keys [password address chain-id tx on-success on-error]}] (fn [{:keys [password address chain-id tx-hash tx-args on-success on-error]}]
(-> (transactions/send-transaction (security/safe-unmask-data password) (-> (transactions/send-transaction (security/safe-unmask-data password)
address address
tx tx-hash
tx-args
chain-id) chain-id)
(promesa/then on-success) (promesa/then on-success)
(promesa/catch on-error)))) (promesa/catch on-error))))

View File

@ -63,7 +63,7 @@
networks) networks)
required-networks-supported? (wallet-connect-core/required-networks-supported? proposal required-networks-supported? (wallet-connect-core/required-networks-supported? proposal
networks)] networks)]
(if required-networks-supported? (if (and (not-empty session-networks) required-networks-supported?)
{:db (update db {:db (update db
:wallet-connect/current-proposal assoc :wallet-connect/current-proposal assoc
:request proposal :request proposal

View File

@ -0,0 +1,31 @@
(ns status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view
(:require [quo.core :as quo]
[quo.foundations.colors :as colors]
[quo.theme]
[status-im.contexts.wallet.wallet-connect.modals.common.style :as style]
[utils.i18n :as i18n]))
(defn- fees-subtitle
[{:keys [text error?]}]
(let [theme (quo.theme/use-theme)]
[quo/text
{:weight :medium
:size :paragraph-2
:style {:color (if error?
(colors/resolve-color :danger theme)
(colors/theme-colors colors/neutral-100
colors/white
theme))}}
text]))
(defn view
[{:keys [fees fees-error]}]
[quo/data-item
{:size :small
:status :default
:card? false
:container-style style/data-item
:title (i18n/label :t/max-fees)
:custom-subtitle (fn [] [fees-subtitle
{:text (or fees (i18n/label :t/no-fees))
:error? (= fees-error :not-enough-assets-to-pay-gas-fees)}])}])

View File

@ -13,7 +13,7 @@
(rf/dispatch [:wallet-connect/respond-current-session password])) (rf/dispatch [:wallet-connect/respond-current-session password]))
(defn view (defn view
[{:keys [warning-label slide-button-text disabed?]} & children] [{:keys [warning-label slide-button-text disabled?]} & children]
(let [{:keys [customization-color]} (rf/sub [:wallet-connect/current-request-account-details])] (let [{:keys [customization-color]} (rf/sub [:wallet-connect/current-request-account-details])]
[rn/view {:style style/content-container} [rn/view {:style style/content-container}
(into [rn/view (into [rn/view
@ -23,7 +23,7 @@
[standard-authentication/slide-button [standard-authentication/slide-button
{:size :size-48 {:size :size-48
:track-text slide-button-text :track-text slide-button-text
:disabled? disabed? :disabled? disabled?
:customization-color customization-color :customization-color customization-color
:on-auth-success on-auth-success :on-auth-success on-auth-success
:auth-button-label (i18n/label :t/confirm)}]] :auth-button-label (i18n/label :t/confirm)}]]

View File

@ -1,8 +1,11 @@
(ns status-im.contexts.wallet.wallet-connect.modals.send-transaction.view (ns status-im.contexts.wallet.wallet-connect.modals.send-transaction.view
(:require [quo.core :as quo] (:require [quo.core :as quo]
[quo.theme]
[react-native.core :as rn] [react-native.core :as rn]
[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.contexts.wallet.wallet-connect.modals.common.data-block.view :as data-block]
[status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as
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]
[status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header] [status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header]
[status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav] [status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav]
@ -40,7 +43,7 @@
:not-enough-assets :not-enough-assets
:t/not-enough-assets))}]) :t/not-enough-assets))}])
[footer/view [footer/view
{:warning-label (i18n/label :t/wallet-connect-send-transaction-warning) {:warning-label (i18n/label :t/wallet-connect-sign-warning)
:slide-button-text (i18n/label :t/slide-to-send) :slide-button-text (i18n/label :t/slide-to-send)
:disabled? error-state} :disabled? error-state}
[quo/data-item [quo/data-item
@ -51,11 +54,7 @@
:subtitle-type :network :subtitle-type :network
:network-image (:source network) :network-image (:source network)
:subtitle (:full-name network)}] :subtitle (:full-name network)}]
[quo/data-item [fees-data-item/view
{:size :small {:fees max-fees-fiat-formatted
:status :default :fees-error error-state}]]]]))
:card? false
:container-style style/data-item
:title (i18n/label :t/max-fees)
:subtitle (or max-fees-fiat-formatted (i18n/label :t/no-fees))}]]]]))

View File

@ -3,6 +3,8 @@
[react-native.core :as rn] [react-native.core :as rn]
[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.contexts.wallet.wallet-connect.modals.common.data-block.view :as data-block]
[status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as
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]
[status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header] [status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header]
[status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav] [status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav]
@ -28,13 +30,7 @@
:account account}] :account account}]
[data-block/view]] [data-block/view]]
[footer/view [footer/view
{:warning-label (i18n/label :t/wallet-connect-sign-message-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)}
[quo/data-item [fees-data-item/view]]]]))
{:size :small
:status :default
:card? false
:container-style style/data-item
:title (i18n/label :t/max-fees)
:subtitle (i18n/label :t/no-fees)}]]]]))

View File

@ -3,6 +3,8 @@
[react-native.core :as rn] [react-native.core :as rn]
[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.contexts.wallet.wallet-connect.modals.common.data-block.view :as data-block]
[status-im.contexts.wallet.wallet-connect.modals.common.fees-data-item.view :as
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]
[status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header] [status-im.contexts.wallet.wallet-connect.modals.common.header.view :as header]
[status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav] [status-im.contexts.wallet.wallet-connect.modals.common.page-nav.view :as page-nav]
@ -40,7 +42,7 @@
:not-enough-assets :not-enough-assets
:t/not-enough-assets))}]) :t/not-enough-assets))}])
[footer/view [footer/view
{:warning-label (i18n/label :t/wallet-connect-sign-transaction-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)
:disabled? error-state} :disabled? error-state}
[quo/data-item [quo/data-item
@ -51,11 +53,7 @@
:subtitle-type :network :subtitle-type :network
:network-image (:source network) :network-image (:source network)
:subtitle (:full-name network)}] :subtitle (:full-name network)}]
[quo/data-item [fees-data-item/view
{:size :small {:fees max-fees-fiat-formatted
:status :default :fees-error error-state}]]]]))
:card? false
:container-style style/data-item
:title (i18n/label :t/max-fees)
:subtitle (or max-fees-fiat-formatted (i18n/label :t/no-fees))}]]]]))

View File

@ -1,18 +1,32 @@
(ns status-im.contexts.wallet.wallet-connect.processing-events (ns status-im.contexts.wallet.wallet-connect.processing-events
(:require [clojure.string :as string] (:require [cljs-bean.core :as bean]
[clojure.string :as string]
[native-module.core :as native-module] [native-module.core :as native-module]
[re-frame.core :as rf] [re-frame.core :as rf]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.contexts.wallet.wallet-connect.core :as wallet-connect-core] [status-im.contexts.wallet.wallet-connect.core :as wallet-connect-core]
[status-im.contexts.wallet.wallet-connect.transactions :as transactions]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[utils.transforms :as transforms])) [utils.transforms :as transforms]))
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/process-session-request :wallet-connect/show-request-modal
(fn [{:keys [db]} [event]] (fn [{:keys [db]}]
(let [method (wallet-connect-core/get-request-method event) (let [event (get-in db [:wallet-connect/current-request :event])
method (wallet-connect-core/get-request-method event)
screen (wallet-connect-core/method-to-screen method)] screen (wallet-connect-core/method-to-screen method)]
(if screen (if screen
{:fx [[:dispatch [:open-modal screen]]]}
(log/error "Didn't find screen for Wallet Connect method"
{:method method
:event :wallet-connect/process-session-request})))))
(rf/reg-event-fx
:wallet-connect/process-session-request
(fn [{:keys [db]} [event]]
(let [method (wallet-connect-core/get-request-method event)
existing-event (get-in db [:wallet-connect/current-request :event])]
;; NOTE: make sure we don't show two requests at the same time
(when-not existing-event
{:db (assoc-in db [:wallet-connect/current-request :event] event) {:db (assoc-in db [:wallet-connect/current-request :event] event)
:fx [(condp = method :fx [(condp = method
constants/wallet-connect-eth-send-transaction-method constants/wallet-connect-eth-send-transaction-method
@ -31,12 +45,7 @@
[:dispatch [:wallet-connect/process-sign-typed]] [:dispatch [:wallet-connect/process-sign-typed]]
constants/wallet-connect-personal-sign-method constants/wallet-connect-personal-sign-method
[:dispatch [:wallet-connect/process-personal-sign]]) [:dispatch [:wallet-connect/process-personal-sign]])]}))))
[:dispatch [:open-modal screen]]]}
(log/error "Didn't find screen for Wallet Connect method"
{:method method
:event :wallet-connect/process-session-request})))))
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/process-personal-sign :wallet-connect/process-personal-sign
@ -48,7 +57,8 @@
assoc assoc
:address (string/lower-case address) :address (string/lower-case address)
:raw-data raw-data :raw-data raw-data
:display-data (or parsed-data raw-data))}))) :display-data (or parsed-data raw-data))
:fx [[:dispatch [:wallet-connect/show-request-modal]]]})))
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/process-eth-sign :wallet-connect/process-eth-sign
@ -60,44 +70,53 @@
assoc assoc
:address (string/lower-case address) :address (string/lower-case address)
:raw-data raw-data :raw-data raw-data
:display-data (or parsed-data raw-data))}))) :display-data (or parsed-data raw-data))
:fx [[:dispatch [:wallet-connect/show-request-modal]]]})))
(rf/reg-event-fx
:wallet-connect/prepare-transaction-success
(fn [{:keys [db]} [prepared-tx chain-id]]
(let [{:keys [tx-args]} prepared-tx
tx (bean/->clj tx-args)
address (-> tx :from string/lower-case)
display-data (transactions/beautify-transaction tx)]
{:db (update-in db
[:wallet-connect/current-request]
assoc
:address address
:raw-data prepared-tx
:transaction tx
:chain-id chain-id
:display-data display-data)
:fx [[:dispatch [:wallet-connect/show-request-modal]]]})))
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/process-eth-send-transaction :wallet-connect/process-eth-send-transaction
(fn [{:keys [db]}] (fn [{:keys [db]}]
(let [event (wallet-connect-core/get-db-current-request-event db) (let [event (wallet-connect-core/get-db-current-request-event db)
display-data (-> event tx (-> event wallet-connect-core/get-request-params first)
clj->js chain-id (-> event
(js/JSON.stringify nil 2)) (get-in [:params :chainId])
wallet-connect-core/eip155->chain-id)]
{:keys [from] :as tx} (-> event wallet-connect-core/get-request-params first) {:fx [[:effects.wallet-connect/prepare-transaction
chain-id (-> event {:tx tx
(get-in [:params :chainId]) :chain-id chain-id
wallet-connect-core/eip155->chain-id)] :on-success #(rf/dispatch [:wallet-connect/prepare-transaction-success % chain-id])
{:db (update-in db :on-error #(rf/dispatch [:wallet-connect/on-processing-error %])}]]})))
[:wallet-connect/current-request]
assoc
:address (string/lower-case from)
:raw-data tx
:chain-id chain-id
:display-data display-data)})))
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/process-eth-sign-transaction :wallet-connect/process-eth-sign-transaction
(fn [{:keys [db]}] (fn [{:keys [db]}]
(let [event (wallet-connect-core/get-db-current-request-event db) (let [event (wallet-connect-core/get-db-current-request-event db)
display-data (.stringify js/JSON (clj->js event) nil 2) tx (-> event wallet-connect-core/get-request-params first)
{:keys [from] :as tx} (-> event wallet-connect-core/get-request-params first) chain-id (-> event
chain-id (-> event (get-in [:params :chainId])
(get-in [:params :chainId]) wallet-connect-core/eip155->chain-id)]
wallet-connect-core/eip155->chain-id)] {:fx [[:effects.wallet-connect/prepare-transaction
{:db (update-in db {:tx tx
[:wallet-connect/current-request] :chain-id chain-id
assoc :on-success #(rf/dispatch [:wallet-connect/prepare-transaction-success % chain-id])
:address (string/lower-case from) :on-error #(rf/dispatch [:wallet-connect/on-processing-error %])}]]})))
:raw-data tx
:chain-id chain-id
:display-data display-data)})))
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/process-sign-typed :wallet-connect/process-sign-typed
@ -108,11 +127,31 @@
(transforms/js-dissoc :types :primaryType) (transforms/js-dissoc :types :primaryType)
(transforms/js-stringify 2)) (transforms/js-stringify 2))
(catch js/Error _ nil))] (catch js/Error _ nil))]
;; TODO: decide if we should proceed if the typed-data is invalid JSON or fail ahead of time (if (nil? parsed-data)
(when (nil? parsed-data) (log/error "Invalid typed data")) {:fx [[:dispatch
{:db (update-in db [:wallet-connect/on-processing-error
[:wallet-connect/current-request] (ex-info "Failed to parse JSON typed data" {:data raw-data})]]]}
assoc {:db (update-in db
:address (string/lower-case address) [:wallet-connect/current-request]
:display-data (or parsed-data raw-data) assoc
:raw-data raw-data)}))) :address (string/lower-case address)
:display-data (or parsed-data raw-data)
:raw-data raw-data)
:fx [[:dispatch [:wallet-connect/show-request-modal]]]}))))
;; TODO: we should reject a request if processing fails
(rf/reg-event-fx
:wallet-connect/on-processing-error
(fn [{:keys [db]} [error]]
(let [{:keys [address event]} (get db :wallet-connect/current-request)
method (wallet-connect-core/get-request-method event)
screen (wallet-connect-core/method-to-screen method)]
(log/error "Failed to process Wallet Connect request"
{:error error
:address address
:method method
:wallet-connect-event event
:event :wallet-connect/on-processing-error})
{:fx [[:dispatch [:dismiss-modal screen]]
[:dispatch [:wallet-connect/reset-current-request]]]})))

View File

@ -58,34 +58,38 @@
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/respond-send-transaction-data :wallet-connect/respond-send-transaction-data
(fn [{:keys [db]} [password]] (fn [{:keys [db]} [password]]
(let [{:keys [chain-id raw-data address]} (get db :wallet-connect/current-request)] (let [{:keys [chain-id raw-data address]} (get db :wallet-connect/current-request)
{:keys [tx-hash tx-args]} raw-data]
{:fx [[:effects.wallet-connect/send-transaction {:fx [[:effects.wallet-connect/send-transaction
{:password password {:password password
:address address :address address
:chain-id chain-id :chain-id chain-id
:tx raw-data :tx-hash tx-hash
:tx-args tx-args
:on-error #(rf/dispatch [:wallet-connect/on-sign-error %]) :on-error #(rf/dispatch [:wallet-connect/on-sign-error %])
:on-success #(rf/dispatch [:wallet-connect/send-response {:result %}])}]]}))) :on-success #(rf/dispatch [:wallet-connect/send-response {:result %}])}]]})))
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/respond-sign-transaction-data :wallet-connect/respond-sign-transaction-data
(fn [{:keys [db]} [password]] (fn [{:keys [db]} [password]]
(let [{:keys [chain-id raw-data address]} (get db :wallet-connect/current-request)] (let [{:keys [chain-id raw-data address]} (get db :wallet-connect/current-request)
{:keys [tx-hash tx-args]} raw-data]
{:fx [[:effects.wallet-connect/sign-transaction {:fx [[:effects.wallet-connect/sign-transaction
{:password password {:password password
:address address :address address
:chain-id chain-id :chain-id chain-id
:tx raw-data :tx-hash tx-hash
:tx-params tx-args
:on-error #(rf/dispatch [:wallet-connect/on-sign-error %]) :on-error #(rf/dispatch [:wallet-connect/on-sign-error %])
:on-success #(rf/dispatch [:wallet-connect/send-response {:result %}])}]]}))) :on-success #(rf/dispatch [:wallet-connect/send-response {:result %}])}]]})))
;; TODO: should reject if "signing" fails
(rf/reg-event-fx (rf/reg-event-fx
:wallet-connect/on-sign-error :wallet-connect/on-sign-error
(fn [{:keys [db]} [error]] (fn [{:keys [db]} [error]]
(let [event (get-in db [:wallet-connect/current-request :event]) (let [{:keys [raw-data address event]} (get db :wallet-connect/current-request)
{:keys [raw-data address]} (get db :wallet-connect/current-request) method (wallet-connect-core/get-request-method event)
method (wallet-connect-core/get-request-method event) screen (wallet-connect-core/method-to-screen method)]
screen (wallet-connect-core/method-to-screen method)]
(log/error "Failed to sign Wallet Connect request" (log/error "Failed to sign Wallet Connect request"
{:error error {:error error
:address address :address address

View File

@ -59,3 +59,8 @@
password password
chain-id chain-id
legacy?)) legacy?))
(defn wallet-get-suggested-fees
[chain-id]
(-> (call-rpc "wallet_getSuggestedFees" chain-id)
(promesa/then transforms/js->clj)))

View File

@ -1,10 +1,24 @@
(ns status-im.contexts.wallet.wallet-connect.transactions (ns status-im.contexts.wallet.wallet-connect.transactions
(:require [cljs-bean.core :as bean] (:require [cljs-bean.core :as bean]
[clojure.string :as string] [clojure.string :as string]
[native-module.core :as native-module]
[promesa.core :as promesa] [promesa.core :as promesa]
[status-im.constants :as constants]
[status-im.contexts.wallet.wallet-connect.core :as core]
[status-im.contexts.wallet.wallet-connect.rpc :as rpc] [status-im.contexts.wallet.wallet-connect.rpc :as rpc]
[utils.money :as money]
[utils.transforms :as transforms])) [utils.transforms :as transforms]))
(defn transaction-request?
[event]
(->> (core/get-request-method event)
(contains? #{constants/wallet-connect-eth-send-transaction-method
constants/wallet-connect-eth-sign-transaction-method})))
;; NOTE: Currently we don't allow the user to configure the tx priority as we don't
;; show the estimated time, but when we implement it, we should allow to change it
(def ^:constant default-tx-priority :medium)
(defn- strip-hex-prefix (defn- strip-hex-prefix
"Strips the extra 0 in hex value if present" "Strips the extra 0 in hex value if present"
[hex-value] [hex-value]
@ -15,12 +29,12 @@
(defn- format-tx-hex-values (defn- format-tx-hex-values
"Due to how status-go expects hex values, we should remove the extra 0s in transaction hex values e.g. 0x0f -> 0xf" "Due to how status-go expects hex values, we should remove the extra 0s in transaction hex values e.g. 0x0f -> 0xf"
[tx] [tx f]
(let [tx-keys [:gasLimit :gas :gasPrice :nonce :value :maxFeePerGas :maxPriorityFeePerGas]] (let [tx-keys [:gasLimit :gas :gasPrice :nonce :value :maxFeePerGas :maxPriorityFeePerGas]]
(reduce (fn [acc tx-key] (reduce (fn [acc tx-key]
(if (and (contains? tx tx-key) (if (and (contains? tx tx-key)
(not (nil? (get tx tx-key)))) (not (nil? (get tx tx-key))))
(update acc tx-key strip-hex-prefix) (update acc tx-key f)
acc)) acc))
tx tx
tx-keys))) tx-keys)))
@ -29,26 +43,100 @@
"Formats the transaction and transforms it into a stringified JS object, ready to be passed to an RPC call." "Formats the transaction and transforms it into a stringified JS object, ready to be passed to an RPC call."
[tx] [tx]
(-> tx (-> tx
format-tx-hex-values ;; NOTE: removing `:nonce` to compute it when building the transaction on status-go
(dissoc :nonce)
(format-tx-hex-values strip-hex-prefix)
bean/->js bean/->js
(transforms/js-stringify 0))) (transforms/js-stringify 0)))
(defn beautify-transaction
[tx]
(let [hex->number #(-> % (subs 2) native-module/hex-to-number)]
(-> tx
(format-tx-hex-values hex->number)
clj->js
(js/JSON.stringify nil 2))))
(defn- gwei->hex
[gwei]
(->> gwei
money/gwei->wei
native-module/number-to-hex
(str "0x")))
(defn- get-max-fee-per-gas-key
"Mapping transaction priority (which determines how quickly a tx is processed)
to the `suggested-routes` key that should be used for `:maxPriorityFeePerGas`.
Returns `:high` | `:medium` | `:low`"
[tx-priority]
(get {:high :maxFeePerGasHigh
:medium :maxFeePerGasMedium
:low :maxFeePerGasLow}
tx-priority))
(defn- dynamic-fee-tx?
"Checks if a transaction has dynamic fees (EIP1559)"
[tx]
(every? tx [:maxFeePerGas :maxPriorityFeePerGas]))
(defn- tx->eip1559-tx
"Adds `:maxFeePerGas` and `:maxPriorityFeePerGas` for dynamic fee support (EIP1559) and
removes `:gasPrice`, if the chain supports EIP1559 and the transaction doesn't already
have dynamic fees."
[tx suggested-fees tx-priority]
(if (and (:eip1559Enabled suggested-fees)
(not (dynamic-fee-tx? tx)))
(let [max-fee-per-gas-key (get-max-fee-per-gas-key tx-priority)
max-fee-per-gas (-> suggested-fees max-fee-per-gas-key gwei->hex)
max-priority-fee-per-gas (-> suggested-fees :maxPriorityFeePerGas gwei->hex)]
(-> tx
(assoc
:maxFeePerGas max-fee-per-gas
:maxPriorityFeePerGas max-priority-fee-per-gas)
;; NOTE: `:gasPrice` is used only for legacy Tx, so we discard it in favor of dynamic fees
(dissoc :gasPrice)))
tx))
(defn- prepare-transaction-fees
"Makes sure the transaction has the correct gas and fees properties"
[tx tx-priority suggested-fees]
(-> (assoc tx
;; NOTE: `gasLimit` is ignored on status-go when building a transaction
;; (`wallet_buildTransaction`), so we're setting it as the `gas` property
:gas
(or (:gasLimit tx)
(:gas tx)))
(dissoc :gasLimit)
(tx->eip1559-tx suggested-fees tx-priority)))
(defn prepare-transaction
"Formats and builds the incoming transaction, adding the missing properties and returning the final
transaction, along with the transaction hash and the suggested fees"
[tx chain-id tx-priority]
(promesa/let [suggested-fees (rpc/wallet-get-suggested-fees chain-id)
{:keys [tx-args message-to-sign]} (->>
(prepare-transaction-fees tx
tx-priority
suggested-fees)
prepare-transaction-for-rpc
(rpc/wallet-build-transaction chain-id))]
{:tx-args tx-args
:tx-hash message-to-sign
:suggested-fees suggested-fees}))
(defn sign-transaction (defn sign-transaction
[password address tx chain-id] [password address tx-hash tx-args chain-id]
(promesa/let (promesa/let
[formatted-tx (prepare-transaction-for-rpc tx) [signature (rpc/wallet-sign-message tx-hash address password)
{:keys [message-to-sign tx-args]} (rpc/wallet-build-transaction chain-id formatted-tx) raw-tx (rpc/wallet-build-raw-transaction chain-id tx-args signature)]
signature (rpc/wallet-sign-message message-to-sign address password)
raw-tx (rpc/wallet-build-raw-transaction chain-id tx-args signature)]
raw-tx)) raw-tx))
(defn send-transaction (defn send-transaction
[password address tx chain-id] [password address tx-hash tx-args chain-id]
(promesa/let (promesa/let
[formatted-tx (prepare-transaction-for-rpc tx) [signature (rpc/wallet-sign-message tx-hash address password)
{:keys [message-to-sign tx-args]} (rpc/wallet-build-transaction chain-id formatted-tx) tx (rpc/wallet-send-transaction-with-signature chain-id
signature (rpc/wallet-sign-message message-to-sign address password) tx-args
tx (rpc/wallet-send-transaction-with-signature chain-id signature)]
tx-args
signature)]
tx)) tx))

View File

@ -4,6 +4,7 @@
[status-im.contexts.wallet.common.utils :as wallet-utils] [status-im.contexts.wallet.common.utils :as wallet-utils]
[status-im.contexts.wallet.common.utils.networks :as networks] [status-im.contexts.wallet.common.utils.networks :as networks]
[status-im.contexts.wallet.wallet-connect.core :as wallet-connect-core] [status-im.contexts.wallet.wallet-connect.core :as wallet-connect-core]
[status-im.contexts.wallet.wallet-connect.transactions :as transactions]
[utils.money :as money])) [utils.money :as money]))
(rf/reg-sub (rf/reg-sub
@ -57,66 +58,95 @@
sessions))) sessions)))
(rf/reg-sub (rf/reg-sub
:wallet-connect/current-request-network :wallet-connect/chain-id
:<- [:wallet-connect/current-request] :<- [:wallet-connect/current-request]
(fn [request] (fn [request]
(-> request (-> request
(get-in [:event :params :chainId]) (get-in [:event :params :chainId])
(wallet-connect-core/eip155->chain-id) (wallet-connect-core/eip155->chain-id))))
(networks/get-network-details))))
(rf/reg-sub
:wallet-connect/current-request-network
:<- [:wallet-connect/chain-id]
(fn [chain-id]
(-> chain-id
(networks/get-network-details)
(wallet-connect-core/add-full-testnet-name))))
(rf/reg-sub
:wallet-connect/transaction-args
:<- [:wallet-connect/current-request]
(fn [{:keys [event transaction]}]
(when (transactions/transaction-request? event)
transaction)))
(rf/reg-sub
:wallet-connect/transaction-suggested-fees
:<- [:wallet-connect/current-request]
(fn [{:keys [event raw-data]}]
(when (transactions/transaction-request? event)
(:suggested-fees raw-data))))
(rf/reg-sub
:wallet-connect/transaction-max-fees-wei
:<- [:wallet-connect/transaction-args]
:<- [:wallet-connect/transaction-suggested-fees]
(fn [[transaction suggested-fees]]
(when transaction
(let [{:keys [gasPrice gas gasLimit maxFeePerGas]} transaction
eip-1559-chain? (:eip1559Enabled suggested-fees)
gas-limit (or gasLimit gas)
max-gas-fee (if eip-1559-chain? maxFeePerGas gasPrice)]
(money/bignumber (* max-gas-fee gas-limit))))))
(rf/reg-sub
:wallet-connect/account-eth-token
:<- [:wallet-connect/current-request-address]
:<- [:wallet/accounts]
(fn [[address accounts]]
(let [fee-token "ETH"
find-account #(when (= (:address %) address) %)
find-token #(when (= (:symbol %) fee-token) %)]
(->> accounts
(some find-account)
:tokens
(some find-token)))))
(rf/reg-sub (rf/reg-sub
:wallet-connect/current-request-transaction-information :wallet-connect/current-request-transaction-information
:<- [:wallet-connect/current-request] :<- [:wallet-connect/chain-id]
:<- [:wallet/accounts] :<- [:wallet-connect/transaction-max-fees-wei]
:<- [:wallet-connect/transaction-args]
:<- [:wallet-connect/account-eth-token]
:<- [:profile/currency] :<- [:profile/currency]
:<- [:profile/currency-symbol] :<- [:profile/currency-symbol]
(fn [[request accounts currency currency-symbol]] (fn [[chain-id max-fees-wei transaction eth-token currency currency-symbol]]
(let [chain-id (-> request (when transaction
(get-in [:raw-data :params :chainId]) (let [max-fees-ether (money/wei->ether max-fees-wei)
(wallet-connect-core/eip155->chain-id)) max-fees-fiat (wallet-utils/calculate-token-fiat-value {:currency currency
all-tokens (->> accounts :balance max-fees-ether
(filter #(= (:address %) :token eth-token})
(:address request))) max-fees-fiat-formatted (-> max-fees-ether
(first) (wallet-utils/get-standard-crypto-format eth-token)
:tokens) (wallet-utils/get-standard-fiat-format currency-symbol
eth-token (->> all-tokens max-fees-fiat))
(filter #(= (:symbol %) "ETH")) balance (-> eth-token
(first)) (get-in [:balances-per-chain chain-id :raw-balance])
{:keys [gasPrice gasLimit value]} (-> request money/bignumber)
:raw-data tx-value (money/bignumber (:value transaction))
wallet-connect-core/get-request-params total-transaction-value (money/add max-fees-wei tx-value)]
first) {:total-transaction-value total-transaction-value
max-fees-wei (money/bignumber (* gasPrice gasLimit))] :balance balance
(when (and gasPrice gasLimit) :max-fees max-fees-wei
(let [max-fees-ether (money/wei->ether max-fees-wei) :max-fees-fiat-value max-fees-fiat
token-fiat-value (wallet-utils/calculate-token-fiat-value {:currency currency :max-fees-fiat-formatted max-fees-fiat-formatted
:balance max-fees-ether :error-state (cond
:token eth-token}) (not (money/sufficient-funds? tx-value balance))
crypto-formatted (wallet-utils/get-standard-crypto-format eth-token max-fees-ether) :not-enough-assets
fiat-formatted (wallet-utils/get-standard-fiat-format crypto-formatted
currency-symbol
token-fiat-value)
balance (-> eth-token
(get-in [:balances-per-chain chain-id :raw-balance])
(money/bignumber))
value (money/bignumber value)
total-transaction-value (money/add max-fees-wei value)]
{:total-transaction-value total-transaction-value
:balance balance
:max-fees max-fees-wei
:max-fees-fiat-value token-fiat-value
:max-fees-fiat-formatted fiat-formatted
:error-state (cond
(and
(money/sufficient-funds? value balance)
(not (money/sufficient-funds? total-transaction-value balance)))
:not-enough-assets-to-pay-gas-fees
(not (money/sufficient-funds? value balance)) (not (money/sufficient-funds? total-transaction-value
:not-enough-assets balance))
:not-enough-assets-to-pay-gas-fees)}))))
:else nil)})))))
(rf/reg-sub (rf/reg-sub
:wallet-connect/current-proposal-request :wallet-connect/current-proposal-request

View File

@ -148,6 +148,10 @@
[n] [n]
(wei-> :gwei n)) (wei-> :gwei n))
(defn gwei->wei
[n]
(->wei :gwei n))
(defn ether->wei (defn ether->wei
[^js bn] [^js bn]
(when bn (when bn

View File

@ -1941,8 +1941,8 @@
"pinned-by": "Pinned by", "pinned-by": "Pinned by",
"pin-limit-reached": "Pin limit reached. Unpin a previous message first.", "pin-limit-reached": "Pin limit reached. Unpin a previous message first.",
"no-fees": "No fees", "no-fees": "No fees",
"not-enough-assets-to-pay-gas-fees": "Not enough assets to pay gas fees.", "not-enough-assets-to-pay-gas-fees": "Not enough assets to pay gas fees",
"not-enough-assets": "Not enough assets to complete transaction.", "not-enough-assets": "Not enough assets to complete transaction",
"max-fee": "Max fee", "max-fee": "Max fee",
"max-fees": "Max fees", "max-fees": "Max fees",
"max-priority-fee": "Max priority fee", "max-priority-fee": "Max priority fee",
@ -2052,6 +2052,7 @@
"wallet-connect-sign-message-header": "wants you to sign the message with", "wallet-connect-sign-message-header": "wants you to sign the message with",
"wallet-connect-send-transaction-header": "wants you to send this transaction with", "wallet-connect-send-transaction-header": "wants you to send this transaction with",
"wallet-connect-sign-transaction-header": "wants you to sign this transaction with", "wallet-connect-sign-transaction-header": "wants you to sign this transaction with",
"wallet-connect-sign-warning": "Sign only if you trust the dApp",
"wallet-connect-sign-message-warning": "Sign messages only if you trust the dApp", "wallet-connect-sign-message-warning": "Sign messages only if you trust the dApp",
"wallet-connect-send-transaction-warning": "Send transactions only if you trust the dApp", "wallet-connect-send-transaction-warning": "Send transactions only if you trust the dApp",
"wallet-connect-sign-transaction-warning": "Sign transactions only if you trust the dApp", "wallet-connect-sign-transaction-warning": "Sign transactions only if you trust the dApp",
@ -2746,7 +2747,6 @@
"saved-address-network-preference-selection-description": "Only change if you know which networks the address owner is happy to to receive funds on", "saved-address-network-preference-selection-description": "Only change if you know which networks the address owner is happy to to receive funds on",
"add-preferences": "Add preferences", "add-preferences": "Add preferences",
"buy-eth": "Buy ETH", "buy-eth": "Buy ETH",
"not-enough-assets": "Not enough assets to pay gas fees",
"send-from-network": "Send from {{network}}", "send-from-network": "Send from {{network}}",
"define-amount-sent-from-network": "Define amount sent from {{network}} network", "define-amount-sent-from-network": "Define amount sent from {{network}} network",
"dont-auto-recalculate-network": "Don't auto recalculate {{network}}", "dont-auto-recalculate-network": "Don't auto recalculate {{network}}",