[ISSUE #2642] Allow to send ERC20 tokens

This commit is contained in:
Julien Eluard 2017-12-16 11:00:56 +01:00 committed by Julien Eluard
parent 38d7485f76
commit 7c4d2f2ece
43 changed files with 702 additions and 412 deletions

3
resources/icons/down.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path opacity=".4" fill="white" d="M12,14.8284271 L8.46236609,11.2907932 C8.07770436,10.9061315 7.44077682,10.9023689 7.05025253,11.2928932 C6.65700558,11.6861402 6.65878803,12.3156423 7.04815252,12.7050068 L11.2949932,16.9518475 C11.4869793,17.1438336 11.741806,17.240936 11.9974742,17.2414902 C12.2565137,17.2439683 12.5106708,17.1461835 12.7050068,16.9518475 L16.9518475,12.7050068 C17.3365092,12.320345 17.3402718,11.6834175 16.9497475,11.2928932 C16.5565005,10.8996463 15.9269984,10.9014287 15.5376339,11.2907932 L12,14.8284271 Z" />
</svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="" fill-rule="evenodd" d="M12,14.8284271 L8.46236609,11.2907932 C8.07770436,10.9061315 7.44077682,10.9023689 7.05025253,11.2928932 C6.65700558,11.6861402 6.65878803,12.3156423 7.04815252,12.7050068 L11.2949932,16.9518475 C11.4869793,17.1438336 11.741806,17.240936 11.9974742,17.2414902 C12.2565137,17.2439683 12.5106708,17.1461835 12.7050068,16.9518475 L16.9518475,12.7050068 C17.3365092,12.320345 17.3402718,11.6834175 16.9497475,11.2928932 C16.5565005,10.8996463 15.9269984,10.9014287 15.5376339,11.2907932 L12,14.8284271 Z" opacity=".4"/>
</svg>

Before

Width:  |  Height:  |  Size: 646 B

3
resources/icons/up.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" x="12" y="12" width="24" height="24" viewBox="0 0 24 24">
<path opacity=".4" fill="white" d="M12,9 C11.7440777,9 11.4881554,9.09763107 11.2928932,9.29289322 L7.29289322,13.2928932 C6.90236893,13.6834175 6.90236893,14.3165825 7.29289322,14.7071068 C7.68341751,15.0976311 8.31658249,15.0976311 8.70710678,14.7071068 L12,11.4142136 L15.2928932,14.7071068 C15.6834175,15.0976311 16.3165825,15.0976311 16.7071068,14.7071068 C17.0976311,14.3165825 17.0976311,13.6834175 16.7071068,13.2928932 L12.7071068,9.29289322 C12.5118446,9.09763107 12.2559223,9 12,9 Z" />
</svg>

After

Width:  |  Height:  |  Size: 606 B

View File

@ -46,11 +46,11 @@
(def mainnet-networks
{"mainnet" {:id "mainnet",
:name "Mainnet",
:config {:NetworkId (ethereum/chain-id :mainnet)
:config {:NetworkId (ethereum/chain-keyword->chain-id :mainnet)
:DataDir "/ethereum/mainnet"}}
"mainnet_rpc" {:id "mainnet_rpc",
:name "Mainnet with upstream RPC",
:config {:NetworkId (ethereum/chain-id :mainnet)
:config {:NetworkId (ethereum/chain-keyword->chain-id :mainnet)
:DataDir "/ethereum/mainnet_rpc"
:UpstreamConfig {:Enabled true
:URL "https://mainnet.infura.io/z6GCTmjdP3FETEJmMBI4"}}}})
@ -58,21 +58,21 @@
(def testnet-networks
{"testnet" {:id "testnet",
:name "Ropsten",
:config {:NetworkId (ethereum/chain-id :ropsten)
:config {:NetworkId (ethereum/chain-keyword->chain-id :testnet)
:DataDir "/ethereum/testnet"}}
"testnet_rpc" {:id "testnet_rpc",
:name "Ropsten with upstream RPC",
:config {:NetworkId (ethereum/chain-id :ropsten)
:config {:NetworkId (ethereum/chain-keyword->chain-id :testnet)
:DataDir "/ethereum/testnet_rpc"
:UpstreamConfig {:Enabled true
:URL "https://ropsten.infura.io/z6GCTmjdP3FETEJmMBI4"}}}
"rinkeby" {:id "rinkeby",
:name "Rinkeby",
:config {:NetworkId (ethereum/chain-id :rinkeby)
:config {:NetworkId (ethereum/chain-keyword->chain-id :rinkeby)
:DataDir "/ethereum/rinkeby"}}
"rinkeby_rpc" {:id "rinkeby_rpc",
:name "Rinkeby with upstream RPC",
:config {:NetworkId (ethereum/chain-id :rinkeby)
:config {:NetworkId (ethereum/chain-keyword->chain-id :rinkeby)
:DataDir "/ethereum/rinkeby_rpc"
:UpstreamConfig {:Enabled true
:URL "https://rinkeby.infura.io/z6GCTmjdP3FETEJmMBI4"}}}})

View File

@ -17,8 +17,8 @@
[status-im.data-store.realm.schemas.account.v16.core :as v16]
[status-im.data-store.realm.schemas.account.v17.core :as v17]
[status-im.data-store.realm.schemas.account.v18.core :as v18]
[status-im.data-store.realm.schemas.account.v19.core :as v19]
))
[status-im.data-store.realm.schemas.account.v19.core :as v19]))
;; TODO(oskarth): Add failing test if directory vXX exists but isn't in schemas.
@ -79,5 +79,5 @@
:migration v18/migration}
{:schema v19/schema
:schemaVersion 19
:migration v19/migration}
])
:migration v19/migration}])

View File

@ -344,8 +344,12 @@
:wallet-choose-from-contacts "Choose From Contacts"
:wallet-address-from-clipboard "Use Address From Clipboard"
:wallet-invalid-address "Invalid address: \n {{data}}"
:wallet-invalid-chain-id "Network does not match: \n {{data}}"
:wallet-invalid-chain-id "Network does not match: \n {{data}} but current chain is {{chain}}"
:wallet-browse-photos "Browse Photos"
:wallet-advanced "Advanced"
:wallet-transaction-fee "Transaction Fee"
:wallet-transaction-fee-details "Gas limit is the amount of gas to send with your transaction. Increasing this number will not get your transaction processed faster"
:wallet-transaction-total-fee "Total Fee"
:validation-amount-invalid-number "Amount is not a valid number"
:validation-amount-is-too-precise "Amount is too precise. The smallest unit you can send is 1 Wei (1x10^-18 ETH)"

View File

@ -70,5 +70,5 @@
[vector-icons/icon :icons/network {:color :white}]]
[react/text {:style (styles/network-text text-color)}
(if (ethereum/testnet? network-id)
(i18n/label :t/testnet-text {:testnet (get-in ethereum/chains [network-id :name] "Unknown")})
(i18n/label :t/testnet-text {:testnet (get-in ethereum/chains [(ethereum/chain-id->chain-keyword network-id) :name] "Unknown")})
(i18n/label :t/mainnet-text))]]]))

View File

@ -42,7 +42,6 @@
:icons/flash-active (slurp-svg "./resources/icons/flash_active.svg")
:icons/flash-inactive (slurp-svg "./resources/icons/flash_inactive.svg")
:icons/attach (slurp-svg "./resources/icons/attach.svg")
:icons/back (slurp-svg "./resources/icons/back.svg")
:icons/browse (slurp-svg "./resources/icons/browse.svg")
:icons/close (slurp-svg "./resources/icons/close.svg")
:icons/copy-from (slurp-svg "./resources/icons/copy_from.svg")
@ -50,7 +49,6 @@
:icons/dots-vertical (slurp-svg "./resources/icons/dots_vertical.svg")
:icons/exclamation_mark (slurp-svg "./resources/icons/exclamation_mark.svg")
:icons/filter (slurp-svg "./resources/icons/filter.svg")
:icons/forward (slurp-svg "./resources/icons/forward.svg")
:icons/fullscreen (slurp-svg "./resources/icons/fullscreen.svg")
:icons/group-big (slurp-svg "./resources/icons/group_big.svg")
:icons/group-chat (slurp-svg "./resources/icons/group_chat.svg")
@ -64,8 +62,11 @@
:icons/search (slurp-svg "./resources/icons/search.svg")
:icons/smile (slurp-svg "./resources/icons/smile.svg")
:icons/commands-list (slurp-svg "./resources/icons/commands_list.svg")
:icons/back (slurp-svg "./resources/icons/back.svg")
:icons/forward (slurp-svg "./resources/icons/forward.svg")
:icons/dropdown-up (slurp-svg "./resources/icons/dropdown_up.svg")
:icons/dropdown (slurp-svg "./resources/icons/dropdown.svg")
:icons/up (slurp-svg "./resources/icons/up.svg")
:icons/down (slurp-svg "./resources/icons/down.svg")
:icons/grab (slurp-svg "./resources/icons/grab.svg")
:icons/share (slurp-svg "./resources/icons/share.svg")
:icons/tooltip-triangle (slurp-svg "./resources/icons/tooltip-triangle.svg")

View File

