diff --git a/resources/images/tokens/mainnet/CK.png b/resources/images/tokens/mainnet/CK.png new file mode 100755 index 0000000000..5375407a3c Binary files /dev/null and b/resources/images/tokens/mainnet/CK.png differ diff --git a/resources/images/tokens/mainnet/CK@2x.png b/resources/images/tokens/mainnet/CK@2x.png new file mode 100755 index 0000000000..c8eb0fac4d Binary files /dev/null and b/resources/images/tokens/mainnet/CK@2x.png differ diff --git a/resources/images/tokens/mainnet/CK@3x.png b/resources/images/tokens/mainnet/CK@3x.png new file mode 100755 index 0000000000..9557561bb3 Binary files /dev/null and b/resources/images/tokens/mainnet/CK@3x.png differ diff --git a/resources/images/tokens/mainnet/EMONA.png b/resources/images/tokens/mainnet/EMONA.png new file mode 100755 index 0000000000..20b6229589 Binary files /dev/null and b/resources/images/tokens/mainnet/EMONA.png differ diff --git a/resources/images/tokens/mainnet/EMONA@2x.png b/resources/images/tokens/mainnet/EMONA@2x.png new file mode 100755 index 0000000000..f3dc12c4a1 Binary files /dev/null and b/resources/images/tokens/mainnet/EMONA@2x.png differ diff --git a/resources/images/tokens/mainnet/EMONA@3x.png b/resources/images/tokens/mainnet/EMONA@3x.png new file mode 100755 index 0000000000..4c4a794338 Binary files /dev/null and b/resources/images/tokens/mainnet/EMONA@3x.png differ diff --git a/resources/images/tokens/mainnet/STRK.png b/resources/images/tokens/mainnet/STRK.png new file mode 100755 index 0000000000..03a38b9aca Binary files /dev/null and b/resources/images/tokens/mainnet/STRK.png differ diff --git a/resources/images/tokens/mainnet/STRK@2x.png b/resources/images/tokens/mainnet/STRK@2x.png new file mode 100755 index 0000000000..065cb5b997 Binary files /dev/null and b/resources/images/tokens/mainnet/STRK@2x.png differ diff --git a/resources/images/tokens/mainnet/STRK@3x.png b/resources/images/tokens/mainnet/STRK@3x.png new file mode 100755 index 0000000000..439864f108 Binary files /dev/null and b/resources/images/tokens/mainnet/STRK@3x.png differ diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index 8eda46e9aa..19b74fe1f2 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -427,6 +427,7 @@ :wallet-exchange "Exchange" :wallet-asset "Asset" :wallet-assets "Assets" + :wallet-collectibles "Collectibles" :wallet-add-asset "Add asset" :wallet-total-value "Total value" :wallet-settings "Wallet settings" @@ -589,6 +590,9 @@ :scan-qr-code "Scan a QR code with a wallet address" :reset-default "Reset to default" + :view-cryptokitties "View in CryptoKitties" + :cryptokitty-name "CryptoKitty #" + ;; network settings :new-network "New network" :add-network "Add network" diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index d8ea156bcd..b6b1ab2920 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -110,6 +110,8 @@ (spec/def :navigation.screen-params/usage-data vector?) +(spec/def :navigation.screen-params/display-collectible map?) + (spec/def :navigation/screen-params (spec/nilable (allowed-keys :opt-un [:navigation.screen-params/network-details :navigation.screen-params/browser :navigation.screen-params/profile-qr-viewer @@ -117,7 +119,8 @@ :navigation.screen-params/group-contacts :navigation.screen-params/edit-contact-group :navigation.screen-params/dapp-description - :navigation.screen-params/usage-data]))) + :navigation.screen-params/usage-data + :navigation.screen-params/display-collectible]))) (spec/def :desktop/desktop (spec/nilable any?)) @@ -129,6 +132,9 @@ (spec/def :inbox/fetching? (spec/nilable boolean?)) (spec/def :inbox/current-id (spec/nilable string?)) +(spec/def ::collectible (spec/nilable map?)) +(spec/def ::collectibles (spec/nilable map?)) + ;;;;NODE (spec/def :node/after-start (spec/nilable vector?)) @@ -249,4 +255,6 @@ :prices/prices :prices/prices-loading? :notifications/notifications - ::device-UUID])) + ::device-UUID + ::collectible + ::collectibles])) diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index cfb31b8991..5904ade26c 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -18,6 +18,7 @@ status-im.ui.screens.profile.events status-im.ui.screens.qr-scanner.events status-im.ui.screens.wallet.events + status-im.ui.screens.wallet.collectibles.events status-im.ui.screens.wallet.send.events status-im.ui.screens.wallet.settings.events status-im.ui.screens.wallet.transactions.events diff --git a/src/status_im/ui/screens/subs.cljs b/src/status_im/ui/screens/subs.cljs index d14eaca773..827f0772e6 100644 --- a/src/status_im/ui/screens/subs.cljs +++ b/src/status_im/ui/screens/subs.cljs @@ -8,6 +8,7 @@ status-im.ui.screens.contacts.subs status-im.ui.screens.group.subs status-im.ui.screens.wallet.subs + status-im.ui.screens.wallet.collectibles.subs status-im.ui.screens.wallet.request.subs status-im.ui.screens.wallet.send.subs status-im.ui.screens.wallet.transactions.subs diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index fc9d969a8b..c4b76103b8 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -24,6 +24,7 @@ [status-im.ui.screens.profile.contact.views :as profile.contact] [status-im.ui.screens.profile.group-chat.views :as profile.group-chat] [status-im.ui.screens.profile.photo-capture.views :refer [profile-photo-capture]] + [status-im.ui.screens.wallet.collectibles.views :as collectibles] [status-im.ui.screens.wallet.send.views :refer [send-transaction send-transaction-modal sign-message-modal]] [status-im.ui.screens.wallet.choose-recipient.views :refer [choose-recipient]] [status-im.ui.screens.wallet.request.views :refer [request-transaction send-transaction-request]] @@ -51,6 +52,7 @@ (defn get-main-component [view-id] (case view-id + :display-collectible collectibles/display-collectible :intro intro :create-account create-account :usage-data usage-data diff --git a/src/status_im/ui/screens/wallet/collectibles/cryptokitties.cljs b/src/status_im/ui/screens/wallet/collectibles/cryptokitties.cljs new file mode 100644 index 0000000000..bb0025dd7f --- /dev/null +++ b/src/status_im/ui/screens/wallet/collectibles/cryptokitties.cljs @@ -0,0 +1,90 @@ +(ns status-im.ui.screens.wallet.collectibles.cryptokitties + (:require-macros [status-im.utils.views :refer [defview letsubs]]) + (:require [re-frame.core :as re-frame] + [status-im.i18n :as i18n] + [status-im.ui.components.action-button.action-button :as action-button] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.react :as react] + [status-im.ui.screens.wallet.collectibles.views :as collectibles] + [status-im.utils.handlers :as handlers]) + (:refer-clojure :exclude [symbol])) + +(def symbol :CK) + +(handlers/register-handler-fx + :load-kitty-success + [re-frame/trim-v] + (fn [{db :db} [[id collectible]]] + {:db (update-in db [:collectibles symbol] assoc id collectible)})) + +(handlers/register-handler-fx + :load-kitty-failure + [re-frame/trim-v] + (fn [{db :db} [_]] + {:db db})) + +(defn parse-payload [o] + (js->clj (js/JSON.parse o) + :keywordize-keys true)) + +(handlers/register-handler-fx + :load-kitties + (fn [{db :db} [_ ids]] + {:db db + :http-get-n (mapv (fn [id] + {:url (str "https://api.cryptokitties.co/kitties/" id) + :success-event-creator (fn [o] + [:load-kitty-success [id (parse-payload o)]]) + :failure-event-creator (fn [o] + [:load-kitty-failure [id (parse-payload o)]])}) + ids)})) + +(defn kitties-url [address] + (str "https://api.cryptokitties.co/kitties?offset=0&limit=100&owner_wallet_address=" address "&parents=false")) + +(handlers/register-handler-fx + :load-kitties-success + (fn [{db :db} [_ ids]] + {:db db + :dispatch [:load-kitties ids]})) + +(defmethod collectibles/load-collectibles-fx symbol [_ address] + {:http-get {:url (kitties-url address) + :success-event-creator (fn [o] + [:load-kitties-success (map :id (:kitties (parse-payload o)))]) + :failure-event-creator (fn [o] + [:load-collectibles-failure (parse-payload o)]) + :timeout-ms 10000}}) + +(defn- kitty-name [{:keys [id name]}] + (or name (str (i18n/label :t/cryptokitty-name) id))) + +(def view-style + {:padding-vertical 10}) + +(def text-style + {:flex 1 + :flex-direction :row + :align-items :center + :padding-horizontal 16}) + +(def name-style + {:color colors/black + :margin-bottom 10}) + +(defmethod collectibles/render-collectible symbol [_ {:keys [id bio image_url] :as m}] + [react/view {:style view-style} + [react/view {:style text-style} + ;; TODO reenable image once SVG is supported + #_[react/image {:style {:width 80 :height 80 :margin 10 :background-color "red"} :source {:uri image_url}}] + [react/view {} + [react/text {:style name-style} + (kitty-name m)] + [react/text {:number-of-lines 3 + :ellipsize-mode :tail} + bio]]] + [action-button/action-button {:label (i18n/label :t/view-cryptokitties) + :icon :icons/address + :icon-opts {:color colors/blue} + :accessibility-label :open-collectible-button + :on-press #(re-frame/dispatch [:open-browser {:url (str "https://www.cryptokitties.co/kitty/" id)}])}]]) diff --git a/src/status_im/ui/screens/wallet/collectibles/events.cljs b/src/status_im/ui/screens/wallet/collectibles/events.cljs new file mode 100644 index 0000000000..dc2eb25cbd --- /dev/null +++ b/src/status_im/ui/screens/wallet/collectibles/events.cljs @@ -0,0 +1,9 @@ +(ns status-im.ui.screens.wallet.collectibles.events + (:require [re-frame.core :as re-frame] + [status-im.utils.handlers :as handlers])) + +(handlers/register-handler-fx + :load-collectibles-failure + [re-frame/trim-v] + (fn [{db :db} [{:keys [message]}]] + {:db (assoc db :collectibles-failure message)})) diff --git a/src/status_im/ui/screens/wallet/collectibles/styles.cljs b/src/status_im/ui/screens/wallet/collectibles/styles.cljs new file mode 100644 index 0000000000..17508ecc4f --- /dev/null +++ b/src/status_im/ui/screens/wallet/collectibles/styles.cljs @@ -0,0 +1,10 @@ +(ns status-im.ui.screens.wallet.collectibles.styles) + +(def default-collectible + {:padding-left 10 + :padding-vertical 20}) + +(def loading-indicator + {:flex 1 + :align-items :center + :justify-content :center}) diff --git a/src/status_im/ui/screens/wallet/collectibles/subs.cljs b/src/status_im/ui/screens/wallet/collectibles/subs.cljs new file mode 100644 index 0000000000..b0c88f4bcf --- /dev/null +++ b/src/status_im/ui/screens/wallet/collectibles/subs.cljs @@ -0,0 +1,6 @@ +(ns status-im.ui.screens.wallet.collectibles.subs + (:require [re-frame.core :as re-frame])) + +(re-frame/reg-sub :collectibles + (fn [db [_ s]] + (vals (get-in db [:collectibles s])))) diff --git a/src/status_im/ui/screens/wallet/collectibles/views.cljs b/src/status_im/ui/screens/wallet/collectibles/views.cljs new file mode 100644 index 0000000000..a23f314b52 --- /dev/null +++ b/src/status_im/ui/screens/wallet/collectibles/views.cljs @@ -0,0 +1,36 @@ +(ns status-im.ui.screens.wallet.collectibles.views + (:require-macros [status-im.utils.views :refer [defview letsubs]]) + (:require [re-frame.core :as re-frame] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.list.views :as list] + [status-im.ui.components.react :as react] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.styles :as component.styles] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.screens.wallet.collectibles.styles :as styles])) + +(defmulti load-collectibles-fx (fn [symbol _] symbol)) + +(defmethod load-collectibles-fx :default [_ _] nil) + +(defmulti render-collectible (fn [symbol _] symbol)) + +(defmethod render-collectible :default [symbol {:keys [id name]}] + [react/view {:style styles/default-collectible} + [react/text (str (clojure.core/name symbol) " #" (or id name))]]) + +(defview display-collectible [] + (letsubs [{:keys [name symbol]} [:get-screen-params]] + (let [collectibles @(re-frame/subscribe [:collectibles symbol])] + [react/view {:style component.styles/flex} + (if (seq collectibles) + [react/view {:style component.styles/flex} + [status-bar/status-bar] + [toolbar/toolbar {} + toolbar/default-nav-back + [toolbar/content-title name]] + [list/flat-list {:data collectibles + :key-fn (comp str :id) + :render-fn #(render-collectible symbol %)}]] + [react/view {:style styles/loading-indicator} + [react/activity-indicator {:animating true :size :large :color colors/blue}]])]))) diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs index ad0d80f5bd..7dca36acac 100644 --- a/src/status_im/ui/screens/wallet/events.cljs +++ b/src/status_im/ui/screens/wallet/events.cljs @@ -12,7 +12,9 @@ status-im.ui.screens.wallet.request.events [status-im.utils.money :as money] [status-im.constants :as constants] - [status-im.ui.screens.navigation :as navigation])) + [status-im.ui.screens.navigation :as navigation] + [status-im.ui.screens.wallet.collectibles.views :as collectibles] + [clojure.set :as set])) (defn get-balance [{:keys [web3 account-id on-success on-error]}] (if (and web3 account-id) @@ -99,14 +101,18 @@ (fn [{:keys [web3 obj success-event]}] (ethereum/estimate-gas-web3 web3 (clj->js obj) #(re-frame/dispatch [success-event %2])))) +(defn tokens-symbols [v chain] + (set/difference (set v) (set (map :symbol (tokens/nfts-for chain))))) + ;; Handlers (handlers/register-handler-fx :update-wallet - (fn [{{:keys [web3 account/account network network-status] {:keys [address settings]} :account/account :as db} :db} _] + (fn [{{:keys [web3 network network-status] {:keys [address settings]} :account/account :as db} :db} _] (let [network (get-in db [:account/account :networks network]) chain (ethereum/network->chain-keyword network) mainnet? (= :mainnet chain) - symbols (get-in settings [:wallet :visible-tokens chain]) + assets (get-in settings [:wallet :visible-tokens chain]) + tokens (tokens-symbols (get-in settings [:wallet :visible-tokens chain]) chain) currency-id (or (get-in settings [:wallet :currency]) :usd) currency (get constants/currencies currency-id)] (when (not= network-status :offline) @@ -116,11 +122,11 @@ :error-event :update-balance-fail} :get-tokens-balance {:web3 web3 :account-id address - :symbols symbols + :symbols assets :chain chain :success-event :update-token-balance-success :error-event :update-token-balance-fail} - :get-prices {:from (if mainnet? (conj symbols "ETH") ["ETH"]) + :get-prices {:from (if mainnet? (conj tokens "ETH") ["ETH"]) :to [(:code currency)] :success-event :update-prices-success :error-event :update-prices-fail} @@ -267,4 +273,11 @@ (fn [{:keys [db]}] {:db (-> db (assoc-in [:wallet :send-transaction] {}) - (navigation/navigate-back))})) \ No newline at end of file + (navigation/navigate-back))})) + +(handlers/register-handler-fx + :wallet/show-collectibles + (fn [_ [_ address {:keys [symbol] :as m}]] + (if-let [fx (collectibles/load-collectibles-fx symbol address)] + (assoc fx :dispatch [:navigate-to :display-collectible m]) + {:show-error (str "Missing implementation for " (name symbol))}))) diff --git a/src/status_im/ui/screens/wallet/styles.cljs b/src/status_im/ui/screens/wallet/styles.cljs index 5c2cb71963..cd46e85673 100644 --- a/src/status_im/ui/screens/wallet/styles.cljs +++ b/src/status_im/ui/screens/wallet/styles.cljs @@ -1,7 +1,6 @@ (ns status-im.ui.screens.wallet.styles (:require-macros [status-im.utils.styles :refer [defstyle]]) (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.react :as react] [status-im.ui.components.styles :as styles])) ;; wallet @@ -153,11 +152,6 @@ :padding-top 16 :background-color colors/white}) -(def asset-section-title - {:font-size 14 - :margin-left 16 - :color colors/gray}) - (def asset-item-container {:flex 1 :flex-direction :row diff --git a/src/status_im/ui/screens/wallet/views.cljs b/src/status_im/ui/screens/wallet/views.cljs index 57fb9c6a0e..fc80e14df1 100644 --- a/src/status_im/ui/screens/wallet/views.cljs +++ b/src/status_im/ui/screens/wallet/views.cljs @@ -9,7 +9,9 @@ [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.screens.wallet.onboarding.views :as onboarding.views] [status-im.ui.screens.wallet.styles :as styles] - [status-im.ui.screens.wallet.utils :as wallet.utils])) + [status-im.ui.screens.wallet.utils :as wallet.utils] + [status-im.utils.money :as money] + status-im.ui.screens.wallet.collectibles.cryptokitties)) (defn toolbar-view [] [toolbar/toolbar {:style styles/toolbar :flat? true} @@ -80,21 +82,56 @@ :number-of-lines 1} (if @asset-value @asset-value "...")]]]))) -(defn- asset-section [assets currency] - [react/view styles/asset-section - [react/text {:style styles/asset-section-title} (i18n/label :t/wallet-assets)] - [list/flat-list - {:default-separator? true - :scroll-enabled false - :key-fn (comp str :symbol) - :data assets - :render-fn (render-asset currency)}]]) +(def item-icon-forward + [list/item-icon {:icon :icons/forward + :icon-opts {:color :gray}}]) + +(defn- render-collectible [address-hex {:keys [symbol icon amount] :as m}] + (let [i (money/to-fixed amount) + details? (pos? i)] + [react/touchable-highlight (when details? + {:on-press #(re-frame/dispatch [:wallet/show-collectibles address-hex m])}) + [react/view {:style styles/asset-item-container} + [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 + :accessibility-label (str (-> symbol name clojure.string/lower-case) "-collectible-value-text")} + (or i 0)] + [react/text {:style styles/asset-item-currency + :uppercase? true + :number-of-lines 1} + (name symbol)]] + (when details? + item-icon-forward)]]])) + +(defn group-assets [v] + (group-by #(if (:nft? %) :nfts :tokens) v)) + +(defn- asset-section [assets currency address-hex] + (let [{:keys [tokens nfts]} (group-assets assets)] + [react/view styles/asset-section + [list/section-list + {:default-separator? true + :scroll-enabled false + :key-fn (comp str :symbol) + :sections [{:title (i18n/label :t/wallet-assets) + :key :assets + :data tokens + :render-fn (render-asset currency)} + {:title (i18n/label :t/wallet-collectibles) + :key :collectibles + :data nfts + :render-fn #(render-collectible address-hex %)}]}]])) (views/defview wallet-root [] (views/letsubs [assets [:wallet/visible-assets-with-amount] currency [:wallet/currency] portfolio-value [:portfolio-value] - {:keys [seed-backed-up?]} [:get-current-account]] + {:keys [seed-backed-up?]} [:get-current-account] + address-hex [:get-current-account-hex]] [react/view styles/main-section [toolbar-view] [react/scroll-view {:refresh-control @@ -110,7 +147,7 @@ [backup-seed-phrase]) [list/action-list actions {:container-style styles/action-section}] - [asset-section assets currency] + [asset-section assets currency address-hex] ;; Hack to allow different colors for bottom scroll view (iOS only) [react/view {:style styles/scroll-bottom}]]])) diff --git a/src/status_im/utils/ethereum/erc721.cljs b/src/status_im/utils/ethereum/erc721.cljs new file mode 100644 index 0000000000..1479aca08a --- /dev/null +++ b/src/status_im/utils/ethereum/erc721.cljs @@ -0,0 +1,11 @@ +(ns status-im.utils.ethereum.erc721 + " + Helper functions to interact with [ERC721](https://eips.ethereum.org/EIPS/eip-721) smart contract + " + (:require [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.erc20 :as erc20])) + +(defn token-of-owner-by-index [web3 contract address index cb] + (ethereum/call web3 + (ethereum/call-params contract "tokenOfOwnerByIndex(address,uint256)" (ethereum/normalized-address address) index) + #(cb %1 (ethereum/hex->bignumber %2)))) diff --git a/src/status_im/utils/ethereum/tokens.cljs b/src/status_im/utils/ethereum/tokens.cljs index 0f7155c4a5..eaeab6d117 100644 --- a/src/status_im/utils/ethereum/tokens.cljs +++ b/src/status_im/utils/ethereum/tokens.cljs @@ -385,7 +385,19 @@ :name "Attention Token of Media" :address "0x9B11EFcAAA1890f6eE52C6bB7CF8153aC5d74139" :decimals 8 - :hidden? true}]) + :hidden? true} + {:symbol :CK + :nft? true + :name "CryptoKitties" + :address "0x06012c8cf97bead5deae237070f9587f8e7a266d"} + {:symbol :EMONA + :nft? true + :name "EtheremonAsset" + :address "0xB2c0782ae4A299f7358758B2D15dA9bF29E1DD99"} + {:symbol :STRK + :nft? true + :name "CryptoStrikers" + :address "0xdcaad9fd9a74144d226dbf94ce6162ca9f09ed7e"}]) :testnet (resolve-icons :testnet [{:name "Status Test Token" @@ -416,6 +428,9 @@ (defn tokens-for [chain] (get all chain)) +(defn nfts-for [chain] + (filter :nft? (tokens-for chain))) + (defn sorted-tokens-for [chain] (->> (tokens-for chain) (filter #(not (:hidden? %)))