Compare commits

...
6 changed files with 388 additions and 25 deletions
+5
View File
@@ -36,11 +36,16 @@
(def ^:private ?hiccup
vector?)
(def ^:private ?promise
[:fn {:error/message "schema.common/promise should be of type Promise"}
(fn [v] (instance? js/Promise v))])
(defn register-schemas
[]
(registry/register ::theme ?theme)
(registry/register ::customization-color ?customization-color)
(registry/register ::public-key ?public-key)
(registry/register ::promise ?promise)
(registry/register ::image-source ?image-source)
(registry/register ::rpc-call ?rpc-call)
(registry/register ::exception ?exception)
@@ -1,6 +1,7 @@
(ns status-im.contexts.wallet.wallet-connect.utils.signing
(:require [native-module.core :as native-module]
[promesa.core :as promesa]
[schema.core :as schema]
[status-im.contexts.wallet.wallet-connect.utils.data-store :as
data-store]
[status-im.contexts.wallet.wallet-connect.utils.networks :as networks]
@@ -21,9 +22,30 @@
:domain
:chainId
number/parse-int)]
(when chain-id-type?
(when (and chain-id-type?
(not (zero? data-chain-id)))
data-chain-id)))
(def ?eip712-type
[:map
[:name :string]
[:type :string]])
(def ?eip712-data
[:map
[:types
[:map
[:EIP712Domain {:optional true} [:vector ?eip712-type]]
[:malli.core/default [:map-of :keyword [:vector ?eip712-type]]]]]
[:domain {:optional true} [:map-of :keyword :any]]
[:primaryType :string]
[:message map?]])
(schema/=> typed-data-chain-id
[:=>
[:cat ?eip712-data]
[:maybe :int]])
(defn eth-sign
[password address data]
(-> {:data data
@@ -33,12 +55,28 @@
native-module/sign-message
(promesa/then data-store/extract-native-call-signature)))
(schema/=> eth-sign
[:=>
[:catn
[:password :string]
[:address :string]
[:data :string]]
:schema.common/promise])
(defn personal-sign
[password address data]
(-> (rpc/wallet-hash-message-eip-191 data)
(promesa/then #(rpc/wallet-sign-message % address password))
(promesa/then hex/prefix-hex)))
(schema/=> personal-sign
[:=>
[:catn
[:password :string]
[:address :string]
[:data :string]]
:schema.common/promise])
(defn eth-sign-typed-data
[password address data chain-id-eip155 version]
(let [legacy? (= version :v1)
@@ -48,3 +86,13 @@
password
chain-id
legacy?)))
(schema/=> eth-sign-typed-data
[:=>
[:catn
[:password :string]
[:address :string]
[:data :string]
[:chain-id-eip155 :string]
[:version [:enum :v1 :v4]]]
:schema.common/promise])
@@ -0,0 +1,39 @@
(ns status-im.contexts.wallet.wallet-connect.utils.signing-test
(:require
[cljs.test :refer-macros [deftest is testing]]
[status-im.contexts.wallet.wallet-connect.utils.signing :as sut]))
(deftest typed-data-chain-id-test
(testing "chainId is extracted correctly"
(let [typed-data {:types {:EIP712Domain [{:name "chainId" :type "string"}]}
:domain {:chainId "1"}
:primaryType "EIP712Domain"
:message {}}]
(is (= (sut/typed-data-chain-id typed-data)
1))))
(testing "chainId not extracted if it is not defined in the domain types"
(let [typed-data {:types {:EIP712Domain []}
:domain {:chainId "1"}
:primaryType "EIP712Domain"
:message {}}]
(is (nil? (sut/typed-data-chain-id typed-data)))))
(testing "chainId not extracted if it is not defined in the domain"
(let [typed-data {:types {:EIP712Domain [{:name "chainId" :type "string"}]}
:domain {}
:primaryType "EIP712Domain"
:message {}}]
(is (nil? (sut/typed-data-chain-id typed-data)))))
(testing "chainId not extracted if chainId is not a valid int"
(let [typed-data {:types {:EIP712Domain [{:name "chainId" :type "string"}]}
:domain {:chainId "not an int"}
:primaryType "EIP712Domain"
:message {}}]
(is (nil? (sut/typed-data-chain-id typed-data)))))
(testing "invalid typed data is passed"
(is (thrown? js/Error (sut/typed-data-chain-id {})))))
(cljs.test/run-tests)
@@ -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])
@@ -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)))))))
+9
View File
@@ -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])