@ -1,24 +1,20 @@
(ns status-im.ui.components.image-button.view
(:require [re-frame.core :refer [subscribe dispatch dispatch-sync]]
[status-im.ui.components.react :refer [view
text
image
touchable-highlight]]
[status-im.ui.components.icons.vector-icons :as vi]
[status-im.ui.components.styles :refer [icon-scan]]
[status-im.i18n :refer [label]]
[status-im.ui.components.image-button.styles :as st]))
(:require [status-im.i18n :as i18n]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.image-button.styles :as styles]
[status-im.ui.components.react :as react]
[status-im.ui.components.styles :as components.styles]))
(defn image-button [{:keys [value style handler]}]
[view st/image-button
[touchable-highlight {:on-press handler}
[view st/image-button-content
[vi/icon :icons/fullscreen {:color :blue :style icon-scan}]
(when text
[text {:style style} value])]]])
(defn- image-button [{:keys [value style handler]}]
[react/view styles/image-button
[react/touchable-highlight {:on-press handler}
[react/view styles/image-button-content
[vector-icons/icon :icons/fullscreen {:color :blue :style components.styles/icon-scan}]
(when value
[react/text {:style style} value])]]])
(defn scan-button [{:keys [show-label? handler]}]
[image-button {:value (if show-label?
(label :t/scan-qr))
:style st/scan-button-text
[image-button {:value (when show-label?
(i18n/label :t/scan-qr))
:style styles/scan-button-text
:handler handler}])

View File

@ -48,6 +48,7 @@
(defn item-icon
[{:keys [icon style icon-opts]}]
{:pre [(not (nil? icon))]}
[react/view {:style style}
[vector-icons/icon icon (merge icon-opts {:style styles/item-icon})]])

View File

@ -55,7 +55,10 @@
(def activity-indicator (get-class "ActivityIndicator"))
(def modal (get-class "Modal"))
(def picker (get-class "Picker"))
(def picker-class (get-class "Picker"))
(def picker-item-class
(when-let [picker (get-react-property "Picker")]
(adapt-class (.-Item picker))))
(def pan-responder (.-PanResponder rn-dependencies/react-native))
(def animated (.-Animated rn-dependencies/react-native))
@ -111,10 +114,6 @@
(merge {:underlay-color :transparent} props)
content])
(def picker-item
(when-let [picker (get-react-property "Picker")]
(adapt-class (.-Item picker))))
(defn get-dimensions [name]
(js->clj (.get dimensions name) :keywordize-keys true))
@ -126,6 +125,14 @@
(defn list-item [component]
(r/as-element component))
(defn picker
([{:keys [style item-style selected on-change]} items]
[picker-class {:selectedValue selected :style style :itemStyle item-style :onValueChange on-change}
(for [{:keys [label value]} items]
^{:key (str value)}
[picker-item-class
{:label (or label value) :value value}])]))
;; Image picker
(def image-picker-class rn-dependencies/image-crop-picker)

View File

@ -3,9 +3,7 @@
(:require [status-im.ui.components.styles :as common]
[status-im.utils.platform :refer [platform-specific]]
[status-im.utils.utils :as u]
[status-im.ui.components.react :refer [view
text
touchable-highlight]]))
[status-im.ui.components.react :as react]))
(def sticky-button-style
{:flex-direction :row
@ -25,8 +23,8 @@
(defn sticky-button
([label on-press] (sticky-button label on-press false))
([label on-press once?]
[touchable-highlight {:on-press (if once? (u/wrap-call-once! on-press) on-press)}
[view sticky-button-style
[text {:style sticky-button-label-style
:uppercase? (get-in platform-specific [:uppercase?])}
[react/touchable-highlight {:on-press (if once? (u/wrap-call-once! on-press) on-press)}
[react/view sticky-button-style
[react/text {:style sticky-button-label-style
:uppercase? (get-in platform-specific [:uppercase?])}
label]]]))

View File

@ -3,11 +3,11 @@
[re-frame.core :refer [subscribe dispatch dispatch-sync]]
[reagent.core :as r]
[status-im.ui.components.react :refer [view
text
animated-text
animated-view
text-input
touchable-opacity]]
text
animated-text
animated-view
text-input
touchable-opacity]]
[status-im.ui.components.text-field.styles :as st]
[status-im.i18n :refer [label]]
[status-im.ui.components.animation :as anim]

View File

@ -1,8 +1,9 @@
(ns status-im.ui.screens.db
(:require-macros [status-im.utils.db :refer [allowed-keys]])
(:require [status-im.constants :as constants]
(:require [cljs.spec.alpha :as spec]
[status-im.constants :as constants]
[status-im.utils.platform :as platform]
[cljs.spec.alpha :as spec]
[status-im.utils.ethereum.core :as ethereum]
status-im.ui.screens.accounts.db
status-im.ui.screens.contacts.db
status-im.ui.screens.qr-scanner.db
@ -14,6 +15,11 @@
status-im.ui.screens.discover.db
status-im.ui.screens.network-settings.db))
(def transaction-send-default
{:symbol :ETH
:gas ethereum/default-transaction-gas
:gas-price ethereum/default-gas-price})
;; initial state of app-db
(def app-db {:current-public-key ""
:status-module-initialized? (or platform/ios? js/goog.DEBUG)
@ -33,6 +39,7 @@
:tags []
:sync-state :done
:wallet.transactions constants/default-wallet-transactions
:wallet {:send-transaction transaction-send-default}
:wallet-selected-asset {}
:prices {}
:notifications {}

View File

@ -2,9 +2,8 @@
(:require [status-im.ui.screens.navigation :as navigation]))
(defmethod navigation/preload-data! :qr-code-view
[{:accounts/keys [current-account-id] :as db} [_ _ {:keys [contact qr-source qr-value amount?]}]]
[{:accounts/keys [current-account-id] :as db} [_ _ {:keys [contact qr-source qr-value]}]]
(update db :qr-modal #(merge % {:contact (or contact
(get-in db [:accounts/accounts current-account-id]))
:qr-source qr-source
:qr-value qr-value
:amount? amount?})))
:qr-value qr-value})))

View File

@ -7,14 +7,12 @@
[status-im.ui.components.status-bar.view :refer [status-bar]]
[status-im.i18n :refer [label]]
[status-im.ui.screens.profile.qr-code.styles :as styles]
[status-im.utils.money :as money]
[status-im.utils.ethereum.eip681 :as eip681])
[status-im.utils.money :as money])
(:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defview qr-code-view []
(letsubs [{:keys [photo-path address name]} [:get-in [:qr-modal :contact]]
{:keys [qr-source qr-value amount? dimensions]} [:get :qr-modal]
{:keys [amount]} [:get :contacts/click-params]
{:keys [qr-source qr-value dimensions]} [:get :qr-modal]
chain-id [:get-network-id]]
[react/view styles/wallet-qr-code
[status-bar {:type :modal}]
@ -37,11 +35,10 @@
:height (.-height layout)}]))}
(when (:width dimensions)
[react/view {:style (styles/qr-code-container dimensions)}
(when-let [value (eip681/generate-uri qr-value (merge {:chain-id chain-id} (when amount? {:value (money/str->wei amount)})))]
[qr-code {:value value
:size (- (min (:width dimensions)
(:height dimensions))
80)}])])]
[qr-code {:value qr-value
:size (- (min (:width dimensions)
(:height dimensions))
80)}]])]
[react/view styles/footer
(if (= :address qr-source)
[react/view styles/wallet-info

View File

@ -4,8 +4,7 @@
[status-im.ui.screens.navigation :as nav]
[status-im.utils.handlers :as u :refer [register-handler]]
[status-im.utils.utils :as utils]
[status-im.i18n :as i18n]
[status-im.utils.ethereum.eip681 :as eip681]))
[status-im.i18n :as i18n]))
(defmethod nav/preload-data! :qr-scanner
[db [_ _ identifier]]
@ -32,10 +31,10 @@
(fn [db [_ identifier]]
(update db :qr-codes dissoc identifier)))
(defn handle-qr-request
(defn- handle-qr-request
[db [_ context data]]
(when-let [handler (get-in db [:qr-codes context])]
(re-frame/dispatch [handler context (:address (eip681/parse-uri data))])))
(re-frame/dispatch [handler context data])))
(defn clear-qr-request [db [_ context]]
(-> db

View File

@ -39,6 +39,7 @@
[status-im.ui.screens.wallet.choose-recipient.views :refer [choose-recipient]]
[status-im.ui.screens.wallet.request.views :refer [request-transaction]]
[status-im.ui.screens.wallet.wallet-list.views :refer [wallet-list-screen]]
[status-im.ui.screens.wallet.send.views :as wallet.send]
[status-im.ui.screens.wallet.settings.views :as wallet-settings]
[status-im.ui.screens.wallet.transactions.views :as wallet-transactions]
[status-im.ui.screens.wallet.send.transaction-sent.views :refer [transaction-sent transaction-sent-modal]]
@ -131,5 +132,6 @@
:wallet-settings-assets wallet-settings/manage-assets
:wallet-send-transaction-modal send-transaction-modal
:wallet-transaction-sent-modal transaction-sent-modal
:wallet-transaction-fee wallet.send/transaction-fee
(throw (str "Unknown modal view: " modal-view)))]
[component])]])]])))))

View File

