diff --git a/package.json b/package.json index 24afc1d..56e005a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.5.9", + "version": "0.5.10", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.jsx b/src/App.jsx index d7a455b..145201c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,7 +14,7 @@ import Sidebar from "./components/Sidebar/Sidebar"; import TopBar from "./components/TopBar/TopBar"; import { shouldTriggerSafetyCheck } from "./helpers"; -import { isNetworkAvailable, isNetworkLegacy } from "./constants"; +import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "./constants"; import useTheme from "./hooks/useTheme"; import { useUnstableProvider } from "./hooks/ghost"; import { dark as darkTheme } from "./themes/dark.js"; @@ -216,9 +216,9 @@ function App() { } } /> } /> - } /> - } /> - } /> + {isGovernanceAvailable(chainId, addressChainId) && } />} + {isGovernanceAvailable(chainId, addressChainId) && } />} + {isGovernanceAvailable(chainId, addressChainId) && } />} > } { const theme = useTheme(); @@ -56,6 +57,7 @@ const Select = ({ onChange={onChange} inputWidth={inputWidth} IconComponent={KeyboardArrowDownIcon} + renderValue={renderValue} displayEmpty > {options.map((opt) => ( diff --git a/src/components/Sidebar/NavContent.jsx b/src/components/Sidebar/NavContent.jsx index 6d0f834..be8de60 100644 --- a/src/components/Sidebar/NavContent.jsx +++ b/src/components/Sidebar/NavContent.jsx @@ -39,7 +39,7 @@ import BondIcon from "../Icon/BondIcon"; import StakeIcon from "../Icon/StakeIcon"; import WrapIcon from "../Icon/WrapIcon"; -import { isNetworkAvailable, isNetworkLegacy } from "../../constants"; +import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "../../constants"; import { AVAILABLE_DEXES } from "../../constants/dexes"; import { ECOSYSTEM } from "../../constants/ecosystem"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; @@ -180,7 +180,7 @@ const NavContent = ({ chainId, addressChainId }) => { /> - + {isGovernanceAvailable(chainId, addressChainId) && } diff --git a/src/constants.ts b/src/constants.ts index cc24806..20fe2d9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,19 @@ export enum NetworkId { TESTNET_MORDOR = 63, } +export const isGovernanceAvailable = (chainId, addressChainId) => { + chainId = addressChainId ? addressChainId : chainId; + let exists = false; + switch (chainId) { + case 11155111: + exists = true + break; + default: + break; + } + return exists; +} + export const isNetworkAvailable = (chainId, addressChainId) => { chainId = addressChainId ? addressChainId : chainId; let exists = false; diff --git a/src/constants/addresses.js b/src/constants/addresses.js index 2a83c00..8e2ecba 100644 --- a/src/constants/addresses.js +++ b/src/constants/addresses.js @@ -1,25 +1,25 @@ import { NetworkId } from "../constants"; export const STAKING_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",//"0xd90E63E88282596E1ea33765b41Ba3d650f4aD52", + [NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506", [NetworkId.TESTNET_HOODI]: "0x25F62eDc6C89FF84E957C22336A35d2dfc861a86", [NetworkId.TESTNET_MORDOR]: "0xC25C9C56a89ebd6ef291b415d00ACfa7913c55e7", }; export const BOND_DEPOSITORY_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",//"0xdcE486113280e49ca2fB200258E5Ee1B2D21D495", + [NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0", [NetworkId.TESTNET_HOODI]: "0x6Ad50B1E293E68B2fC230c576220a93A9D311571", [NetworkId.TESTNET_MORDOR]: "0x7C85cDEddBAd0f50453d373F7332BEa11ECa7BAf", }; export const DAO_TREASURY_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",//"0x93dd30f819403710de7933B79A74C4A42438458D", + [NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E", [NetworkId.TESTNET_HOODI]: "0x1a1b29b18f714fac9dDabEf530dFc4f85b56A6e8", [NetworkId.TESTNET_MORDOR]: "0x5883C8e2259556B534036c7fDF4555E09dE9f243", }; export const FTSO_DAI_LP_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x1394dC3f7bABaa2F0CA80353648087DAB1BF3fd6", // TBD + [NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D", [NetworkId.TESTNET_HOODI]: "0xf7B2d44209E70782d93A70F7D8eC50010dF7ae50", [NetworkId.TESTNET_MORDOR]: "0xE6546D12665dB5B22Cb92FB9e0221aE51A57aeaa", }; @@ -31,7 +31,7 @@ export const FTSO_STNK_LP_ADDRESSES = { } export const RESERVE_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",//"0x5f63a27a9214a0352F2EF8dAF1eD4974d713192B", + [NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", [NetworkId.TESTNET_HOODI]: "0x80c6676c334BCcE60b3CC852085B72143379CE58", [NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC", }; @@ -43,37 +43,35 @@ export const WETH_ADDRESSES = { }; export const GHST_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",//"0xdf2e5306A3dCcfA4e21bbF4226C17Ff5B008dDC4", + [NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392", [NetworkId.TESTNET_HOODI]: "0xE98f7426457E6533B206e91B7EcA97aa8A258B46", [NetworkId.TESTNET_MORDOR]: "0x14b5787F8a1E62786F50A7998A9b14aa24298423", }; export const STNK_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",//"0x02C296A27eA779d5a16F934337c12062C5E3c0D9", + [NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7", [NetworkId.TESTNET_HOODI]: "0xF07e9303A9f16Afd82f4f57Fd6fca68Aa0AB6D7F", [NetworkId.TESTNET_MORDOR]: "0x137bA9403885D8ECEa95AaFBb8734F5a16121bAC", }; export const FTSO_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",//"0xcFedFFEB3FdeCd2196820Ba3b71f3F84A1255f93", + [NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A", [NetworkId.TESTNET_HOODI]: "0xb184e423811b644A1924334E63985c259F5D0033", [NetworkId.TESTNET_MORDOR]: "0xeA170CC0faceC531a6a9e93a28C4330Ac50343a1", }; export const DISTRIBUTOR_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",//"0x8fbF8eB4Fcd451EF62Aee33508D46FE120963194", + [NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544", [NetworkId.TESTNET_HOODI]: "0xdF49dC81c457c6f92e26cf6d686C7a8715255842", [NetworkId.TESTNET_MORDOR]: "0xaf5e76706520db7fb01096E322940206bf3fce57", }; export const GHOST_GOVERNANCE_ADDRESSES = { [NetworkId.TESTNET_SEPOLIA]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4", - [NetworkId.TESTNET_HOODI]: "0x1B96B792840d4d19d5097ee007392Ed4d851e64F", - [NetworkId.TESTNET_MORDOR]: "0x3dD438416D9593A58193fC52850E588efAa3D57E", }; export const BONDING_CALCULATOR_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",//"0x4896bFc6256A57Df826d7144E48c9633d51d6319", + [NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3", [NetworkId.TESTNET_HOODI]: "0x2635d526Ad24b98082563937f7b996075052c6Fd", [NetworkId.TESTNET_MORDOR]: "0x0c4C7C49a173E2a3f9Eed93125F3F146D8e17bCb", } diff --git a/src/containers/Governance/NewProposal.jsx b/src/containers/Governance/NewProposal.jsx index c6dbfd8..202bc43 100644 --- a/src/containers/Governance/NewProposal.jsx +++ b/src/containers/Governance/NewProposal.jsx @@ -1,4 +1,5 @@ import { useState, useMemo } from "react"; +import toast from "react-hot-toast"; import { Box, @@ -24,6 +25,7 @@ import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react"; import Paper from "../../components/Paper/Paper"; import PageTitle from "../../components/PageTitle/PageTitle"; import { PrimaryButton, TertiaryButton } from "../../components/Button"; +import { useTokenSymbol } from "../../hooks/tokens"; import ProposalModal from "./components/ProposalModal"; import { parseFunctionCalldata } from "./components/functions/index"; @@ -35,6 +37,8 @@ const NewProposal = ({ config, address, connect, chainId }) => { const theme = useTheme(); + const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); + const [isModalOpened, setIsModalOpened] = useState(false); const [proposalFunctions, setProposalFunctions] = useState([]); @@ -47,13 +51,20 @@ const NewProposal = ({ config, address, connect, chainId }) => { }, [config]); const submitProposal = () => { - alert("Proposal created"); + toast.success("Coming soon! It's already connected to the chain data!", { duration: 5000 }); setProposalFunctions([]); } return ( <> - setIsModalOpened(false)} /> + setIsModalOpened(false)} + /> { Create new proposal by adding functions below - - Learn more - - + Learn more + } - - setIsModalOpened(true)}>Add New - submitProposal()}>Submit Proposal + + setIsModalOpened(true)}>Add New + submitProposal()}>Submit Proposal @@ -138,8 +140,8 @@ const NewProposal = ({ config, address, connect, chainId }) => { Value - {proposalFunctions.map((calldata, index) => { - return parseFunctionCalldata(calldata, index, nativeCurrency, removeCalldata); + {proposalFunctions.map((metadata, index) => { + return parseFunctionCalldata(metadata, index, chainId, nativeCurrency, removeCalldata); })} diff --git a/src/containers/Governance/components/Metric.jsx b/src/containers/Governance/components/Metric.jsx index 9ee5822..db5a646 100644 --- a/src/containers/Governance/components/Metric.jsx +++ b/src/containers/Governance/components/Metric.jsx @@ -25,8 +25,10 @@ export const MinQuorumPercentage = props => { label: `Min Quorum`, tooltip: `Minimum $${props.ghstSymbol} turnout required for the proposal to become valid`, }; + const tokenValue = formatCurrency(value?.toString(), 2, props.ghstSymbol); + const percentageValue = formatNumber(percentage * 100, 2); - if (percentage) _props.metric = `${formatCurrency(value?.toString(), 2, props.ghstSymbol)} (${formatNumber(percentage * 100, 2)}%)`; + if (percentage) _props.metric = `${tokenValue} (${percentageValue}%)`; else _props.isLoading = true; return ; @@ -41,22 +43,22 @@ export const ProposalThreshold = props => { tooltip: `Minimum $${props.ghstSymbol} required to be locked to create a proposal`, }; - if (threshold) _props.metric = `${formatCurrency(threshold.toString(), 0, props.ghstSymbol)}`; + if (threshold) _props.metric = `${formatCurrency(threshold.toString(), 2, props.ghstSymbol)}`; else _props.isLoading = true; return ; } export const ProposalsCount = props => { - const { proposalsCount } = useProposalCount(props.chainId); + const { proposalCount } = useProposalCount(props.chainId); const _props = { ...props, - label: `Proposals Count`, + label: `Proposal Count`, tooltip: `Total proposals created`, }; - if (proposalsCount) _props.metric = `${formatNumber(proposalsCount.toString(), 0)}`; + if (proposalCount || proposalCount === 0n) _props.metric = `${formatNumber(proposalCount.toString(), 0)}`; else _props.isLoading = true; return ; diff --git a/src/containers/Governance/components/ProposalModal.jsx b/src/containers/Governance/components/ProposalModal.jsx index 8d94de1..9218b5b 100644 --- a/src/containers/Governance/components/ProposalModal.jsx +++ b/src/containers/Governance/components/ProposalModal.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo, useCallback } from "react"; -import { Box, Typography } from "@mui/material"; +import { Box, Typography, useTheme } from "@mui/material"; import Modal from "../../../components/Modal/Modal"; import Select from "../../../components/Select/Select"; @@ -12,7 +12,7 @@ import { allPossibleFunctions } from "./functions"; -const ProposalModal = ({ isOpened, closeModal, addCalldata }) => { +const ProposalModal = ({ isOpened, closeModal, nativeCurrency, ftsoSymbol, addCalldata, chainId }) => { const [selectedOption, setSelectedOption] = useState(); const [renderArguments, setRenderArguments] = useState(false); @@ -29,10 +29,10 @@ const ProposalModal = ({ isOpened, closeModal, addCalldata }) => { }, [selectedOption, renderArguments]); const handleCalldata = useCallback(() => { - addCalldata(getFunctionCalldata(selectedOption)); + addCalldata(getFunctionCalldata(selectedOption, chainId)); setSelectedOption(null); closeModal(); - }, [selectedOption]); + }, [selectedOption, chainId]); const handleClose = () => { setSelectedOption(null); @@ -45,7 +45,7 @@ const ProposalModal = ({ isOpened, closeModal, addCalldata }) => { handleClose(); } - const ArgumentsSteps = useMemo(() => getFunctionArguments(selectedOption), [selectedOption]); + const ArgumentsSteps = useMemo(() => getFunctionArguments(selectedOption, chainId), [selectedOption]); return ( { {renderArguments ? setRenderArguments(false)} /> @@ -79,6 +82,7 @@ const ProposalModal = ({ isOpened, closeModal, addCalldata }) => { } const InitialStep = ({ selectedOption, handleChange, handleCalldata, handleProceed, ready }) => { + const theme = useTheme(); const functionDescription = useMemo(() => getFunctionDescription(selectedOption), [selectedOption]); return ( @@ -87,6 +91,17 @@ const InitialStep = ({ selectedOption, handleChange, handleCalldata, handleProce onChange={handleChange} options={allPossibleFunctions} inputWidth="100%" + renderValue={(selected) => { + if (!selected || selected.length === 0) { + return ( + + Select function + + ); + } + + return allPossibleFunctions.find(opt => opt.value === selected)?.label || selected; + }} /> {functionDescription} {ready diff --git a/src/containers/Governance/components/ProposalsList.jsx b/src/containers/Governance/components/ProposalsList.jsx index 151c987..6a6dcd0 100644 --- a/src/containers/Governance/components/ProposalsList.jsx +++ b/src/containers/Governance/components/ProposalsList.jsx @@ -120,22 +120,12 @@ const ProposalsList = ({ chainId, config }) => { Proposals - - View Forum - - + View Forum + } > @@ -165,8 +155,8 @@ const ProposalTable = ({ children }) => ( - Proposal ID - Status + Proposal ID + Status Vote Ends Voting Stats @@ -222,14 +212,14 @@ const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => { {(proposal.status === "Active" || proposal.status === "Succeeded") && openProposal()} - sx={{ maxWidth: "150px" }} + sx={{ maxWidth: "130px" }} > {proposal.status === "Succeeded" ? "Execute" : "Vote"} } {(proposal.status !== "Active" && proposal.status !== "Succeeded") && openProposal()} - sx={{ maxWidth: "150px" }} + sx={{ maxWidth: "130px" }} > View } diff --git a/src/containers/Governance/components/functions/AuditReserves.jsx b/src/containers/Governance/components/functions/AuditReserves.jsx index de6834c..ff64ef7 100644 --- a/src/containers/Governance/components/functions/AuditReserves.jsx +++ b/src/containers/Governance/components/functions/AuditReserves.jsx @@ -1,11 +1,26 @@ -import { Box, Typography, TableCell, TableRow, useTheme } from "@mui/material"; -import { TertiaryButton } from "../../../../components/Button"; +import { useMemo } from "react"; +import { encodeFunctionData } from 'viem'; + + +import { + DAO_TREASURY_ADDRESSES, +} from "../../../../constants/addresses"; +import { abi as TreasuryAbi } from "../../../../abi/GhostTreasury.json"; + +import { ParsedCell } from "./index"; export const prepareAuditReservesCalldata = (chainId) => { - return "0xauditReserves"; + const value = 0n; + const label = "Audit Reserves"; + const target = DAO_TREASURY_ADDRESSES[chainId]; + const calldata = encodeFunctionData({ + abi: TreasuryAbi, + functionName: 'auditReserves', + }); + return { label, target, calldata, value }; } -export const prepareAuditReservesDescription = "Explanation about what is audit reserves, pros and cons should be here, we need to fill as much text here as possible to make initial screen usable and not empty."; +export const prepareAuditReservesDescription = "Audit Reserves function audits and updates the protocol's total reserve value. It sums the value of all approved reserve and liquidity tokens, then stores and logs the new total."; export const AuditReservesSteps = () => { return null; @@ -20,26 +35,5 @@ export const AuditReservesParsed = (props) => { } const AuditReservesParsedCell = (props) => { - return ( - - - Audit Reserves - - - - {props.target} - - - - {props.value} {props.nativeCoin} - - - {props.remove && - - alert("Do we need it????")}>Edit - props.remove()}>Delete - - } - - ) + return } diff --git a/src/containers/Governance/components/functions/CreateBond.jsx b/src/containers/Governance/components/functions/CreateBond.jsx index 63c8106..26e2d7c 100644 --- a/src/containers/Governance/components/functions/CreateBond.jsx +++ b/src/containers/Governance/components/functions/CreateBond.jsx @@ -1,14 +1,32 @@ -import { useRef, useState } from "react"; +import { useRef, useMemo, useState, useEffect } from "react"; +import { encodeFunctionData } from 'viem'; import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material"; +import Select from "../../../../components/Select/Select"; import { PrimaryButton, TertiaryButton } from "../../../../components/Button"; -import { BooleanTrigger, ArgumentInput } from "./index"; +import { shorten } from "../../../../helpers"; +import { BooleanTrigger, ArgumentInput, ParsedCell } from "./index"; -export const prepareCreateBondCalldata = (chainId) => { - return "0xcreateBond"; +import { + RESERVE_ADDRESSES, + FTSO_DAI_LP_ADDRESSES, + BOND_DEPOSITORY_ADDRESSES, +} from "../../../../constants/addresses"; +import { abi as DepositoryAbi } from "../../../../abi/GhostBondDepository.json"; + +export const prepareCreateBondCalldata = (chainId, markets, terms, quoteToken, intervals, booleans) => { + const value = 0n; + const label = "Create Bond"; + const target = BOND_DEPOSITORY_ADDRESSES[chainId]; + const calldata = encodeFunctionData({ + abi: DepositoryAbi, + functionName: 'create', + args: [markets, terms, quoteToken, intervals, booleans] + }); + return { label, target, calldata, value }; } -export const prepareCreateBondDescription = "Explanation about what is bond creation, pros and cons should be here, we need to fill as much text here as possible to make initial screen usable and not empty."; +export const prepareCreateBondDescription = "Create Bond function creates a new bond market by processing pricing, capacity, and term inputs. It initializes and stores the new bond market's complete configuration in the protocol."; export const CreateBondParsed = (props) => { return ( @@ -19,41 +37,21 @@ export const CreateBondParsed = (props) => { } const CreateBondParsedCell = (props) => { - return ( - - - Create Bond - - - - {props.target} - - - - {props.value} {props.nativeCoin} - - - {props.remove && - - alert("Do we need it????")}>Edit - props.remove()}>Delete - - } - - ) + return } -export const CreateBondSteps = ({ toInitialStep, addCalldata }) => { +export const CreateBondSteps = ({ nativeCurrency, ftsoSymbol, chainId, toInitialStep, addCalldata }) => { const [step, setStep] = useState(1); + const [nextDisabled, setNextDisabled] = useState(false); const [capacity, setCapacity] = useState(); const [initialPrice, setInitialPrice] = useState(); const [debtBuffer, setDebtBuffer] = useState(); - const [bondVesting, setBondVesting] = useState(); - const [bondDuration, setBondDuration] = useState(); + const [depositInterval, setDepositInterval] = useState(); + const [tuneInterval, setTuneInterval] = useState(); - const [tokenAddress, setTokenAddress] = useState(""); + const [tokenAddress, setTokenAddress] = useState(); const [capacityInQuote, setCapacityInQuote] = useState(true); const [fixedTerm, setFixedTerm] = useState(false); @@ -61,7 +59,12 @@ export const CreateBondSteps = ({ toInitialStep, addCalldata }) => { const [conclusionTimestamp, setConclusionTimestamp] = useState(); const handleProceed = () => { - addCalldata(prepareCreateBondCalldata()) + const markets = [capacity, initialPrice, debtBuffer]; + const terms = [vestingLength, conclusionTimestamp]; + const intervals = [depositInterval, tuneInterval]; + const booleans = [capacityInQuote, fixedTerm]; + + addCalldata(prepareCreateBondCalldata(chainId, markets, terms, tokenAddress, intervals, booleans)) } const incrementStep = () => { @@ -73,17 +76,42 @@ export const CreateBondSteps = ({ toInitialStep, addCalldata }) => { else toInitialStep(); } + const isNextDisabled = useMemo(() => { + switch (step) { + case 1: + return tokenAddress === undefined; + case 2: + return !capacity || !initialPrice || !debtBuffer; + case 3: + return !depositInterval || !tuneInterval; + case 4: + return true; + default: + return false; + } + }, [step, tokenAddress, capacity, initialPrice, debtBuffer, depositInterval, tuneInterval]); + + const possibleTokens = [ + { value: FTSO_DAI_LP_ADDRESSES[chainId], label: `${ftsoSymbol}-${nativeCurrency} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` }, + { value: RESERVE_ADDRESSES[chainId], label: `${ftsoSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` }, + ]; + return ( - {step === 1 && } {step === 2 && { debtBuffer={debtBuffer} setDebtBuffer={setDebtBuffer} />} - {step === 3 && } - {step === 4 && { decrementStep()} fullWidth>Back - incrementStep()} fullWidth>Next + incrementStep()} fullWidth>Next {step === 4 && handleProceed()} fullWidth>Create Function} @@ -116,6 +144,9 @@ export const CreateBondSteps = ({ toInitialStep, addCalldata }) => { } const MarketArguments = ({ + capacityInQuote, + nativeCurrency, + ftsoSymbol, capacity, setCapacity, initialPrice, @@ -126,57 +157,57 @@ const MarketArguments = ({ return ( - - ) -} - -const TermsArguments = ({ - bondVesting, - setBondVesting, - bondDuration, - setBondDuration, -}) => { - return ( - - - ) } const IntervalsArguments = ({ + depositInterval, + setDepositInterval, + tuneInterval, + setTuneInterval, +}) => { + return ( + + + + + ) +} + +const TermsAgruments = ({ fixedTerm, vestingLength, setVestingLength, @@ -187,40 +218,52 @@ const IntervalsArguments = ({ ) } -const AddressAndBooleansArguments = ({ - tokenAddress, +const TokenAndBooleansArguments = ({ + possibleTokens, + nativeCurrency, + ftsoSymbol, setTokenAddress, capacityInQuote, setCapacityInQuote, fixedTerm, setFixedTerm, }) => { + const theme = useTheme(); + const [selectedOption, setSelectedOption] = useState(); + + const handleChange = (event) => { + setSelectedOption(event.target.value); + setTokenAddress(event.target.value); + }; + return ( setCapacityInQuote(true)} setRightValue={() => setCapacityInQuote(false)} - tooltip="WTF is this" + tooltip="Capacity is set in terms of the asset being paid, otherwise it is set in the asset being earned" /> setFixedTerm(true)} setRightValue={() => setFixedTerm(false)} - tooltip="Fixed term... No idea what it is" + tooltip="Determines whether the bond has a fixed expiration date or a fixed duration starting from the time of purchase" /> - { + if (!selected || selected.length === 0) { + return ( + + Select payment token + + ); + } + + return possibleTokens.find(opt => opt.value === selected)?.label || selected; + }} /> ) diff --git a/src/containers/Governance/components/functions/SetAdjustment.jsx b/src/containers/Governance/components/functions/SetAdjustment.jsx index b4047c7..32102a3 100644 --- a/src/containers/Governance/components/functions/SetAdjustment.jsx +++ b/src/containers/Governance/components/functions/SetAdjustment.jsx @@ -1,15 +1,29 @@ import { useState } from "react"; +import { encodeFunctionData } from 'viem'; import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material"; import { PrimaryButton, TertiaryButton } from "../../../../components/Button"; -import { BooleanTrigger, ArgumentInput } from "./index"; +import { + DISTRIBUTOR_ADDRESSES, +} from "../../../../constants/addresses"; +import { abi as DistributorAbi } from "../../../../abi/GhostDistributor.json"; -export const prepareSetAdjustmentCalldata = (chainId) => { - return "0xsetAdjustment"; +import { BooleanTrigger, ArgumentInput, ParsedCell } from "./index"; + +export const prepareSetAdjustmentCalldata = (chainId, rateChange, targetRate, increase) => { + const value = 0n; + const label = "Set Adjustment"; + const target = DISTRIBUTOR_ADDRESSES[chainId]; + const calldata = encodeFunctionData({ + abi: DistributorAbi, + functionName: 'setAdjustment', + args: [rateChange, targetRate, increase] + }); + return { label, target, calldata, value }; } -export const prepareSetAdjustmentDescription = "Explanation about what is set adjustment (APY change), pros and cons should be here, we need to fill as much text here as possible to make initial screen usable and not empty."; +export const prepareSetAdjustmentDescription = "Set Adjustment function schedules a gradual change to the staking APY. It increases or decreases the staking APY by a specified rate per epoch until a target rate reached."; export const SetAdjustmentParsed = (props) => { return ( @@ -20,37 +34,16 @@ export const SetAdjustmentParsed = (props) => { } const SetAdjustmentParsedCell = (props) => { - return ( - - - Set Adjustment - - - - {props.target} - - - - {props.value} {props.nativeCoin} - - - {props.remove && - - alert("Do we need it????")}>Edit - props.remove()}>Delete - - } - - ) + return } -export const SetAdjustmentSteps = ({ toInitialStep, addCalldata }) => { +export const SetAdjustmentSteps = ({ chainId, toInitialStep, addCalldata }) => { const [rate, setRate] = useState(); const [target, setTarget] = useState(); const [increase, setIncrease] = useState(false); const handleProceed = () => { - addCalldata(prepareSetAdjustmentCalldata()) + addCalldata(prepareSetAdjustmentCalldata(chainId, rate, target, increase)); } return ( @@ -63,21 +56,21 @@ export const SetAdjustmentSteps = ({ toInitialStep, addCalldata }) => { setLeftValue={() => setIncrease(true)} setRightValue={() => setIncrease(false)} label="Direction" - tooltip="To add or not to add" + tooltip="Determines the direction of the adjustment. When Add the parameter value will gradually increase; when Sub, it will decrease toward the target rate" /> @@ -85,7 +78,13 @@ export const SetAdjustmentSteps = ({ toInitialStep, addCalldata }) => { Back Next - handleProceed()} fullWidth>Create Function + handleProceed()} + fullWidth + > + Create Function + ); diff --git a/src/containers/Governance/components/functions/index.jsx b/src/containers/Governance/components/functions/index.jsx index 4cc094c..c140f9c 100644 --- a/src/containers/Governance/components/functions/index.jsx +++ b/src/containers/Governance/components/functions/index.jsx @@ -1,49 +1,85 @@ -import { useRef } from "react"; -import { Box, Typography, useTheme } from "@mui/material"; +import { useRef, useMemo } from "react"; +import { Box, Typography, Link, TableCell, TableRow, useTheme } from "@mui/material"; +import { decodeFunctionData } from 'viem'; import { StyledInputBase } from "../../../../components/Swap/SwapCard"; +import { TertiaryButton } from "../../../../components/Button"; import InfoTooltip from "../../../../components/Tooltip/InfoTooltip"; +import { abi as TreasuryAbi } from "../../../../abi/GhostTreasury.json"; +import { abi as DistributorAbi } from "../../../../abi/GhostDistributor.json"; +import { abi as DepositoryAbi } from "../../../../abi/GhostBondDepository.json"; + +import { config } from "../../../../config"; +import { shorten, formatCurrency } from "../../../../helpers"; + import { prepareAuditReservesDescription, prepareAuditReservesCalldata, AuditReservesSteps, AuditReservesParsed } from "./AuditReserves"; import { prepareSetAdjustmentDescription, prepareSetAdjustmentCalldata, SetAdjustmentSteps, SetAdjustmentParsed } from "./SetAdjustment"; import { prepareCreateBondDescription, prepareCreateBondCalldata, CreateBondSteps, CreateBondParsed } from "./CreateBond"; -const DEFAULT_DESCRIPTION = "Select function for the proposal to start. Make sure you are ready to fill the proposal, here should be pretty long text just to fill space. Here we are trying to make everything looks cool." +const DEFAULT_DESCRIPTION = "Please select the function to include in your proposal. Multi-functional proposals are allowed, but each included function should be clearly specified." export const allPossibleFunctions = [ { value: "auditReserves", label: "Audit Reserves" }, - { value: "setAdjustment", label: "Change APY" }, - { value: "createBond", label: "Create Bond" }, + { value: "setAdjustment", label: "Change APY" }, + { value: "createBond", label: "Create Bond" }, ]; -export const parseFunctionCalldata = (calldata, index, nativeCoin, removeCalldata) => { - const target = "0x5324..123123"; - const value = 0; +const allAbis = [TreasuryAbi, DistributorAbi, DepositoryAbi]; + +const identifyAction = (calldata) => { + let decoded = { functionName: "Unknown", args: [] }; + for (const abi of allAbis) { + try { + decoded = decodeFunctionData({ + abi: abi, + data: calldata, + }); + return decoded; + } catch (err) { + continue; + } + } + return decoded; +} + +export const parseFunctionCalldata = (metadata, index, chainId, nativeCoin, removeCalldata) => { + const { label, calldata, target, value } = metadata; + const { functionName, args } = identifyAction(calldata); const remove = () => removeCalldata(index); - switch (true) { - case calldata.includes("auditReserves"): + console.log(`function arguments for ${label}: ${args}`); + + switch (functionName) { + case "auditReserves": return ; - case calldata.includes("setAdjustment"): + case "setAdjustment": return ; - case calldata.includes("createBond"): + case "create": return ; default: - return null; + return ; } } @@ -68,16 +113,16 @@ export const getFunctionArguments = (functionName) => { } } -export const getFunctionCalldata = (functionName) => { +export const getFunctionCalldata = (functionName, chainId) => { switch (functionName) { case "auditReserves": - return prepareAuditReservesCalldata(); + return prepareAuditReservesCalldata(chainId); case "setAdjustment": - return prepareSetAdjustmentCalldata(); + return prepareSetAdjustmentCalldata(chainId); case "createBond": - return prepareCreateBondCalldata(); + return prepareCreateBondCalldata(chainId); default: - return ""; + return null; } } @@ -106,10 +151,18 @@ export const BooleanValue = ({ left, text, isSelected, setSelected }) => { cursor: "pointer", borderRadius: left ? "12px 0 0 12px" : "0 12px 12px 0", flex: 1, - border: `1px solid #fff` + border: "2px solid #fff", + borderLeft: left ? "2px solid #fff" : "none", + borderRight: left ? "none" : "2px solid #fff", + background: `${isSelected ? "#fff" : theme.colors.gray[600] }` }} > - {text} + + {text} + ) } @@ -186,3 +239,39 @@ export const ArgumentInput = ({ ) } + +export const ParsedCell = (props) => { + const etherscanLink = useMemo(() => { + const client = config.getClient(); + let url = client?.chain?.blockExplorers?.default?.url; + if (url) { + url = url + `/address/${props.target}`; + } + return url; + }, [props]); + + return ( + + + {props.label} + + + + + {shorten(props.target)} + + + + + {formatCurrency(props.value, 2, props.nativeCoin)} + + + {props.remove && + + alert("Do we need it????")}>Edit + props.remove()}>Delete + + } + + ) +} diff --git a/src/hooks/bonds/index.jsx b/src/hooks/bonds/index.js similarity index 100% rename from src/hooks/bonds/index.jsx rename to src/hooks/bonds/index.js diff --git a/src/hooks/governance/index.js b/src/hooks/governance/index.js index 02eb64a..3784a32 100644 --- a/src/hooks/governance/index.js +++ b/src/hooks/governance/index.js @@ -1,29 +1,70 @@ +import { useReadContract, useReadContracts } from "wagmi"; +import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core"; +import toast from "react-hot-toast"; + +import { config } from "../../config"; + +import { + GHOST_GOVERNANCE_ADDRESSES, +} from "../../constants/addresses"; +import { abi as GovernorAbi } from "../../abi/Governor.json"; +import { abi as GovernorStorageAbi } from "../../abi/GovernorStorage.json"; +import { abi as GovernorVotesQuorumFractionAbi } from "../../abi/GovernorVotesQuorumFraction.json"; + import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { getTokenDecimals } from "../helpers"; export const useMinQuorum = (chainId) => { - const numerator = 69n; - const denominator = 100n; + const { data: quorumNumerator, refetch: quorumNumeratorRefetch } = useReadContract({ + abi: GovernorVotesQuorumFractionAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "quorumNumerator", + scopeKey: `quorumNumerator-${chainId}`, + chainId: chainId, + }); - let percentage = 0; + const { data: quorumDenominator, refetch: quorumDenominatorRefetch } = useReadContract({ + abi: GovernorVotesQuorumFractionAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "quorumDenominator", + scopeKey: `quorumDenominator-${chainId}`, + chainId: chainId, + }); - if (numerator && denominator && denominator > 0n) { - percentage = Number(100n * numerator / denominator) / 100; - } + const numerator = quorumNumerator ?? 0n; + const denominator = quorumDenominator ?? 1n; + const percentage = Number(100n * numerator / denominator) / 100; return { numerator, denominator, percentage } } export const useProposalThreshold = (chainId, name) => { const decimals = getTokenDecimals(name); - const threshold = new DecimalBigNumber(420_000_000_000_000_000_000n, decimals); + const { data, refetch } = useReadContract({ + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalThreshold", + scopeKey: `proposalThreshold-${chainId}`, + chainId: chainId, + }); + + const threshold = new DecimalBigNumber(data ?? 0n, decimals); return { threshold }; } export const useProposalCount = (chainId) => { - const proposalsCount = 1337n; - return { proposalsCount }; + const { data, refetch } = useReadContract({ + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalCount", + scopeKey: `proposalCount-${chainId}`, + chainId: chainId, + }); + + const proposalCount = data ?? 0n; + + return { proposalCount }; } export const useProposalStatus = (chainId, proposalId) => { @@ -73,11 +114,7 @@ export const useProposalVotingDelay = (chainId, proposalId) => { export const useProposals = (chainId, depth) => { const decimals = getTokenDecimals(name); - const { proposalsCount } = useProposalCount(chainId); - - let iterator = proposalsCount ? proposalsCount : 0n; - const bigIntDepth = BigInt(depth); - const edgeProposalId = iterator > bigIntDepth ? iterator - bigIntDepth : 0n; + const { proposalCount } = useProposalCount(chainId); const statuses = [ "Active", @@ -88,17 +125,18 @@ export const useProposals = (chainId, depth) => { ]; let proposals = []; - while (iterator > proposalsCount - bigIntDepth) { - iterator -= 1n; + const start = Number(proposalCount); + const end = Math.max(0, start - depth); + for (let i = start - 1; i >= end; --i) { const voteEnds = 50n; const yesVotes = new DecimalBigNumber(1337_000_000_000_000_000_000, decimals); const noVotes = new DecimalBigNumber(420_000_000_000_000_000_000, decimals); proposals.push({ - id: iterator, + id: i, discussion: "https://google.com", - status: statuses[Number(iterator) % statuses.length], + status: statuses[i % statuses.length], voteEnds, yesVotes, noVotes