add ghost connect into right side panel

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-07-13 18:49:24 +03:00
parent 0879af509e
commit 85055ff4c5
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
7 changed files with 230 additions and 19 deletions

View File

@ -1,7 +1,7 @@
{
"name": "ghost-dao-interface",
"private": true,
"version": "0.7.54",
"version": "0.7.55",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -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)}
>
<Box width="120px" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography style={{ lineHeight: 1.1, fontWeight: 600, fontSize: "0.975rem" }} color="textSecondary">
MY WALLET
{chainName?.toUpperCase()}
</Typography>
<Typography style={{ fontSize: ".85rem", lineHeight: 1.1 }} variant="h3">
{shorten(address)}
</Typography>
</Box>
<Box pt="1px">
{tokens?.ghst && <Box pt="1px">
<GhostStyledIcon
viewBox="0 0 24 24"
component={LoopIcon}
@ -96,12 +97,11 @@ const WalletTotalValue = ({ address, tokens }) => {
transform: topHovered ? "rotateZ(-180deg)" : "rotateZ(0deg)",
}}
/>
</Box>
</Box>}
</Box>
<Typography style={{ fontWeight: 700 }} variant="h3">
{formatCurrency(walletValue[currency], 2, currency === "USD" ? "USD" : "👻" )}
{formatCurrency(walletValue[currency], currency === "USD" ? 2 : 5, currency === "USD" ? "USD" : "👻" )}
</Typography>
</Box>
);
@ -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 }) {
<Paper>
<Box sx={{ padding: theme.spacing(0, 3), display: "flex", flexDirection: "column", minHeight: "100vh" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", padding: theme.spacing(3, 0, 1) }}>
<WalletTotalValue address={address} tokens={tokens} />
<WalletTotalValue
currency={currency}
setCurrency={setCurrency}
chainName={chainName}
address={address}
tokens={tokens}
/>
<CloseButton size="small" onClick={onClose} aria-label="close wallet">
<GhostStyledIcon component={CloseIcon} />
</CloseButton>
@ -158,7 +187,7 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
<Divider />
</Box>
<Box sx={{ display: "flex", flexDirection: "column" }} style={{ gap: theme.spacing(1) }}>
<Box sx={{ display: "flex", flexDirection: "column" }} >
<Tokens address={address} tokens={tokens} onClose={onClose} />
</Box>
@ -166,6 +195,46 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
<Divider />
</Box>
{!isExtensionMissing
? <>
<Box sx={{ display: "flex", flexDirection: "column" }} style={{ gap: theme.spacing(2, 1) }}>
<WalletTotalValue
currency={currency}
setCurrency={setCurrency}
chainName={chainSpec?.chainName ?? ""}
address={account ?? "...wait"}
tokens={nativeGhst}
/>
<NativeToken {...nativeGhst} />
</Box>
<Box sx={{ margin: theme.spacing(2, -3, 3) }}>
<Divider />
</Box>
</>
: <Box
sx={{ borderRadius: "15px", background: theme.colors.paper.background }}
width="100%"
height="100%"
padding="20px 30px"
display="flex"
flexDirection="column"
justifyContent="space-around"
alignItems="center"
gap="10px"
>
<Typography sx={{ textAlign: "center" }} variant="subtitle1">GHOST Connect is not detected!</Typography>
<Typography sx={{ textAlign: "center" }} variant="body2">Download GHOST Connect for bounties, validator stats, and (10, 10) {"Stake\u00B2"} rewards.</Typography>
<PrimaryButton
fullWidth
onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}
>
Get GHOST Connect
</PrimaryButton>
</Box>
}
<Box sx={{ width: "100%", marginTop: "auto", marginX: "auto", padding: theme.spacing(2, 0) }}>
<DisconnectButton onClose={onClose} />
</Box>

View File

@ -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 = ({
</Box>
);
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 (
<Accordion expanded={props.expanded} onChange={props.onChangeExpanded}>
<AccordionSummary expandIcon={<GhostStyledIcon component={ExpandMoreIcon} color="disabled" />} >
<Box sx={{ display: "flex", justifyContent: "space-between", width: "100%", marginRight: "10px" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<SingleToken
sx={{
width: "30px",
height: "30px",
}}
name="GHST"
isNative
/>
<Typography>{props.symbol}</Typography>
</Box>
<BalanceValue
balance={props.balance}
balanceValueUSD={props.balance.mul(props.price)}
/>
</Box>
</AccordionSummary>
<AccordionDetails style={{ margin: "auto", padding: theme.spacing(1, 0) }}>
<Box
sx={{ display: "flex", flexDirection: "column", flex: 1, mx: "32px", justifyContent: "center" }}
style={{ gap: theme.spacing(1) }}
>
<Box display="flex" flexDirection="column" className="ghst-pairs" style={{ width: "100%" }}>
<PrimaryButton
onClick={() =>{ navigate({ pathname: `${chainName}/bridge` }); onClose(); }}
fullWidth
>
<Typography>Get (10, 10) Stake</Typography>
</PrimaryButton>
</Box>
</Box>
</AccordionDetails>
</Accordion>
);
}
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) => {
</Box>
<BalanceValue
balance={balance}
balanceValueUSD={balance.mul(price)}
balanceValueUSD={balance?.mul(price)}
/>
</Box>
</AccordionSummary>
@ -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) => (
<Token key={i} isNative={i === 0} {...tokenProps(token)} />
<Token key={i} isNative={i == 0} {...tokenProps(token)} />
))}
</>
);

View File

@ -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 (
<UnstableProvider.Provider value={value}>

View File

@ -14,3 +14,5 @@ export * from "./useBabeSlots";
export * from "./useErasTotalStaked";
export * from "./useLatestBlockNumber";
export * from "./useEraIndex";
export * from "./useSystemAccount";
export * from "./useChainSpec";

View File

@ -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
}

View File

@ -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
}