diff --git a/package.json b/package.json index 46385b6..0f6e77a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.7.54", + "version": "0.7.55", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/TopBar/Wallet/InitialWalletView.tsx b/src/components/TopBar/Wallet/InitialWalletView.tsx index 68bbf8a..7f0ce4a 100644 --- a/src/components/TopBar/Wallet/InitialWalletView.tsx +++ b/src/components/TopBar/Wallet/InitialWalletView.tsx @@ -18,13 +18,14 @@ import { ReactElement, useState, useEffect } from "react"; import { useNavigate, useLocation, createSearchParams } from "react-router-dom"; import GhostStyledIcon from "../../Icon/GhostIcon"; -import { Tokens, useWallet } from "./Token"; +import { Tokens, NativeToken, useWallet } from "./Token"; import { PrimaryButton, SecondaryButton } from "../../Button"; import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react"; import { formatCurrency, shorten } from "../../../helpers"; import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber"; import { RESERVE_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses"; +import { useUnstableProvider, useSystemAccount, useChainSpec } from "../../../hooks/ghost" import { useAccount, useDisconnect, useSwitchChain } from "wagmi"; @@ -52,12 +53,12 @@ const CloseButton = styled(IconButton)(theme => ({ }, })); -const WalletTotalValue = ({ address, tokens }) => { - const ghstPrice = tokens.ghst.price; - const [currency, setCurrency] = useState("USD"); +const WalletTotalValue = ({ chainName, address, tokens, currency, setCurrency }) => { + const ghstPrice = tokens?.ghst ? tokens.ghst.price : tokens?.price; + const tokensObject = tokens?.ghst ? tokens : { tokens }; const [topHovered, setTopHovered] = useState(false); - const walletTotalValueUSD = Object.values(tokens).reduce( + const walletTotalValueUSD = Object.values(tokensObject).reduce( (totalValue, token) => totalValue.add(token.balance.mul(token.price)), new DecimalBigNumber(0n, 0), ); @@ -77,16 +78,16 @@ const WalletTotalValue = ({ address, tokens }) => { onMouseEnter={() => setTopHovered(true)} onMouseLeave={() => setTopHovered(false)} > - + - MY WALLET + {chainName?.toUpperCase()} {shorten(address)} - + {tokens?.ghst && { transform: topHovered ? "rotateZ(-180deg)" : "rotateZ(0deg)", }} /> - + } - - {formatCurrency(walletValue[currency], 2, currency === "USD" ? "USD" : "👻" )} + {formatCurrency(walletValue[currency], currency === "USD" ? 2 : 5, currency === "USD" ? "USD" : "👻" )} ); @@ -112,6 +112,9 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) { const navigate = useNavigate(); const location = useLocation(); const tokens = useWallet(chainId, address); + + const [currency, setCurrency] = useState("USD"); + const { account, isExtensionMissing } = useUnstableProvider(); const { chains } = useSwitchChain(); const chainName = useMemo(() => { @@ -120,6 +123,26 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) { return chain.name.toLowerCase() }, [chains, chainId]) + const chainSpec = useChainSpec(); + const accountBalanceRaw = useSystemAccount({ account }); + + const accountBalance = useMemo(() => { + const decimals = chainSpec ? chainSpec.properties.tokenDecimals : 18; + if (!accountBalanceRaw) return new DecimalBigNumber(0n, decimals); + const balance = accountBalanceRaw.data.free + + accountBalanceRaw.data.frozen + + accountBalanceRaw.data.reserved; + + return new DecimalBigNumber(balance, decimals); + }, [accountBalanceRaw, chainSpec]) + + const nativeGhst = useMemo(() => { + return { + ...tokens.ghst, + balance: accountBalance, + } + }, [tokens, accountBalance]) + const onBtnClick = (dexName, from, to) => { navigate({ pathname: `${chains.find(chain => chain.id === chainId).name.toLowerCase()}/dex/` + dexName, @@ -148,7 +171,13 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) { - + @@ -158,7 +187,7 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) { - + @@ -166,6 +195,46 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) { + {!isExtensionMissing + ? <> + + + + + + + + + : + GHOST Connect is not detected! + Download GHOST Connect for bounties, validator stats, and (10, 10) {"Stake\u00B2"} rewards. + window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')} + > + Get GHOST Connect + + + } + + + diff --git a/src/components/TopBar/Wallet/Token.tsx b/src/components/TopBar/Wallet/Token.tsx index dfcb609..ac03c71 100644 --- a/src/components/TopBar/Wallet/Token.tsx +++ b/src/components/TopBar/Wallet/Token.tsx @@ -19,6 +19,7 @@ import { EMPTY_ADDRESS } from "../../../constants/addresses"; import GhostStyledIcon from "../../Icon/GhostIcon"; import TokenStack from "../../TokenStack/TokenStack"; +import SingleToken from "../../Token/Token"; import { PrimaryButton, SecondaryButton } from "../../Button"; import { useBalance, useTokenSymbol } from "../../../hooks/tokens"; @@ -68,6 +69,57 @@ const BalanceValue = ({ ); +export const NativeToken = (props) => { + const theme = useTheme(); + const navigate = useNavigate(); + const chainId = useChainId(); + + const { chains } = useSwitchChain(); + + const chainName = useMemo(() => { + return chains.find(chain => chain.id === chainId).name.toLowerCase(); + }, [chains, chainId]) + + return ( + + } > + + + + {props.symbol} + + + + + + + + { navigate({ pathname: `${chainName}/bridge` }); onClose(); }} + fullWidth + > + Get (10, 10) Stake + + + + + + ); +} + export const Token = (props) => { const { isNative, @@ -75,7 +127,7 @@ export const Token = (props) => { faucetPath, icons, address, - price = 0, + price, balance, onAddTokenToWallet, expanded, @@ -136,7 +188,7 @@ export const Token = (props) => { @@ -294,7 +346,7 @@ export const useWallet = (chainId, userAddress) => { }, {}); }; -export const Tokens = ({ address, tokens, onClose }) => { +export const Tokens = ({ address, tokens, isNative, onClose }) => { const [expanded, setExpanded] = useState(null); const alwaysShowTokens = [tokens.native, tokens.reserve, tokens.ftso, tokens.ghst, tokens.reserveFtso]; @@ -310,7 +362,7 @@ export const Tokens = ({ address, tokens, onClose }) => { return ( <> {alwaysShowTokens.map((token, i) => ( - + ))} ); diff --git a/src/hooks/ghost/UnstableProvider.jsx b/src/hooks/ghost/UnstableProvider.jsx index 9857b37..157339d 100644 --- a/src/hooks/ghost/UnstableProvider.jsx +++ b/src/hooks/ghost/UnstableProvider.jsx @@ -24,6 +24,21 @@ export const UnstableProviderProvider = ({ children }) => { () => providerDetail ? providerDetail.provider : null ); + const { data: accounts } = useSWR( + () => providerDetail ? `providerDetail.${providerDetail.info.uuid}.provider.getAccounts(${chainId})` : null, + async () => { + if (!providerDetail) return [] + const activeProvider = await providerDetail.provider; + return activeProvider.getAccounts(chainId); + }, + ) + + const account = useMemo(() => { + if (Array.isArray(accounts)) return accounts.at(0).address; + if (typeof accounts === 'string') return accounts.address; + return undefined + }, [accounts]) + const connectionState = useMemo(() => { if (!providerDetail) return 'no-extension'; if (!provider) return 'loading'; @@ -52,10 +67,11 @@ export const UnstableProviderProvider = ({ children }) => { providerDetail, connectProviderByIndex: setProviderIndex, chainId, + account, client, setChainId, chainHead$ - }), [providerDetails, providerDetail, chainId, client, chainHead$]); + }), [providerDetails, providerDetail, chainId, client, account, chainHead$]); return ( diff --git a/src/hooks/ghost/index.js b/src/hooks/ghost/index.js index 02d4cea..eafb10e 100644 --- a/src/hooks/ghost/index.js +++ b/src/hooks/ghost/index.js @@ -14,3 +14,5 @@ export * from "./useBabeSlots"; export * from "./useErasTotalStaked"; export * from "./useLatestBlockNumber"; export * from "./useEraIndex"; +export * from "./useSystemAccount"; +export * from "./useChainSpec"; diff --git a/src/hooks/ghost/useChainSpec.js b/src/hooks/ghost/useChainSpec.js new file mode 100644 index 0000000..814289b --- /dev/null +++ b/src/hooks/ghost/useChainSpec.js @@ -0,0 +1,31 @@ +import useSWR from "swr" +import { useUnstableProvider } from "./UnstableProvider" + +export const useChainSpec = () => { + const { client } = useUnstableProvider() + const { data: chainSpecV1 } = useSWR( + client ? ["chainSpec_v1_", client] : null, + async ([prefix, client]) => { + const methods = ["chainName", "properties", "genesisHash"] + const responses = await Promise.all(methods.map((method) => { + return new Promise((resolve, reject) => + client._request(`${prefix}${method}`, [], { + onSuccess: resolve, + onError: reject, + }), + ) + .catch((_) => undefined) + })) + return { + chainName: responses?.at(0), + properties: responses?.at(1), + genesisHash: responses?.at(2) + } + }, + { + revalidateOnFocus: false, + refreshInterval: 0 + } + ) + return chainSpecV1 +} diff --git a/src/hooks/ghost/useSystemAccount.js b/src/hooks/ghost/useSystemAccount.js new file mode 100644 index 0000000..624427e --- /dev/null +++ b/src/hooks/ghost/useSystemAccount.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useSystemAccount = ({ account }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: systemAccount } = useSWRSubscription( + chainHead$ && chainId && account && metadata + ? ["systemAccount", chainHead$, chainId, account, metadata] + : null, + ([_, chainHead$, chainId, account, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const storageAccount = builder.buildStorage("System", "Account") + return storage$(blockInfo?.hash, "value", () => + storageAccount?.keys.enc(account) + ).pipe( + filter(Boolean), + distinct(), + map((value) => storageAccount?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(systemAccount) { + next(null, systemAccount) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return systemAccount +}