Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fabd686cb | ||
|
|
3c00b7fdae |
@@ -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)))))))
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user