Complete implementation of the `/send` command + tests

This commit is contained in:
janherich 2018-07-16 01:07:34 +02:00
parent 6672400041
commit 03598d47c2
No known key found for this signature in database
GPG Key ID: C23B473AFBE94D13
7 changed files with 287 additions and 40 deletions

View File

@ -1,14 +1,21 @@
(ns status-im.chat.commands.impl.transactions (ns status-im.chat.commands.impl.transactions
(:require-macros [status-im.utils.views :refer [defview letsubs]]) (:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [re-frame.core :as re-frame] (:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.chat.commands.protocol :as protocol] [status-im.chat.commands.protocol :as protocol]
[status-im.chat.commands.impl.transactions.styles :as transactions-styles]
[status-im.chat.events.requests :as request-events]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.ui.components.list.views :as list] [status-im.ui.components.list.views :as list]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.chat.commands.impl.transactions.styles :as transactions-styles] [status-im.constants :as constants]
[status-im.chat.styles.message.message :as message-styles])) [status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.ui.screens.wallet.send.events :as send.events]
[status-im.ui.screens.wallet.choose-recipient.events :as choose-recipient.events]
[status-im.ui.screens.navigation :as navigation]))
(defn- render-asset [selected-event-creator] (defn- render-asset [selected-event-creator]
(fn [{:keys [name symbol amount decimals] :as asset}] (fn [{:keys [name symbol amount decimals] :as asset}]
@ -38,7 +45,7 @@
:keyboardShouldPersistTaps :always :keyboardShouldPersistTaps :always
:bounces false}]])) :bounces false}]]))
(defn send-short-preview (defn personal-send-request-short-preview
[{:keys [content]}] [{:keys [content]}]
(let [parameters (:params content)] (let [parameters (:params content)]
[react/text {} [react/text {}
@ -52,12 +59,12 @@
tx-exists? [:wallet-transaction-exists? tx-hash]] tx-exists? [:wallet-transaction-exists? tx-hash]]
[react/touchable-highlight {:on-press #(when tx-exists? [react/touchable-highlight {:on-press #(when tx-exists?
(re-frame/dispatch [:show-transaction-details tx-hash]))} (re-frame/dispatch [:show-transaction-details tx-hash]))}
[react/view message-styles/command-send-status-container [react/view transactions-styles/command-send-status-container
[vector-icons/icon (if confirmed? :icons/check :icons/dots) [vector-icons/icon (if confirmed? :icons/check :icons/dots)
{:color colors/blue {:color colors/blue
:container-style (message-styles/command-send-status-icon outgoing)}] :container-style (transactions-styles/command-send-status-icon outgoing)}]
[react/view [react/view
[react/text {:style message-styles/command-send-status-text} [react/text {:style transactions-styles/command-send-status-text}
(i18n/label (cond (i18n/label (cond
confirmed? :status-confirmed confirmed? :status-confirmed
tx-exists? :status-pending tx-exists? :status-pending
@ -69,65 +76,159 @@
(let [{{:keys [amount fiat-amount tx-hash asset currency] send-network :network} :params} content (let [{{:keys [amount fiat-amount tx-hash asset currency] send-network :network} :params} content
recipient-name (get-in content [:params :bot-db :public :recipient]) recipient-name (get-in content [:params :bot-db :public :recipient])
network-mismatch? (and (seq send-network) (not= network send-network))] network-mismatch? (and (seq send-network) (not= network send-network))]
[react/view message-styles/command-send-message-view [react/view transactions-styles/command-send-message-view
[react/view [react/view
[react/view message-styles/command-send-amount-row [react/view transactions-styles/command-send-amount-row
[react/view message-styles/command-send-amount [react/view transactions-styles/command-send-amount
[react/text {:style message-styles/command-send-amount-text [react/text {:style transactions-styles/command-send-amount-text
:font :medium} :font :medium}
amount amount
[react/text {:style (message-styles/command-amount-currency-separator outgoing)} [react/text {:style (transactions-styles/command-amount-currency-separator outgoing)}
"."] "."]
[react/text {:style (message-styles/command-send-currency-text outgoing) [react/text {:style (transactions-styles/command-send-currency-text outgoing)
:font :default} :font :default}
asset]]]] asset]]]]
(when fiat-amount (when fiat-amount
[react/view message-styles/command-send-fiat-amount [react/view transactions-styles/command-send-fiat-amount
[react/text {:style message-styles/command-send-fiat-amount-text} [react/text {:style transactions-styles/command-send-fiat-amount-text}
(str "~ " fiat-amount " " (or currency (i18n/label :usd-currency)))]]) (str "~ " fiat-amount " " (or currency (i18n/label :usd-currency)))]])
(when (and group-chat (when (and group-chat
recipient-name) recipient-name)
[react/text {:style message-styles/command-send-recipient-text} [react/text {:style transactions-styles/command-send-recipient-text}
(str (str
(i18n/label :send-sending-to) (i18n/label :send-sending-to)
" " " "
recipient-name)]) recipient-name)])
[react/view [react/view
[react/text {:style (message-styles/command-send-timestamp outgoing)} [react/text {:style (transactions-styles/command-send-timestamp outgoing)}
(str (i18n/label :sent-at) " " timestamp-str)]] (str (i18n/label :sent-at) " " timestamp-str)]]
[send-status tx-hash outgoing] [send-status tx-hash outgoing]
(when network-mismatch? (when network-mismatch?
[react/text send-network])]]))) [react/text send-network])]])))
(def personal-send-request-params
[{:id :asset
:type :text
:placeholder "Currency"
;; Suggestion components should be structured in such way that they will just take
;; one argument, event-creator fn used to construct event to fire whenever something
;; is selected.
:suggestions choose-asset}
{:id :amount
:type :number
:placeholder "Amount"}])
;;TODO(goranjovic): currently we only allow tokens which are enabled in Manage assets here
;; because balances are only fetched for them. Revisit this decision with regard to battery/network consequences
;; if we were to update all balances.
(defn- allowed-assets [{:account/keys [account] :keys [chain]}]
(let [chain-keyword (keyword chain)
visible-tokens (get-in account [:settings :wallet :visible-tokens chain-keyword])]
(into {"ETH" 18}
(comp (filter #(and (not (:nft? %))
(contains? visible-tokens (:symbol %))))
(map (juxt (comp name :symbol) :decimals)))
(tokens/tokens-for chain-keyword))))
(defn- personal-send-request-validation [{:keys [asset amount]} {:keys [db]}]
(let [asset-decimals (get (allowed-assets db) asset)]
(cond
(not asset-decimals)
{:title "Invalid Asset"
:description (str "Unknown token - " asset)}
(not amount)
{:title "Amount"
:description "Amount must be specified"}
:else
(let [sanitised-str (string/replace amount #"," ".")
portions (string/split sanitised-str ".")
decimals (get portions 1)
amount (js/parseFloat sanitised-str)]
(cond
(or (js/isNaN amount)
(> (count portions) 2))
{:title "Amount"
:description "Amount is not valid number"}
(and decimals (> decimals asset-decimals))
{:title "Amount"
:description (str "Max number of decimals is " asset-decimals)})))))
;; TODO(goranjovic) - update to include tokens in https://github.com/status-im/status-react/issues/3233
(defn- transaction-details [contact symbol]
(-> contact
(select-keys [:name :address :whisper-identity])
(assoc :symbol symbol
:gas (ethereum/estimate-gas symbol)
:from-chat? true)))
;; `/send` command
(deftype PersonalSendCommand [] (deftype PersonalSendCommand []
protocol/Command protocol/Command
(id [_] (id [_]
:send) "send")
(scope [_] (scope [_]
#{:personal-chats}) #{:personal-chats})
(parameters [_] (parameters [_]
[{:id :asset personal-send-request-params)
:type :text (validate [_ parameters cofx]
:placeholder "Currency" ;; Only superficial/formatting validation, "real validation" will be performed
;; Suggestion components should be structured in such way that they will just take ;; by the wallet, where we yield control in the next step
;; one argument, event-creator fn used to construct event to fire whenever something (personal-send-request-validation parameters cofx))
;; is selected. (yield-control [_ parameters {:keys [db]}]
:suggestions choose-asset} ;; Prefill wallet and navigate there
{:id :amount (let [recipient-contact (get-in db [:contacts/contacts (:current-chat-id db)])
:type :number sender-account (:account/account db)
:placeholder "Amount"}]) chain (keyword (:chain db))
(validate [_ _ _] symbol (-> parameters :asset keyword)
;; There is no validation for the `/send` command, as it's fully delegated to the wallet {:keys [decimals]} (tokens/asset-for chain symbol)]
nil) (merge {:db (-> db
(yield-control [_ parameters cofx] (send.events/set-and-validate-amount-db (:amount parameters) symbol decimals)
;; navigate to wallet (choose-recipient.events/fill-request-details
nil) (transaction-details recipient-contact symbol))
(on-send [_ message-id parameters cofx] (update-in [:wallet :send-transaction] dissoc :id :password :wrong-password?)
(when-let [tx-hash (get-in cofx [:db :wallet :send-transaction :tx-hash])] (navigation/navigate-to
{:dispatch [:update-transactions]})) (if (:wallet-set-up-passed? sender-account)
:wallet-send-transaction-chat
:wallet-onboarding-setup)))}
(send.events/update-gas-price db false))))
(on-send [_ _ _ _]
;; TODO(janherich) - remove this once periodic updates are implemented
{:dispatch [:update-transactions]})
(on-receive [_ _ _] (on-receive [_ _ _]
nil) ;; TODOD(janherich) - this just copyies the current logic but still seems super weird,
;; remove/reconsider once periodic updates are implemented
{:dispatch [:update-transactions]
:dispatch-later [{:ms constants/command-send-status-update-interval-ms
:dispatch [:update-transactions]}]})
(short-preview [_ command-message _] (short-preview [_ command-message _]
(send-short-preview command-message)) (personal-send-request-short-preview command-message))
(preview [_ command-message _] (preview [_ command-message _]
(send-preview command-message))) (send-preview command-message)))
;; `/request` command
(deftype PersonalRequestCommand []
protocol/Command
(id [_]
"request")
(scope [_]
#{:personal-chats})
(parameters [_]
personal-send-request-params)
(validate [_ parameters cofx]
(personal-send-request-validation parameters cofx))
(yield-control [_ _ _])
(on-send [_ _ _ _])
(on-receive [_ command-message cofx]
(let [{:keys [chat-id message-id]} command-message]
(request-events/add-request chat-id message-id cofx)))
(short-preview [_ command-message _]
(personal-send-request-short-preview command-message))
(preview [_ command-message _]
nil))

View File

@ -1,4 +1,5 @@
(ns status-im.chat.commands.impl.transactions.styles (ns status-im.chat.commands.impl.transactions.styles
(:require-macros [status-im.utils.styles :refer [defstyle]])
(:require [status-im.ui.components.colors :as colors])) (:require [status-im.ui.components.colors :as colors]))
(def asset-container (def asset-container
@ -33,3 +34,75 @@
{:height 1 {:height 1
:background-color colors/gray-light :background-color colors/gray-light
:margin-left 56}) :margin-left 56})
(def command-send-status-container
{:margin-top 6
:flex-direction :row})
(defn command-send-status-icon [outgoing]
{:background-color (if outgoing
colors/blue-darker
colors/blue-transparent)
:width 24
:height 24
:border-radius 16
:padding-top 4
:padding-left 4})
(defstyle command-send-status-text
{:color colors/blue
:android {:margin-top 3}
:ios {:margin-top 4}
:margin-left 6
:font-size 12})
(def command-send-message-view
{:flex-direction :column
:align-items :flex-start})
(def command-send-amount-row
{:flex-direction :row
:justify-content :space-between})
(def command-send-amount
{:flex-direction :column
:align-items :flex-end
:max-width 250})
(defstyle command-send-amount-text
{:font-size 22
:color colors/blue
:ios {:letter-spacing -0.5}})
(def command-send-currency
{:flex-direction :column
:align-items :flex-end})
(defn command-amount-currency-separator [outgoing]
{:opacity 0
:color (if outgoing colors/hawkes-blue colors/white)})
(defn command-send-currency-text [outgoing]
{:font-size 22
:margin-left 4
:letter-spacing 1
:color (if outgoing colors/wild-blue-yonder colors/blue-transparent-40)})
(def command-send-fiat-amount
{:flex-direction :column
:justify-content :flex-end
:margin-top 6})
(def command-send-fiat-amount-text
{:font-size 12
:color colors/black})
(def command-send-recipient-text
{:color colors/blue
:font-size 14
:line-height 18})
(defn command-send-timestamp [outgoing]
{:color (if outgoing colors/wild-blue-yonder colors/gray)
:margin-top 6
:font-size 12})

View File

@ -27,7 +27,8 @@
(validate [this parameters cofx] (validate [this parameters cofx]
"Function validating the parameters once command is send. Takes parameters map "Function validating the parameters once command is send. Takes parameters map
and `cofx` map as argument, returns either `nil` meaning that no errors were and `cofx` map as argument, returns either `nil` meaning that no errors were
found and command send workflow can proceed, or sequence of errors to display") found and command send workflow can proceed, or one/more errors to display.
Each error is represented by the map containing `:title` and `:description` keys.")
(yield-control [this parameters cofx] (yield-control [this parameters cofx]
"Optional function, which if implemented, can step out of the normal command "Optional function, which if implemented, can step out of the normal command
workflow (`validate-and-send`) and yield control back to application before sending. workflow (`validate-and-send`) and yield control back to application before sending.

View File

@ -0,0 +1,22 @@
(ns status-im.chat.commands.styles.validation
(:require [status-im.ui.components.styles :as common]))
(defn root [bottom]
{:flex-direction :column
:left 0
:right 0
:bottom bottom
:position :absolute})
(def message-container
{:background-color common/color-red
:padding 16})
(def message-title
{:color common/color-white
:font-size 12})
(def message-description
{:color common/color-white
:font-size 12
:opacity 0.9})

View File

@ -0,0 +1,10 @@
(ns status-im.chat.commands.validation
(:require [status-im.ui.components.react :as react]
[status-im.chat.commands.styles.validation :as styles]))
(defn validation-message [{:keys [title description]}]
[react/view styles/message-container
[react/text {:style styles/message-title}
title]
[react/text {:style styles/message-description}
description]])

View File

@ -0,0 +1,38 @@
(ns status-im.test.chat.commands.impl.transactions
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im.chat.commands.impl.transactions :as transactions]
[status-im.chat.commands.protocol :as protocol]))
(def personal-send-command (transactions/PersonalSendCommand.))
(def cofx {:db {:account/account {:settings {:wallet {:visible-tokens {:mainnet #{:SNT}}}}
:wallet-set-up-passed? true}
:chain "mainnet"
:current-chat-id "recipient"
:contacts/contacts {"recipient" {:name "Recipient"
:address "0xAA"
:whisper-identity "0xBB"}}}})
(deftest personal-send-command-test
(testing "That correct parameters are defined"
(is (= (into #{} (map :id) (protocol/parameters personal-send-command))
#{:asset :amount})))
(testing "Parameters validation"
(is (= (protocol/validate personal-send-command {:asset "TST"} cofx)
{:title "Invalid Asset"
:description "Unknown token - TST"}))
(is (= (protocol/validate personal-send-command {:asset "SNT"} cofx)
{:title "Amount"
:description "Amount must be specified"}))
(is (= (protocol/validate personal-send-command {:asset "SNT" :amount "a"} cofx)
{:title "Amount"
:description "Amount is not valid number"}))
(is (= (protocol/validate personal-send-command {:asset "ETH" :amount "0.54354353454353453453454353453445345545"} cofx)
{:title "Amount"
:description "Max number of decimals is 18"}))
(is (= (protocol/validate personal-send-command {:asset "ETH" :amount "0.01"} cofx)
nil)))
(testing "Yielding control prefills wallet"
(let [fx (protocol/yield-control personal-send-command {:asset "ETH" :amount "0.01"} cofx)]
(is (= (get-in fx [:db :wallet :send-transaction :amount-text]) "0.01"))
(is (= (get-in fx [:db :wallet :send-transaction :symbol]) :ETH)))))

View File

@ -27,6 +27,7 @@
[status-im.test.chat.views.message] [status-im.test.chat.views.message]
[status-im.test.chat.views.photos] [status-im.test.chat.views.photos]
[status-im.test.chat.commands.core] [status-im.test.chat.commands.core]
[status-im.test.chat.commands.impl.transactions]
[status-im.test.i18n] [status-im.test.i18n]
[status-im.test.protocol.web3.inbox] [status-im.test.protocol.web3.inbox]
[status-im.test.utils.utils] [status-im.test.utils.utils]
@ -82,6 +83,7 @@
'status-im.test.chat.views.message 'status-im.test.chat.views.message
'status-im.test.chat.views.photos 'status-im.test.chat.views.photos
'status-im.test.chat.commands.core 'status-im.test.chat.commands.core
'status-im.test.chat.commands.impl.transactions
'status-im.test.i18n 'status-im.test.i18n
'status-im.test.transport.core 'status-im.test.transport.core
'status-im.test.transport.inbox 'status-im.test.transport.inbox