diff --git a/package.json b/package.json index da381aa..1409f5c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.7.58", + "version": "0.7.59-beta-2", "type": "module", "scripts": { "dev": "vite", @@ -55,5 +55,6 @@ "sass-embedded": "^1.85.1", "vite": "^6.2.0", "vite-plugin-svgr": "^4.3.0" - } + }, + "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" } diff --git a/src/App.jsx b/src/App.jsx index bc78fc0..d0924c1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -41,6 +41,7 @@ const NotFound = lazy(() => import("./containers/NotFound/NotFound")); const Governance = lazy(() => import("./containers/Governance/Governance")); const ProposalDetails = lazy(() => import("./containers/Governance/ProposalDetails")); const NewProposal = lazy(() => import("./containers/Governance/NewProposal")); +const History = lazy(() => import("./containers/History/History")); const PREFIX = "App"; @@ -204,6 +205,7 @@ function App() { } /> } /> } /> + } /> } { to={`/${chainName}/bonds`} children={ - {sortedGhostBonds.length > 0 && - Bond Discounts - } {sortedGhostBonds.map((bond, index) => { return ( { - + diff --git a/src/config.js b/src/config.js index cdab829..ee7461c 100644 --- a/src/config.js +++ b/src/config.js @@ -1,45 +1,46 @@ import { defineChain } from 'viem' import { http, fallback, createConfig } from 'wagmi' -import { sepolia, hoodi } from 'wagmi/chains' +import { hoodi } from 'wagmi/chains' -const mordor = defineChain({ - id: 63, - name: 'Mordor', - nativeCurrency: { - decimals: 18, - name: 'METC', - symbol: 'mETC', - }, - rpcUrls: { - default: { http: ['https://rpc.mordor.etccooperative.org'] }, - }, - blockExplorers: { - default: { - name: 'Blockscout', - url: 'https://etc-mordor.blockscout.com/' - }, - }, -}) +// const mordor = defineChain({ +// id: 63, +// name: 'Mordor', +// nativeCurrency: { +// decimals: 18, +// name: 'METC', +// symbol: 'mETC', +// }, +// rpcUrls: { +// default: { http: ['https://rpc.mordor.etccooperative.org'] }, +// }, +// blockExplorers: { +// default: { +// name: 'Blockscout', +// url: 'https://etc-mordor.blockscout.com/' +// }, +// }, +// }) export const config = createConfig({ - chains: [sepolia, hoodi, mordor], + chains: [hoodi], transports: { - [sepolia.id]: fallback([ - http('https://ethereum-sepolia-rpc.publicnode.com'), - http('https://1rpc.io/sepolia'), - http('https://eth-sepolia.public.blastapi.io'), - http('https://0xrpc.io/sep'), - http('https://api.zan.top/eth-sepolia'), - http('https://eth-sepolia.api.onfinality.io/public'), - http('https://sepolia.drpc.org') - ]), + // [sepolia.id]: fallback([ + // http('https://ethereum-sepolia-rpc.publicnode.com'), + // http('https://1rpc.io/sepolia'), + // http('https://eth-sepolia.public.blastapi.io'), + // http('https://0xrpc.io/sep'), + // http('https://api.zan.top/eth-sepolia'), + // http('https://eth-sepolia.api.onfinality.io/public'), + // http('https://sepolia.drpc.org') + // ]), [hoodi.id]: fallback([ http('https://rpc.hoodi.ethpandaops.io'), + http('https://rpc.sentio.xyz/hoodi'), http('https://0xrpc.io/hoodi'), ]), - [mordor.id]: fallback([ - http('https://rpc.mordor.etccooperative.org'), - http('https://geth-mordor.etc-network.info'), - ]) + // [mordor.id]: fallback([ + // http('https://rpc.mordor.etccooperative.org'), + // http('https://geth-mordor.etc-network.info'), + // ]) }, }) diff --git a/src/constants/addresses.js b/src/constants/addresses.js index 26247eb..fbd8333 100644 --- a/src/constants/addresses.js +++ b/src/constants/addresses.js @@ -4,25 +4,25 @@ export const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; export const STAKING_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506", - [NetworkId.TESTNET_HOODI]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506", + [NetworkId.TESTNET_HOODI]: "0x5d659FDEFc9c1552A3a53f661Cf4F52b0d7dDb40", [NetworkId.TESTNET_MORDOR]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506", }; export const BOND_DEPOSITORY_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0", - [NetworkId.TESTNET_HOODI]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0", + [NetworkId.TESTNET_HOODI]: "0x4105D6b48f0fEfA8033b8B01d89972c2CBd787bD", [NetworkId.TESTNET_MORDOR]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0", }; export const DAO_TREASURY_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E", - [NetworkId.TESTNET_HOODI]: "0x05D797f9F34844594C956da58f1785997397f02E", + [NetworkId.TESTNET_HOODI]: "0xeA5113bF7e0af48B6D0Cf1a4B1e4dD0184708a09", [NetworkId.TESTNET_MORDOR]: "0x05D797f9F34844594C956da58f1785997397f02E", }; -export const FTSO_DAI_LP_ADDRESSES = { +export const FTSO_RESERVE_LP_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D", - [NetworkId.TESTNET_HOODI]: "0x32388605b5E83Ea79CDdC479AA9939DBCF98f59D", + [NetworkId.TESTNET_HOODI]: "0xf3F2cc4DaB20F09eee0d07d8456924927ceA1847", [NetworkId.TESTNET_MORDOR]: "0x53B13C4722081c405ce25c7A7629fC326A49a469", }; @@ -46,43 +46,43 @@ export const WETH_ADDRESSES = { export const GHST_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392", - [NetworkId.TESTNET_HOODI]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392", + [NetworkId.TESTNET_HOODI]: "0x594a5CD093859B3FD48aA901d86F71a7D537834a", [NetworkId.TESTNET_MORDOR]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392", }; export const STNK_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7", - [NetworkId.TESTNET_HOODI]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7", + [NetworkId.TESTNET_HOODI]: "0x7a7d0F214b8C03bD962B88AC0a7F543085aB817B", [NetworkId.TESTNET_MORDOR]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7", }; export const FTSO_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A", - [NetworkId.TESTNET_HOODI]: "0x7ebd1224D36d64eA09312073e60f352d1383801A", + [NetworkId.TESTNET_HOODI]: "0x07427B220934B3421223969eA61a247bFA37cF03", [NetworkId.TESTNET_MORDOR]: "0x7ebd1224D36d64eA09312073e60f352d1383801A", }; export const DISTRIBUTOR_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544", - [NetworkId.TESTNET_HOODI]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544", + [NetworkId.TESTNET_HOODI]: "0xe503d65C9574bD3C18c8d725cD346eCBFAE24b4d", [NetworkId.TESTNET_MORDOR]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544", }; export const GHOST_GOVERNANCE_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1", - [NetworkId.TESTNET_HOODI]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010", + [NetworkId.TESTNET_HOODI]: "0x0000000000000000000000000000000000000000", [NetworkId.TESTNET_MORDOR]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010", }; export const BONDING_CALCULATOR_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3", - [NetworkId.TESTNET_HOODI]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3", + [NetworkId.TESTNET_HOODI]: "0xf13A5319aD8071e4dcE7776728Ef9Cc221A5c098", [NetworkId.TESTNET_MORDOR]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3", } export const GATEKEEPER_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0xd735cA07984a16911222c08411A80e24EB38869B", - [NetworkId.TESTNET_HOODI]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4", + [NetworkId.TESTNET_HOODI]: "0xc9D5f50e17eeEAF39E0D0cDbbcF4B04FA4A4Bbd6", [NetworkId.TESTNET_MORDOR]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4", } diff --git a/src/containers/Bond/BondModal.jsx b/src/containers/Bond/BondModal.jsx index 1409d08..2503817 100644 --- a/src/containers/Bond/BondModal.jsx +++ b/src/containers/Bond/BondModal.jsx @@ -1,13 +1,15 @@ -import { ArrowBack } from "@mui/icons-material"; -import { Box, Link, Skeleton, Typography } from "@mui/material"; +import { Box, Container, Skeleton, Typography, useMediaQuery } from "@mui/material"; import { useEffect, useState, useMemo, useCallback } from "react"; -import { Link as RouterLink, useLocation, useParams, useNavigate } from "react-router-dom"; +import { useLocation, useParams, useNavigate } from "react-router-dom"; import { useAccount, useSwitchChain } from "wagmi"; import { isAddress } from "viem"; import ReactGA from "react-ga4"; +import Countdown from "../../components/Countdown/Countdown"; +import Paper from "../../components/Paper/Paper"; import PageTitle from "../../components/PageTitle/PageTitle"; import Metric from "../../components/Metric/Metric"; +import MetricCollection from "../../components/Metric/MetricCollection"; import TokenStack from "../../components/TokenStack/TokenStack"; import BondPrice from "./components/BondPrice"; @@ -40,6 +42,9 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => { const { network } = useParams(); const { pathname } = useLocation(); + const isSmallScreen = useMediaQuery("(max-width: 650px)"); + const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); + const [isSettingsOpen, setSettingsOpen] = useState(false); const { getStorageValue, setStorageValue } = useLocalStorage(); @@ -102,17 +107,9 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => { return ( - - - - - Back - - - - { /> - {preparedQuoteToken.name} + {preparedQuoteToken.name} Bond @@ -139,30 +136,45 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => { onDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)} /> - - - ) + + + + Bond Expires  + + + } - /> - } - /> - } - /> - + > + + )} + /> + } + /> + } + /> + + + - + { + const theme = useTheme(); + return ( + Confirm Bond Purchase} + onClose={!isPending && handleConfirmClose} + > + + I confirm the bond purchase with a negative discount + + + {discount}% + + + + Confirm + + + + ) +} + +export const BondConfirmModal = ({ chainId, bond, slippage, @@ -51,6 +77,8 @@ const BondConfirmModal = ({ const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false); const [isPending, setIsPending] = useState(false); + const { tryStoreBondInHistory } = useLocalStorage(); + useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]); const AcknowledgeWarmupCheckbox = () => { @@ -104,21 +132,19 @@ const BondConfirmModal = ({ const one = BigInt(shares * 100); const floatSlippage = slippage === "" ? 0 : parseFloat(slippage); const bigIntSlippage = one + BigInt(Math.round(floatSlippage * shares)); - const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one; - const referral = import.meta.env.VITE_APP_REFERRAL_ADDRESS; - await purchaseBond({ + const txHash = await purchaseBond({ chainId, bondId: bond.id, amount: spendAmountValue._value.toBigInt(), maxPrice, user: recipientAddress, sender, - referral, isNative }); + tryStoreBondInHistory(chainId, sender, txHash); setIsPending(false); handleConfirmCloseMaster(); } @@ -212,5 +238,3 @@ const BondConfirmModal = ({ ); }; - -export default BondConfirmModal; diff --git a/src/containers/Bond/components/BondInputArea.jsx b/src/containers/Bond/components/BondInputArea.jsx index eccc359..f6a251b 100644 --- a/src/containers/Bond/components/BondInputArea.jsx +++ b/src/containers/Bond/components/BondInputArea.jsx @@ -1,7 +1,8 @@ import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material"; -import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material"; +import { Box, Container, Checkbox, FormControlLabel, Typography, useMediaQuery } from "@mui/material"; import { useState, useMemo, useCallback } from "react"; import { useLocation } from "react-router-dom"; +import SettingsIcon from '@mui/icons-material/Settings'; import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses"; import { shorten, formatNumber, formatCurrency } from "../../../helpers"; @@ -9,10 +10,11 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber"; import BondDiscount from "./BondDiscount"; import BondVesting from "./BondVesting"; -import BondConfirmModal from "./BondConfirmModal"; +import { BondConfirmModal, DiscountConfirmModal } from "./BondConfirmModal"; import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard"; import { PrimaryButton } from "../../../components/Button"; +import GhostStyledIcon from "../../../components/Icon/GhostIcon"; import SwapCard from "../../../components/Swap/SwapCard"; import SwapCollection from "../../../components/Swap/SwapCollection"; import TokenStack from "../../../components/TokenStack/TokenStack"; @@ -33,6 +35,7 @@ const BondInputArea = ({ preparedQuoteToken, connect }) => { + const isSmallScreen = useMediaQuery("(max-width: 650px)"); const isSemiSmallScreen = useMediaQuery("(max-width: 480px)"); const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); const { pathname } = useLocation(); @@ -45,7 +48,9 @@ const BondInputArea = ({ const [amount, setAmount] = useState(""); const [checked, setChecked] = useState(false); + const [discountConfirmed, setDiscountConfirmed] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); + const [discountConfirmOpen, setDiscountConfirmOpen] = useState(false); const parsedAmount = useMemo(() => { return new DecimalBigNumber(amount, bond.quoteToken.decimals); @@ -60,12 +65,24 @@ const BondInputArea = ({ return new DecimalBigNumber("0").gt(bond.discount); }, [bond]); + const setModalOpen = () => { + if (showDisclaimer && !discountConfirmOpen && !discountConfirmed) setDiscountConfirmOpen(true); + else setConfirmOpen(true); + } + const handleConfirmClose = () => { setAmount(""); setConfirmOpen(false); + setDiscountConfirmOpen(false); refetch(); } + const confirmDiscount = () => { + setConfirmOpen(false); + setDiscountConfirmOpen(false); + setDiscountConfirmed(true); + } + const setMax = useCallback(() => { if (!balance) return; @@ -95,143 +112,32 @@ const BondInputArea = ({ }, [amountInBaseToken, baseTokenString, balance, parsedAmount]); return ( - - - - - - } - tokenName={preparedQuoteToken.name} - info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)} - endString="Max" - endStringOnClick={setMax} - value={amount} - onChange={event => setAmount(event.currentTarget.value)} - inputProps={{ "data-testid": "fromInput" }} - />} - LowerSwapCard={ - - } - tokenName={bond.baseToken.name} - value={amountInBaseToken.toString({ decimals: 9 })} - inputProps={{ "data-testid": "toInput" }} - /> - } - /> - + + + } + headerContent={ + Bond + } + > + confirmDiscount()} + handleConfirmClose={() => handleConfirmClose()} + discount={bond + ? formatNumber(Number(bond.discount.mul(new DecimalBigNumber("100"))).toString(), 2) + : "Unknown" + } + /> - - First time bonding {bond.quoteToken.name}?
Please approve ghostDAO to use your{" "} - {bond.quoteToken.name} for bonding. - - } - > - {showDisclaimer && ( - setChecked(event.target.checked)} - icon={} - checkedIcon={} - /> - } - label="I understand that I'm buying a negative discount bond" - /> - )} - setConfirmOpen(true)} - > - Bond - -
- - - - - {formatCurrency(amountInBaseToken, formatDecimals, ftsoSymbol)} - {" "} - {!!currentIndex && ( - - (≈{formatCurrency(amountInBaseToken.div(currentIndex), formatDecimals, ghstSymbol)}) - - )} - - } - tooltip={`Total amount of ${ftsoSymbol} you will receive from this bond purchase`} - /> - - - {bond.baseToken.tokenAddress.toUpperCase() === bond.quoteToken.quoteTokenAddress.toUpperCase() - ? `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)}` - : `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)} (≈${formatCurrency(baseTokenString.div(currentIndex), formatDecimals, ghstSymbol)})`} - - } - /> - - } - tooltip={`The discount (or premium) between ${ftsoSymbol} market price and bond price. Higher discount = better deal for bond buyers.`} - /> - - } - tooltip={"Time until bond fully vests and becomes claimable. Vesting period aligns bond buyer with the protocol by encouraging longer-term holding."} - /> - - {recipientAddress !== address && ( - - )} - - -
-
handleConfirmClose()} /> -
- ); - }; + + + + } + tokenName={preparedQuoteToken.name} + info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)} + endString="Max" + endStringOnClick={setMax} + value={amount} + onChange={event => setAmount(event.currentTarget.value)} + inputProps={{ "data-testid": "fromInput" }} + />} + LowerSwapCard={ + + } + tokenName={bond.baseToken.name} + value={amountInBaseToken.toString({ decimals: 9 })} + inputProps={{ "data-testid": "toInput" }} + /> + } + /> + + + + {formatCurrency(amountInBaseToken, formatDecimals, ftsoSymbol)} + {" "} + {!!currentIndex && ( + + (≈{formatCurrency(amountInBaseToken.div(currentIndex), formatDecimals, ghstSymbol)}) + + )} + + } + tooltip={`Total amount of ${ftsoSymbol} you will receive from this bond purchase`} + /> + + + {bond.baseToken.tokenAddress.toUpperCase() === bond.quoteToken.quoteTokenAddress.toUpperCase() + ? `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)}` + : `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)} (≈${formatCurrency(baseTokenString.div(currentIndex), formatDecimals, ghstSymbol)})`} + + } + /> + + } + tooltip={"Time until bond fully vests and becomes claimable. Vesting period aligns bond buyer with the protocol by encouraging longer-term holding."} + /> + + {recipientAddress !== address && ( + {shorten(recipientAddress)}} + tooltip="The address that will receive the bond. To change it from your connected account, update the settings via the gear icon." + /> + )} + + + + First time bonding {bond.quoteToken.name}?
Please approve ghostDAO to use your{" "} + {bond.quoteToken.name} for bonding. + + } + > + setModalOpen(true)} + > + Bond + +
+
+ +
+ ) +}; export default BondInputArea; diff --git a/src/containers/Dex/PoolContainer.jsx b/src/containers/Dex/PoolContainer.jsx index 205904b..e2289a6 100644 --- a/src/containers/Dex/PoolContainer.jsx +++ b/src/containers/Dex/PoolContainer.jsx @@ -18,6 +18,7 @@ import { addLiquidity, addLiquidityETH, } from "../../hooks/uniswapv2"; +import { useLocalStorage } from "../../hooks/localstorage"; const PoolContainer = ({ chainSymbol, @@ -37,6 +38,7 @@ const PoolContainer = ({ }) => { const theme = useTheme(); const isSmallScreen = useMediaQuery("(max-width: 456px)"); + const { tryStoreDexInHistory } = useLocalStorage(); const [isPending, setIsPending] = useState(false); const [amountTop, setAmountTop] = useState(""); @@ -191,12 +193,16 @@ const PoolContainer = ({ deadline, } + let txHash; + if (topIsNative || bottomIsNative) { - await addLiquidityETH(params) + txHash = await addLiquidityETH(params) } else { - await addLiquidity(params); + txHash = await addLiquidity(params); } + if (txHash) tryStoreDexInHistory(chainId, address, txHash); + await balanceRefetchTop(); await balanceRefetchBottom(); await pairAddressRefetch(); @@ -215,7 +221,7 @@ const PoolContainer = ({ }, [pairAddress]) return ( - + diff --git a/src/containers/Dex/SwapContainer.jsx b/src/containers/Dex/SwapContainer.jsx index 856ded1..4a90579 100644 --- a/src/containers/Dex/SwapContainer.jsx +++ b/src/containers/Dex/SwapContainer.jsx @@ -21,6 +21,7 @@ import { swapExactETHForTokens, swapExactTokensForETH, } from "../../hooks/uniswapv2"; +import { useLocalStorage } from "../../hooks/localstorage"; import { EMPTY_ADDRESS } from "../../constants/addresses"; @@ -45,6 +46,7 @@ const SwapContainer = ({ }) => { const theme = useTheme(); const isSmallScreen = useMediaQuery("(max-width: 456px)"); + const { tryStoreDexInHistory } = useLocalStorage(); const [isPending, setIsPending] = useState(false); const [amountBottom, setAmountBottom] = useState(""); @@ -173,10 +175,12 @@ const SwapContainer = ({ const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals))); const amountBMin = amountBDesired * bigIntSlippage / one; + let txHash; + if (isWrapping) { - await depositNative(chainId, address, amountADesired); + txHash = await depositNative(chainId, address, amountADesired); } else if (isUnwrapping) { - await withdrawWeth(chainId, address, amountADesired); + txHash = await withdrawWeth(chainId, address, amountADesired); } else { const params = { chainId, @@ -190,14 +194,16 @@ const SwapContainer = ({ }; if (topIsNative) { - await swapExactETHForTokens(params) + txHash = await swapExactETHForTokens(params) } else if (bottomIsNative) { - await swapExactTokensForETH(params) + txHash = await swapExactTokensForETH(params) } else { - await swapExactTokensForTokens(params); + txHash = await swapExactTokensForTokens(params); } } + if (txHash) tryStoreDexInHistory(chainId, address, txHash); + await balanceRefetchTop(); await balanceRefetchBottom(); await pairReservesRefetch(); @@ -207,11 +213,11 @@ const SwapContainer = ({ } return ( - + diff --git a/src/containers/Governance/components/functions/CreateBond.jsx b/src/containers/Governance/components/functions/CreateBond.jsx index b84a344..4d5a6e2 100644 --- a/src/containers/Governance/components/functions/CreateBond.jsx +++ b/src/containers/Governance/components/functions/CreateBond.jsx @@ -12,7 +12,7 @@ import { ArgumentsWrapper, BooleanTrigger, ArgumentInput, ParsedCell } from "./i import { RESERVE_ADDRESSES, - FTSO_DAI_LP_ADDRESSES, + FTSO_RESERVE_LP_ADDRESSES, BOND_DEPOSITORY_ADDRESSES, } from "../../../../constants/addresses"; import { abi as DepositoryAbi } from "../../../../abi/GhostBondDepository.json"; @@ -114,7 +114,7 @@ export const CreateBondSteps = ({ nativeCurrency, ftsoSymbol, chainId, toInitial }, [step, tokenAddress, capacity, initialPrice, debtBuffer, depositInterval, tuneInterval]); const possibleTokens = [ - { value: FTSO_DAI_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` }, + { value: FTSO_RESERVE_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_RESERVE_LP_ADDRESSES[chainId])}` }, { value: RESERVE_ADDRESSES[chainId], symbol: reserveSymbol, label: `${reserveSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` }, ]; diff --git a/src/containers/History/DexViewModal.jsx b/src/containers/History/DexViewModal.jsx new file mode 100644 index 0000000..8370d5f --- /dev/null +++ b/src/containers/History/DexViewModal.jsx @@ -0,0 +1,191 @@ +import { useTransactionReceipt, useToken } from 'wagmi'; +import { decodeEventLog } from 'viem'; +import { useMemo } from "react"; + +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; +import { Box, Typography, Link } from "@mui/material"; + +import Token from "../../components/Token/Token"; +import Modal from "../../components/Modal/Modal"; +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import { PrimaryButton } from "../../components/Button"; + +import { abi as UniswapV2PairAbi } from "../../abi/UniswapV2Pair.json"; +import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; +import { formatNumber, shorten } from "../../helpers"; + +const TokenBox = ({ tokenAmount, tokenSymbol, decimals, chainTokenSymbol }) => { + return ( + + + + + {formatNumber( + new DecimalBigNumber(tokenAmount ? tokenAmount : 0n, decimals), + 5 + )} + + + ) +} + +const DexViewModal = ({ chainId, isOpen, close, config, historyRecord }) => { + if (historyRecord === null) return null; + + const { data: receipt, isLoading, error } = useTransactionReceipt({ + hash: historyRecord?.txHash, + query: { + enabled: !!historyRecord?.txHash && isOpen, + } + }); + + const allDecodedLogs = useMemo(() => { + if (!receipt?.logs) return []; + + return receipt.logs.map((log) => { + try { + const decoded = decodeEventLog({ + abi: UniswapV2PairAbi, + data: log.data, + topics: log.topics, + }); + + return { + eventName: decoded.eventName, + args: decoded.args, + address: log.address + }; + } catch (e) { + return null; + } + }).filter(Boolean); + }, [receipt]); + + const swapInfo = useMemo(() => { + const swapEvent = allDecodedLogs.find(l => l.eventName === 'Swap'); + if (!swapEvent) return null; + + const isZeroForOne = swapEvent.args.amount0In > 0n; + + const transferLogs = allDecodedLogs.filter(l => l.eventName === 'Transfer'); + + const tokenInAddress = isZeroForOne + ? transferLogs.find(t => t.args.value === swapEvent.args.amount0In)?.address + : transferLogs.find(t => t.args.value === swapEvent.args.amount1In)?.address; + + const tokenOutAddress = isZeroForOne + ? transferLogs.find(t => t.args.value === swapEvent.args.amount1Out)?.address + : transferLogs.find(t => t.args.value === swapEvent.args.amount0Out)?.address; + + return { + from: swapEvent.args.sender, + to: swapEvent.args.to, + tokenInAddress, + tokenOutAddress, + amountIn: isZeroForOne ? swapEvent.args.amount0In : swapEvent.args.amount1In, + amountOut: isZeroForOne ? swapEvent.args.amount1Out : swapEvent.args.amount0Out, + }; + }, [allDecodedLogs]); + + const { data: tokenIn } = useToken({ + address: swapInfo?.tokenInAddress, + scopeKey: `token-${swapInfo ? swapInfo.tokenInAddress : ""}-${chainId}`, + chainId + }); + + const { data: tokenOut } = useToken({ + address: swapInfo?.tokenOutAddress, + scopeKey: `token-${swapInfo ? swapInfo.tokenOutAddress : ""}-${chainId}`, + chainId + }); + + const chainInfo = useMemo(() => { + const client = config?.getClient(); + + return { + url: client?.chain?.blockExplorers?.default?.url, + symbol: client?.chain?.nativeCurrency?.symbol, + }; + }, [config]); + + return ( + + {swapInfo && "DEX Swap"} + + } + onClose={() => close()} + > + + + + {tokenIn ? tokenIn.symbol : "Unknown"} Sender + + {swapInfo ? shorten(swapInfo.from, 12, -10) : ""} + + + + + {tokenOut ? tokenOut.symbol : "Unknown"} Receiver + + {swapInfo ? shorten(swapInfo.to, 12, -10) : ""} + + + + + + + + + + + + + + View Transaction Details + + + + ) +} + +export default DexViewModal; diff --git a/src/containers/History/EraseModal.jsx b/src/containers/History/EraseModal.jsx new file mode 100644 index 0000000..d0f5dd0 --- /dev/null +++ b/src/containers/History/EraseModal.jsx @@ -0,0 +1,23 @@ +import { Box, Typography } from "@mui/material"; + +import Modal from "../../components/Modal/Modal"; +import { PrimaryButton } from "../../components/Button"; + +const EraseModal = ({ isOpen, close, submit, isAll }) => { + return ( + close()} + > + + {`Erasing ${isAll ? "all records" : "this record"} is permanent, and cannot be recovered.`} + Erase {isAll ? "All Records" : ""} + + + ) +} + +export default EraseModal; diff --git a/src/containers/History/History.jsx b/src/containers/History/History.jsx new file mode 100644 index 0000000..701d623 --- /dev/null +++ b/src/containers/History/History.jsx @@ -0,0 +1,401 @@ +import { + Box, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + Container, + FormControlLabel, + FormControl, + Checkbox, + Link, + useMediaQuery, + useTheme, +} from "@mui/material"; + +import { useLocation } from "react-router-dom"; +import { useEffect, useState, useMemo, useCallback } from "react"; +import ReactGA from "react-ga4"; +import SettingsIcon from '@mui/icons-material/Settings'; + +import Modal from "../../components/Modal/Modal"; +import Paper from "../../components/Paper/Paper"; +import PageTitle from "../../components/PageTitle/PageTitle"; +import TokenStack from "../../components/TokenStack/TokenStack"; +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import Chip from "../../components/Chip/Chip"; + +import { PrimaryButton, TertiaryButton } from "../../components/Button"; +import { Tab, Tabs } from "../../components/Tabs/Tabs"; + +import { useTokenSymbol } from "../../hooks/tokens"; +import { useLocalStorage } from "../../hooks/localstorage"; +import { shorten } from "../../helpers"; + +import EraseModal from "./EraseModal"; +import SettingsModal from "./SettingsModal"; +import DexViewModal from "./DexViewModal"; +import StakingViewModal from "./StakingViewModal"; + +const HistoryItem = ({ item, chainId, config, currentTime, eraseRecord, setViewModalOpen, isSmallScreen }) => { + const theme = useTheme(); + const isVerySmallScreen = useMediaQuery("(max-width: 450px)"); + + const localString = new Date(item.submitted).toLocaleString(); + const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO") + + const chainInfo = useMemo(() => { + const client = config?.getClient(); + return { + url: client?.chain?.blockExplorers?.default?.url, + symbol: client?.chain?.nativeCurrency?.symbol, + }; + }, [config]); + + const chipStatus = useMemo(() => { + const delay = 3600000; // useEvmNetwork({ evmChainId: chainId }) + const width = isVerySmallScreen ? "21px" : "120px" + + if (item.submitted + delay > currentTime) { + return + } + + // darkGray for final + + return + }, [item, currentTime, isVerySmallScreen]) + + // TODO: put correct values into TokenStack based on type + + if (isSmallScreen || isVerySmallScreen) { + return ( + <> + + + + + + {localString} + + {shorten(item.txHash)} + + + + + {chipStatus} + + + + Erase + View + + + + ) + } + + return ( + + + + + + {shorten(item.txHash, 9, -7)} + + + + + + {chipStatus} + + + + {localString} + + + + + Erase + View + + + + ) +} + +const HistoryList = ({ chainId, storedTransactions, historyType, config, eraseRecord }) => { + const isSmallScreen = useMediaQuery("(max-width: 900px)"); + + const [currentTime, setCurrentTime] = useState(Date.now()); + const [viewModalOpen, setViewModalOpen] = useState(null); + + const { + BRIDGE_IN, + BRIDGE_OUT, + DEX_ACTION, + STAKE_ACTION, + BOND_ACTION + } = useLocalStorage(); + + useEffect(() => { + const intervalId = setInterval(() => { + setCurrentTime(Date.now()); + }, 5000); + return () => clearInterval(intervalId); + }, []) + + const historyContainer = useMemo(() => { + if (isSmallScreen) { + return ( + + {storedTransactions?.map((item, i) => { + return ( + eraseRecord(i)} + setViewModalOpen={() => setViewModalOpen(i)} + isSmallScreen={isSmallScreen} + /> + ) + })} + + ) + } else { + return ( + + + + + Transaction + Status + Submitted + + + + + {storedTransactions?.map((item, i) => { + return ( + eraseRecord(i)} + setViewModalOpen={() => setViewModalOpen(i)} + isSmallScreen={isSmallScreen} + /> + ) + })} + +
+
+ ) + } + }, [isSmallScreen, storedTransactions, config]); + + return ( + <> + {viewModalOpen !== null && historyType === DEX_ACTION && ( + setViewModalOpen(null)} + isOpen={true} + historyRecord={storedTransactions.at(viewModalOpen)} + /> + )} + + {viewModalOpen !== null && historyType === STAKE_ACTION && ( + setViewModalOpen(null)} + isOpen={true} + historyRecord={storedTransactions.at(viewModalOpen)} + /> + )} + + {historyContainer} + + ) +} + +const History = ({ chainId, config, tryConnectInjected, address }) => { + const isSmallScreen = useMediaQuery("(max-width: 650px)"); + const isSemiSmallScreen = useMediaQuery("(max-width: 745px)"); + const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); + + const [historyTypeIndex, setHistoryTypeIndex] = useState(0); + const [erasingIndex, setErasingIndex] = useState(null); + const [settingsOpen, setSettingsOpen] = useState(false); + const [erasingOpen, setErasingOpen] = useState(false); + + const location = useLocation(); + + useEffect(() => { + ReactGA.send({ hitType: "pageview", page: location.pathname }); + }, [location]); + + const { + POSSIBLE_HISTORY_TYPES, + getHistoryTypes, + setHistoryTypes, + getStorageValue, + setStorageValue + } = useLocalStorage(); + + const [storedHistoryTypes, setStoredHistoryTypes] = useState(() => + getHistoryTypes(chainId, address) + ) + const [storedTransactions, setStoredTransactions] = useState(() => + getStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []) + ); + + useEffect(() => { + setStoredTransactions(getStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], [])); + }, [chainId, address, historyTypeIndex]); + + const handleHistoryTypesChange = useCallback((values) => { + setHistoryTypes(chainId, address, values); + setStoredHistoryTypes(getHistoryTypes(chainId, address)); + }, [chainId, address]); + + const handleConfirmErase = useCallback(() => { + if (erasingIndex !== null) { + const updated = storedTransactions.filter((_, index) => index !== erasingIndex); + setStoredTransactions(updated); + setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], updated); + } else { + setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []); + setStoredTransactions([]) + } + setErasingOpen(false); + setErasingIndex(null); + }, [storedTransactions, erasingIndex, chainId, address, historyTypeIndex]) + + return ( + + + + { + setErasingOpen(false); + setErasingIndex(null); + }} + submit={handleConfirmErase} + isAll={erasingIndex === null} + /> + + setSettingsOpen(false)} + onChange={handleHistoryTypesChange} + selectedValues={storedHistoryTypes} + possibleValues={POSSIBLE_HISTORY_TYPES} + /> + + + + setSettingsOpen(true)} + /> + + } + headerContent={ + + { + const index = storedHistoryTypes.indexOf(selectedValue); + if (index !== -1) setHistoryTypeIndex(index); + }} + TabIndicatorProps={{ style: { display: "none" } }} + > + {storedHistoryTypes.map((type, i) => { + return ( + + ) + })} + + + setErasingOpen(true)} + sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }} + > + Erase All Records + + + } + > + { + setErasingIndex(index); + setErasingOpen(true); + }} + historyType={storedHistoryTypes[historyTypeIndex]} + storedTransactions={storedTransactions} + /> + + + +
+ ); +} + +export default History; diff --git a/src/containers/History/SettingsModal.jsx b/src/containers/History/SettingsModal.jsx new file mode 100644 index 0000000..5c2fd91 --- /dev/null +++ b/src/containers/History/SettingsModal.jsx @@ -0,0 +1,49 @@ +import { Box, FormControlLabel, Checkbox, Typography } from "@mui/material"; + +import Modal from "../../components/Modal/Modal"; +import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material"; + +const SettingsModal = ({ isOpen, close, submit, selectedValues, possibleValues, onChange }) => { + const handleCheckboxChange = (type, isChecked) => { + const updatedTypes = possibleValues.filter(v => { + if (v === type) return isChecked + return selectedValues.includes(v) + }) + onChange(updatedTypes); + }; + + return ( + close()} + > + + {possibleValues.map((type, i) => { + const isChecked = selectedValues.includes(type); + return ( + handleCheckboxChange(type, event.target.checked)} + icon={} + checkedIcon={} + /> + } + label={{isChecked ? "Disable" : "Enable"} {type} History Storage} + /> + ) + })} + + + ) +} + +export default SettingsModal; diff --git a/src/containers/History/StakingViewModal.jsx b/src/containers/History/StakingViewModal.jsx new file mode 100644 index 0000000..1a32f87 --- /dev/null +++ b/src/containers/History/StakingViewModal.jsx @@ -0,0 +1,159 @@ +import { useTransactionReceipt } from 'wagmi'; +import { decodeEventLog } from 'viem'; +import { useMemo } from "react"; + +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; +import { Box, Typography, Link } from "@mui/material"; + +import Token from "../../components/Token/Token"; +import Modal from "../../components/Modal/Modal"; +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import { PrimaryButton } from "../../components/Button"; + +import { abi as StakingAbi } from "../../abi/GhostStaking.json"; +import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; +import { formatNumber, shorten } from "../../helpers"; + +import { useTokenSymbol } from "../../hooks/tokens"; +import { getTokenDecimals } from "../../hooks/helpers"; + +const TokenBox = ({ tokenAmount, tokenSymbol, decimals, chainTokenSymbol }) => { + return ( + + + + + {tokenSymbol} + + {formatNumber( + new DecimalBigNumber(tokenAmount ? tokenAmount : 0n, decimals), + 5 + )} + + + ) +} + +const StakingViewModal = ({ chainId, isOpen, close, config, historyRecord }) => { + if (historyRecord === null) return null; + + const { data: receipt, isLoading, error } = useTransactionReceipt({ + hash: historyRecord?.txHash, + query: { + enabled: !!historyRecord?.txHash && isOpen, + } + }); + + const allDecodedLogs = useMemo(() => { + if (!receipt?.logs) return []; + + return receipt.logs.map((log) => { + try { + const decoded = decodeEventLog({ + abi: StakingAbi, + data: log.data, + topics: log.topics, + }); + + return { + eventName: decoded.eventName, + args: decoded.args, + address: log.address + }; + } catch (e) { + return null; + } + }).filter(Boolean); + }, [receipt]); + + const stakeInfo = useMemo(() => { + return allDecodedLogs.find(l => l.eventName === 'Staked'); + }, [allDecodedLogs]); + + const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); + const ftsoDecimals = getTokenDecimals("FTSO"); + + const chainInfo = useMemo(() => { + const client = config?.getClient(); + + return { + url: client?.chain?.blockExplorers?.default?.url, + symbol: client?.chain?.nativeCurrency?.symbol, + }; + }, [config]); + + return ( + + {stakeInfo && "Staked"} + + } + onClose={() => close()} + > + + + + + + + Receiver + + {shorten(stakeInfo.args.receiver, 9, -7)} + + + + Amount + {formatNumber( + new DecimalBigNumber(stakeInfo ? stakeInfo.args.amount : 0n, ftsoDecimals), + 5 + )} {ftsoSymbol} + + + + + + View Transaction Details + + + + ) +} + +export default StakingViewModal; diff --git a/src/containers/Stake/StakeContainer.jsx b/src/containers/Stake/StakeContainer.jsx index 00802dc..5a62505 100644 --- a/src/containers/Stake/StakeContainer.jsx +++ b/src/containers/Stake/StakeContainer.jsx @@ -78,7 +78,6 @@ export const StakeContainer = ({ chainId, address, config, connect }) => { Rebase  -
} > diff --git a/src/containers/Stake/components/StakeConfirmationModal.jsx b/src/containers/Stake/components/StakeConfirmationModal.jsx index 0a17e97..dc3052f 100644 --- a/src/containers/Stake/components/StakeConfirmationModal.jsx +++ b/src/containers/Stake/components/StakeConfirmationModal.jsx @@ -12,6 +12,7 @@ import Token from "../../../components/Token/Token"; import { PrimaryButton, SecondaryButton } from "../../../components/Button"; import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard"; +import { useLocalStorage } from "../../../hooks/localstorage"; import { useWarmupLength, stake, unstake, wrap, unwrap } from "../../../hooks/staking"; import { formatNumber } from "../../../helpers"; @@ -22,6 +23,8 @@ const StakeConfirmationModal = (props) => { const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false); const [isPending, setIsPending] = useState(false); + const { tryStoreStakeInHistory } = useLocalStorage(); + useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]) const AcknowledgeWarmupCheckbox = () => { @@ -73,11 +76,12 @@ const StakeConfirmationModal = (props) => { setIsPending(true); const actionAmount = new DecimalBigNumber(props.amount, props.spendDecimals); let isRebase = false; + let txHash = null; switch (props.action.toUpperCase()) { case "STAKE": isRebase = props.bottomToken === props.stnkSymbol; - await stake( + txHash = await stake( props.chainId, props.address, actionAmount._value.toBigInt(), @@ -90,7 +94,7 @@ const StakeConfirmationModal = (props) => { break; case "UNSTAKE": isRebase = props.upperToken === props.stnkSymbol; - await unstake( + txHash = await unstake( props.chainId, props.address, actionAmount._value.toBigInt(), @@ -102,7 +106,7 @@ const StakeConfirmationModal = (props) => { ); break; case "WRAP": - await wrap( + txHash = await wrap( props.chainId, props.address, actionAmount._value.toBigInt(), @@ -110,7 +114,7 @@ const StakeConfirmationModal = (props) => { ); break; case "UNWRAP": - await unwrap( + txHash = await unwrap( props.chainId, props.address, actionAmount._value.toBigInt(), @@ -121,6 +125,8 @@ const StakeConfirmationModal = (props) => { console.log("Wrong action to be executed!"); } + if (txHash) tryStoreStakeInHistory(props.chainId, props.address, txHash) + setIsPending(false); props.onClose(); } @@ -156,7 +162,7 @@ const StakeConfirmationModal = (props) => { chainTokenName={props.chainSymbol} name={props.upperToken} /> - {props.upperToken} + {props.upperToken}
diff --git a/src/hooks/bonds/index.js b/src/hooks/bonds/index.js index 5983218..e924bfe 100644 --- a/src/hooks/bonds/index.js +++ b/src/hooks/bonds/index.js @@ -273,24 +273,26 @@ export const useNotes = (chainId, address) => { return { notes, refetch }; } -export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => { - const args = [bondId, amount, maxPrice, user, referral]; +export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, isNative }) => { + const args = [bondId, amount, maxPrice, user]; const messages = { replacedMsg: "Bond transaction was replaced. Wait for inclusion please.", successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`, errorMsg: "Bond transaction failed. Check logs for error detalization.", }; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, messages, abi: BondAbi, address: BOND_DEPOSITORY_ADDRESSES[chainId], functionName: "deposit", - account: user, + account: sender, value: isNative ? amount : undefined }); + + return txHash; } export const redeem = async ({ chainId, user, isGhst, indexes }) => { diff --git a/src/hooks/breakoutModal/index.jsx b/src/hooks/breakoutModal/index.jsx index 6f7ed1b..c2f8a8c 100644 --- a/src/hooks/breakoutModal/index.jsx +++ b/src/hooks/breakoutModal/index.jsx @@ -1,5 +1,4 @@ import { createContext, useContext, useState, useMemo } from "react"; -import { claim } from "../../hooks/staking"; const emptyFunction = () => {}; const BreakoutModalContext = createContext(); diff --git a/src/hooks/ghost/UnstableProvider.jsx b/src/hooks/ghost/UnstableProvider.jsx index 9f9a9ed..07ca64d 100644 --- a/src/hooks/ghost/UnstableProvider.jsx +++ b/src/hooks/ghost/UnstableProvider.jsx @@ -5,7 +5,7 @@ import { getObservableClient } from "@polkadot-api/observable-client" import useSWR from "swr" const MAX_BLOCK_TIMEOUT = 15000 -const DEFAULT_CHAIN_ID = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3" +const DEFAULT_CHAIN_ID = "0xcaf9c5ddccc7ba6f96149a6b8a81c26a3641742795cd5e7e0aee79bf3f4e77b7" const UnstableProvider = createContext(null) export const useUnstableProvider = () => useContext(UnstableProvider) @@ -39,11 +39,11 @@ export const UnstableProviderProvider = ({ children }) => { return undefined }, [accounts]) - const connectionState = useMemo(() => { + const connectionState = useMemo(async () => { if (!providerDetail) return 'no-extension'; if (!provider) return 'loading'; - const chains = provider.getChains(); + const chains = await provider.getChains(); if (chains[chainId]) return 'connected'; return 'wrong-network'; diff --git a/src/hooks/helpers.js b/src/hooks/helpers.js index 89dac6f..d1cabaa 100644 --- a/src/hooks/helpers.js +++ b/src/hooks/helpers.js @@ -6,7 +6,7 @@ import { FTSO_ADDRESSES, STNK_ADDRESSES, GHST_ADDRESSES, - FTSO_DAI_LP_ADDRESSES, + FTSO_RESERVE_LP_ADDRESSES, WETH_ADDRESSES, } from "../constants/addresses"; import { config } from "../config"; @@ -134,10 +134,10 @@ export const getTokenAddress = (chainId, name) => { address = GHST_ADDRESSES[chainId]; break; case "GDAI_FTSO": - address = FTSO_DAI_LP_ADDRESSES[chainId]; + address = FTSO_RESERVE_LP_ADDRESSES[chainId]; break; case "RESERVE_FTSO": - address = FTSO_DAI_LP_ADDRESSES[chainId]; + address = FTSO_RESERVE_LP_ADDRESSES[chainId]; break; case "WETH": address = WETH_ADDRESSES[chainId]; @@ -173,7 +173,7 @@ export const getTokenIcons = (chainId, address) => { case GHST_ADDRESSES[chainId]: icons = ["GHST"]; break; - case FTSO_DAI_LP_ADDRESSES[chainId]: + case FTSO_RESERVE_LP_ADDRESSES[chainId]: icons = ["WETH", "FTSO"]; break; default: @@ -185,7 +185,7 @@ export const getTokenIcons = (chainId, address) => { export const getChainTokenNames = (chainId, address) => { let icons = [undefined]; switch (address) { - case FTSO_DAI_LP_ADDRESSES[chainId]: + case FTSO_RESERVE_LP_ADDRESSES[chainId]: icons = [undefined, "WETH"]; break; case RESERVE_ADDRESSES[chainId]: @@ -199,7 +199,7 @@ export const getChainTokenNames = (chainId, address) => { export const getBondNameDisplayName = (chainId, tokenAddress, baseTokenSymbol) => { let stringValue = tokenNameConverter(chainId, "WETH") - if (tokenAddress.toUpperCase() === FTSO_DAI_LP_ADDRESSES[chainId].toUpperCase()) { + if (tokenAddress.toUpperCase() === FTSO_RESERVE_LP_ADDRESSES[chainId].toUpperCase()) { stringValue = `${baseTokenSymbol}-${stringValue} LP`; } return stringValue; @@ -214,7 +214,7 @@ export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => { purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/wrapper`; } break; - case FTSO_DAI_LP_ADDRESSES[chainId]: + case FTSO_RESERVE_LP_ADDRESSES[chainId]: purchaseUrl += `?pool=true&from=${RESERVE_ADDRESSES[chainId]}&to=${FTSO_ADDRESSES[chainId]}`; break; } @@ -248,6 +248,7 @@ export const executeOnChainTransaction = async ({ onReplaced: () => toast(messages.replacedMsg), chainId }); + // TODO: onReplaced toast.success(messages.successMsg); diff --git a/src/hooks/localstorage/index.jsx b/src/hooks/localstorage/index.jsx index 1f2dc8e..cb6dec8 100644 --- a/src/hooks/localstorage/index.jsx +++ b/src/hooks/localstorage/index.jsx @@ -3,6 +3,17 @@ import { createContext, useContext, useState, useEffect } from "react"; const LocalStorageContext = createContext(); export const useLocalStorage = () => useContext(LocalStorageContext); +const BRIDGE_IN = "Bridge-In"; +const BRIDGE_OUT = "Bridge-Out"; +const DEX_ACTION = "DEX"; +const STAKE_ACTION = "Stake"; +const BOND_ACTION = "Bond"; + +const POSSIBLE_HISTORY_TYPES = [BRIDGE_IN, BRIDGE_OUT, DEX_ACTION, STAKE_ACTION, BOND_ACTION]; +const DEFAULT_HISTORY_TYPES = [BRIDGE_IN, BRIDGE_OUT]; + +const HISTORY_PREFIX = "history" + export const LocalStorageProvider = ({ children }) => { const getStorageKey = (chainId, address, target) => { return `${chainId}:${address}:${target}`; @@ -19,8 +30,58 @@ export const LocalStorageProvider = ({ children }) => { localStorage.setItem(key, JSON.stringify(value)); } + const getHistoryTypes = (chainId, address) => { + const key = getStorageKey(chainId, address, HISTORY_PREFIX); + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : DEFAULT_HISTORY_TYPES; + } + + const setHistoryTypes = (chainId, address, value) => { + const key = getStorageKey(chainId, address, HISTORY_PREFIX); + localStorage.setItem(key, JSON.stringify(value)); + } + + const tryStoreInHistory = (chainId, address, target, txHash) => { + const historyTypes = getHistoryTypes(chainId, address); + if (!historyTypes.includes(target)) return; + + const key = getStorageKey(chainId, address, target); + const stored = localStorage.getItem(key); + const values = stored ? JSON.parse(stored) : []; + + const value = { txHash, submitted: Date.now() }; + const newValues = [value, ...values]; + localStorage.setItem(key, JSON.stringify(newValues)); + } + + const tryStoreBondInHistory = (chainId, address, txHash) => { + tryStoreInHistory(chainId, address, BOND_ACTION, txHash); + } + + const tryStoreStakeInHistory = (chainId, address, txHash) => { + tryStoreInHistory(chainId, address, STAKE_ACTION, txHash); + } + + const tryStoreDexInHistory = (chainId, address, txHash) => { + tryStoreInHistory(chainId, address, DEX_ACTION, txHash); + } + return ( - + {children} ) diff --git a/src/hooks/prices/index.js b/src/hooks/prices/index.js index 4579970..6cd4b13 100644 --- a/src/hooks/prices/index.js +++ b/src/hooks/prices/index.js @@ -5,7 +5,7 @@ import { useCurrentIndex, useGhostedSupply } from "../staking"; import { useUniswapV2PairReserves } from "../uniswapv2"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { - FTSO_DAI_LP_ADDRESSES, + FTSO_RESERVE_LP_ADDRESSES, RESERVE_ADDRESSES, FTSO_ADDRESSES, CEX_TICKERS, @@ -135,7 +135,7 @@ export const useReservePrice = (chainId) => { }; export const useFtsoPrice = (chainId) => { - const { reserves, tokens, refetch } = useUniswapV2PairReserves(chainId, FTSO_DAI_LP_ADDRESSES[chainId]); + const { reserves, tokens, refetch } = useUniswapV2PairReserves(chainId, FTSO_RESERVE_LP_ADDRESSES[chainId]); const reservePrice = useReservePrice(chainId); const reserveAddress = RESERVE_ADDRESSES[chainId]; diff --git a/src/hooks/staking/index.js b/src/hooks/staking/index.js index abe11df..ecbbc2e 100644 --- a/src/hooks/staking/index.js +++ b/src/hooks/staking/index.js @@ -181,7 +181,7 @@ export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSym successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warm-up period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`, errorMsg: "Staking transaction failed. Check logs for error detalization.", }; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, messages, @@ -190,6 +190,7 @@ export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSym address: STAKING_ADDRESSES[chainId], functionName: "stake", }); + return txHash; } export const unstake = async (chainId, account, amount, isTrigger, isRebase, ftsoSymbol, stnkSymbol, ghstSymbol) => { @@ -199,7 +200,7 @@ export const unstake = async (chainId, account, amount, isTrigger, isRebase, fts successMsg: `${isRebase ? stnkSymbol : ghstSymbol} tokens unstaked successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`, errorMsg: "Unstake transaction failed. Check logs for error detalization.", }; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, messages, @@ -208,6 +209,7 @@ export const unstake = async (chainId, account, amount, isTrigger, isRebase, fts address: STAKING_ADDRESSES[chainId], functionName: "unstake", }); + return txHash; } export const forfeit = async (chainId, account, ftsoSymbol) => { @@ -216,7 +218,7 @@ export const forfeit = async (chainId, account, ftsoSymbol) => { successMsg: `Tokens forfeited successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`, errorMsg: "Forfeit transaction failed. Check logs for error detalization.", }; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, messages, account, @@ -225,6 +227,7 @@ export const forfeit = async (chainId, account, ftsoSymbol) => { address: STAKING_ADDRESSES[chainId], functionName: "forfeit", }); + return txHash; } export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => { @@ -234,7 +237,7 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`, errorMsg: "Claim transaction failed. Check logs for error detalization.", }; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, messages, @@ -243,6 +246,7 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => address: STAKING_ADDRESSES[chainId], functionName: "claim", }); + return txHash; } export const breakout = async (chainId, account, receiver, amount) => { @@ -261,7 +265,6 @@ export const breakout = async (chainId, account, receiver, amount) => { address: STAKING_ADDRESSES[chainId], functionName: "breakout", }); - return txHash; } @@ -281,6 +284,7 @@ export const unwrap = async (chainId, account, amount, stnkSymbol) => { address: STAKING_ADDRESSES[chainId], functionName: "unwrap", }); + return txHash; } export const wrap = async (chainId, account, amount, ghstSymbol) => { @@ -299,6 +303,7 @@ export const wrap = async (chainId, account, amount, ghstSymbol) => { address: STAKING_ADDRESSES[chainId], functionName: "wrap", }); + return txHash; } export const ghost = async (chainId, account, receiver, amount) => { @@ -317,6 +322,5 @@ export const ghost = async (chainId, account, receiver, amount) => { address: STAKING_ADDRESSES[chainId], functionName: "ghost", }); - return txHash; } diff --git a/src/hooks/uniswapv2/UniswapV2Router.js b/src/hooks/uniswapv2/UniswapV2Router.js index 488674b..05c359f 100644 --- a/src/hooks/uniswapv2/UniswapV2Router.js +++ b/src/hooks/uniswapv2/UniswapV2Router.js @@ -33,7 +33,7 @@ export const swapExactETHForTokens = async ({ deadline ]; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, abi: RouterAbi, @@ -43,6 +43,8 @@ export const swapExactETHForTokens = async ({ messages: swapMessages, value: amountADesired, }); + + return txHash; } export const swapExactTokensForETH = async ({ @@ -63,7 +65,7 @@ export const swapExactTokensForETH = async ({ deadline ]; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, abi: RouterAbi, @@ -72,6 +74,8 @@ export const swapExactTokensForETH = async ({ account: address, messages: swapMessages, }); + + return txHash; } export const swapExactTokensForTokens = async ({ @@ -92,7 +96,7 @@ export const swapExactTokensForTokens = async ({ deadline ]; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, abi: RouterAbi, @@ -101,6 +105,8 @@ export const swapExactTokensForTokens = async ({ account: address, messages: swapMessages, }); + + return txHash; } export const addLiquidity = async ({ @@ -126,7 +132,7 @@ export const addLiquidity = async ({ deadline ]; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, abi: RouterAbi, @@ -135,6 +141,8 @@ export const addLiquidity = async ({ account: address, messages: addMessages, }); + + return txHash; } export const addLiquidityETH = async ({ @@ -172,7 +180,7 @@ export const addLiquidityETH = async ({ deadline ]; - await executeOnChainTransaction({ + const txHash = await executeOnChainTransaction({ chainId, args, abi: RouterAbi, @@ -182,4 +190,6 @@ export const addLiquidityETH = async ({ messages: addMessages, value: amountETHDesired, }); + + return txHash; }