@ -1,6 +1,7 @@
(ns status-im.ui.screens.wallet.choose-recipient.events
(:require [status-im.constants :as constants]
[status-im.i18n :as i18n]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.eip681 :as eip681]
[status-im.utils.handlers :as handlers]))
@ -11,36 +12,37 @@
toggled-state (if (= :on flashlight-state) :off :on)]
(assoc-in db [:wallet :send-transaction :camera-flashlight] toggled-state))))
(defn choose-address-and-name [db address name amount]
(defn- fill-request-details [db address name amount]
(update-in
db [:wallet :send-transaction]
#(cond-> (assoc % :to-address address :to-name name)
#(cond-> (assoc % :to address :to-name name)
amount (assoc :amount amount))))
(defn- extract-details
"First try to parse as EIP681 URI, if not assume this is an address directly.
Returns a map containing at least the `address` and `chain-id` keys"
[s network]
(or (eip681/parse-uri s) {:address s :chain-id network}))
Returns a map containing at least the `address`, `symbol` and `chain-id` keys"
[s chain-id]
(or (let [m (eip681/parse-uri s)]
(merge m (eip681/extract-request-details m)))
(when (ethereum/address? s)
{:address s :chain-id chain-id :symbol :ETH})))
(handlers/register-handler-fx
:choose-recipient
:wallet/fill-request-from-url
(fn [{{:keys [web3 network] :as db} :db} [_ data name]]
(let [{:keys [view-id]} db
current-network-id (get-in constants/default-networks [network :raw-config :NetworkId])
{:keys [address chain-id] :as m} (extract-details data current-network-id)
;; isAddress works with or without address with leading '0x'
valid-address? (.isAddress web3 address)
valid-network? (boolean (= current-network-id chain-id))]
current-chain-id (get-in constants/default-networks [network :raw-config :NetworkId])
{:keys [address chain-id value]} (extract-details data current-chain-id)
valid-network? (boolean (= current-chain-id chain-id))]
(cond-> {:db db}
(and valid-address? (= :choose-recipient view-id)) (assoc :dispatch [:navigate-back])
(and valid-network? valid-address?) (update :db #(choose-address-and-name % address name (eip681/parse-value m)))
(not valid-address?) (assoc :show-error (i18n/label :t/wallet-invalid-address {:data data}))
(and valid-address? (not valid-network?)) (assoc :show-error (i18n/label :t/wallet-invalid-chain-id {:data data}))))))
(and address (= :choose-recipient view-id)) (assoc :dispatch [:navigate-back])
(and address valid-network?) (update :db #(fill-request-details % address name value))
(not address) (assoc :show-error (i18n/label :t/wallet-invalid-address {:data data}))
(and address (not valid-network?)) (assoc :show-error (i18n/label :t/wallet-invalid-chain-id {:data data :chain current-chain-id}))))))
(handlers/register-handler-fx
:wallet-open-send-transaction
(fn [{db :db} [_ address name]]
{:db (choose-address-and-name db address name nil)
:wallet/fill-request-from-contact
(fn [{db :db} [_ {:keys [address name]}]]
{:db (fill-request-details db address name nil)
:dispatch-n [[:navigate-back]
[:navigate-back]]}))

View File

@ -65,9 +65,9 @@
{:flex 1})
(def preview
{:flex 1
:justify-content :flex-end
:align-items :center})
{:flex 1
:justifyContent :flex-end
:alignItems :center})
(def corner-dimensions
{:position :absolute

View File

@ -16,7 +16,7 @@
(defn choose-from-contacts []
(re-frame/dispatch [:navigate-to-modal
:contact-list-modal
{:handler #(re-frame/dispatch [:wallet-open-send-transaction (:address %1) (:name %1)])
{:handler #(re-frame/dispatch [:wallet/fill-request-from-contact %])
:action :send
:params {:hide-actions? true}}]))
@ -41,7 +41,7 @@
[react/touchable-highlight {:style (styles/recipient-touchable true)
:on-press #(react/get-from-clipboard
(fn [clipboard]
(re-frame/dispatch [:choose-recipient (string/trim-newline clipboard) nil])))}
(re-frame/dispatch [:wallet/fill-request-from-url (string/trim-newline clipboard) nil])))}
[react/view {:style styles/recipient-button}
[react/text {:style styles/recipient-button-text}
(i18n/label :t/wallet-address-from-clipboard)]
@ -89,6 +89,6 @@
:aspect :fill
:captureAudio false
:torchMode (camera/set-torch camera-flashlight)
:onBarCodeRead #(re-frame/dispatch [:choose-recipient (camera/get-qr-code-data %) nil])}])
:onBarCodeRead #(re-frame/dispatch [:wallet/fill-request-from-url (camera/get-qr-code-data %) nil])}])
[viewfinder camera-dimensions]]
[recipient-buttons]]))

View File

