diff --git a/packages/mobile/src/assets/logo/app-icon.png b/packages/mobile/src/assets/logo/app-icon.png new file mode 100644 index 0000000000..dcdc86ab53 Binary files /dev/null and b/packages/mobile/src/assets/logo/app-icon.png differ diff --git a/packages/mobile/src/components/drawer/chain-infos-view.tsx b/packages/mobile/src/components/drawer/chain-infos-view.tsx new file mode 100644 index 0000000000..a0ced49282 --- /dev/null +++ b/packages/mobile/src/components/drawer/chain-infos-view.tsx @@ -0,0 +1,180 @@ +import React, { FunctionComponent, useState } from "react"; +import { Text, View, ViewStyle } from "react-native"; +import { useStyle } from "styles/index"; +import { BlurBackground } from "components/new/blur-background/blur-background"; +import { RectButton } from "components/rect-button"; +import FastImage from "react-native-fast-image"; +import { VectorCharacter } from "components/vector-character"; +import { titleCase } from "utils/format/format"; +import { CheckIcon } from "components/new/icon/check"; +import { ChainInfoWithCoreTypes } from "@keplr-wallet/background"; +import { ChainInfoInner } from "@keplr-wallet/stores"; +import { useStore } from "stores/index"; +import { XmarkIcon } from "components/new/icon/xmark"; +import Toast from "react-native-toast-message"; +import { ConfirmCardModel } from "components/new/confirm-modal"; + +interface ChainInfosViewProps { + chainInfos: ChainInfoInner[]; + onPress: (chainInfo: ChainInfoInner) => void; +} + +export const ChainInfosView: FunctionComponent = ({ + chainInfos, + onPress, +}) => { + const style = useStyle(); + const { chainStore } = useStore(); + const betaChainList = chainStore.chainInfosInUI.filter( + (chainInfo) => chainInfo.beta + ); + const [showConfirmModal, setConfirmModal] = useState(false); + const [removedChain, setRemovedChain] = + useState | null>(null); + const renderTokenIcon = ( + chainInfo: ChainInfoInner + ) => { + const { chainSymbolImageUrl, chainName } = chainInfo.raw; + return chainSymbolImageUrl && chainSymbolImageUrl.startsWith("http") ? ( + + ) : ( + + ); + }; + + const handleRemoveChain = async ( + chainInfo: ChainInfoInner + ) => { + try { + await chainStore.removeChainInfo(chainInfo.chainId); + Toast.show({ + type: "success", + text1: "Chain removed successfully!", + }); + } catch (error) { + Toast.show({ + type: "error", + text1: "Failed to remove chain.", + text2: error?.message || "An unexpected error occurred.", + }); + } + setConfirmModal(false); + }; + + const handleConfirmRemove = ( + chainInfo: ChainInfoInner + ) => { + setRemovedChain(chainInfo); + setConfirmModal(true); + }; + + return ( + + {chainInfos.map((chainInfo) => { + const selected = chainStore.current.chainId === chainInfo.chainId; + const isBeta = betaChainList.some( + (betaChain) => betaChain.chainId === chainInfo.chainId + ); + + return ( + + onPress(chainInfo)} + style={ + style.flatten( + [ + "flex-row", + "height-62", + "items-center", + "padding-x-12", + "justify-between", + ], + [selected && "background-color-indigo", "border-radius-12"] + ) as ViewStyle + } + activeOpacity={0.5} + underlayColor={ + style.flatten(["color-gray-50", "dark:color-platinum-500"]) + .color + } + > + + + {renderTokenIcon(chainInfo)} + + + {titleCase(chainInfo.chainName)} + + + + {selected ? ( + + ) : ( + isBeta && ( + handleConfirmRemove(chainInfo)} + > + + + ) + )} + + + + ); + })} + {removedChain && ( + setConfirmModal(false)} + title="Remove Chain" + subtitle={`Are you sure you want to remove ${removedChain.chainName}?`} + select={async (confirm: boolean) => { + if (confirm && removedChain) { + await handleRemoveChain(removedChain); + } + }} + /> + )} + + ); +}; diff --git a/packages/mobile/src/components/drawer/index.tsx b/packages/mobile/src/components/drawer/index.tsx index 67bceed1c0..c0bd4eb18e 100644 --- a/packages/mobile/src/components/drawer/index.tsx +++ b/packages/mobile/src/components/drawer/index.tsx @@ -12,44 +12,52 @@ import { } from "@react-navigation/native"; import { Platform, Text, View, ViewStyle } from "react-native"; import { useStyle } from "styles/index"; -import { RectButton } from "components/rect-button"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import FastImage from "react-native-fast-image"; -import { VectorCharacter } from "components/vector-character"; -import { BlurBackground } from "components/new/blur-background/blur-background"; -import { CheckIcon } from "components/new/icon/check"; import { IconButton } from "components/new/button/icon"; import { XmarkIcon } from "components/new/icon/xmark"; import { SearchIcon } from "components/new/icon/search-icon"; import { EmptyView } from "components/new/empty"; -import { titleCase } from "utils/format/format"; import { Button } from "components/button"; import { InputCardView } from "components/new/card-view/input-card"; +import { TabBarView } from "components/new/tab-bar/tab-bar"; +import { ChainInfosView } from "components/drawer/chain-infos-view"; + +export enum NetworkEnum { + Cosmos = "Cosmos", + EVM = "EVM", +} export const DrawerContent: FunctionComponent = observer((props) => { const { chainStore, analyticsStore } = useStore(); const navigation = useNavigation(); - + const [selectedTab, setSelectedTab] = useState(NetworkEnum.Cosmos); const safeAreaInsets = useSafeAreaInsets(); - const { ...rest } = props; - const style = useStyle(); const [search, setSearch] = useState(""); const [filterChainInfos, setFilterChainInfos] = useState( chainStore.chainInfosInUI ); + const mainChainList = chainStore.chainInfos.filter( + (chainInfo) => !chainInfo.beta && !chainInfo.features?.includes("evm") + ); + const evmChainList = chainStore.chainInfos.filter((chainInfo) => + chainInfo.features?.includes("evm") + ); useEffect(() => { - const searchTrim = search.trim(); - const newChainInfos = chainStore.chainInfosInUI.filter((chainInfo) => { - return chainInfo.chainName - .toLowerCase() - .includes(searchTrim.toLowerCase()); - }); - setFilterChainInfos(newChainInfos); - }, [chainStore.chainInfosInUI, search]); + const searchTrim = search.trim().toLowerCase(); + const filteredChains = + selectedTab == NetworkEnum.Cosmos + ? mainChainList.filter((chainInfo) => + chainInfo.chainName.toLowerCase().includes(searchTrim) + ) + : evmChainList.filter((chainInfo) => + chainInfo.chainName.toLowerCase().includes(searchTrim) + ); + setFilterChainInfos(filteredChains); + }, [chainStore.chainInfosInUI, search, selectedTab]); return ( = }} {...rest} > - + = Change Network - } + onPress={() => { + setSearch(""); + navigation.dispatch(DrawerActions.closeDrawer()); + }} + iconStyle={ style.flatten([ - "height-1", - "justify-center", - "items-center", + "padding-8", + "border-width-1", + "border-color-gray-400", ]) as ViewStyle } - > - } - backgroundBlur={false} - blurIntensity={20} - borderRadius={50} - onPress={() => { - setSearch(""); - navigation.dispatch(DrawerActions.closeDrawer()); - }} - iconStyle={ - style.flatten([ - "padding-8", - "border-width-1", - "border-color-gray-400", - ]) as ViewStyle - } - /> - + /> + { - setSearch(text); - }} + onChangeText={setSearch} rightIcon={} containerStyle={style.flatten(["margin-top-24"]) as ViewStyle} /> @@ -139,6 +134,7 @@ export const DrawerContent: FunctionComponent = navigation.dispatch( StackActions.push("ChainList", { screen: "Setting.ChainList", + params: { selectedTab: selectedTab }, }) ); analyticsStore.logEvent("manage_networks_click", { @@ -149,99 +145,15 @@ export const DrawerContent: FunctionComponent = {filterChainInfos.length === 0 ? ( ) : ( - filterChainInfos.map((chainInfo) => { - const selected = chainStore.current.chainId === chainInfo.chainId; - - return ( - - { - setSearch(""); - chainStore.selectChain(chainInfo.chainId); - chainStore.saveLastViewChainId(); - navigation.dispatch(DrawerActions.closeDrawer()); - }} - style={ - style.flatten( - [ - "flex-row", - "height-62", - "items-center", - "padding-x-12", - "justify-between", - ], - [ - selected && "background-color-indigo", - "border-radius-12", - ] - ) as ViewStyle - } - activeOpacity={0.5} - underlayColor={ - style.flatten([ - "color-gray-50", - "dark:color-platinum-500", - ]).color - } - > - - - {chainInfo.raw.chainSymbolImageUrl ? ( - - ) : ( - - )} - - - {titleCase(chainInfo.chainName)} - - - {selected ? : null} - - - ); - }) + { + setSearch(""); + chainStore.selectChain(chainInfo.chainId); + chainStore.saveLastViewChainId(); + navigation.dispatch(DrawerActions.closeDrawer()); + }} + /> )} diff --git a/packages/mobile/src/components/icon/dot.tsx b/packages/mobile/src/components/icon/dot.tsx new file mode 100644 index 0000000000..46d93e7544 --- /dev/null +++ b/packages/mobile/src/components/icon/dot.tsx @@ -0,0 +1,8 @@ +import Svg, { Circle } from "react-native-svg"; +import React from "react"; + +export const DotIcon = () => ( + + + +); diff --git a/packages/mobile/src/components/icon/github.tsx b/packages/mobile/src/components/icon/github.tsx new file mode 100644 index 0000000000..875c6e17bb --- /dev/null +++ b/packages/mobile/src/components/icon/github.tsx @@ -0,0 +1,11 @@ +import Svg, { Path } from "react-native-svg"; +import React from "react"; + +export const GithubIcon = () => ( + + + +); diff --git a/packages/mobile/src/components/new/address-copyable/index.tsx b/packages/mobile/src/components/new/address-copyable/index.tsx index a28384ed49..779340ccc1 100644 --- a/packages/mobile/src/components/new/address-copyable/index.tsx +++ b/packages/mobile/src/components/new/address-copyable/index.tsx @@ -12,7 +12,8 @@ export const AddressCopyable: FunctionComponent<{ style?: ViewStyle; address: string; maxCharacters: number; -}> = ({ style: propStyle, address, maxCharacters }) => { + isEvm?: boolean; +}> = ({ style: propStyle, address, maxCharacters, isEvm = false }) => { const style = useStyle(); const { isTimedOut, setTimer } = useSimpleTimer(); @@ -31,7 +32,7 @@ export const AddressCopyable: FunctionComponent<{ activeOpacity={0.2} > - {Bech32Address.shortenAddress(address, maxCharacters)} + {isEvm ? address : Bech32Address.shortenAddress(address, maxCharacters)} {isTimedOut ? ( diff --git a/packages/mobile/src/config.ts b/packages/mobile/src/config.ts index 5d40d3c868..38b3bc2476 100644 --- a/packages/mobile/src/config.ts +++ b/packages/mobile/src/config.ts @@ -60,6 +60,166 @@ export const EmbedChainInfos: ChainInfo[] = [ walletUrlForStaking: "https://browse-fetchhub.fetch.ai/validators", govUrl: "https://www.mintscan.io/fetchai/proposals/", }, + { + rpc: "https://mainnet.infura.io/v3/f40158f0c03842f5a18e409ffe09192c", + rest: "https://mainnet.infura.io/v3/f40158f0c03842f5a18e409ffe09192c/", + chainId: "1", + chainName: "Ethereum", + explorerUrl: "https://etherscan.io", + hideInUI: true, + stakeCurrency: { + coinDenom: "ETH", + coinMinimalDenom: "eth", + coinDecimals: 18, + coinGeckoId: "ethereum", + }, + bip44: { + coinType: 60, + }, + bech32Config: Bech32Address.defaultBech32Config("fetch"), + currencies: [ + { + coinDenom: "ETH", + coinMinimalDenom: "eth", + coinDecimals: 18, + coinGeckoId: "ethereum", + }, + { + coinDenom: "FET", + coinMinimalDenom: + "erc20:0xaea46A60368A7bD060eec7DF8CBa43b7EF41Ad85:Fetch.ai", + coinDecimals: 18, + coinGeckoId: "fetch-ai", + contractAddress: "0xaea46A60368A7bD060eec7DF8CBa43b7EF41Ad85", + type: "erc20", + coinImageUrl: + "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + }, + ], + feeCurrencies: [ + { + coinDenom: "ETH", + coinMinimalDenom: "eth", + coinDecimals: 18, + coinGeckoId: "ethereum", + gasPriceStep: { + low: 40000000000, + average: 40000000000, + high: 40000000000, + }, + }, + ], + chainSymbolImageUrl: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/eip155:1/chain.png", + features: ["evm"], + // walletUrlForStaking: "https://browse-bnbhub.bnb.ai/validators", + // govUrl: "https://bnbstation.azoyalabs.com/mainnet/governance/", + }, + { + rpc: "https://bsc-dataseed.binance.org", + rest: "https://bsc-dataseed.binance.org/", + chainId: "56", + chainName: "Binance Smart Chain", + explorerUrl: "https://bscscan.com", + hideInUI: true, + stakeCurrency: { + coinDenom: "BNB", + coinMinimalDenom: "bnb", + coinDecimals: 18, + coinGeckoId: "binancecoin", + }, + bip44: { + coinType: 60, + }, + bech32Config: Bech32Address.defaultBech32Config("fetch"), + currencies: [ + { + coinDenom: "BNB", + coinMinimalDenom: "bnb", + coinDecimals: 18, + coinGeckoId: "binancecoin", + }, + // { + // coinDenom: "USDT", + // coinMinimalDenom: "erc20:0x55d398326f99059fF775485246999027B3197955:Tether USD", + // coinDecimals: 18, + // coinGeckoId: "binancecoin", + // contractAddress: "0x55d398326f99059fF775485246999027B3197955", + // type: "erc20" + // }, + ], + feeCurrencies: [ + { + coinDenom: "BNB", + coinMinimalDenom: "bnb", + coinDecimals: 18, + coinGeckoId: "binancecoin", + gasPriceStep: { + low: 3000000000, + average: 3000000000, + high: 3000000000, + }, + }, + ], + chainSymbolImageUrl: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/eip155:56/chain.png", + features: ["evm"], + // walletUrlForStaking: "https://browse-bnbhub.bnb.ai/validators", + // govUrl: "https://bnbstation.azoyalabs.com/mainnet/governance/", + }, + { + rpc: "https://goerli.infura.io/v3/f40158f0c03842f5a18e409ffe09192c", + rest: "https://goerli.infura.io/v3/f40158f0c03842f5a18e409ffe09192c/", + chainId: "5", + chainName: "Goerli-eth (Testnet)", + + stakeCurrency: { + coinDenom: "ETH", + coinMinimalDenom: "eth", + coinDecimals: 18, + coinGeckoId: "ethereum", + }, + type: "testnet", + hideInUI: true, + isTestnet: true, + bip44: { + coinType: 60, + }, + bech32Config: Bech32Address.defaultBech32Config("fetch"), + currencies: [ + { + coinDenom: "ETH", + coinMinimalDenom: "eth", + coinDecimals: 18, + }, + // { + // coinDenom: "FET", + // coinMinimalDenom: + // "erc20:0xaea46A60368A7bD060eec7DF8CBa43b7EF41Ad85:Fetch.ai", + // coinDecimals: 18, + // coinGeckoId: "fetch-ai", + // contractAddress: "0xaea46A60368A7bD060eec7DF8CBa43b7EF41Ad85", + // type: "erc20", + // coinImageUrl: + // "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + // }, + ], + feeCurrencies: [ + { + coinDenom: "ETH", + coinMinimalDenom: "eth", + coinDecimals: 18, + coinGeckoId: "ethereum", + gasPriceStep: { + low: 40000000000, + average: 40000000000, + high: 40000000000, + }, + }, + ], + features: ["evm"], + explorerUrl: "https://goerli.etherscan.io", + }, { rpc: "https://rpc-cosmoshub.keplr.app", rest: "https://lcd-cosmoshub.keplr.app", diff --git a/packages/mobile/src/modals/chain-suggestion/community-info.tsx b/packages/mobile/src/modals/chain-suggestion/community-info.tsx new file mode 100644 index 0000000000..104a25a32b --- /dev/null +++ b/packages/mobile/src/modals/chain-suggestion/community-info.tsx @@ -0,0 +1,130 @@ +import React, { FunctionComponent } from "react"; +import { ChainInfo } from "@keplr-wallet/types"; +import { useStyle } from "styles/index"; +import * as WebBrowser from "expo-web-browser"; +import { Image, Text, TouchableOpacity, View, ViewStyle } from "react-native"; +import Skeleton from "react-native-reanimated-skeleton"; +import { BlurBackground } from "components/new/blur-background/blur-background"; +import FastImage from "react-native-fast-image"; +import { VectorCharacter } from "components/vector-character"; +import { DotIcon } from "components/icon/dot"; +import { BlurButton } from "components/new/button/blur-button"; +import { GithubIcon } from "components/icon/github"; + +export const CommunityInfo: FunctionComponent<{ + isNotReady: boolean; + chainInfo: ChainInfo; + onPress?: () => void; + communityChainInfoUrl?: string; +}> = ({ chainInfo, isNotReady, communityChainInfoUrl }) => { + const style = useStyle(); + const chainSymbolImageUrl = chainInfo.chainSymbolImageUrl; + + const openWebView = () => { + if (communityChainInfoUrl) { + WebBrowser.openBrowserAsync(communityChainInfoUrl); + } + }; + return ( + + + + + {chainSymbolImageUrl ? ( + + ) : ( + + )} + + + + + {[...Array(3)].map((_, index) => ( + + ))} + + + + + + + + + + {`Add ${chainInfo.chainName}`} + + + } + containerStyle={ + style.flatten([ + "padding-x-12", + "margin-top-18", + "border-radius-32", + ]) as ViewStyle + } + textStyle={style.flatten(["h7", "color-white@70%"]) as ViewStyle} + text="Community driven" + /> + + + + + {`ASI Wallet would like to add blockchain ${chainInfo.chainName}`} + + + + ); +}; diff --git a/packages/mobile/src/modals/chain-suggestion/index.tsx b/packages/mobile/src/modals/chain-suggestion/index.tsx new file mode 100644 index 0000000000..de0e1a799e --- /dev/null +++ b/packages/mobile/src/modals/chain-suggestion/index.tsx @@ -0,0 +1,26 @@ +import React, { FunctionComponent } from "react"; +import { CardModal } from "../card"; +import { useStore } from "stores/index"; +import { observer } from "mobx-react-lite"; +import { SuggestChainPageImpl } from "modals/chain-suggestion/suggestion-chain"; + +export const SuggestChainModal: FunctionComponent<{ + isOpen: boolean; + close: () => void; +}> = observer(({ isOpen, close }) => { + const { chainSuggestStore } = useStore(); + const waitingData = chainSuggestStore.waitingSuggestedChainInfo; + if (!isOpen || !waitingData) { + return null; + } + return ( + + + + ); +}); diff --git a/packages/mobile/src/modals/chain-suggestion/suggestion-chain.tsx b/packages/mobile/src/modals/chain-suggestion/suggestion-chain.tsx new file mode 100644 index 0000000000..bc1a724e60 --- /dev/null +++ b/packages/mobile/src/modals/chain-suggestion/suggestion-chain.tsx @@ -0,0 +1,109 @@ +import React, { FunctionComponent, useEffect, useState } from "react"; +import { InteractionWaitingData } from "@keplr-wallet/background"; +import { ChainInfo } from "@keplr-wallet/types"; +import { observer } from "mobx-react-lite"; +import { useStyle } from "styles/index"; +import { useStore } from "stores/index"; +import { View, ViewStyle } from "react-native"; +import { CommunityInfo } from "modals/chain-suggestion/community-info"; +import { BlurButton } from "components/new/button/blur-button"; + +export const SuggestChainPageImpl: FunctionComponent<{ + waitingData: InteractionWaitingData<{ + chainInfo: ChainInfo; + origin: string; + }>; +}> = observer(({ waitingData }) => { + const style = useStyle(); + const { chainSuggestStore } = useStore(); + const [isLoadingPlaceholder, setIsLoadingPlaceholder] = useState(true); + const queryCommunityChainInfo = chainSuggestStore.getCommunityChainInfo( + waitingData?.data.chainInfo.chainId + ); + const communityChainInfo = queryCommunityChainInfo.chainInfo; + + useEffect(() => { + if (!queryCommunityChainInfo.isLoading) { + setIsLoadingPlaceholder(false); + } + }, [queryCommunityChainInfo.isLoading]); + + useEffect(() => { + setTimeout(() => { + setIsLoadingPlaceholder(false); + }, 3000); + }, []); + + const reject = async () => { + await chainSuggestStore.reject(); + }; + + const approve = async () => { + const chainInfo = communityChainInfo || waitingData.data.chainInfo; + console.log("Hey", waitingData.data.chainInfo); + console.log("Hey2", communityChainInfo); + await chainSuggestStore.approve({ + ...chainInfo, + updateFromRepoDisabled: false, + }); + }; + + return ( + + + + + + + + ); +}); diff --git a/packages/mobile/src/modals/ledger/ledger-evm-model.tsx b/packages/mobile/src/modals/ledger/ledger-evm-model.tsx new file mode 100644 index 0000000000..a7191fa65e --- /dev/null +++ b/packages/mobile/src/modals/ledger/ledger-evm-model.tsx @@ -0,0 +1,80 @@ +import React, { FunctionComponent, useEffect, useState } from "react"; +import { ConfirmCardModel } from "components/new/confirm-modal"; +import { useStore } from "stores/index"; +import { observer } from "mobx-react-lite"; +import { useLoadingScreen } from "providers/loading-screen"; + +export const LedgerEvmModel: FunctionComponent = observer(() => { + const [isLedgerEVM, setIsLedgerEVM] = useState(false); + const { chainStore, ledgerInitStore, accountStore } = useStore(); + const account = accountStore.getAccount(chainStore.current.chainId); + const loadingScreen = useLoadingScreen(); + + // [prev, current] + const [prevChainId, setPrevChainId] = useState<[string | undefined, string]>( + () => [undefined, chainStore.current.chainId] + ); + useEffect(() => { + setPrevChainId((state) => { + if (state[1] !== chainStore.current.chainId) { + return [state[1], chainStore.current.chainId]; + } else { + return [state[0], state[1]]; + } + }); + }, [chainStore, chainStore.current.chainId]); + + const isOpen = (() => { + if ( + account.rejectionReason && + account.rejectionReason.message === + "No Ethereum public key. Initialize Ethereum app on Ledger by selecting the chain in the extension" + ) { + return true; + } + + return false; + })(); + + useEffect(() => { + if (isOpen) { + setIsLedgerEVM(true); + } + }, [isOpen]); + + return ( + setIsLedgerEVM(false)} + confirmButtonText={"Connect"} + title={"Please Connect your Ledger device"} + subtitle={` + For making an address for ${chainStore.current.chainName}, you need to + connect your Ledger device through the Ethereum app. + `} + select={async (confirm) => { + if (confirm) { + loadingScreen.setIsLoading(true); + + try { + await ledgerInitStore.tryNonDefaultLedgerAppOpen(); + account.disconnect(); + + await account.init(); + } catch (e) { + console.log("Ledger EVM Error", e); + } finally { + loadingScreen.setIsLoading(false); + } + } else { + if (prevChainId[0]) { + chainStore.selectChain(prevChainId[0]); + } else { + chainStore.selectChain(chainStore.chainInfos[0].chainId); + } + chainStore.saveLastViewChainId(); + } + }} + /> + ); +}); diff --git a/packages/mobile/src/modals/ledger/ledger-selector.tsx b/packages/mobile/src/modals/ledger/ledger-selector.tsx index e098af846d..3fb805f10d 100644 --- a/packages/mobile/src/modals/ledger/ledger-selector.tsx +++ b/packages/mobile/src/modals/ledger/ledger-selector.tsx @@ -5,6 +5,7 @@ import { BluetoothMode } from "."; import { ViewStyle } from "react-native"; import { BlurButton } from "components/new/button/blur-button"; import TransportBLE from "@ledgerhq/react-native-hw-transport-ble"; +import { useStore } from "stores/index"; export const LedgerNanoBLESelector: FunctionComponent<{ deviceId: string; @@ -25,12 +26,24 @@ export const LedgerNanoBLESelector: FunctionComponent<{ setIsPaired, }) => { const style = useStyle(); - + const { chainStore } = useStore(); + const isEvm = chainStore.current.features?.includes("evm") ?? false; // const [pairingText, setIsPairingText] = useState(""); const [isConnecting, setIsConnecting] = useState(false); const testLedgerConnection = async () => { let initErrorOn: LedgerInitErrorOn | undefined; + let app: LedgerApp = LedgerApp.Cosmos; + let cosmosLikeApp: string = "Cosmos"; + let content: string = + "Open Cosmos app on your ledger and pair with ASI Alliance Wallet"; + + if (isEvm) { + app = LedgerApp.Ethereum; + cosmosLikeApp = "Ethereum"; + content = + "Open Ethereum app on your ledger and pair with ASI Alliance Wallet"; + } try { setIsPaired(false); @@ -41,12 +54,10 @@ export const LedgerNanoBLESelector: FunctionComponent<{ const ledger = await Ledger.init( () => TransportBLE.open(deviceId), undefined, - LedgerApp.Cosmos, - "Cosmos" - ); - setMainContent( - "Open Cosmos app on your ledger and pair with ASI Alliance Wallet" + app, + cosmosLikeApp ); + setMainContent(content); setBluetoothMode(BluetoothMode.Pairing); setIsPairingText("Waiting to pair..."); setTimeout(function () { @@ -70,9 +81,7 @@ export const LedgerNanoBLESelector: FunctionComponent<{ initErrorOn = e.errorOn; if (initErrorOn === LedgerInitErrorOn.App) { setBluetoothMode(BluetoothMode.Device); - setMainContent( - "Open Cosmos app on your ledger and pair with ASI Alliance Wallet" - ); + setMainContent(content); setIsConnecting(false); } else if (initErrorOn === LedgerInitErrorOn.Transport) { setMainContent("Please unlock ledger nano X"); diff --git a/packages/mobile/src/navigation/navigation.tsx b/packages/mobile/src/navigation/navigation.tsx index f990f6e4ff..a4c05ea4fd 100644 --- a/packages/mobile/src/navigation/navigation.tsx +++ b/packages/mobile/src/navigation/navigation.tsx @@ -29,6 +29,7 @@ import { ViewStyle } from "react-native"; import { StakeNavigation } from "./stake-navigation"; import { MoreNavigation } from "./more-navigation"; import { routingInstrumentation } from "../../index"; +import { AddEvmChain } from "screens/setting/screens/chain-list/add-evm-chain"; export const Stack = createStackNavigator(); @@ -91,6 +92,14 @@ export const ChainListStackScreen: FunctionComponent = () => { name="Setting.ChainList" component={SettingChainListScreen} /> + ); }; diff --git a/packages/mobile/src/navigation/smart-navigation.ts b/packages/mobile/src/navigation/smart-navigation.ts index 08652b50a2..b771d5f957 100644 --- a/packages/mobile/src/navigation/smart-navigation.ts +++ b/packages/mobile/src/navigation/smart-navigation.ts @@ -13,6 +13,7 @@ import { import { NewMnemonicConfig } from "screens/register/mnemonic"; import { BIP44HDPath, ExportKeyRingData } from "@keplr-wallet/background"; import { ActivityEnum } from "screens/activity"; +import { NetworkEnum } from "components/drawer"; interface Configs { amount: string; @@ -271,6 +272,9 @@ const { SmartNavigatorProvider, useSmartNavigation } = Activity: { tabId?: ActivityEnum; }; + "Setting.ChainList": { + selectedTab: NetworkEnum; + }; }>() ); diff --git a/packages/mobile/src/providers/interaction-modals-provider/index.tsx b/packages/mobile/src/providers/interaction-modals-provider/index.tsx index 7398a67447..9144136c9e 100644 --- a/packages/mobile/src/providers/interaction-modals-provider/index.tsx +++ b/packages/mobile/src/providers/interaction-modals-provider/index.tsx @@ -11,6 +11,7 @@ import { KeyRingStatus } from "@keplr-wallet/background"; import { NetworkErrorModal } from "modals/network"; import { useNetInfo } from "@react-native-community/netinfo"; import { LoadingScreenModal } from "providers/loading-screen/modal"; +import { SuggestChainModal } from "modals/chain-suggestion"; export const InteractionModalsProvider: FunctionComponent = observer( ({ children }) => { @@ -18,6 +19,7 @@ export const InteractionModalsProvider: FunctionComponent = observer( keyRingStore, ledgerInitStore, permissionStore, + chainSuggestStore, signInteractionStore, walletConnectStore, } = useStore(); @@ -114,6 +116,13 @@ export const InteractionModalsProvider: FunctionComponent = observer( signInteractionStore.rejectAll(); }} /> + + { + chainSuggestStore.rejectAll(); + }} + /> ledgerInitStore.abortAll()} diff --git a/packages/mobile/src/screens/activity/activity-transaction/index.tsx b/packages/mobile/src/screens/activity/activity-transaction/index.tsx index 95dcd8aa99..74cb7aad0c 100644 --- a/packages/mobile/src/screens/activity/activity-transaction/index.tsx +++ b/packages/mobile/src/screens/activity/activity-transaction/index.tsx @@ -36,7 +36,7 @@ export const ActivityNativeTab: FunctionComponent<{ const style = useStyle(); const { chainStore, activityStore } = useStore(); const current = chainStore.current; - + const isEvm = chainStore.current.features?.includes("evm") ?? false; const [_date, setDate] = useState(""); const [isLoading, setIsLoading] = useState(true); @@ -139,7 +139,9 @@ export const ActivityNativeTab: FunctionComponent<{ data.length > 0 && activities.length > 0 ? ( renderList(data) - ) : activities.length == 0 && isLoading ? ( + ) : isEvm && activities.length === 0 ? ( + + ) : activities.length === 0 && isLoading ? ( currency.coinMinimalDenom === "uusdc" ); - const delegated = queryDelegated.total; + const isEvm = chainStore.current.features?.includes("evm") ?? false; + const stakable = (() => { + if (isNoble && hasUSDC) { + return balanceQuery.getBalanceFromCurrency(hasUSDC); + } - const queryUnbonding = - queries.cosmos.queryUnbondingDelegations.getQueryBech32Address( - account.bech32Address - ); + return balanceStakableQuery.balance; + })(); + // const queryDelegated = queries.cosmos.queryDelegations.getQueryBech32Address( + // account.bech32Address + // ); + const delegated = queries.cosmos.queryDelegations + .getQueryBech32Address(account.bech32Address) + .total.upperCase(true); + + // const queryUnbonding = + // queries.cosmos.queryUnbondingDelegations.getQueryBech32Address( + // account.bech32Address + // ); const rewards = queries.cosmos.queryRewards.getQueryBech32Address( account.bech32Address ); const stakableReward = rewards.stakableReward; - const unbonding = queryUnbonding.total; + const unbonding = queries.cosmos.queryUnbondingDelegations + .getQueryBech32Address(account.bech32Address) + .total.upperCase(true); const stakedSum = delegated.add(unbonding); @@ -219,7 +238,7 @@ export const AccountSection: FunctionComponent<{ useEffect(() => { /* this is required because accountInit sets the nodes on reload, - so we wait for accountInit to set the proposal nodes and then we + so we wait for accountInit to set the proposal nodes and then we store the proposal votes from api in activity store */ const timeout = setTimeout(async () => { const nodes = activityStore.sortedNodesProposals; @@ -254,6 +273,18 @@ export const AccountSection: FunctionComponent<{ } }, [activityStore.getPendingTxn]); + function getAddress() { + if (isEvm || account.hasEthereumHexAddress) { + if (account.ethereumHexAddress.length === 42) + return `${account.ethereumHexAddress.slice( + 0, + 10 + )}...${account.ethereumHexAddress.slice(-3)}`; + } + + return account.bech32Address || "..."; + } + return ( {account.name} - + )} - - {tokenState ? ( + {isEvm ? ( + + + + {tokenState ? ( + + + {tokenState.type === "positive" && "+"} + {changeInDollarsValue.toFixed(4)} {totalDenom} ( + {tokenState.type === "positive" ? "+" : "-"} + {parseFloat(tokenState.diff).toFixed(2)}%) + + + {tokenState.time} + + + ) : null} + + ) : ( + + )} + + {tokenState && !isEvm && ( {tokenState.type === "positive" && "+"} - {changeInDollarsValue.toFixed(4)} {totalDenom}( + {changeInDollarsValue.toFixed(4)} {totalDenom} ( {tokenState.type === "positive" ? "+" : "-"} - {parseFloat(tokenState.diff).toFixed(2)} %) + {parseFloat(tokenState.diff).toFixed(2)}%) - ) : null} + )} { tokenName={chainStore.current.feeCurrencies[0].coinGeckoId} height={windowHeight / graphHeight} /> + ); diff --git a/packages/mobile/src/screens/portfolio/index.tsx b/packages/mobile/src/screens/portfolio/index.tsx index e6ae38815b..9adfc34ce8 100644 --- a/packages/mobile/src/screens/portfolio/index.tsx +++ b/packages/mobile/src/screens/portfolio/index.tsx @@ -9,6 +9,8 @@ import { NativeTokensSection } from "screens/portfolio/native-tokens-section"; import { TokensSection } from "screens/portfolio/tokens-section"; import { TabBarView } from "components/new/tab-bar/tab-bar"; import { useStore } from "stores/index"; +import { RowFrame } from "components/new/icon/row-frame"; +import { EmptyView } from "components/new/empty"; enum AssetsSectionEnum { Tokens = "Tokens", @@ -19,7 +21,8 @@ export const PortfolioScreen: FunctionComponent = observer(() => { const style = useStyle(); const scrollViewRef = useRef(null); const [selectedId, setSelectedId] = useState(AssetsSectionEnum.Tokens); - const { analyticsStore } = useStore(); + const { analyticsStore, chainStore } = useStore(); + const isEvm = chainStore.current.features?.includes("evm") ?? false; useEffect(() => { analyticsStore.logEvent(`${selectedId.toLowerCase()}_tab_click`, { @@ -31,6 +34,7 @@ export const PortfolioScreen: FunctionComponent = observer(() => { { )} - {selectedId === AssetsSectionEnum.Stats && ( - - )} + {selectedId === AssetsSectionEnum.Stats && + (isEvm ? ( + } + text="Feature not available on this network" + textStyle={style.flatten(["h3"]) as ViewStyle} + containerStyle={style.flatten(["flex-1"])} + /> + ) : ( + + + + ))} ); }); diff --git a/packages/mobile/src/screens/setting/screens/chain-list/add-evm-chain.tsx b/packages/mobile/src/screens/setting/screens/chain-list/add-evm-chain.tsx new file mode 100644 index 0000000000..1c8142b3c0 --- /dev/null +++ b/packages/mobile/src/screens/setting/screens/chain-list/add-evm-chain.tsx @@ -0,0 +1,346 @@ +import React, { FunctionComponent, useState } from "react"; +import { Bech32Address } from "@keplr-wallet/cosmos"; +import { ChainInfo } from "@keplr-wallet/types"; +import { PageWithScrollView } from "components/page"; +import { InputCardView } from "components/new/card-view/input-card"; +import { useStore } from "stores/index"; +import { Button } from "components/button/button"; +import { Text, View, ViewStyle } from "react-native"; +import Toast from "react-native-toast-message"; +import { useNavigation } from "@react-navigation/native"; +import axios from "axios"; +import { useStyle } from "styles/index"; +import { LoadingSpinner } from "components/spinner"; + +interface FieldLabel { + label: string; + editable: boolean; +} +const fieldLabels: Record = { + rpc: { + label: "RPC URL", + editable: true, + }, + chainId: { label: "Chain ID", editable: false }, + chainName: { label: "Chain Name", editable: false }, + symbol: { label: "Symbol", editable: false }, + decimal: { label: "Decimals", editable: false }, + explorerUrl: { label: "Explorer URL", editable: true }, +}; + +export const AddEvmChain: FunctionComponent = () => { + const style = useStyle(); + const { chainStore, analyticsStore } = useStore(); + const [hasErrors, setHasErrors] = useState(false); + const [info, setInfo] = useState(""); + const [loading, setLoading] = useState(false); + const navigation = useNavigation(); + + const initialState: ChainInfo = { + chainName: "", + rpc: "", + rest: "", + chainId: "", + stakeCurrency: { + coinDenom: "", + coinMinimalDenom: "", + coinDecimals: 0, + }, + bip44: { + coinType: 60, + }, + bech32Config: Bech32Address.defaultBech32Config("fetch"), + currencies: [ + { + coinDenom: "", + coinMinimalDenom: "", + coinDecimals: 0, + // coinGeckoId: "", + }, + ], + feeCurrencies: [ + { + coinDenom: "", + coinMinimalDenom: "", + coinDecimals: 0, + + gasPriceStep: { + low: 10000000000, + average: 10000000000, + high: 10000000000, + }, + }, + ], + chainSymbolImageUrl: "", + features: ["evm"], + explorerUrl: "", + }; + const [newChainInfo, setNewChainInfo] = useState(initialState); + const getChainInfo = async (rpcUrl: string) => { + setLoading(true); + try { + const response = await axios.post( + rpcUrl, + { + jsonrpc: "2.0", + id: 1, + method: "eth_chainId", + params: [], + }, + { timeout: 5000 } + ); + + if (response.status !== 200 || !response.data.result) { + setInfo( + "The rpc seems to be invalid. Please recheck the RPC url provided" + ); + setHasErrors(true); + return; + } + const chainId = parseInt(response.data.result, 16); + if (chainStore.hasChain(chainId.toString())) { + setInfo( + "Network already exists. You can go to network settings if you want to update the RPC" + ); + setHasErrors(true); + return; + } + + setNewChainInfo({ + ...newChainInfo, + chainId: chainId.toString(), + }); + + const chains = await axios.get("https://chainid.network/chains.json"); + if (chains.status !== 200) { + setInfo( + "We've fetched chain id based on the provided RPC. You will need to enter other details manaually" + ); + return; + } + + const chainData = chains.data.find( + (element: any) => chainId === element.chainId + ); + + if (chainData) { + const successMessage = + "We've fetched information based on the provided RPC."; + setInfo(successMessage); + Toast.show({ type: "success", text1: successMessage }); + + const symbol = chainData.nativeCurrency.symbol; + setNewChainInfo({ + ...newChainInfo, + currencies: [ + { + coinDenom: symbol, + coinMinimalDenom: symbol, + coinDecimals: chainData.nativeCurrency + ? chainData.nativeCurrency.decimals + : 0, + }, + ], + stakeCurrency: { + coinDenom: symbol, + coinMinimalDenom: symbol, + coinDecimals: chainData.nativeCurrency + ? chainData.nativeCurrency.decimals + : 0, + }, + feeCurrencies: [ + { + coinDenom: symbol, + coinMinimalDenom: symbol, + coinDecimals: chainData.nativeCurrency + ? chainData.nativeCurrency.decimals + : 0, + gasPriceStep: { + low: 10000000000, + average: 10000000000, + high: 10000000000, + }, + }, + ], + rpc: rpcUrl, + rest: rpcUrl, + chainId: chainId.toString(), + chainName: chainData.name, + chainSymbolImageUrl: chainData.chainSymbolImageUrl, + bech32Config: Bech32Address.defaultBech32Config(symbol.toLowerCase()), + explorerUrl: + chainData.explorers && chainData.explorers.length > 0 + ? chainData.explorers[0].url + : undefined, + }); + } else { + const warningMessage = + "We've fetched the chain ID based on the provided RPC. You will need to enter other details manually."; + setInfo(warningMessage); + } + } catch (error) { + setNewChainInfo({ ...initialState, rpc: rpcUrl }); + const errorMessage = + "We could not fetch chain details, please try again."; + Toast.show({ type: "error", text1: errorMessage }); + } finally { + setLoading(false); + } + }; + + const isUrlValid = (url: string) => { + try { + const parsedUrl = new URL(url); + if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { + return false; + } + return true; + } catch (err) { + return false; + } + }; + const handleChange = async (name: string, value: string) => { + const rpcUrl = value.trim(); + setInfo(""); + setHasErrors(false); + analyticsStore.logEvent("add_evm_chain_click"); + + if (name === "rpc") { + setNewChainInfo({ + ...newChainInfo, + rpc: rpcUrl, + chainId: "", + }); + + if (isUrlValid(rpcUrl)) { + await getChainInfo(rpcUrl); + } + } else if (name === "decimal") { + setNewChainInfo({ + ...newChainInfo, + currencies: [ + { + ...newChainInfo.currencies[0], + coinDecimals: parseInt(rpcUrl), + }, + ], + stakeCurrency: { + ...newChainInfo.stakeCurrency, + coinDenom: rpcUrl, + coinMinimalDenom: rpcUrl, + }, + feeCurrencies: [ + { + ...newChainInfo.feeCurrencies[0], + coinDenom: rpcUrl, + coinMinimalDenom: rpcUrl, + }, + ], + }); + } else if (name === "symbol") { + setNewChainInfo({ + ...newChainInfo, + currencies: [ + { + ...newChainInfo.currencies[0], + coinDenom: rpcUrl, + coinMinimalDenom: rpcUrl, + }, + ], + stakeCurrency: { + ...newChainInfo.stakeCurrency, + coinDenom: rpcUrl, + coinMinimalDenom: rpcUrl, + }, + feeCurrencies: [ + { + ...newChainInfo.feeCurrencies[0], + coinDenom: rpcUrl, + coinMinimalDenom: rpcUrl, + }, + ], + }); + } else { + setNewChainInfo({ + ...newChainInfo, + [name]: rpcUrl, + }); + } + }; + + const isValid = !hasErrors && newChainInfo.rpc && newChainInfo.chainId; + const handleSubmit = async () => { + try { + await chainStore.addEVMChainInfo(newChainInfo); + Toast.show({ + type: "success", + text1: "Chain added successfully!", + }); + chainStore.selectChain(newChainInfo.chainId); + chainStore.saveLastViewChainId(); + navigation.goBack(); + } catch (error) { + Toast.show({ + type: "error", + text1: error?.message, + }); + } + }; + return ( + + {Object.entries(fieldLabels).map(([field, { label, editable }]) => ( + + handleChange(field, value)} + editable={editable} + containerStyle={style.flatten(["margin-y-4"]) as ViewStyle} + rightIcon={ + field === "rpc" && loading ? ( + + ) : undefined + } + /> + + {field === "rpc" && info && ( + + {info} + + )} + + ))} + + +