From 3c00b7fdae27c39b935a9e95c19bd7504e6c4e44 Mon Sep 17 00:00:00 2001 From: Cristian Lungu Date: Tue, 17 Sep 2024 18:18:14 +0300 Subject: [PATCH] test: wallet-connect transaction utils --- .../wallet_connect/utils/transactions.cljs | 162 +++++++++++++++--- .../utils/transactions_test.cljs | 148 ++++++++++++++++ src/utils/hex.cljs | 9 + 3 files changed, 295 insertions(+), 24 deletions(-) create mode 100644 src/status_im/contexts/wallet/wallet_connect/utils/transactions_test.cljs diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/transactions.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/transactions.cljs index ceebd945a5..0f2deff465 100644 --- a/src/status_im/contexts/wallet/wallet_connect/utils/transactions.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/utils/transactions.cljs @@ -3,13 +3,32 @@ [clojure.string :as string] [native-module.core :as native-module] [promesa.core :as promesa] + [schema.core :as schema] [status-im.constants :as constants] [status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store] [status-im.contexts.wallet.wallet-connect.utils.rpc :as rpc] + [utils.hex :as hex] [utils.money :as money] [utils.transforms :as transforms])) +(def ^:private ?string-or-number + [:or number? string?]) + +(def ?transaction + [:map + [:to :string] + [:from :string] + [:value ?string-or-number] + [:gas {:optional true} ?string-or-number] + [:gasPrice {:optional true} ?string-or-number] + [:gasLimit {:optional true} ?string-or-number] + [:nonce {:optional true} ?string-or-number] + [:maxFeePerGas {:optional true} ?string-or-number] + [:maxPriorityFeePerGas {:optional true} ?string-or-number] + [:input {:optional true} [:maybe :string]] + [:data {:optional true} [:maybe :string]]]) + (defn transaction-request? [event] (->> (data-store/get-request-method event) @@ -20,7 +39,7 @@ ;; 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" [hex-value] (let [formatted-hex (string/replace hex-value #"^0x0*" "0x")] @@ -28,8 +47,8 @@ "0x0" formatted-hex))) -(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" +(defn format-tx-hex-values + "Apply f on transaction keys that are hex numbers" [tx f] (let [tx-keys [:gasLimit :gas :gasPrice :nonce :value :maxFeePerGas :maxPriorityFeePerGas]] (reduce (fn [acc tx-key] @@ -40,7 +59,14 @@ tx tx-keys))) -(defn- prepare-transaction-for-rpc +(schema/=> format-tx-hex-values + [:=> + [:catn + [:tx ?transaction] + [:f fn?]] + ?transaction]) + +(defn prepare-transaction-for-rpc "Formats the transaction and transforms it into a stringified JS object, ready to be passed to an RPC call." [tx] (-> tx @@ -50,42 +76,67 @@ bean/->js (transforms/js-stringify 0))) +(schema/=> prepare-transaction-for-rpc + [:=> + [:cat ?transaction] + :string]) + (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)))) + (-> tx + (format-tx-hex-values hex/hex-to-number) + clj->js + (js/JSON.stringify nil 2))) -(defn- gwei->hex +(schema/=> beautify-transaction + [:=> + [:cat ?transaction] + :string]) + +(defn gwei->hex [gwei] (->> gwei money/gwei->wei native-module/number-to-hex (str "0x"))) +(schema/=> gwei->hex + [:=> + [:cat ?string-or-number] + :string]) + (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`" + to the `suggested-routes` key that should be used for `:maxPriorityFeePerGas` " [tx-priority] (get {:high :maxFeePerGasHigh :medium :maxFeePerGasMedium :low :maxFeePerGasLow} tx-priority)) -(defn- dynamic-fee-tx? +(def ?tx-priority [:enum :high :medium :low]) +(def ?max-fee-priority [:enum :maxFeePerGasHigh :maxFeePerGasMedium :maxFeePerGasLow]) + +(schema/=> get-max-fee-per-gas-key + [:=> + [:cat ?tx-priority] + ?max-fee-priority]) + +(defn dynamic-fee-tx? "Checks if a transaction has dynamic fees (EIP1559)" [tx] (every? tx [:maxFeePerGas :maxPriorityFeePerGas])) +(schema/=> dynamic-fee-tx? + [:=> + [:cat ?transaction] + :boolean]) + (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] + [tx tx-priority suggested-fees] (if (and (:eip1559Enabled suggested-fees) (not (dynamic-fee-tx? tx))) (let [max-fee-per-gas-key (get-max-fee-per-gas-key tx-priority) @@ -99,17 +150,49 @@ (dissoc :gasPrice))) tx)) -(defn- prepare-transaction-fees +(def ?suggested-fees + [:map + [:eip1559Enabled boolean?] + [:maxFeePerGasLow number?] + [:maxFeePerGasMedium number?] + [:maxFeePerGasHigh number?] + [:maxPriorityFeePerGas number?] + [:gasPrice {:optional true} number?] + [:baseFee {:optional true} number?] + [:l1GasFee {:optional true} number?]]) + +(schema/=> tx->eip1559-tx + [:=> + [:catn + [:tx ?transaction] + [:tx-priority ?tx-priority] + [:suggested-fees ?suggested-fees]] + ?transaction]) + +(defn rename-gas-limit + [tx] + (if (:gasLimit tx) + (-> tx + ;; NOTE: `gasLimit` is ignored on status-go when building a transaction + ;; (`wallet_buildTransaction`), so we're setting it as the `gas` property + (assoc :gas (:gasLimit tx)) + (dissoc :gasLimit)) + 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))) + (-> tx + rename-gas-limit + (tx->eip1559-tx tx-priority suggested-fees))) + +(schema/=> prepare-transaction-fees + [:=> + [:catn + [:tx ?transaction] + [:tx-priority ?tx-priority] + [:suggested-fees ?suggested-fees]] + ?transaction]) (defn prepare-transaction "Formats and builds the incoming transaction, adding the missing properties and returning the final @@ -126,6 +209,17 @@ :tx-hash message-to-sign :suggested-fees suggested-fees})) +(schema/=> prepare-transaction + [:=> + [:catn + [:tx ?transaction + :chain-id :int + :tx-priority ?tx-priority]] + [:map {:closed true} + [:tx-args :string] + [:tx-hash :string] + [:suggested-fees ?suggested-fees]]]) + (defn sign-transaction [password address tx-hash tx-args chain-id] (promesa/let @@ -133,6 +227,16 @@ raw-tx (rpc/wallet-build-raw-transaction chain-id tx-args signature)] raw-tx)) +(schema/=> sign-transaction + [:=> + [:catn + [:password :string] + [:address :string] + [:tx-hash :string] + [:tx-args ?transaction] + [:chain-id :int]] + :string]) + (defn send-transaction [password address tx-hash tx-args chain-id] (promesa/let @@ -141,3 +245,13 @@ tx-args signature)] tx)) + +(schema/=> sign-transaction + [:=> + [:catn + [:password :string] + [:address :string] + [:tx-hash :string] + [:tx-args ?transaction] + [:chain-id :int]] + :string]) diff --git a/src/status_im/contexts/wallet/wallet_connect/utils/transactions_test.cljs b/src/status_im/contexts/wallet/wallet_connect/utils/transactions_test.cljs new file mode 100644 index 0000000000..020e33c6ce --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/utils/transactions_test.cljs @@ -0,0 +1,148 @@ +(ns status-im.contexts.wallet.wallet-connect.utils.transactions-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [status-im.contexts.wallet.wallet-connect.utils.transactions :as sut] + [utils.transforms :as transforms])) + +(deftest strip-hex-prefix-test + (testing "passed value with extra 0 hex prefix" + (is (= (sut/strip-hex-prefix "0x0123") + "0x123"))) + + (testing "passed empty 0x value" + (is (= (sut/strip-hex-prefix "0x") + "0x0"))) + + (testing "passed value without 0 in prefix" + (is (= (sut/strip-hex-prefix "0x123") + "0x123")))) + +(deftest format-tx-hex-values-test + (testing "applies f only on the tx keys that are hex numbers" + (let [tx {:to "0x0123" + :from "0x0456" + :value "0x0fff" + :gas "0x01"} + expected-tx {:to "0x0123" + :from "0x0456" + :value "0xfff" + :gas "0x1"}] + (is (= (sut/format-tx-hex-values tx sut/strip-hex-prefix) + expected-tx))))) + +(cljs.test/test-var #'format-tx-hex-values-test) + +(deftest prepare-transaction-for-rpc-test + (testing "original transaction nonce is removed" + (let [tx {:to "0x123" + :from "0x456" + :value "0xfff" + :nonce "0x1"}] + (is (-> (sut/prepare-transaction-for-rpc tx) + (transforms/json->clj) + :nonce + nil?)))) + (testing "transaction hex numbers are formatted" + (let [tx {:to "0x123" + :from "0x456" + :value "0x0fff" + :nonce "0x1"}] + (is (-> (sut/prepare-transaction-for-rpc tx) + (transforms/json->clj) + :value + (= "0xfff")))))) + +(deftest beautify-transaction-test + (testing "hex number values are converted to utf-8" + (let [tx {:to "0x123" + :from "0x456" + :value "0xfff"}] + (is (-> (sut/beautify-transaction tx) + (transforms/json->clj) + :value + (= 4095)))))) + +(cljs.test/test-var #'beautify-transaction-test) + +(deftest gwei->hex-test + (testing "gwei amount is converted to wei as hex" + (is (-> (sut/gwei->hex "1000000000") + (= "0xde0b6b3a7640000"))))) + +(deftest dynamic-fee-tx?-test + (testing "correctly asserts tx as dynamic" + (is (-> {:to "0x123" + :from "0x123" + :value "0x123" + :maxFeePerGas "0x123" + :maxPriorityFeePerGas "0x1"} + sut/dynamic-fee-tx?))) + + (testing "correnctly asserts tx as not dynamic (legacy)" + (is (-> {:to "0x123" + :from "0x123" + :value "0x123"} + sut/dynamic-fee-tx? + not))) + + (testing "asserts tx as not dynamic (legacy) when required keys are only partially present" + (is (-> {:to "0x123" + :from "0x123" + :value "0x123" + :maxFeePerGas "0x123"} + sut/dynamic-fee-tx? + not)))) + +(deftest prepare-transaction-fees-test + (let [suggested-fees {:eip1559Enabled true + :maxFeePerGasLow 1 + :maxFeePerGasMedium 2 + :maxFeePerGasHigh 3 + :maxPriorityFeePerGas 100}] + + (testing "correctly prepares eip1559 gas values" + (let [tx {:to "0x123" + :from "0x123" + :value "0x123"} + tx-priority :high] + (is (= (sut/prepare-transaction-fees tx + tx-priority + suggested-fees) + {:to (:to tx) + :from (:from tx) + :value (:value tx) + :maxFeePerGas "0xb2d05e00" + :maxPriorityFeePerGas "0x174876e800"})))) + + (testing "renames gasLimit key to gas and removed gasPrice" + (let [gasLimit "0x1234" + prepared-tx (sut/prepare-transaction-fees {:to "0x123" + :from "0x123" + :value "0x123" + :gasPrice "0x123" + :gasLimit gasLimit} + :high + suggested-fees)] + (is (and (= (:gas prepared-tx) gasLimit) + (nil? (:gasLimit prepared-tx)) + (nil? (:gasPrice prepared-tx)))))) + + (testing "returns original tx for non-dynamic transactions" + (let [tx {:to "0x123" + :from "0x123" + :value "0x123" + :gasPrice "0x123"} + suggested-fees (assoc suggested-fees :eip1559Enabled false)] + (is (= (sut/prepare-transaction-fees tx :high suggested-fees) + tx)))) + + (testing "throws a schema exception due wrong arguments" + (let [tx {:to "0x123" + :from "0x123" + :value "0x123"} + wrong-tx-priority :wrong + wrong-suggested-fees {}] + (is (thrown? js/Error + (sut/prepare-transaction-fees tx + wrong-tx-priority + wrong-suggested-fees))))))) diff --git a/src/utils/hex.cljs b/src/utils/hex.cljs index 649ca121e9..7e16901451 100644 --- a/src/utils/hex.cljs +++ b/src/utils/hex.cljs @@ -37,3 +37,12 @@ [:=> [:cat [:or :string :int]] :string]) + +(defn hex-to-number + [hex] + (-> hex normalize-hex native-module/hex-to-number)) + +(schema/=> hex-to-number + [:=> + [:cat :string] + :int])