@ -1,15 +1,19 @@
(ns status-im.ui.screens.wallet.components.views
(:require-macros [status-im.utils.views :as views])
(:require [status-im.ui.components.react :as react]
(:require [re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im.ui.components.react :as react]
[status-im.ui.components.styles :as components.styles]
[status-im.ui.screens.wallet.components.styles :as styles]
[status-im.i18n :as i18n]
[reagent.core :as reagent]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.animation :as animation]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.money :as money]
[status-im.utils.platform :as platform]
[status-im.ui.screens.wallet.components.animations :as animations]))
[status-im.ui.screens.wallet.components.animations :as animations]
[status-im.ui.screens.wallet.utils :as wallet.utils]))
(views/defview tooltip [label & [style]]
(views/letsubs [bottom-value (animation/create-value 16)
@ -48,68 +52,78 @@
(when-not (nil? error)
[tooltip error])]]))))
;;TODO (andrey) this should be choose component with the list of currencies
(defn choose-currency [& [style]]
[react/view
[react/text {:style styles/label} (i18n/label :t/currency)]
[react/view (merge styles/currency-container
style)
[react/text {:style styles/wallet-name} "ETH"]]])
(defn choose-recipient-disabled [{:keys [address name]}]
[react/view
[react/text {:style styles/label} (i18n/label :t/recipient)]
[react/view (merge styles/recipient-container
styles/container-disabled)
(when name
[react/view styles/recipient-name-container
[react/text {:style (styles/participant true)
:number-of-lines 1}
name]])
[react/view components.styles/flex
[react/text {:style (styles/participant (not name))
:number-of-lines 1
:ellipsizeMode :middle}
address]]]])
(defn choose-recipient [{:keys [address name on-press style]}]
(let [address? (and (not (nil? address)) (not= address ""))]
[react/touchable-highlight {:on-press on-press}
[react/view
[react/text {:style styles/label} (i18n/label :t/recipient)]
[react/view (merge styles/recipient-container
style)
(when name
[react/view styles/recipient-name-container
[react/text {:style (styles/participant true)
:number-of-lines 1}
name]])
[react/view components.styles/flex
[react/text {:style (styles/participant (and (not name) address?))
:number-of-lines 1
:ellipsizeMode :middle}
(if address? address "Choose recipient...")]]
[vector-icons/icon :icons/forward {:color :white}]]]]))
;;TODO (andrey) this should be choose component with the list of wallets
(views/defview choose-wallet [& [style]]
(views/letsubs [balance [:balance]]
(views/defview view-currency [style]
(views/letsubs [visible-tokens [:wallet.settings/visible-tokens]
symbol [:wallet.send/symbol]]
[react/view
[react/text {:style styles/label} (i18n/label :t/wallet)]
[react/view (merge styles/wallet-container
[react/text {:style styles/label} (i18n/label :t/currency)]
[react/view (merge styles/currency-container
style)
[react/text {:style styles/wallet-name} (i18n/label :t/main-wallet)]
(if balance
[react/view {:style styles/wallet-value-container}
[react/text {:style (merge styles/wallet-value styles/wallet-value-amount)
:number-of-lines 1
:ellipsize-mode :tail}
(str (money/wei->ether (:ETH balance)))] ;; TODO(jeluard) update based on currency selected
[react/text {:style styles/wallet-value}
(i18n/label :t/eth)]]
[react/text {:style styles/wallet-value}
"..."])]]))
[react/text
(name symbol)]]]))
(views/defview choose-currency [style]
(views/letsubs [visible-tokens [:wallet.settings/visible-tokens]
symbol [:wallet.send/symbol]]
[react/view
[react/text {:style styles/label} (i18n/label :t/currency)]
[react/view (merge styles/currency-container
style)
[react/picker {:selected (name symbol)
:style {:color "white"}
:item-style styles/wallet-name
:on-change #(re-frame/dispatch [:wallet.send/set-symbol (keyword %)])}
(map (fn [s] {:value (name s) :color "white"}) (conj visible-tokens (:symbol tokens/ethereum)))]]]))
(defn choose-recipient-content [{:keys [address name on-press style]}]
(let [address? (and (not (nil? address)) (not= address ""))]
[react/view
[react/text {:style styles/label} (i18n/label :t/recipient)]
[react/view (merge styles/recipient-container
(when-not on-press styles/container-disabled)
style)
(when name
[react/view styles/recipient-name-container
[react/text {:style (styles/participant true)
:number-of-lines 1}
name]])
[react/view components.styles/flex
[react/text {:style (styles/participant (and (not name) address?))
:number-of-lines 1
:ellipsizeMode :middle}
(if address? address "Choose recipient...")]]
(when on-press
[vector-icons/icon :icons/forward {:color :white}])]]))
(defn choose-recipient [{:keys [on-press] :as m}]
(if on-press
[react/touchable-highlight {:on-press on-press}
[react/view ;; TODO(jeluard) remove extra view when migrating to latest RN
[choose-recipient-content m]]]
[react/view
[choose-recipient-content m]]))
(views/defview choose-wallet [& [style]]
(views/letsubs [network [:network]
balance [:balance]
symbol [:wallet.send/symbol]]
(let [amount (get balance symbol)
decimals (:decimals (tokens/asset-for (ethereum/network->chain-keyword network) symbol))]
[react/view
[react/text {:style styles/label} (i18n/label :t/wallet)]
[react/view (merge styles/wallet-container
style)
[react/text {:style styles/wallet-name} (i18n/label :t/main-wallet)]
(if amount
[react/view {:style styles/wallet-value-container}
[react/text {:style (merge styles/wallet-value styles/wallet-value-amount)
:number-of-lines 1
:ellipsize-mode :tail}
(wallet.utils/format-amount amount decimals)]
[react/text {:style styles/wallet-value}
(name symbol)]]
[react/text {:style styles/wallet-value}
"..."])]])))
(defn separator []
[react/view styles/separator])

View File

@ -56,9 +56,9 @@
(reg-fx
:get-tokens-balance
(fn [{:keys [web3 symbols chain-id account-id success-event error-event]}]
(fn [{:keys [web3 symbols chain account-id success-event error-event]}]
(doseq [symbol symbols]
(let [contract (:address (tokens/token-for chain-id symbol))]
(let [contract (:address (tokens/symbol->token chain symbol))]
(get-token-balance {:web3 web3
:contract contract
:account-id account-id
@ -94,7 +94,7 @@
:get-tokens-balance {:web3 web3
:account-id current-account-id
:symbols symbols
:chain-id (ethereum/network->chain-id network)
:chain (ethereum/network->chain-keyword network)
:success-event :update-token-balance-success
:error-event :update-token-balance-fail}
:get-prices {:from "ETH"

View File

@ -16,25 +16,18 @@
[status-im.utils.utils :as utils]
[status-im.ui.screens.wallet.main.styles :as styles]
[status-im.ui.screens.wallet.styles :as wallet.styles]
[status-im.ui.screens.wallet.utils :as wallet.utils]
[status-im.ui.components.styles :as components.styles]
[status-im.ui.screens.wallet.components.views :as components]
[status-im.ui.components.button.styles :as button.styles]
[status-im.ui.screens.wallet.views :as wallet.views]))
(defn- show-not-implemented! []
(utils/show-popup "TODO" "Not implemented yet!"))
(defn toolbar-title []
[react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to :wallet-list])}
[react/view {:style styles/toolbar-title-container}
[react/text {:style styles/toolbar-title-text
:font :toolbar-title
:number-of-lines 1}
(i18n/label :t/main-wallet)]
[vi/icon
:icons/dropdown
{:container-style styles/toolbar-title-icon
:color :white}]]])
[react/view {:style styles/toolbar-title-container}
[react/text {:style styles/toolbar-title-text
:font :toolbar-title
:number-of-lines 1}
(i18n/label :t/main-wallet)]])
(def transaction-history-action
{:icon :icons/transaction-history
@ -62,10 +55,6 @@
[react/view {:style styles/total-balance}
[react/text {:style styles/total-balance-value} usd-value]
[react/text {:style styles/total-balance-currency} (i18n/label :t/usd-currency)]]
[react/view {:style styles/value-variation}
[react/text {:style styles/value-variation-title}
(i18n/label :t/wallet-total-value)]
[components/change-display change]]
[react/view {:style (merge button.styles/buttons-container styles/buttons)}
[btn/button {:disabled? syncing?
:on-press #(re-frame/dispatch [:navigate-to :wallet-send-transaction])
@ -78,43 +67,27 @@
[btn/button {:disabled? true :style (button.styles/button-bar :last) :text-style styles/main-button-text}
(i18n/label :t/wallet-exchange)]]]])
(defn add-asset []
[list/touchable-item show-not-implemented!
[react/view
[list/item
[list/item-icon {:icon :icons/add :style styles/add-asset-icon :icon-opts {:color :blue}}]
[react/view {:style styles/asset-item-value-container}
[react/text {:style styles/add-asset-text}
(i18n/label :t/wallet-add-asset)]]]]])
(defn render-asset [{:keys [name symbol icon decimals amount] :as asset}]
(if name ;; If no 'name' then this the dummy value used to render `add-asset`
[list/touchable-item #(re-frame/dispatch [:navigate-to-asset asset])
[react/view
[list/item
[list/item-image icon]
[react/view {:style styles/asset-item-value-container}
[react/text {:style styles/asset-item-value
:number-of-lines 1
:ellipsize-mode :tail}
(money/to-fixed (money/token->unit (or amount (money/bignumber 0)) decimals))]
[react/text {:style styles/asset-item-currency
:uppercase? true
:number-of-lines 1}
(clojure.core/name symbol)]]
[list/item-icon {:icon :icons/forward}]]]]
[add-asset]))
(defn tokens-for [network]
(get tokens/all (ethereum/network->chain-id network)))
(defn- render-asset [{:keys [name symbol icon decimals amount] :as asset}]
[react/view
[list/item
[list/item-image icon]
[react/view {:style styles/asset-item-value-container}
[react/text {:style styles/asset-item-value
:number-of-lines 1
:ellipsize-mode :tail}
(wallet.utils/format-amount amount decimals)]
[react/text {:style styles/asset-item-currency
:uppercase? true
:number-of-lines 1}
(clojure.core/name symbol)]]]])
(defn asset-section [network balance visible-tokens prices-loading? balance-loading?]
(let [tokens (filter #(contains? visible-tokens (:symbol %)) (tokens-for network))
(let [tokens (filter #(contains? visible-tokens (:symbol %)) (tokens/tokens-for (ethereum/network->chain-keyword network)))
assets (map #(assoc % :amount (get balance (:symbol %))) (concat [tokens/ethereum] (when config/erc20-enabled? tokens)))]
[react/view {:style styles/asset-section}
[react/text {:style styles/asset-section-title} (i18n/label :t/wallet-assets)]
[list/flat-list
{:data assets ;; TODO(jeluard) Reenable once we `add-an-asset` story is flecthed out ;; (concat assets [{}]) ;; Extra map triggers rendering for add-asset
{:data assets
:render-fn render-asset
:on-refresh #(re-frame/dispatch [:update-wallet (when config/erc20-enabled? (map :symbol tokens))])
:refreshing (boolean (or prices-loading? balance-loading?))}]]))

View File

@ -1,11 +1,13 @@
(ns status-im.ui.screens.wallet.navigation
(:require [re-frame.core :as re-frame]
[status-im.ui.screens.db :as db]
[status-im.ui.screens.navigation :as navigation]
[status-im.ui.screens.wallet.main.views :as main]))
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.tokens :as tokens]))
(defmethod navigation/preload-data! :wallet
[db _]
(re-frame/dispatch [:update-wallet (map :symbol (main/tokens-for (:network db)))])
(re-frame/dispatch [:update-wallet (map :symbol (tokens/tokens-for (ethereum/network->chain-keyword (:network db))))])
(assoc-in db [:wallet :current-tab] 0))
(defmethod navigation/preload-data! :transactions-history
@ -17,8 +19,11 @@
[db _]
(dissoc db :wallet/request-transaction))
(defn- dissoc-transaction-details [m]
(apply dissoc m (apply disj (set (keys m)) (keys db/transaction-send-default))))
(defmethod navigation/preload-data! :wallet-send-transaction
[db [event]]
(if (= event :navigate-back)
db
(update db :wallet dissoc :send-transaction)))
(update-in db [:wallet :send-transaction] dissoc-transaction-details)))

View File

@ -15,6 +15,7 @@
[status-im.ui.components.styles :as components.styles]
[status-im.i18n :as i18n]
[status-im.utils.platform :as platform]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.eip681 :as eip681]
[status-im.utils.money :as money]))
@ -34,7 +35,7 @@
(views/letsubs [account [:get-current-account]
chain-id [:get-network-id]]
[components.qr-code/qr-code
{:value (eip681/generate-uri (:address account) (merge {:chain-id chain-id} (when amount {:value amount})))
{:value (eip681/generate-uri (ethereum/normalized-address (:address account)) (merge {:chain-id chain-id} (when amount {:value amount})))
:size 256}]))
(views/defview request-transaction []

View File

@ -4,7 +4,7 @@
[status-im.utils.money :as money]))
(spec/def ::amount (spec/nilable money/valid?))
(spec/def ::to-address (spec/nilable string?))
(spec/def ::to (spec/nilable string?))
(spec/def ::to-name (spec/nilable string?))
(spec/def ::amount-error (spec/nilable string?))
(spec/def ::password (spec/nilable string?))
@ -20,9 +20,14 @@
(spec/def ::camera-permitted? boolean?)
(spec/def ::in-progress? boolean?)
(spec/def ::from-chat? (spec/nilable boolean?))
(spec/def ::symbol (spec/nilable keyword?))
(spec/def ::gas (spec/nilable money/valid?))
(spec/def ::gas-price (spec/nilable money/valid?))
(spec/def ::advanced? boolean?)
(spec/def :wallet/send-transaction (allowed-keys
:opt-un [::amount ::to-address ::to-name ::amount-error ::password
:opt-un [::amount ::to ::to-name ::amount-error ::password
::waiting-signal? ::signing? ::id ::later?
::camera-dimensions ::camera-flashlight ::in-progress?
::wrong-password? ::camera-permitted? ::from-chat?]))
::wrong-password? ::camera-permitted? ::from-chat? ::symbol ::advanced?
::gas ::gas-price]))

View File

@ -3,12 +3,15 @@
[re-frame.core :as re-frame]
[status-im.i18n :as i18n]
[status-im.native-module.core :as status]
[status-im.utils.handlers :as handlers]
[status-im.ui.screens.wallet.db :as wallet.db]
[status-im.utils.types :as types]
[status-im.utils.money :as money]
[status-im.utils.utils :as utils]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.erc20 :as erc20]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.handlers :as handlers]
[status-im.utils.hex :as utils.hex]
[status-im.utils.money :as money]
[status-im.utils.types :as types]
[status-im.utils.utils :as utils]
[status-im.constants :as constants]))
;;;; FX
@ -18,13 +21,21 @@
(fn [{:keys [password id on-completed]}]
(status/complete-transactions (list id) password on-completed)))
(defn- send-ethers [{:keys [web3 from to value gas gas-price]}]
(.sendTransaction (.-eth web3)
(clj->js {:from from :to to :value value :gas gas :gasPrice gas-price})
#()))
(defn- send-tokens [{:keys [web3 from to value gas gas-price symbol network]}]
(let [contract (:address (tokens/symbol->token (ethereum/network->chain-keyword network) symbol))]
(erc20/transfer web3 contract from to value {:gas gas :gasPrice gas-price} #())))
(re-frame/reg-fx
::send-transaction
(fn [{:keys [web3] :as params}]
(when web3
(.sendTransaction (.-eth web3)
(clj->js (select-keys params [:from :to :value]))
#()))))
(fn [{:keys [symbol] :as params}]
(case symbol
:ETH (send-ethers params)
(send-tokens params))))
(re-frame/reg-fx
::show-transaction-moved
@ -59,6 +70,23 @@
(assoc-in [:wallet :send-transaction :amount] (money/ether->wei value))
(assoc-in [:wallet :send-transaction :amount-error] error))})))
(defn- estimated-gas [symbol]
(if (tokens/ethereum? symbol)
ethereum/default-transaction-gas
;; TODO(jeluard) Rely on estimateGas call
(.times ethereum/default-transaction-gas 5)))
(handlers/register-handler-fx
:wallet.send/set-symbol
(fn [{:keys [db]} [_ symbol]]
{:db (-> (assoc-in db [:wallet :send-transaction :symbol] symbol)
(assoc-in [:wallet :send-transaction :gas] (estimated-gas symbol)))}))
(handlers/register-handler-fx
:wallet.send/toggle-advanced
(fn [{:keys [db]} [_ advanced?]]
{:db (assoc-in db [:wallet :send-transaction :advanced?] advanced?)}))
(def ^:private clear-send-properties {:id nil
:signing? false
:wrong-password? false
@ -76,11 +104,12 @@
[(re-frame/inject-cofx :now)]
(fn [{:keys [db now]} [_ {:keys [id message_id args] :as transaction}]]
(if (transaction-valid? transaction)
(let [{:keys [from to value data gas gasPrice]} args
(let [{:keys [from to value symbol data gas gasPrice]} args
;;TODO (andrey) revisit this map later (this map from old transactions, idk if we need all these fields)
transaction {:id id
:from from
:to to
:symbol symbol
:value (money/bignumber (or value 0))
:data data
:gas (money/to-decimal gas)
@ -160,8 +189,9 @@
:wallet/sign-transaction
(fn [{{:keys [web3]
:accounts/keys [accounts current-account-id] :as db} :db} [_ later?]]
(let [db' (assoc-in db [:wallet :send-transaction :wrong-password?] false)
{:keys [amount id password to-address]} (get-in db [:wallet :send-transaction])]
(let [db' (assoc-in db [:wallet :send-transaction :wrong-password?] false)
network (:network db)
{:keys [amount id password to symbol gas gas-price] :as m} (get-in db [:wallet :send-transaction])]
(if id
{::accept-transaction {:id id
:password password
@ -171,10 +201,14 @@
:waiting-signal? true
:later? later?
:in-progress? true)
::send-transaction {:web3 web3
:from (get-in accounts [current-account-id :address])
:to to-address
:value amount}}))))
::send-transaction {:web3 web3
:from (get-in accounts [current-account-id :address])
:to to
:value amount
:gas gas
:gas-price gas-price
:symbol symbol
:network network}}))))
(handlers/register-handler-fx
:wallet/sign-transaction-modal
@ -226,3 +260,13 @@
:wallet.send/set-signing?
(fn [{:keys [db]} [_ signing?]]
{:db (assoc-in db [:wallet :send-transaction :signing?] signing?)}))
(handlers/register-handler-fx
:wallet.send/set-gas
(fn [{:keys [db]} [_ gas]]
{:db (assoc-in db [:wallet :send-transaction :gas] (money/bignumber gas))}))
(handlers/register-handler-fx
:wallet.send/set-gas-price
(fn [{:keys [db]} [_ gas-price]]
{:db (assoc-in db [:wallet :send-transaction :gas-price] (money/bignumber gas-price))}))

View File

@ -1,6 +1,7 @@
(ns status-im.ui.screens.wallet.send.styles
(:require-macros [status-im.utils.styles :refer [defnstyle defstyle]])
(:require [status-im.ui.components.styles :as styles]))
(:require [status-im.ui.components.styles :as styles]
[status-im.ui.screens.wallet.components.styles :as wallet.components.styles]))
(def toolbar
{:background-color styles/color-blue5
@ -76,3 +77,70 @@
{:text-align :center
:margin-top 22
:margin-horizontal 92})
(def advanced-button
{:flex-direction :row
:background-color styles/color-blue6
:border-radius 50
:padding 8})
(def advanced-button-wrapper
{:align-items :center})
(def advanced-wrapper
{:margin-horizontal 15})
(def advanced-options-wrapper
{:height 52
:background-color styles/color-white-transparent-4
:border-radius 4
:margin-top 16
:margin-bottom 16
:align-items :center
:flex-direction :row})
(def advanced-options-text-wrapper
{:flex 1
:flex-direction :row
:justify-content :space-between
:margin-horizontal 15})
(def advanced-label
{:text-align-vertical :center
:margin-left 4})
(def advanced-fees-text
{:color styles/color-white})
(def advanced-fees-details-text
{:color styles/color-white-transparent})
(def transaction-fee-block-wrapper
{:flex-direction :row
:margin-top 15})
(def transaction-fee-column-wrapper
{:flex 0.5
:margin-horizontal 15})
(def transaction-fee-bubble
(merge advanced-options-wrapper
{:flex-direction :row
:justify-content :space-between
:padding-horizontal 15}))
(def transaction-fee-bubble-read-only
(merge transaction-fee-bubble
{:background-color styles/color-blue6}))
(def transaction-fee-info
{:margin 15})
(def transaction-fee-input
{:flex 1
:keyboard-type :numeric
:auto-capitalize "none"
:placeholder "0.000"
:placeholder-text-color styles/color-white-transparent
:selection-color :white
:style wallet.components.styles/text-input})

View File

@ -16,6 +16,16 @@
(fn [wallet]
(:send-transaction wallet)))
(re-frame/reg-sub :wallet.send/symbol
:<- [::send-transaction]
(fn [send-transaction]
(:symbol send-transaction)))
(re-frame/reg-sub :wallet.send/advanced?
:<- [::send-transaction]
(fn [send-transaction]
(:advanced? send-transaction)))
(re-frame/reg-sub :wallet.send/camera-dimensions
:<- [::send-transaction]
(fn [send-transaction]
@ -55,29 +65,27 @@
(merge send-transaction
unsigned-transaction))))
(defn sign-enabled? [amount-error to-address amount]
(defn sign-enabled? [amount-error to amount]
(and
(nil? amount-error)
(not (nil? to-address)) (not= to-address "")
(not (nil? to)) (not= to "")
(not (nil? amount)) (not= amount "")))
(re-frame/reg-sub :wallet.send/transaction
:<- [::send-transaction]
:<- [:balance]
(fn [[{:keys [amount to] :as transaction} balance]]
(fn [[{:keys [amount to symbol] :as transaction} balance]]
(assoc transaction :sufficient-funds? (or (nil? amount)
;; TODO(jeluard) Modify to consider tokens
(money/sufficient-funds? amount (get balance :ETH))))))
(money/sufficient-funds? amount (get balance symbol))))))
(re-frame/reg-sub :wallet.send/unsigned-transaction
:<- [::unsigned-transaction]
:<- [:contacts/by-address]
:<- [:balance]
(fn [[{:keys [value to] :as transaction} contacts balance]]
(fn [[{:keys [value to symbol] :as transaction} contacts balance]]
(when transaction
(let [contact (contacts (utils.hex/normalize-hex to))
;; TODO(jeluard) Modify to consider tokens
sufficient-funds? (money/sufficient-funds? value (get balance :ETH))]
sufficient-funds? (money/sufficient-funds? value (get balance symbol))]
(cond-> (assoc transaction
:amount value
:sufficient-funds? sufficient-funds?)

View File

@ -1,15 +1,17 @@
(ns status-im.ui.screens.wallet.send.views
(:require [re-frame.core :as re-frame]
[status-im.i18n :as i18n]
[status-im.ui.components.animation :as animation]
[status-im.ui.components.camera :as camera]
[status-im.ui.components.common.common :as common]
[status-im.ui.components.styles :as styles]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.react :as react]
[status-im.ui.components.status-bar.view :as status-bar]
[status-im.ui.components.styles :as components.styles]
[status-im.ui.components.toolbar.actions :as act]
[status-im.ui.components.toolbar.view :as toolbar]
[status-im.i18n :as i18n]
[status-im.ui.screens.wallet.components.styles :as wallet.components.styles]
[status-im.ui.screens.wallet.components.views :as components]
[status-im.ui.screens.wallet.send.animations :as send.animations]
[status-im.ui.screens.wallet.send.styles :as send.styles]
@ -19,13 +21,6 @@
[status-im.utils.utils :as utils])
(:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defn toolbar-view [signing?]
[toolbar/toolbar {:style wallet.styles/toolbar}
[toolbar/nav-button (act/back-white (if signing?
#(re-frame/dispatch [:wallet/discard-transaction-navigate-back])
act/default-handler))]
[toolbar/content-title {:color :white} (i18n/label :t/send-transaction)]])
(defn sign-later-popup
[from-chat?]
(utils/show-question
@ -52,7 +47,7 @@
{:auto-focus true
:secure-text-entry true
:placeholder (i18n/label :t/enter-password)
:placeholder-text-color "#939ba1"
:placeholder-text-color components.styles/color-gray4
:on-change-text #(re-frame/dispatch [:wallet.send/set-password %])
:style send.styles/password}]]]
(when wrong-password?
@ -72,15 +67,15 @@
[components/button-text (i18n/label :t/transactions-sign-transaction)]
[vector-icons/icon :icons/forward {:color :white :container-style wallet.styles/forward-icon-container}]]]]))
(defn sign-enabled? [amount-error to-address amount]
(defn- sign-enabled? [amount-error to amount]
(and
(nil? amount-error)
(not (nil? to-address)) (not= to-address "")
(not (nil? to)) (not= to "")
(not (nil? amount))))
;; "Sign Later" and "Sign Transaction >" buttons
(defn- sign-buttons [amount-error to-address amount sufficient-funds? sign-later-handler]
(let [sign-enabled? (sign-enabled? amount-error to-address amount)
(defn- sign-buttons [amount-error to amount sufficient-funds? sign-later-handler]
(let [sign-enabled? (sign-enabled? amount-error to amount)
immediate-sign-enabled? (and sign-enabled? sufficient-funds?)]
[react/view wallet.styles/buttons-container
(when sign-enabled?
@ -94,7 +89,7 @@
[components/button-text (i18n/label :t/transactions-sign-transaction)]
[vector-icons/icon :icons/forward {:color :white :container-style wallet.styles/forward-icon-container}]]]]))
(defn request-camera-permissions []
(defn- request-camera-permissions []
(when platform/android?
(re-frame/dispatch [:request-permissions [:camera]]))
(camera/request-access
@ -102,87 +97,163 @@
(re-frame/dispatch [:set-in [:wallet :send-transaction :camera-permitted?] permitted?])
(re-frame/dispatch [:navigate-to :choose-recipient]))))
(defview send-transaction []
(letsubs [transaction [:wallet.send/transaction]
scroll (atom nil)]
(let [{:keys [amount amount-error signing? to-address to-name in-progress? sufficient-funds?]} transaction]
[react/keyboard-avoiding-view wallet.styles/wallet-modal-container
[react/view components.styles/flex
[status-bar/status-bar {:type :wallet}]
[toolbar-view signing?]
[common/network-info {:text-color :white}]
[react/scroll-view {:keyboardShouldPersistTaps :always
:ref #(reset! scroll %)}
[react/view components.styles/flex
[react/view wallet.styles/choose-participant-container
[components/choose-recipient {:address to-address
:name to-name
:on-press request-camera-permissions}]]
[react/view wallet.styles/choose-wallet-container
[components/choose-wallet]]
[react/view wallet.styles/amount-container
[components/amount-input
{:error (or amount-error
(when-not sufficient-funds? (i18n/label :t/wallet-insufficient-funds)))
:input-options {:auto-focus true
:on-focus (fn [] (when @scroll (js/setTimeout #(.scrollToEnd @scroll) 100)))
:default-value (str (money/to-fixed (money/wei->ether amount)))
:on-change-text #(re-frame/dispatch [:wallet.send/set-and-validate-amount %])}}]
[react/view wallet.styles/choose-currency-container
[components/choose-currency wallet.styles/choose-currency]]]]]
[components/separator]
(if signing?
[signing-buttons
#(re-frame/dispatch [:wallet/discard-transaction])
#(re-frame/dispatch [:wallet/sign-transaction])
in-progress?]
[sign-buttons amount-error to-address amount sufficient-funds? #(sign-later-popup false)])
(when signing?
[sign-panel])]
(when in-progress? [react/view send.styles/processing-view])])))
(defn toolbar-modal [from-chat?]
(defn- toolbar-modal [from-chat?]
[toolbar/toolbar {:style wallet.styles/toolbar}
[toolbar/nav-button (act/close-white (if from-chat?
#(re-frame/dispatch [:wallet/discard-transaction-navigate-back])
act/default-handler))]
[toolbar/content-title {:color :white} (i18n/label :t/send-transaction)]])
(defn- toolbar-view [signing?]
[toolbar/toolbar {:style wallet.styles/toolbar}
[toolbar/nav-button (act/back-white (if signing?
#(re-frame/dispatch [:wallet/discard-transaction-navigate-back])
act/default-handler))]
[toolbar/content-title {:color :white} (i18n/label :t/send-transaction)]])
(defn- transaction-fee-toolbar []
[toolbar/toolbar {:style wallet.styles/toolbar}
[toolbar/nav-button (act/close-white #(re-frame/dispatch [:wallet/discard-transaction-navigate-back]))]
[toolbar/content-title {:color :white} (i18n/label :t/wallet-transaction-fee)]])
(defn- max-fee [gas gas-price]
(when (and gas gas-price)
(money/wei->ether (.times gas gas-price))))
(defview transaction-fee []
(letsubs [{:keys [amount gas gas-price symbol]} [:wallet.send/transaction]]
[react/keyboard-avoiding-view wallet.styles/wallet-modal-container
[react/view components.styles/flex
[status-bar/status-bar {:type :modal-wallet}]
[transaction-fee-toolbar]
[react/view send.styles/transaction-fee-block-wrapper
[react/view send.styles/transaction-fee-column-wrapper
[react/text {:style send.styles/advanced-fees-text}
(i18n/label :t/gas-limit)]
[react/view send.styles/advanced-options-wrapper
[react/text-input (merge send.styles/transaction-fee-input
{:on-change-text #(re-frame/dispatch [:wallet.send/set-gas %])
:default-value (str (money/to-fixed gas))})]]]
[react/view send.styles/transaction-fee-column-wrapper
[react/text {:style send.styles/advanced-fees-text}
(i18n/label :t/gas-price)]
[react/view send.styles/transaction-fee-bubble
[react/text-input (merge send.styles/transaction-fee-input
{:on-change-text #(re-frame/dispatch [:wallet.send/set-gas-price (money/->wei :gwei %)])
:default-value (str (money/to-fixed (money/wei-> :gwei gas-price)))})]
[react/text {:style send.styles/advanced-fees-details-text}
"Gwei"]]]]
[react/view send.styles/transaction-fee-info
[react/text {:style send.styles/advanced-fees-text}
(i18n/label :t/wallet-transaction-fee-details)]]
[components/separator]
[react/view send.styles/transaction-fee-block-wrapper
[react/view send.styles/transaction-fee-column-wrapper
[react/text {:style send.styles/advanced-fees-text}
(i18n/label :t/amount)]
[react/view send.styles/transaction-fee-bubble-read-only
[react/text {:style send.styles/advanced-fees-text}
(str (money/to-fixed (money/wei->ether amount)))]
[react/text {:style send.styles/advanced-fees-details-text}
(name symbol)]]]
[react/view send.styles/transaction-fee-column-wrapper
[react/text {:style send.styles/advanced-fees-text}
(i18n/label :t/wallet-transaction-total-fee)]
[react/view send.styles/transaction-fee-bubble-read-only
[react/text {:style send.styles/advanced-fees-text}
(str (money/to-fixed (max-fee gas gas-price)))]
[react/text {:style send.styles/advanced-fees-details-text}
"ETH"]]]]]]))
(defn- advanced-options-wrapper [on-press content]
(if on-press
[react/touchable-highlight {:on-press on-press}
[react/view
content]]
[react/view
content]))
(defn- advanced-options [{:keys [gas gas-price]} on-press]
[react/touchable-highlight {:on-press on-press}
[react/view
[react/text {:style wallet.components.styles/label}
(i18n/label :t/wallet-transaction-fee)]
[advanced-options-wrapper on-press
[react/view send.styles/advanced-options-wrapper
[react/view send.styles/advanced-options-text-wrapper
[react/text {:style send.styles/advanced-fees-text}
(str (money/to-fixed (max-fee gas gas-price)) " ETH")]
[react/text {:style send.styles/advanced-fees-details-text}
(str (money/to-fixed gas) " * " (money/to-fixed (money/wei-> :gwei gas-price)) "GWEI")]]
(when on-press
[vector-icons/icon :icons/forward {:color :white}])]]]])
(defn- send-transaction-panel [{:keys [modal? transaction scroll advanced?] :as transaction}]
(let [{:keys [amount amount-error signing? to to-name sufficient-funds? in-progress? from-chat?]} transaction]
[react/keyboard-avoiding-view wallet.styles/wallet-modal-container
[react/view components.styles/flex
[status-bar/status-bar {:type (if modal? :modal-wallet :wallet)}]
(if modal? [toolbar-modal from-chat?] [toolbar-view signing?])
[common/network-info {:text-color :white}]
[react/scroll-view (merge {:keyboardShouldPersistTaps :always} (when-not modal? {:ref #(reset! scroll %)}))
[react/view components.styles/flex
[react/view wallet.styles/choose-participant-container
[components/choose-recipient (merge {:address to
:name to-name}
(when-not modal?
{:on-press request-camera-permissions}))]]
[react/view wallet.styles/choose-wallet-container
[components/choose-wallet]]
[react/view wallet.styles/amount-container
[components/amount-input
(merge
{:error (or amount-error
(when-not sufficient-funds? (i18n/label :t/wallet-insufficient-funds)))
:input-options {:default-value (str (money/wei->ether amount))
:on-focus (fn [] (when @scroll (js/setTimeout #(.scrollToEnd @scroll) 100)))
:on-change-text #(re-frame/dispatch [:wallet.send/set-and-validate-amount %])}}
(when modal?
{:disabled? true}))]
(if modal?
[react/view wallet.styles/choose-currency-container
[components/view-currency wallet.styles/choose-currency]]
[react/view wallet.styles/choose-currency-container
[components/choose-currency wallet.styles/choose-currency]])]
[react/view {:style send.styles/advanced-wrapper}
[react/touchable-highlight {:on-press #(re-frame/dispatch [:wallet.send/toggle-advanced (not advanced?)])}
[react/view {:style send.styles/advanced-button-wrapper}
[react/view {:style send.styles/advanced-button}
[react/text {:style (merge wallet.components.styles/label send.styles/advanced-label)}
(i18n/label :t/wallet-advanced)]
[vector-icons/icon (if advanced? :icons/up :icons/down) {:color :white}]]]]
(when advanced?
[advanced-options transaction (when-not modal? #(re-frame/dispatch [:navigate-to-modal :wallet-transaction-fee]))])]]]
[components/separator]
(if signing?
[signing-buttons
#(re-frame/dispatch (if modal? [:wallet/cancel-signing-modal] [:wallet/discard-transaction]))
#(re-frame/dispatch (if modal? [:wallet/sign-transaction-modal] [:wallet/sign-transaction]))
in-progress?]
[sign-buttons amount-error to amount sufficient-funds? (if modal? (if from-chat?
#(sign-later-popup true)
#(re-frame/dispatch [:navigate-back]))
#(sign-later-popup false))])
(when signing?
[sign-panel])
(when in-progress? [react/view send.styles/processing-view])]]))
(defview send-transaction []
(letsubs [transaction [:wallet.send/transaction]
advanced? [:wallet.send/advanced?]
scroll (atom nil)]
[send-transaction-panel {:modal? false :transaction transaction :scroll scroll :advanced? advanced?}]))
(defview send-transaction-modal []
(letsubs [transaction [:wallet.send/unsigned-transaction]]
(letsubs [transaction [:wallet.send/unsigned-transaction]
advanced? [:wallet.send/advanced?]]
(if transaction
(let [{:keys [amount amount-error signing? to to-name sufficient-funds? in-progress? from-chat?]} transaction]
[react/keyboard-avoiding-view wallet.styles/wallet-modal-container
[react/view components.styles/flex
[status-bar/status-bar {:type :modal-wallet}]
[toolbar-modal from-chat?]
[common/network-info {:text-color :white}]
[react/scroll-view {:keyboardShouldPersistTaps :always}
[react/view components.styles/flex
[react/view wallet.styles/choose-participant-container
[components/choose-recipient-disabled {:address to
:name to-name}]]
[react/view wallet.styles/choose-wallet-container
[components/choose-wallet]]
[react/view wallet.styles/amount-container
[components/amount-input
{:error (when-not sufficient-funds? (i18n/label :t/wallet-insufficient-funds))
:disabled? true
:input-options {:default-value (str (money/wei->ether amount))}}]
[react/view wallet.styles/choose-currency-container
[components/choose-currency wallet.styles/choose-currency]]]]]
[components/separator]
(if signing?
[signing-buttons
#(re-frame/dispatch [:wallet/cancel-signing-modal])
#(re-frame/dispatch [:wallet/sign-transaction-modal])
in-progress?]
[sign-buttons amount-error to amount sufficient-funds? (if from-chat?
#(sign-later-popup true)
#(re-frame/dispatch [:navigate-back]))])
(when signing?
[sign-panel])
(when in-progress? [react/view send.styles/processing-view])]])
[send-transaction-panel {:modal? true :transaction transaction :advanced? advanced?}]
[react/view wallet.styles/wallet-modal-container
[react/view components.styles/flex
[status-bar/status-bar {:type :modal-wallet}]

View File

@ -11,10 +11,10 @@
(handlers/register-handler-fx
:wallet.settings/toggle-visible-token
(fn [{{:keys [network] :accounts/keys [current-account-id] :as db} :db} [_ symbol checked?]]
(let [chain-id (ethereum/network->chain-id network)
(let [chain (ethereum/network->chain-keyword network)
path [:accounts/accounts current-account-id :settings]
settings (get-in db path)
new-settings (update-in settings [:wallet :visible-tokens chain-id] #(toggle-checked % symbol checked?))]
new-settings (update-in settings [:wallet :visible-tokens chain] #(toggle-checked % symbol checked?))]
(-> db
(assoc-in path new-settings)
(accounts/update-wallet-settings new-settings)))))

View File

@ -7,5 +7,5 @@
:<- [:network]
:<- [:get-current-account]
(fn [[network current-account]]
(let [chain-id (ethereum/network->chain-id network)]
(get-in current-account [:settings :wallet :visible-tokens chain-id]))))
(let [chain (ethereum/network->chain-keyword network)]
(get-in current-account [:settings :wallet :visible-tokens chain]))))

View File

@ -21,11 +21,10 @@
(defview manage-assets []
(letsubs [network [:network]
visible-tokens [:wallet.settings/visible-tokens]]
(let [chain-id (ethereum/network->chain-id network)]
[react/view components.styles/flex
[toolbar/toolbar {}
[toolbar/nav-clear-text (i18n/label :t/done)]
[toolbar/content-title (i18n/label :t/wallet-assets)]]
[react/view {:style components.styles/flex}
[list/flat-list {:data (tokens/tokens-for chain-id)
:render-fn #(render-token % visible-tokens)}]]])))
[react/view components.styles/flex
[toolbar/toolbar {}
[toolbar/nav-clear-text (i18n/label :t/done)]
[toolbar/content-title (i18n/label :t/wallet-assets)]]
[react/view {:style components.styles/flex}
[list/flat-list {:data (tokens/tokens-for (ethereum/network->chain-keyword network))
:render-fn #(render-token % visible-tokens)}]]]))

View File

@ -47,11 +47,10 @@
(fn [transactions]
(group-by :type (vals transactions))))
(defn format-unsigned-transaction [{:keys [id] :as transaction}]
(defn- format-unsigned-transaction [{:keys [id] :as transaction}]
(assoc transaction
:type :unsigned
:confirmations 0
:symbol "ETH"
;; TODO (andrey) revisit this, we shouldn't set not hash value to the hash field
:hash id))

View File

@ -0,0 +1,8 @@
(ns status-im.ui.screens.wallet.utils
(:require [status-im.utils.money :as money]))
(defn format-amount [amount decimals]
(-> amount
(or (money/bignumber 0))
(money/token->unit decimals)
money/to-fixed))

View File

@ -1,10 +1,8 @@
(ns status-im.utils.db
(:require [clojure.string :as string]
[cljs.spec.alpha :as spec]
[status-im.js-dependencies :as dependencies]))
(defn address? [s]
(.isAddress dependencies/Web3.prototype s))
[status-im.js-dependencies :as dependencies]
[status-im.utils.ethereum.core :as ethereum]))
(defn hex-string? [s]
(let [s' (if (string/starts-with? s "0x")
@ -20,8 +18,8 @@
(and (= 128 length) (not (string/includes? identity "0x")))
(and (= 130 length) (string/starts-with? identity "0x"))
(and (= 132 length) (string/starts-with? identity "0x04"))
(address? identity)))))
(ethereum/address? identity)))))
(spec/def :global/not-empty-string (spec/and string? not-empty))
(spec/def :global/public-key (spec/and :global/not-empty-string valid-length?))
(spec/def :global/address address?)
(spec/def :global/address ethereum/address?)

View File

@ -7,14 +7,17 @@
(def chains
{:mainnet {:id 1 :name "Mainnet"}
:ropsten {:id 3 :name "Ropsten"}
:testnet {:id 3 :name "Ropsten"}
:rinkeby {:id 4 :name "Rinkeby"}})
(defn chain-id [k]
(defn chain-id->chain-keyword [i]
(some #(when (= i (:id (val %))) (key %)) chains))
(defn chain-keyword->chain-id [k]
(get-in chains [k :id]))
(defn testnet? [id]
(contains? #{(chain-id :ropsten) (chain-id :rinkeby)} id))
(contains? #{(chain-keyword->chain-id :testnet) (chain-keyword->chain-id :rinkeby)} id))
(defn network-with-upstream-rpc? [networks network]
(get-in networks [network :raw-config :UpstreamConfig :Enabled]))
@ -27,7 +30,11 @@
address
(str hex-prefix address))))
(defn network->chain-id [network]
(defn address? [s]
(when s
(.isAddress dependencies/Web3.prototype s)))
(defn network->chain-keyword [network]
(when network
(keyword (string/replace network "_rpc" ""))))
@ -47,7 +54,7 @@
(.toHex dependencies/Web3.prototype i))
(defn hex->bignumber [s]
(money/bignumber (if (= s "0x") 0 s)))
(money/bignumber (if (= s hex-prefix) 0 s)))
(defn zero-pad-64 [s]
(str (apply str (drop (count s) (repeat 64 "0"))) s))
@ -70,3 +77,9 @@
(defn call-params [contract method-sig & params]
(let [data (apply format-call-params (sig->method-id method-sig) params)]
{:to contract :data data}))
(defn send-transaction [web3 params cb]
(.sendTransaction (.-eth web3) (clj->js params) cb))
(def default-transaction-gas (money/bignumber 21000))
(def default-gas-price (money/->wei :gwei 21))

View File

@ -4,8 +4,10 @@
This EIP standardize how ethereum payment request can be represented as URI (say to embed them in a QR code).
e.g. ethereum:0x1234@1/transfer?to=0x5678&value=1e18&gas=5000"
(:require [clojure.string :as string]
(:require [clojure.set :as set]
[clojure.string :as string]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.money :as money]))
(def scheme "ethereum")
@ -17,21 +19,27 @@
(def key-value-separator "=")
(def uri-pattern (re-pattern (str scheme scheme-separator "([^" query-separator "]*)(?:\\" query-separator "(.*))?")))
(def authority-path-pattern (re-pattern (str "^([^" chain-id-separator "]*)(?:" chain-id-separator "(\\d))?(?:" function-name-separator "(\\w*))?")))
(def authority-path-pattern (re-pattern (str "^([^" chain-id-separator function-name-separator "]*)(?:" chain-id-separator "(\\d))?(?:" function-name-separator "(\\w*))?")))
(def key-value-format (str "([^" parameter-separator key-value-separator "]+)"))
(def query-pattern (re-pattern (str key-value-format key-value-separator key-value-format)))
(defn- parse-query [s]
(when s
(into {} (for [[_ k v] (re-seq query-pattern s)]
[(keyword k) v]))))
(def valid-native-arguments #{:value :gas})
(defn parse-value [{:keys [value function-name]}]
"Takes a map as returned by `parse-uri` and returns value as BigNumber"
(when (and value (not function-name)) ;; TODO(jeluard) Add ERC20 support
(let [eth? (string/ends-with? value "ETH")
n (money/bignumber (string/replace value "ETH" ""))]
(if eth? (.times n 1e18) n))))
(defn- parse-query [s]
(into {} (for [[_ k v] (re-seq query-pattern (or s ""))]
[(keyword k) v])))
(defn- parse-native-arguments [m]
(when (set/superset? valid-native-arguments (set (keys m)))
m))
(defn- parse-arguments [function-name s]
(let [m (parse-query s)]
(if function-name
(merge {:function-name function-name} (when-not (empty? m) {:function-arguments m}))
(parse-native-arguments m))))
;; TODO add ENS support
(defn parse-uri
"Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address`.
@ -42,10 +50,32 @@
(let [[_ authority-path query] (re-find uri-pattern s)]
(when authority-path
(let [[_ address chain-id function-name] (re-find authority-path-pattern authority-path)]
(when-not (or (string/blank? address) function-name) ;; Native token support only TODO(jeluard) Add ERC20 support
(merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-id :mainnet))}
(parse-query query))))))))
(when (ethereum/address? address)
(when-let [arguments (parse-arguments function-name query)]
(merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-keyword->chain-id :mainnet))}
arguments))))))))
(defn parse-eth-value [s]
"Takes a map as returned by `parse-uri` and returns value as BigNumber"
(when (string? s)
(let [eth? (string/ends-with? s "ETH")
n (money/bignumber (string/replace s "ETH" ""))]
(if eth? (.times n 1e18) n))))
(defn extract-request-details [{:keys [value address chain-id function-name function-arguments]}]
"Return a map encapsulating request details (with keys `value`, `address` and `symbol`) from a parsed URI.
Supports ethereum and erc20 token."
(when address
(case function-name
nil
{:value (parse-eth-value value)
:symbol :ETH
:address address}
"transfer"
{:value (money/bignumber (:uint256 function-arguments))
:symbol (:symbol (tokens/address->token (ethereum/chain-id->chain-keyword chain-id) address))
:address (:address function-arguments)}
nil)))
(defn- generate-query-string [m]
(string/join parameter-separator
@ -55,12 +85,14 @@
(defn generate-uri
"Generate a EIP 681 URI based on `address` and a map (keywords / {bignumbers/strings} ) of extra properties.
No validation of address format is performed."
[address {:keys [function-name chain-id] :as m}]
(when (and address (not function-name)) ;; Native token support only TODO(jeluard) Add ERC20 support
[address {:keys [chain-id function-name function-arguments] :as m}]
(when (ethereum/address? address)
(let [parameters (dissoc (into {} (filter second m)) :chain-id)] ;; filter nil values
(str scheme scheme-separator address
(when (and chain-id (not= chain-id (ethereum/chain-id :mainnet)))
(when (and chain-id (not= chain-id (ethereum/chain-keyword->chain-id :mainnet)))
;; Add chain-id if specified and is not main-net
(str chain-id-separator chain-id))
(when-not (empty? parameters)
(str query-separator (generate-query-string parameters)))))))
(if function-name
(str function-name-separator function-name query-separator (generate-query-string function-arguments))
(str query-separator (generate-query-string parameters))))))))

View File

@ -34,25 +34,27 @@
(defn balance-of [web3 contract address cb]
(ethereum/call web3
(ethereum/call-params contract "balanceOf(address)" address)
(ethereum/call-params contract "balanceOf(address)" (ethereum/normalized-address address))
#(cb %1 (ethereum/hex->bignumber %2))))
(defn transfer [web3 contract address value cb]
(ethereum/call web3
(ethereum/call-params contract "transfer(address, uint256)" address (ethereum/int->hex value))
(defn transfer [web3 contract from address value params cb]
(ethereum/send-transaction web3
(merge (ethereum/call-params contract "transfer(address,uint256)" (ethereum/normalized-address address) (ethereum/int->hex value))
{:from from}
params)
#(cb %1 (ethereum/hex->boolean %2))))
(defn transfer-from [web3 contract from-address to-address value cb]
(ethereum/call web3
(ethereum/call-params contract "transferFrom(address, address, uint256)" from-address to-address (ethereum/int->hex value))
(ethereum/call-params contract "transferFrom(address,address,uint256)" (ethereum/normalized-address from-address) (ethereum/normalized-address to-address) (ethereum/int->hex value))
#(cb %1 (ethereum/hex->boolean %2))))
(defn approve [web3 contract address value cb]
(ethereum/call web3
(ethereum/call-params contract "approve(address, uint256)" address (ethereum/int->hex value))
(ethereum/call-params contract "approve(address,uint256)" (ethereum/normalized-address address) (ethereum/int->hex value))
#(cb %1 (ethereum/hex->boolean %2))))
(defn allowance [web3 contract owner-address spender-address cb]
(ethereum/call web3
(ethereum/call-params contract "allowance(address, address)" owner-address spender-address)
(ethereum/call-params contract "allowance(address,address)" (ethereum/normalized-address owner-address) (ethereum/normalized-address spender-address))
#(cb %1 (ethereum/hex->bignumber %2))))

View File

@ -12,6 +12,11 @@
:icon {:source (js/require "./resources/images/assets/ethereum.png")
:style (asset-border styles/color-light-blue-transparent)}})
(defn ethereum? [k]
(= k (:symbol ethereum)))
;; symbol are used as global identifier (per network) so they must be unique
(def all
{:mainnet
(resolve-icons
@ -374,8 +379,16 @@
:decimals 18
:address "0xc55cf4b03948d7ebc8b9e8bad92643703811d162"}])})
(defn tokens-for [chain-id]
(get all chain-id))
(defn tokens-for [chain]
(get all chain))
(defn token-for [chain-id symbol]
(some #(if (= symbol (:symbol %)) %) (tokens-for chain-id)))
(defn symbol->token [chain symbol]
(some #(when (= symbol (:symbol %)) %) (tokens-for chain)))
(defn address->token [chain address]
(some #(when (= address (:address %)) %) (tokens-for chain)))
(defn asset-for [chain symbol]
(if (= (:symbol ethereum) symbol)
ethereum
(symbol->token chain symbol)))

View File

@ -68,6 +68,10 @@
(when-let [bn (bignumber n)]
(.dividedBy bn (eth-units unit))))
(defn ->wei [unit n]
(when-let [bn (bignumber n)]
(.times bn (eth-units unit))))
(defn to-fixed [bn]
(when bn
(.toFixed bn)))

View File

@ -10,35 +10,52 @@
(is (= nil (eip681/parse-uri "ethereum:")))
(is (= nil (eip681/parse-uri "ethereum:?value=1")))
(is (= nil (eip681/parse-uri "bitcoin:0x1234")))
(is (= {:address "0x1234" :chain-id 1} (eip681/parse-uri "ethereum:0x1234")))
(is (= {:address "0x1234" :value "1" :chain-id 1} (eip681/parse-uri "ethereum:0x1234?value=1")))
(is (= {:address "0x1234" :value "-1e18" :chain-id 1} (eip681/parse-uri "ethereum:0x1234?value=-1e18")))
(is (= {:address "0x1234" :value "+1E18" :chain-id 1} (eip681/parse-uri "ethereum:0x1234?value=+1E18")))
(is (= {:address "0x1234" :value "1E18" :gas "100" :chain-id 1} (eip681/parse-uri "ethereum:0x1234?value=1E18&gas=100")))
(is (= {:address "0x1234" :value "NOT_NUMBER" :chain-id 1} (eip681/parse-uri "ethereum:0x1234?value=NOT_NUMBER")))
(is (= {:address "0x1234" :value "1ETH" :chain-id 1} (eip681/parse-uri "ethereum:0x1234?value=1ETH")))
(is (= {:address "0xadaf150b905cf5e6a778e553e15a139b6618bbb7" :value "1e18" :gas "5000" :chain-id 1} (eip681/parse-uri "ethereum:0xadaf150b905cf5e6a778e553e15a139b6618bbb7@1?value=1e18&gas=5000")))
(is (= {:address "0xadaf150b905cf5e6a778e553e15a139b6618bbb7" :value "1e18" :gas "5000" :chain-id 3} (eip681/parse-uri "ethereum:0xadaf150b905cf5e6a778e553e15a139b6618bbb7@3?value=1e18&gas=5000")))
(is (= nil (eip681/parse-uri "ethereum:0xadaf150b905cf5e6a778e553e15a139b6618bbb7@2/transfer?value=1e18&gas=5000"))))
(is (= nil(eip681/parse-uri "ethereum:0x1234")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1")))
(is (= nil (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?unknown=1")))
(is (= nil (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?address=0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "2.014e18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=2.014e18")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "-1e18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=-1e18")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "+1E18" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=+1E18")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1E18" :gas "100" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1E18&gas=100")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "NOT_NUMBER" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=NOT_NUMBER")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1ETH" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1ETH")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@1?value=1e18&gas=5000")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :gas "5000" :chain-id 3} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1e18&gas=5000")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer"} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer")))
(is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1"))))
(deftest generate-uri
(is (= nil (eip681/generate-uri nil nil)))
(is (= "ethereum:0x1234" (eip681/generate-uri "0x1234" nil)))
(is (= "ethereum:0x1234" (eip681/generate-uri "0x1234" {})))
(is (= "ethereum:0x1234" (eip681/generate-uri "0x1234" {:value nil})))
(is (= "ethereum:0xadaf150b905cf5e6a778e553e15a139b6618bbb7?value=1" (eip681/generate-uri "0xadaf150b905cf5e6a778e553e15a139b6618bbb7" {:value (money/bignumber 1)})))
(is (= "ethereum:0xadaf150b905cf5e6a778e553e15a139b6618bbb7?value=1000000000000000000" (eip681/generate-uri "0xadaf150b905cf5e6a778e553e15a139b6618bbb7" {:value (money/bignumber 1e18)})))
(is (= "ethereum:0xadaf150b905cf5e6a778e553e15a139b6618bbb7?value=1&gas=100" (eip681/generate-uri "0xadaf150b905cf5e6a778e553e15a139b6618bbb7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1})))
(is (= "ethereum:0xadaf150b905cf5e6a778e553e15a139b6618bbb7@3?value=1&gas=100" (eip681/generate-uri "0xadaf150b905cf5e6a778e553e15a139b6618bbb7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 3})))
(is (= nil (eip681/generate-uri "0x1234" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1 :function-name "transfer"}))))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" nil)))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value nil})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1)})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1000000000000000000" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1e18)})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1&gas=100" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@3?value=1&gas=100" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 3})))
(is (= "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1" (eip681/generate-uri "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" {:value (money/bignumber 1) :gas (money/bignumber 100) :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 1}}))))
(deftest parse-value
(is (= nil (eip681/parse-value nil)))
(is (= nil (eip681/parse-value 1)))
(is (= nil (eip681/parse-value {:value "NOT_NUMBER"})))
(is (= nil (eip681/parse-value {:value "1" :function-name "transfer"})))
(is (.equals (money/bignumber 1) (eip681/parse-value {:value "1"})))
(is (.equals (money/bignumber 1e18) (eip681/parse-value {:value "1ETH"})))
(is (.equals (money/bignumber -1e18) (eip681/parse-value {:value "-1e18"})))
(is (.equals (money/bignumber 1e18) (eip681/parse-value {:value "1E18"})))
(is (.equals (money/bignumber "111122223333441239") (eip681/parse-value {:value "111122223333441239"}))))
(deftest parse-eth-value
(is (= nil (eip681/parse-eth-value nil)))
(is (= nil (eip681/parse-eth-value 1)))
(is (= nil (eip681/parse-eth-value "NOT_NUMBER")))
(is (.equals (money/bignumber 1) (eip681/parse-eth-value "1")))
(is (.equals (money/bignumber 2.014e18) (eip681/parse-eth-value "2.014e18")))
(is (.equals (money/bignumber 1e18) (eip681/parse-eth-value "1ETH")))
(is (.equals (money/bignumber -1e18) (eip681/parse-eth-value "-1e18")))
(is (.equals (money/bignumber 1e18) (eip681/parse-eth-value "1E18")))
(is (.equals (money/bignumber "111122223333441239") (eip681/parse-eth-value "111122223333441239"))))
(deftest extract-request-details
(let [{:keys [value symbol address]} (eip681/extract-request-details {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1ETH"})]
(is (.equals (money/ether->wei (money/bignumber 1)) value))
(is (= :ETH symbol))
(is (= "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" address)))
(is (nil? (eip681/extract-request-details {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e" :chain-id 1 :function-name "unknown"})))
(let [{:keys [value symbol address]} (eip681/extract-request-details {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e" :chain-id 1
:function-name "transfer" :function-arguments {:uint256 1000 :address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"}})]
(is (.equals (money/bignumber 1000) value))
(is (= :SNT symbol))
(is (= "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" address))))