diff --git a/package.json b/package.json index b4d51f8..65e54b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.4.7", + "version": "0.5.20", "type": "module", "scripts": { "dev": "vite", @@ -16,6 +16,7 @@ "@ethersproject/bignumber": "^5.8.0", "@ethersproject/units": "^5.8.0", "@mui/icons-material": "^6.4.7", + "@mui/lab": "6.0.1-beta.36", "@mui/material": "^6.4.7", "@mui/utils": "^6.4.6", "@polkadot-api/metadata-builders": "0.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 183862c..28d471f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@mui/icons-material': specifier: ^6.4.7 version: 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) + '@mui/lab': + specifier: 6.0.1-beta.36 + version: 6.0.1-beta.36(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/material': specifier: ^6.4.7 version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -525,6 +528,21 @@ packages: '@ethersproject/units@5.8.0': resolution: {integrity: sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==} + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -638,6 +656,18 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} + '@mui/base@5.0.0-beta.70': + resolution: {integrity: sha512-Tb/BIhJzb0pa5zv/wu7OdokY9ZKEDqcu1BDFnohyvGCoHuSXbEr90rPq1qeNW3XvTBIbNWHEF7gqge+xpUo6tQ==} + engines: {node: '>=14.0.0'} + deprecated: This package has been replaced by @base-ui/react + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/core-downloads-tracker@6.4.7': resolution: {integrity: sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==} @@ -652,6 +682,27 @@ packages: '@types/react': optional: true + '@mui/lab@6.0.1-beta.36': + resolution: {integrity: sha512-af9lDmA9SZGEWF1XXk0EVBpfCITk9IKsvh9lLOZGdYaaHfQeCsqxGEDMvNO66j0P8EYoxpyry84LFCJYuLVtVw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material': ^6.5.0 + '@mui/material-pigment-css': ^6.5.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + '@mui/material@6.4.7': resolution: {integrity: sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==} engines: {node: '>=14.0.0'} @@ -682,6 +733,16 @@ packages: '@types/react': optional: true + '@mui/private-theming@6.4.9': + resolution: {integrity: sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/styled-engine@6.4.6': resolution: {integrity: sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==} engines: {node: '>=14.0.0'} @@ -695,6 +756,19 @@ packages: '@emotion/styled': optional: true + '@mui/styled-engine@6.5.0': + resolution: {integrity: sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/system@6.4.7': resolution: {integrity: sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==} engines: {node: '>=14.0.0'} @@ -711,6 +785,22 @@ packages: '@types/react': optional: true + '@mui/system@6.5.0': + resolution: {integrity: sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + '@mui/types@7.2.21': resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==} peerDependencies: @@ -719,6 +809,14 @@ packages: '@types/react': optional: true + '@mui/types@7.2.24': + resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/utils@6.4.6': resolution: {integrity: sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==} engines: {node: '>=14.0.0'} @@ -729,6 +827,16 @@ packages: '@types/react': optional: true + '@mui/utils@6.4.9': + resolution: {integrity: sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@noble/ciphers@1.2.1': resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} engines: {node: ^14.21.3 || >=16} @@ -3423,6 +3531,23 @@ snapshots: '@ethersproject/constants': 5.8.0 '@ethersproject/logger': 5.8.0 + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@floating-ui/utils@0.2.10': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -3615,6 +3740,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@mui/base@5.0.0-beta.70(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@floating-ui/react-dom': 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/types': 7.2.24(@types/react@19.0.10) + '@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0) + '@popperjs/core': 2.11.8 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@mui/core-downloads-tracker@6.4.7': {} '@mui/icons-material@6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)': @@ -3625,6 +3764,23 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@mui/lab@6.0.1-beta.36(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/base': 5.0.0-beta.70(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/system': 6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) + '@mui/types': 7.2.24(@types/react@19.0.10) + '@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) + '@types/react': 19.0.10 + '@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.9 @@ -3648,16 +3804,38 @@ snapshots: '@mui/private-theming@6.4.6(@types/react@19.0.10)(react@19.0.0)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.6 '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) prop-types: 15.8.1 react: 19.0.0 optionalDependencies: '@types/react': 19.0.10 + '@mui/private-theming@6.4.9(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0) + prop-types: 15.8.1 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@mui/styled-engine@6.4.6(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.6 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.0.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) + + '@mui/styled-engine@6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 @@ -3684,10 +3862,30 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) '@types/react': 19.0.10 + '@mui/system@6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/private-theming': 6.4.9(@types/react@19.0.10)(react@19.0.0) + '@mui/styled-engine': 6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0) + '@mui/types': 7.2.24(@types/react@19.0.10) + '@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.0.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) + '@types/react': 19.0.10 + '@mui/types@7.2.21(@types/react@19.0.10)': optionalDependencies: '@types/react': 19.0.10 + '@mui/types@7.2.24(@types/react@19.0.10)': + optionalDependencies: + '@types/react': 19.0.10 + '@mui/utils@6.4.6(@types/react@19.0.10)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.9 @@ -3700,6 +3898,18 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@mui/utils@6.4.9(@types/react@19.0.10)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/types': 7.2.24(@types/react@19.0.10) + '@types/prop-types': 15.7.14 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.0.0 + react-is: 19.0.0 + optionalDependencies: + '@types/react': 19.0.10 + '@noble/ciphers@1.2.1': {} '@noble/ciphers@1.3.0': {} @@ -5042,7 +5252,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.6 cosmiconfig: 7.1.0 resolve: 1.22.10 @@ -5238,7 +5448,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.6 csstype: 3.1.3 dot-case@3.0.4: diff --git a/src/App.jsx b/src/App.jsx index 3363c68..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"; @@ -31,6 +31,9 @@ const Wrapper = lazy(() => import("./containers/WethWrapper/WethWrapper")); const Dex = lazy(() => import("./containers/Dex/Dex")); const Bridge = lazy(() => import("./containers/Bridge/Bridge")); 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 PREFIX = "App"; @@ -213,6 +216,9 @@ function App() { } } /> } /> + {isGovernanceAvailable(chainId, addressChainId) && } />} + {isGovernanceAvailable(chainId, addressChainId) && } />} + {isGovernanceAvailable(chainId, addressChainId) && } />} > } prop !== "barBackground" && prop !== 'barColor' && prop !== 'height' +})(({ theme, barColor, barBackground, height }) => ({ + height: height || 8, + borderRadius: 4, + backgroundColor: barBackground || theme.palette.grey[300], + '& .MuiLinearProgress-bar': { + backgroundColor: barColor || theme.palette.primary.main + } +})); + +const LinearProgressBar = (props) => { + return ( + + + {props.target && } + + ) +} + +export default LinearProgressBar; diff --git a/src/components/Select/Select.jsx b/src/components/Select/Select.jsx new file mode 100644 index 0000000..3d6dbb2 --- /dev/null +++ b/src/components/Select/Select.jsx @@ -0,0 +1,74 @@ +import { Box, MenuItem, Select as MuiSelect, Typography, useTheme } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; + +const StyledSelectInput = styled(MuiSelect, { + shouldForwardProp: prop => prop !== "inputWidth" +})(({ theme, inputWidth }) => ({ + width: "100%", + "& .MuiSelect-select": { + padding: 0, + height: "24px !important", + minHeight: "24px !important", + display: "flex", + alignItems: "center", + fontSize: "18px", + fontWeight: 500, + paddingRight: "24px", + }, + "& .MuiOutlinedInput-notchedOutline": { + border: "none", + }, + "& .MuiSelect-icon": { + right: 0, + position: "absolute", + color: theme.colors.gray[500], + } +})); + +const Select = ({ + label, + value, + onChange, + options, + inputWidth, + renderValue = null, + width = "100%", +}) => { + const theme = useTheme(); + return ( + + {label && ( + + {label} + + )} + + + + {options.map((opt) => ( + + {opt.label} + + ))} + + + + ); +}; + +export default Select; diff --git a/src/components/Sidebar/NavContent.jsx b/src/components/Sidebar/NavContent.jsx index a4f7ab4..be8de60 100644 --- a/src/components/Sidebar/NavContent.jsx +++ b/src/components/Sidebar/NavContent.jsx @@ -24,6 +24,8 @@ import TelegramIcon from '@mui/icons-material/Telegram'; import HowToVoteIcon from '@mui/icons-material/HowToVote'; import HubIcon from '@mui/icons-material/Hub'; import PublicIcon from '@mui/icons-material/Public'; +import ForkRightIcon from '@mui/icons-material/ForkRight'; +import GavelIcon from '@mui/icons-material/Gavel'; import ForumIcon from '@mui/icons-material/Forum'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import BookIcon from '@mui/icons-material/Book'; @@ -37,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"; @@ -177,7 +179,8 @@ const NavContent = ({ chainId, addressChainId }) => { } /> - + + {isGovernanceAvailable(chainId, addressChainId) && } diff --git a/src/components/Swap/SwapCard.jsx b/src/components/Swap/SwapCard.jsx index 4f7b416..160549e 100644 --- a/src/components/Swap/SwapCard.jsx +++ b/src/components/Swap/SwapCard.jsx @@ -7,7 +7,7 @@ import Token from "../Token/Token"; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })( +export const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })( ({ inputWidth, inputFontSize }) => ({ "& .MuiInputBase-input": { padding: 0, diff --git a/src/constants.ts b/src/constants.ts index a5b306b..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; @@ -26,9 +39,6 @@ export const isNetworkAvailable = (chainId, addressChainId) => { export const isNetworkLegacy = (chainId) => { let exists = false; switch (chainId) { - case 11155111: - exists = true - break; case 560048: exists = true break; diff --git a/src/constants/addresses.js b/src/constants/addresses.js index 3fed2ae..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]: "0xd90E63E88282596E1ea33765b41Ba3d650f4aD52", + [NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506", [NetworkId.TESTNET_HOODI]: "0x25F62eDc6C89FF84E957C22336A35d2dfc861a86", [NetworkId.TESTNET_MORDOR]: "0xC25C9C56a89ebd6ef291b415d00ACfa7913c55e7", }; export const BOND_DEPOSITORY_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xdcE486113280e49ca2fB200258E5Ee1B2D21D495", + [NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0", [NetworkId.TESTNET_HOODI]: "0x6Ad50B1E293E68B2fC230c576220a93A9D311571", [NetworkId.TESTNET_MORDOR]: "0x7C85cDEddBAd0f50453d373F7332BEa11ECa7BAf", }; export const DAO_TREASURY_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x93dd30f819403710de7933B79A74C4A42438458D", + [NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E", [NetworkId.TESTNET_HOODI]: "0x1a1b29b18f714fac9dDabEf530dFc4f85b56A6e8", [NetworkId.TESTNET_MORDOR]: "0x5883C8e2259556B534036c7fDF4555E09dE9f243", }; export const FTSO_DAI_LP_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x1394dC3f7bABaa2F0CA80353648087DAB1BF3fd6", + [NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D", [NetworkId.TESTNET_HOODI]: "0xf7B2d44209E70782d93A70F7D8eC50010dF7ae50", [NetworkId.TESTNET_MORDOR]: "0xE6546D12665dB5B22Cb92FB9e0221aE51A57aeaa", }; @@ -31,49 +31,47 @@ export const FTSO_STNK_LP_ADDRESSES = { } export const RESERVE_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x5f63a27a9214a0352F2EF8dAF1eD4974d713192B", + [NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", [NetworkId.TESTNET_HOODI]: "0x80c6676c334BCcE60b3CC852085B72143379CE58", [NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC", }; export const WETH_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xfff9976782d46cc05630d1f6ebab18b2324d6b14", + [NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", [NetworkId.TESTNET_HOODI]: "0xE69a5c6dd88cA798b93c3C92fc50c51Fd5305eB4", [NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC", }; export const GHST_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xdf2e5306A3dCcfA4e21bbF4226C17Ff5B008dDC4", + [NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392", [NetworkId.TESTNET_HOODI]: "0xE98f7426457E6533B206e91B7EcA97aa8A258B46", [NetworkId.TESTNET_MORDOR]: "0x14b5787F8a1E62786F50A7998A9b14aa24298423", }; export const STNK_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x02C296A27eA779d5a16F934337c12062C5E3c0D9", + [NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7", [NetworkId.TESTNET_HOODI]: "0xF07e9303A9f16Afd82f4f57Fd6fca68Aa0AB6D7F", [NetworkId.TESTNET_MORDOR]: "0x137bA9403885D8ECEa95AaFBb8734F5a16121bAC", }; export const FTSO_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xcFedFFEB3FdeCd2196820Ba3b71f3F84A1255f93", + [NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A", [NetworkId.TESTNET_HOODI]: "0xb184e423811b644A1924334E63985c259F5D0033", [NetworkId.TESTNET_MORDOR]: "0xeA170CC0faceC531a6a9e93a28C4330Ac50343a1", }; export const DISTRIBUTOR_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x8fbF8eB4Fcd451EF62Aee33508D46FE120963194", + [NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544", [NetworkId.TESTNET_HOODI]: "0xdF49dC81c457c6f92e26cf6d686C7a8715255842", [NetworkId.TESTNET_MORDOR]: "0xaf5e76706520db7fb01096E322940206bf3fce57", }; export const GHOST_GOVERNANCE_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0xDab0c51918E6990d8763FAC8a04AE159e44e0c4f", - [NetworkId.TESTNET_HOODI]: "0x1B96B792840d4d19d5097ee007392Ed4d851e64F", - [NetworkId.TESTNET_MORDOR]: "0x3dD438416D9593A58193fC52850E588efAa3D57E", + [NetworkId.TESTNET_SEPOLIA]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4", }; export const BONDING_CALCULATOR_ADDRESSES = { - [NetworkId.TESTNET_SEPOLIA]: "0x4896bFc6256A57Df826d7144E48c9633d51d6319", + [NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3", [NetworkId.TESTNET_HOODI]: "0x2635d526Ad24b98082563937f7b996075052c6Fd", [NetworkId.TESTNET_MORDOR]: "0x0c4C7C49a173E2a3f9Eed93125F3F146D8e17bCb", } @@ -111,6 +109,10 @@ export const NATIVE_TICKERS = { } export const CEX_TICKERS = { + [NetworkId.TESTNET_SEPOLIA]: [ + "https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT", + "https://api.coinbase.com/v2/prices/ETH-USDT/spot", + ], [NetworkId.TESTNET_MORDOR]: [ "https://api.binance.com/api/v3/ticker/price?symbol=ETCUSDT", "https://api.coinbase.com/v2/prices/ETC-USDT/spot", diff --git a/src/containers/Bond/Bonds.jsx b/src/containers/Bond/Bonds.jsx index 058c831..55c7ad0 100644 --- a/src/containers/Bond/Bonds.jsx +++ b/src/containers/Bond/Bonds.jsx @@ -1,6 +1,5 @@ -import { Box, Tab, Tabs, Container, useMediaQuery } from "@mui/material"; +import { Box, Tab, Tabs, Typography, Container, useMediaQuery } from "@mui/material"; import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; import ReactGA from "react-ga4"; import Paper from "../../components/Paper/Paper"; @@ -21,7 +20,6 @@ import { useTokenSymbol } from "../../hooks/tokens"; const Bonds = ({ chainId, address, connect }) => { const [isZoomed] = useState(false); - const navigate = useNavigate(); const [secondsTo, setSecondsTo] = useState(0); const isSmallScreen = useMediaQuery("(max-width: 650px)"); @@ -29,7 +27,7 @@ const Bonds = ({ chainId, address, connect }) => { useEffect(() => { ReactGA.send({ hitType: "pageview", page: "/bonds" }); - }, []) + }, []); const { liveBonds } = useLiveBonds(chainId); const totalReserves = useTotalReserves(chainId); @@ -59,7 +57,17 @@ const Bonds = ({ chainId, address, connect }) => { }} > - + + + Active bonds + + + } + > { warmupLength={warmupInfo.expiry - epoch.number} setPreClaimConfirmed={() => setPreClaimConfirmed(true)} /> - + + + Your Bonds + + + } + > Payout Options diff --git a/src/containers/Bridge/Bridge.jsx b/src/containers/Bridge/Bridge.jsx index d047050..b0b9de5 100644 --- a/src/containers/Bridge/Bridge.jsx +++ b/src/containers/Bridge/Bridge.jsx @@ -140,7 +140,7 @@ const Bridge = ({ chainId, address, config, connect }) => { const transactionApplaused = useMemo(() => { return transactionApplausedDirect || transactionApplausedIncremented; - }, [transactionApplausedDirect, transactionApplausedIncremented]) + }, [transactionApplausedDirect, transactionApplausedIncremented]); const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n); @@ -252,10 +252,10 @@ const Bridge = ({ chainId, address, config, connect }) => { if (commit.disabled || blockNumber < blocksInFourHours) { continue; } - certainty += (commit?.lastStoredBlock ?? 0n) - (blockNumber - blocksInFourHours); + certainty += (commit?.lastStoredBlock ?? 0n) + BigInt(finalityDelay) - (blockNumber - blocksInFourHours); } return Math.max(Number(certainty * 100n / (blocksInFourHours * BigInt(length))), 0); - }, [latestCommits, blockNumber]); + }, [latestCommits, blockNumber, finalityDelay]); const timeToNextEpoch = useMemo(() => { if (!currentSession || !genesisSlot || !currentSlot) { @@ -302,7 +302,7 @@ const Bridge = ({ chainId, address, config, connect }) => { return ( <> - + { handleButtonProceed={handleButtonProceed} /> { transactionEta={slowestEvmBlock ? Number(slowestEvmBlock) : undefined} timeToNextEpoch={timeToNextEpoch ? Number(timeToNextEpoch) : undefined} isSmallScreen={isSmallScreen} + maxDelay={14400 + finalityDelay * Number(networkAvgBlockSpeed(chainId))} /> @@ -358,7 +360,7 @@ const Bridge = ({ chainId, address, config, connect }) => { onClick={() => setBridgeAction(!bridgeAction)} />)} { - bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction History" + bridgeAction ? `Bridge-In $${ghstSymbol}` : "Transaction History" } } diff --git a/src/containers/Bridge/BridgeHeader.jsx b/src/containers/Bridge/BridgeHeader.jsx index 5a173a5..1b5fe49 100644 --- a/src/containers/Bridge/BridgeHeader.jsx +++ b/src/containers/Bridge/BridgeHeader.jsx @@ -12,6 +12,7 @@ export const BridgeHeader = ({ bridgeStability, transactionEta, timeToNextEpoch, + maxDelay, isSmallScreen }) => { const theme = useTheme(); @@ -92,7 +93,7 @@ export const BridgeHeader = ({ 14400 ? "∞" : formatTime(transactionEta)} + metric={transactionEta > maxDelay ? "∞" : formatTime(transactionEta)} label="Max Bridge ETA" tooltip="Maximum estimated time for finalizing bridge transactions based on the latest update." /> diff --git a/src/containers/Bridge/BridgeModal.jsx b/src/containers/Bridge/BridgeModal.jsx index 60b449e..b11a7c1 100644 --- a/src/containers/Bridge/BridgeModal.jsx +++ b/src/containers/Bridge/BridgeModal.jsx @@ -19,12 +19,13 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste'; import InfoTooltip from "../../components/Tooltip/InfoTooltip"; import Modal from "../../components/Modal/Modal"; import GhostStyledIcon from "../../components/Icon/GhostIcon"; -import { PrimaryButton } from "../../components/Button"; +import { PrimaryButton, TertiaryButton } from "../../components/Button"; import { formatCurrency } from "../../helpers"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; export const BridgeModal = ({ + providerDetail, currentRecord, activeTxIndex, setActiveTxIndex, @@ -88,7 +89,15 @@ export const BridgeModal = ({ minHeight={"100px"} > - + {!providerDetail && + window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')} + > + Get GHOST Connect + + } + {providerDetail && {currentRecord?.finalization > 0 && ( <> > )} - + } diff --git a/src/containers/Bridge/ValidatorTable.jsx b/src/containers/Bridge/ValidatorTable.jsx index 4542853..311d8dc 100644 --- a/src/containers/Bridge/ValidatorTable.jsx +++ b/src/containers/Bridge/ValidatorTable.jsx @@ -105,11 +105,11 @@ export const ValidatorTable = ({ {!providerDetail && - GHOST Wallet is not detected on your browser! - Download GHOST Wallet Extension for real-time visibility into validator status and related transaction risks. - Important: The GHOST Wallet Extension is optional, but be aware that your bridge transaction will succeed or fail irreversibly based on the condition of the validators. + GHOST Connect is not detected on your browser! + Download GHOST Connect browser extension for real-time visibility into validator status and related transaction risks. + Important: The GHOST Connect is optional, but be aware that your bridge transaction will succeed or fail irreversibly based on the condition of the validators. window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}> - Get GHOST Extension + Get GHOST Connect } diff --git a/src/containers/Dex/Dex.jsx b/src/containers/Dex/Dex.jsx index 073d8a7..82fa6bd 100644 --- a/src/containers/Dex/Dex.jsx +++ b/src/containers/Dex/Dex.jsx @@ -186,7 +186,7 @@ const Dex = ({ chainId, address, connect }) => { { - + { + const isSemiSmallScreen = useMediaQuery("(max-width: 745px)"); + const isSmallScreen = useMediaQuery("(max-width: 650px)"); + const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); + const navigate = useNavigate(); + + const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); + + const handleModal = () => { + const navigate = useNavigate(); + } + + useEffect(() => { + ReactGA.send({ hitType: "pageview", page: "/governance" }); + }, []); + + return ( + + + + + + + Proposal Requirements + + + } + > + + + + + + + + + + + + + + navigate(`/governance/create`)} + sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }} + > + Create Proposal + + + + + + + + + + + ) +} + +export default Governance; diff --git a/src/containers/Governance/NewProposal.jsx b/src/containers/Governance/NewProposal.jsx new file mode 100644 index 0000000..cf0d619 --- /dev/null +++ b/src/containers/Governance/NewProposal.jsx @@ -0,0 +1,226 @@ +import { useState, useMemo, useCallback, useEffect } from "react"; +import ReactGA from "react-ga4"; + +import { + Box, + Container, + TableContainer, + Table, + TableRow, + TableBody, + TableHead, + TableCell, + Typography, + Link, + OutlinedInput, + InputLabel, + FormControl, + useMediaQuery, + useTheme +} from "@mui/material"; + +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react"; +import { GHOST_GOVERNANCE_ADDRESSES, GHST_ADDRESSES } from "../../constants/addresses"; + +import Paper from "../../components/Paper/Paper"; +import PageTitle from "../../components/PageTitle/PageTitle"; +import { PrimaryButton, TertiaryButton } from "../../components/Button"; +import { TokenAllowanceGuard } from "../../components/TokenAllowanceGuard/TokenAllowanceGuard"; + +import { useTokenSymbol, useBalance } from "../../hooks/tokens"; +import { useProposalThreshold, useProposalHash, propose } from "../../hooks/governance"; +import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; + +import ProposalModal from "./components/ProposalModal"; +import { parseFunctionCalldata } from "./components/functions/index"; +import { MY_PROPOSALS_PREFIX } from "./helpers"; + +const NewProposal = ({ config, address, connect, chainId }) => { + const isSemiSmallScreen = useMediaQuery("(max-width: 745px)"); + const isSmallScreen = useMediaQuery("(max-width: 650px)"); + const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); + + const theme = useTheme(); + + const myStoredProposals = localStorage.getItem(MY_PROPOSALS_PREFIX); + const [myProposals, setMyProposals] = useState( + myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : [] + ); + + const [isPending, setIsPending] = useState(false); + const [isModalOpened, setIsModalOpened] = useState(false); + const [proposalFunctions, setProposalFunctions] = useState([]); + + const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); + const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); + const { balance: ghstBalance } = useBalance(chainId, ghstSymbol, address) + const { threshold } = useProposalThreshold(chainId, ghstSymbol); + const { + proposalHash, + proposalDescription + } = useProposalHash(chainId, proposalFunctions); + + useEffect(() => { + const toStore = JSON.stringify(myProposals.map(id => id.toString())); + localStorage.setItem(MY_PROPOSALS_PREFIX, toStore); + }, [myProposals]); + + useEffect(() => { + ReactGA.send({ hitType: "pageview", page: "/governance/create" }); + }, []); + + const addCalldata = (calldata) => setProposalFunctions(prev => [...prev, calldata]); + const removeCalldata = (index) => setProposalFunctions(prev => prev.filter((_, i) => i !== index)); + + const storeProposal = (proposalId) => setMyProposals(prev => [...prev, proposalId]); + const removeProposal = (proposalId) => setMyProposals(prev => prev.filter(item => item !== proposalId)); + + const nativeCurrency = useMemo(() => { + const client = config?.getClient(); + return client?.chain?.nativeCurrency?.symbol; + }, [config]); + + const submitProposal = useCallback(async () => { + setIsPending(true); + + const result = await propose(chainId, address, proposalFunctions, proposalDescription); + if (result) { + storeProposal(proposalHash); + setProposalFunctions([]); + } + + setIsPending(false); + }, [chainId, address, proposalHash, proposalFunctions, proposalDescription]); + + return ( + <> + setIsModalOpened(false)} + /> + + + + + + + Proposal Functions + + + } + topRight={ + + Explore Governance + + } + > + + + {proposalFunctions.length === 0 && + + Create new proposal by adding one or more of the functions below. + + } + + + + + + + submitProposal()} + > + {isPending ? "Submitting..." : "Submit Proposal"} + + setIsModalOpened(true)} + > + Add New + + + + + + + + + {proposalFunctions.length > 0 && + + Proposal Functions + + + } + > + + + + + Function + Target + Calldata + Value + + + {proposalFunctions.map((metadata, index) => { + return parseFunctionCalldata(metadata, index, chainId, nativeCurrency, removeCalldata); + })} + + + } + + + + > + ) +} + +export default NewProposal; diff --git a/src/containers/Governance/ProposalDetails.jsx b/src/containers/Governance/ProposalDetails.jsx new file mode 100644 index 0000000..353e8a9 --- /dev/null +++ b/src/containers/Governance/ProposalDetails.jsx @@ -0,0 +1,483 @@ +import { useEffect, useState, useMemo } from "react"; +import ReactGA from "react-ga4"; +import { useParams } from 'react-router-dom'; +import { useBlock, useBlockNumber } from 'wagmi' + +import { + Box, + Container, + Typography, + Link, + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + useMediaQuery, + useTheme +} from "@mui/material"; + +import Paper from "../../components/Paper/Paper"; +import PageTitle from "../../components/PageTitle/PageTitle"; +import LinearProgressBar from "../../components/Progress/LinearProgressBar"; +import InfoTooltip from "../../components/Tooltip/InfoTooltip"; +import Chip from "../../components/Chip/Chip"; +import { PrimaryButton, SecondaryButton } from "../../components/Button"; + +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react"; + +import { formatNumber, formatCurrency, shorten } from "../../helpers"; +import { prettifySecondsInDays } from "../../helpers/timeUtil"; +import { DecimalBigNumber, } from "../../helpers/DecimalBigNumber"; + +import { convertStatusToTemplate, convertStatusToLabel } from "./helpers"; +import { parseFunctionCalldata } from "./components/functions"; + +import { networkAvgBlockSpeed } from "../../constants"; + +import { useTokenSymbol, usePastTotalSupply, usePastVotes, useBalance } from "../../hooks/tokens"; +import { + useProposalState, + useProposalProposer, + useProposalLocked, + useProposalQuorum, + useProposalVoteOf, + useProposalVotes, + useProposalDetails, + useProposalSnapshot, + useProposalDeadline, + useProposalVotingDelay, + castVote, + executeProposal, + releaseLocked +} from "../../hooks/governance"; + +import { VOTED_PROPOSALS_PREFIX } from "./helpers"; + +/////////////////////////////////////////////////////// +import Timeline from '@mui/lab/Timeline'; +import TimelineItem from '@mui/lab/TimelineItem'; +import TimelineSeparator from '@mui/lab/TimelineSeparator'; +import TimelineConnector from '@mui/lab/TimelineConnector'; +import TimelineContent from '@mui/lab/TimelineContent'; +import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent'; +import TimelineDot from '@mui/lab/TimelineDot'; +/////////////////////////// +import FastfoodIcon from '@mui/icons-material/Fastfood'; +import LaptopMacIcon from '@mui/icons-material/LaptopMac'; +import HotelIcon from '@mui/icons-material/Hotel'; +import RepeatIcon from '@mui/icons-material/Repeat'; + +const HUNDRED = new DecimalBigNumber(100n, 0); + +const ProposalDetails = ({ chainId, address, connect, config }) => { + const { id } = useParams(); + const proposalId = BigInt(id); + + const [isPending, setIsPending] = useState(false); + const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined); + + const isSemiSmallScreen = useMediaQuery("(max-width: 745px)"); + const isSmallScreen = useMediaQuery("(max-width: 650px)"); + const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); + + const theme = useTheme(); + + const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); + const { balance } = useBalance(chainId, "GHST", address); + + const { state: proposalState } = useProposalState(chainId, proposalId); + const { proposer: proposalProposer } = useProposalProposer(chainId, proposalId); + const { locked: proposalLocked } = useProposalLocked(chainId, proposalId); + const { quorum: proposalQuorum } = useProposalQuorum(chainId, proposalId); + + const { voteOf } = useProposalVoteOf(chainId, proposalId, address); + const { forVotes, againstVotes, totalVotes } = useProposalVotes(chainId, proposalId); + const { proposalDetails } = useProposalDetails(chainId, proposalId); + const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId); + + const { pastTotalSupply: totalSupply } = usePastTotalSupply(chainId, "GHST", proposalSnapshot); + const { pastVotes } = usePastVotes(chainId, "GHST", proposalSnapshot, address); + + useEffect(() => { + ReactGA.send({ hitType: "pageview", page: `/governance/${id}` }); + }, []); + + const quorumPercentage = useMemo(() => { + if (totalSupply._value === 0n) return 0; + return proposalQuorum / totalSupply * HUNDRED; + }, [proposalQuorum, totalSupply]); + + const votePercentage = useMemo(() => { + if (totalSupply._value === 0n) return 0; + const value = (totalVotes?._value ?? 0n) * 100n / (totalSupply?._value ?? 1n); + return new DecimalBigNumber(value, 0); + }, [totalVotes, totalSupply]); + + const forPercentage = useMemo(() => { + if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); + const value = (forVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n); + return new DecimalBigNumber(value, 2); + }, [forVotes, totalSupply]); + + const againstPercentage= useMemo(() => { + if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); + const value = (againstVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n); + return new DecimalBigNumber(value, 2); + }, [againstVotes, totalSupply]); + + const voteWeightPercentage = useMemo(() => { + if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); + const value = (pastVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n); + return new DecimalBigNumber(value, 2); + }, [pastVotes, totalSupply]); + + const voteValue = useMemo(() => { + if (totalVotes?._value == 0n) { + return 0; + } + return Number(forVotes._value * 100n / totalVotes._value); + }, [forVotes, totalVotes]); + + const voteTarget = useMemo(() => { + const first = (5n * againstVotes._value + forVotes._value); + const second = BigInt(Math.floor(Math.sqrt(Number(totalVotes._value)))); + const bias = 3n * first + second; + const denominator = totalVotes._value + bias; + + if (denominator === 0n) { + return 80; + } + + return Number(totalVotes?._value * 100n / denominator); + }, [againstVotes, forVotes, totalVotes]); + + const nativeCurrency = useMemo(() => { + const client = config?.getClient(); + return client?.chain?.nativeCurrency?.symbol; + }, [config]); + + const etherscanLink = useMemo(() => { + const client = config.getClient(); + let url = client?.chain?.blockExplorers?.default?.url; + if (url) { + url = url + `/address/${proposalProposer}`; + } + return url; + }, [proposalProposer, config]); + + const handleVote = async (against) => { + setIsPending(true); + const support = against ? 0 : 1; + const result = await castVote(chainId, address, proposalId, support); + + if (result) { + const toStore = JSON.stringify(proposalId.toString()); + localStorage.setItem(VOTED_PROPOSALS_PREFIX, toStore); + } + + setIsPending(true); + } + + const handleExecute = async () => { + setIsPending(true); + await executeProposal(chainId, address, proposalId); + setIsPending(true); + } + + const handleRelease = async (proposalId) => { + setIsPending(true); + await releaseLocked(chainId, address, proposalId); + setIsPending(true); + } + + return ( + + + Proposal details, need more in-depth description + + } + /> + + + + + + Progress + + + + } + topRight={ + + View Forum + + } + > + + + + + For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forPercentage?.toString(), 1)}%) + + + Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstPercentage?.toString(), 1)}%) + + + + + + + + Proposer + + {`${shorten(proposalProposer)}`} + + + + Locked + {formatCurrency(proposalLocked, 4, ghstSymbol)} + + + + + + + Quorum + + + {formatNumber(proposalQuorum.toString(), 4)} ({formatNumber(quorumPercentage, 1)}%) + + + + + Total + + + {formatNumber(totalSupply.toString(), 4)} ({formatNumber(votePercentage, 1)}%) + + + + + Votes + + + {formatNumber(pastVotes.toString(), 4)} ({formatNumber(voteWeightPercentage, 1)}%) + + + + + {address === undefined || address === "" + ? connect()}>Connect + : voteOf === 0n + ? <> + handleVote(1)} + > + For + + handleVote(0)} + > + Against + + > + : + {`Voted ${voteOf === 1n ? "Against" : "For"}`} + + } + + + + + + + Timeline + + + } + > + + + + + + + + Executable Code + + + } + > + + + + + Function + Target + Calldata + Value + + + {proposalDetails?.map((metadata, index) => { + return parseFunctionCalldata(metadata, index, chainId, nativeCurrency); + })} + + + + + + ) +} + +const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked, proposalId, chainId, state, address, isProposer }) => { + const { delay: propsalVotingDelay } = useProposalVotingDelay(chainId, proposalId); + const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId); + const { deadline: proposalDeadline } = useProposalDeadline(chainId, proposalId); + + const voteStarted = useMemo(() => { + if (proposalSnapshot && propsalVotingDelay) { + return proposalSnapshot > propsalVotingDelay ? proposalSnapshot - propsalVotingDelay : 0; + } + return 0n; + }, [proposalSnapshot, propsalVotingDelay]); + + return ( + + + + + + + + {isProposer && address === "" ? connect() : handleRelease()} + > + {address === "" ? "Connect" : "Release"} + } + address === "" ? connect() : handleExecute()} + > + {address === "" ? "Connect" : "Execute"} + + + + ) +} + +const VotingTimelineItem = ({ position, isFirst, isLast, chainId, occured, message }) => { + const { data: blockNumber } = useBlockNumber({ chainId, watch: true }); + const { data: blockInfo } = useBlock({ + chainId: chainId, + blockNumber: occured, + query: { + enabled: Boolean(occured) + } + }); + + const timestamp = useMemo(() => { + if (blockInfo && blockNumber > occured) { + const timestamp = Number(blockInfo?.timestamp ?? 0n) * 1000; + return new Date(timestamp).toLocaleString(); + } + + const blocksRemaining = Number(occured) - Number(blockNumber); + const secondsRemaining = blocksRemaining * Number(networkAvgBlockSpeed(chainId)); + const predictedTimestamp = Math.floor(Date.now() / 1000) + secondsRemaining; + + return new Date(predictedTimestamp * 1000).toLocaleString(); + }, [chainId, occured, blockInfo, blockNumber]); + + return ( + + + {message} + + + + + + + + + + {timestamp} + + + ) +} + +export default ProposalDetails; diff --git a/src/containers/Governance/components/GovernanceInfoText.jsx b/src/containers/Governance/components/GovernanceInfoText.jsx new file mode 100644 index 0000000..f551d7f --- /dev/null +++ b/src/containers/Governance/components/GovernanceInfoText.jsx @@ -0,0 +1,23 @@ +import { Link, Typography, useTheme } from "@mui/material"; + +const GovernanceInfoText = () => { + const theme = useTheme(); + return ( + + ghostDAO’s adaptive governance system algorithmically sets minimum collateral based on activity. + Learn more here. + + ) +}; + +export default GovernanceInfoText; diff --git a/src/containers/Governance/components/Metric.jsx b/src/containers/Governance/components/Metric.jsx new file mode 100644 index 0000000..9f82084 --- /dev/null +++ b/src/containers/Governance/components/Metric.jsx @@ -0,0 +1,65 @@ +import Metric from "../../../components/Metric/Metric"; + +import { formatCurrency, formatNumber } from "../../../helpers"; +import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber"; +import { getTokenDecimals } from "../../../hooks/helpers"; +import { useTotalSupply } from "../../../hooks/tokens"; +import { + useMinQuorum, + useProposalThreshold, + useProposalCount +} from "../../../hooks/governance"; + +export const MinQuorumPercentage = props => { + const { numerator, denominator, percentage } = useMinQuorum(props.chainId); + const { totalSupply } = useTotalSupply(props.chainId, "GHST"); + const decimals = getTokenDecimals(props.ghstSymbol); + + const value = new DecimalBigNumber( + (totalSupply?._value ?? 0n) * numerator / denominator, + decimals + ); + + const _props = { + ...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 = `${tokenValue} (${percentageValue}%)`; + else _props.isLoading = true; + + return ; +}; + +export const ProposalThreshold = props => { + const { threshold } = useProposalThreshold(props.chainId, props.ghstSymbol); + + const _props = { + ...props, + label: "Min Collateral", + tooltip: `Minimum $${props.ghstSymbol} required to be locked to create a proposal`, + }; + + if (threshold) _props.metric = `${formatCurrency(threshold.toString(), 2, props.ghstSymbol)}`; + else _props.isLoading = true; + + return ; +} + +export const ProposalsCount = props => { + const { proposalCount } = useProposalCount(props.chainId); + + const _props = { + ...props, + label: `Proposal Count`, + tooltip: `Total proposals created from current governor of ghostDAO`, + }; + + if (proposalCount || proposalCount === 0n) _props.metric = `${formatNumber(proposalCount.toString(), 0)}`; + else _props.isLoading = true; + + return ; +} diff --git a/src/containers/Governance/components/ProposalInfoText.jsx b/src/containers/Governance/components/ProposalInfoText.jsx new file mode 100644 index 0000000..2913acc --- /dev/null +++ b/src/containers/Governance/components/ProposalInfoText.jsx @@ -0,0 +1,23 @@ +import { Link, Typography, useTheme } from "@mui/material"; + +const ProposalInfoText = () => { + const theme = useTheme(); + return ( + + Important: Only the 10 most recent proposals are displayed. Only one proposal can be active at a time. + Learn more here. + + ) +}; + +export default ProposalInfoText; diff --git a/src/containers/Governance/components/ProposalModal.jsx b/src/containers/Governance/components/ProposalModal.jsx new file mode 100644 index 0000000..9218b5b --- /dev/null +++ b/src/containers/Governance/components/ProposalModal.jsx @@ -0,0 +1,115 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; +import { Box, Typography, useTheme } from "@mui/material"; + +import Modal from "../../../components/Modal/Modal"; +import Select from "../../../components/Select/Select"; +import { PrimaryButton, TertiaryButton } from "../../../components/Button"; + +import { + getFunctionArguments, + getFunctionCalldata, + getFunctionDescription, + allPossibleFunctions +} from "./functions"; + +const ProposalModal = ({ isOpened, closeModal, nativeCurrency, ftsoSymbol, addCalldata, chainId }) => { + const [selectedOption, setSelectedOption] = useState(); + const [renderArguments, setRenderArguments] = useState(false); + + const handleChange = (event) => { + setSelectedOption(event.target.value); + }; + + const headerLabel = useMemo(() => { + const data = allPossibleFunctions.find(obj => obj.value === selectedOption); + if (data && renderArguments) { + return data.label; + } + return "Executable Code"; + }, [selectedOption, renderArguments]); + + const handleCalldata = useCallback(() => { + addCalldata(getFunctionCalldata(selectedOption, chainId)); + setSelectedOption(null); + closeModal(); + }, [selectedOption, chainId]); + + const handleClose = () => { + setSelectedOption(null); + setRenderArguments(false); + closeModal(); + } + + const handleAddCalldata = (calldata) => { + addCalldata(calldata); + handleClose(); + } + + const ArgumentsSteps = useMemo(() => getFunctionArguments(selectedOption, chainId), [selectedOption]); + + return ( + + {headerLabel} + + } + open={isOpened} + onClose={handleClose} + maxWidth="460px" + minHeight="200px" + > + + {renderArguments + ? setRenderArguments(false)} + /> + : setRenderArguments(true)} + ready={ArgumentsSteps !== null} + /> + } + + + ) +} + +const InitialStep = ({ selectedOption, handleChange, handleCalldata, handleProceed, ready }) => { + const theme = useTheme(); + const functionDescription = useMemo(() => getFunctionDescription(selectedOption), [selectedOption]); + return ( + + { + if (!selected || selected.length === 0) { + return ( + + Select function + + ); + } + + return allPossibleFunctions.find(opt => opt.value === selected)?.label || selected; + }} + /> + {functionDescription} + {ready + ? handleProceed()} fullWidth>Proceed + : handleCalldata()} fullWidth>Create Function + } + + ) +} + +export default ProposalModal; diff --git a/src/containers/Governance/components/ProposalsList.jsx b/src/containers/Governance/components/ProposalsList.jsx new file mode 100644 index 0000000..e8534e6 --- /dev/null +++ b/src/containers/Governance/components/ProposalsList.jsx @@ -0,0 +1,360 @@ +import { useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; + +import { + Box, + Link, + Tabs, + Tab, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme, + useMediaQuery +} from "@mui/material"; +import { getBlockNumber } from "@wagmi/core"; + +import GhostStyledIcon from "../../../components/Icon/GhostIcon"; +import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react"; + +import { networkAvgBlockSpeed } from "../../../constants"; +import { prettifySecondsInDays, prettifySeconds } from "../../../helpers/timeUtil"; + +import Chip from "../../../components/Chip/Chip"; +import Modal from "../../../components/Modal/Modal"; +import Paper from "../../../components/Paper/Paper"; +import LinearProgressBar from "../../../components/Progress/LinearProgressBar"; +import { PrimaryButton, TertiaryButton } from "../../../components/Button"; + +import ProposalInfoText from "./ProposalInfoText"; +import { + convertStatusToTemplate, + convertStatusToLabel, + MY_PROPOSALS_PREFIX, + VOTED_PROPOSALS_PREFIX +} from "../helpers"; + +import { useScreenSize } from "../../../hooks/useScreenSize"; + +import { + useProposals, +} from "../../../hooks/governance"; + +const MAX_PROPOSALS_TO_SHOW = 10; + +const ProposalsList = ({ chainId, address, config }) => { + const isSmallScreen = useScreenSize("md"); + const navigate = useNavigate(); + const theme = useTheme(); + + const [proposalsFilter, setProposalFilter] = useState("active"); + + const myStoredProposals = localStorage.getItem(MY_PROPOSALS_PREFIX); + const [myProposals, setMyProposals] = useState( + myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : [] + ); + + const storedVotedProposals = localStorage.getItem(VOTED_PROPOSALS_PREFIX); + const [votedProposals, setVotedProposals] = useState( + storedVotedProposals ? JSON.parse(storedVotedProposals).map(id => BigInt(id)) : [] + ); + + const searchedIndexes = useMemo(() => { + switch (proposalsFilter) { + case "voted": + return votedProposals; + case "created": + return myProposals; + default: + return undefined; + } + }, [proposalsFilter]); + + const [blockNumber, setBlockNumber] = useState(0n); + const { proposals } = useProposals(chainId, MAX_PROPOSALS_TO_SHOW, searchedIndexes); + + getBlockNumber(config).then(block => setBlockNumber(block)); + + if (proposals?.length === 0 && proposalsFilter === "active") { + return ( + + No proposals yet + + ); + } + + if (isSmallScreen) { + return ( + + + Proposals + + + } + > + + {proposals?.map(proposal => ( + navigate(`/governance/${proposal.hashes.full}`)} + /> + ))} + + + {proposalsFilter === "active" && + + } + + ); + } + + return ( + + + Proposals + + + + View Forum + + + } + > + + + + {proposals?.map(proposal => ( + navigate(`/governance/${proposal.hashes.full}`)} + /> + ))} + + + {proposalsFilter === "active" && + + } + + ); +} + +const ProposalTable = ({ children }) => ( + + + + + Proposal ID + Status + Vote Ends + Voting Stats + + + + + {children} + + +); + +const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => { + const theme = useTheme(); + + const voteValue = useMemo(() => { + const againstVotes = proposal?.votes?.at(0)?._value ?? 0n; + const forVotes = proposal?.votes?.at(1)?._value ?? 0n; + const totalVotes = againstVotes + forVotes; + if (totalVotes == 0) { + return 0; + } + return Number(forVotes * 100n / totalVotes); + }, [proposal]); + + const voteTarget = useMemo(() => { + const againstVotes = proposal?.votes?.at(0)?._value ?? 0n; + const forVotes = proposal?.votes?.at(1)?._value ?? 0n; + const totalVotes = againstVotes + forVotes; + + const first = (5n * againstVotes + forVotes); + const second = BigInt(Math.floor(Math.sqrt(Number(totalVotes)))); + const bias = 3n * first + second; + const denominator = totalVotes + bias; + + if (denominator === 0n) { + return 80; + } + + return Number(totalVotes * 100n / denominator); + }, [proposal]); + + return ( + + + GDP-{proposal.hashes.short} + + + + + + + + + {convertDeadline( + proposal.deadline, + blockNumber, + chainId + )} + + + + + + + + + + + {(proposal.state === "Active" || proposal.state === "Succeeded") && openProposal()} + sx={{ maxWidth: "130px" }} + > + {proposal.state === "Succeeded" ? "Execute" : "Vote"} + } + {(proposal.state !== "Active" && proposal.state !== "Succeeded") && openProposal()} + sx={{ alignSelf: "right", maxWidth: "130px" }} + > + View + } + + + ); +} + +const ProposalCard = ({ proposal, blockNumber, openProposal, chainId }) => { + const theme = useTheme(); + const isSmallScreen = useMediaQuery('(max-width: 450px)'); + + return ( + + + + + GIP-{proposal.hashes.short} + + + + {convertDeadline( + proposal.deadline, + blockNumber, + chainId + )} + + + + + + + + + {(proposal.state === "Active" || proposal.state === "Succeeded") && openProposal()} + > + {proposal.state === "Succeeded" ? "Execute" : "Vote"} + } + {(proposal.state !== "Active" && proposal.state !== "Succeeded") && openProposal()} + > + View + } + + + ); +}; + +const ProposalFilterTrigger = ({ trigger, setTrigger }) => { + return ( + setTrigger(view)} + TabIndicatorProps={{ style: { display: "none" } }} + > + + + + + ) +} + +const convertDeadline = (deadline, blockNumber, chainId) => { + const diff = blockNumber > deadline ? blockNumber - deadline : deadline - blockNumber; + const voteSeconds = Number(diff * networkAvgBlockSpeed(chainId)); + + const result = prettifySeconds(voteSeconds, "mins"); + if (result === "now") { + return new Date(Date.now()).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + } + + return `in ${result}`; +} + +export default ProposalsList; diff --git a/src/containers/Governance/components/functions/AuditReserves.jsx b/src/containers/Governance/components/functions/AuditReserves.jsx new file mode 100644 index 0000000..f1de4a3 --- /dev/null +++ b/src/containers/Governance/components/functions/AuditReserves.jsx @@ -0,0 +1,39 @@ +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) => { + const value = 0n; + const label = "auditReserves"; + const target = DAO_TREASURY_ADDRESSES[chainId]; + const calldata = encodeFunctionData({ + abi: TreasuryAbi, + functionName: 'auditReserves', + }); + return { label, target, calldata, value }; +} + +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; +} + +export const AuditReservesParsed = (props) => { + return ( + <> + {props.isTable && } + > + ) +} + +const AuditReservesParsedCell = (props) => { + return +} diff --git a/src/containers/Governance/components/functions/CreateBond.jsx b/src/containers/Governance/components/functions/CreateBond.jsx new file mode 100644 index 0000000..047a30e --- /dev/null +++ b/src/containers/Governance/components/functions/CreateBond.jsx @@ -0,0 +1,334 @@ +import { useRef, useMemo, useState, useEffect } from "react"; +import { encodeFunctionData } from 'viem'; +import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material"; + +import Modal from "../../../../components/Modal/Modal"; +import Select from "../../../../components/Select/Select"; +import { PrimaryButton, TertiaryButton } from "../../../../components/Button"; +import { shorten } from "../../../../helpers"; +import { useTokenSymbol } from "../../../../hooks/tokens"; + +import { ArgumentsWrapper, BooleanTrigger, ArgumentInput, ParsedCell } from "./index"; + +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"; + 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 = "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) => { + const [isOpened, setIsOpened] = useState(false); + const { symbol: ftsoSymbol } = useTokenSymbol(props.chainId, "FTSO"); + return ( + <> + + View create + + } + open={isOpened} + onClose={() => setIsOpened(false)} + maxWidth="460px" + minHeight="200px" + > + + + + + {props.isTable && } + > + ) +} + +const CreateBondParsedCell = (props) => { + return +} + +export const CreateBondSteps = ({ nativeCurrency, ftsoSymbol, chainId, toInitialStep, addCalldata, args }) => { + const createMode = args === undefined; + + const [step, setStep] = useState(1); + const [nextDisabled, setNextDisabled] = useState(false); + + const [capacity, setCapacity] = useState(args?.at(0)?.at(0)); + const [initialPrice, setInitialPrice] = useState(args?.at(0)?.at(1)); + const [debtBuffer, setDebtBuffer] = useState(args?.at(0)?.at(2)); + + const [depositInterval, setDepositInterval] = useState(args?.at(3)?.at(0)); + const [tuneInterval, setTuneInterval] = useState(args?.at(3)?.at(1)); + + const [tokenAddress, setTokenAddress] = useState(args?.at(2)); + const [capacityInQuote, setCapacityInQuote] = useState(args?.at(4)?.at(0) ?? true); + const [fixedTerm, setFixedTerm] = useState(args?.at(4)?.at(1) ?? false); + + const [vestingLength, setVestingLength] = useState(args?.at(1)?.at(0)); + const [conclusionTimestamp, setConclusionTimestamp] = useState(args?.at(1)?.at(1)); + + const { symbol: reserveSymbol } = useTokenSymbol(chainId, RESERVE_ADDRESSES[chainId]); + + const handleProceed = () => { + 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 empty = () => {}; + + const incrementStep = () => { + setStep(prev => prev + 1); + } + + const decrementStep = () => { + if (step > 1) setStep(prev => prev - 1); + 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], symbol: `${ftsoSymbol}-${nativeCurrency} LP`, label: `${ftsoSymbol}-${nativeCurrency} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` }, + { value: RESERVE_ADDRESSES[chainId], symbol: reserveSymbol, label: `${reserveSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` }, + ]; + + return ( + + {step === 1 && } + {step === 2 && token.value === tokenAddress).symbol} + ftsoSymbol={ftsoSymbol} + capacity={capacity} + setCapacity={createMode ? setCapacity : empty} + initialPrice={initialPrice} + setInitialPrice={createMode ? setInitialPrice : empty} + debtBuffer={debtBuffer} + setDebtBuffer={createMode ? setDebtBuffer : empty} + />} + {step === 3 && } + {step === 4 && } + + + decrementStep()} fullWidth>Back + incrementStep()} fullWidth>Next + + {(step === 4 && createMode) && handleProceed()} fullWidth>Create Function} + + + ); +} + +const MarketArguments = ({ + capacityInQuote, + currencySymbol, + ftsoSymbol, + capacity, + setCapacity, + initialPrice, + setInitialPrice, + debtBuffer, + setDebtBuffer +}) => { + return ( + + + + + + ) +} + +const IntervalsArguments = ({ + depositInterval, + setDepositInterval, + tuneInterval, + setTuneInterval, +}) => { + return ( + + + + + ) +} + +const TermsAgruments = ({ + fixedTerm, + vestingLength, + setVestingLength, + conclusionTimestamp, + setConclusionTimestamp, +}) => { + return ( + + + + + ) +} + +const TokenAndBooleansArguments = ({ + createMode, + tokenAddress, + possibleTokens, + nativeCurrency, + ftsoSymbol, + setTokenAddress, + capacityInQuote, + setCapacityInQuote, + fixedTerm, + setFixedTerm, +}) => { + const theme = useTheme(); + const [selectedOption, setSelectedOption] = useState(tokenAddress); + + const handleChange = (event) => { + if (createMode) { + setSelectedOption(event.target.value); + setTokenAddress(event.target.value); + } + }; + + return ( + + setCapacityInQuote(true)} + setRightValue={() => setCapacityInQuote(false)} + tooltip={`Determines how the bond market capacity is measured. True = measured in ${nativeCurrency}, False = measured in ${ftsoSymbol}.`} + /> + setFixedTerm(true)} + setRightValue={() => setFixedTerm(false)} + tooltip={`Defines the bond maturity model. True = each purchase matures after vesting duration from its purchase date. False = all purchases mature at the conclusion timestamp.`} + /> + + { + 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 new file mode 100644 index 0000000..fd488d5 --- /dev/null +++ b/src/containers/Governance/components/functions/SetAdjustment.jsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { encodeFunctionData } from 'viem'; +import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material"; + +import Modal from "../../../../components/Modal/Modal"; +import { PrimaryButton, TertiaryButton } from "../../../../components/Button"; + +import { + DISTRIBUTOR_ADDRESSES, +} from "../../../../constants/addresses"; +import { abi as DistributorAbi } from "../../../../abi/GhostDistributor.json"; + +import { BooleanTrigger, ArgumentInput, ParsedCell } from "./index"; + +export const prepareSetAdjustmentCalldata = (chainId, rateChange, targetRate, increase) => { + const value = 0n; + const label = "setAdjustment"; + const target = DISTRIBUTOR_ADDRESSES[chainId]; + const calldata = encodeFunctionData({ + abi: DistributorAbi, + functionName: 'setAdjustment', + args: [rateChange, targetRate, increase] + }); + return { label, target, calldata, value }; +} + +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) => { + const [isOpened, setIsOpened] = useState(false); + return ( + <> + + View setAdjustment + + } + open={isOpened} + onClose={() => setIsOpened(false)} + maxWidth="460px" + minHeight="200px" + > + + + + + {props.isTable && } + > + ) +} + +const SetAdjustmentParsedCell = (props) => { + return +} + +export const SetAdjustmentSteps = ({ chainId, toInitialStep, addCalldata, args }) => { + const createMode = args === undefined; + + const [rate, setRate] = useState(args?.at(0)); + const [target, setTarget] = useState(args?.at(1)); + const [increase, setIncrease] = useState(args?.at(1) ?? true); + + const handleProceed = () => { + addCalldata(prepareSetAdjustmentCalldata(chainId, rate, target, increase)); + } + + return ( + + + createMode ? setIncrease(true) : {}} + setRightValue={() => createMode ? setIncrease(false) : {}} + label="add" + tooltip="Adjusts the current rate toward the target eCSPR staking rate. True = increase rate, False = decrease rate." + /> + {}} + tooltip="Each epoch, the current staking rate increases by this amount until the target rate is reached [e.g. 154 => APY = (1 + (Current + 154)/1,000,000)^(365*3)]." + /> + {}} + tooltip="The target staking rate to be achieved [e.g. 633 => APY = (1 + 633/1,000,000)^(365*3) – 1 = 100%]." + /> + + {createMode && + + Back + Next + + handleProceed()} + fullWidth + > + Create Function + + } + + ); +} diff --git a/src/containers/Governance/components/functions/index.jsx b/src/containers/Governance/components/functions/index.jsx new file mode 100644 index 0000000..ca7e58f --- /dev/null +++ b/src/containers/Governance/components/functions/index.jsx @@ -0,0 +1,308 @@ +import { useRef, useMemo, useState } from "react"; +import { Box, Typography, Link, TableCell, TableRow, useTheme } from "@mui/material"; +import { decodeFunctionData } from 'viem'; +import toast from "react-hot-toast"; + +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 = "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: "auditReserves" }, + { value: "setAdjustment", label: "setAdjustment" }, + { value: "create", label: "create" }, +]; + +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 labelOrName = label ?? (functionName ?? "Unknown"); + + const remove = removeCalldata && (() => removeCalldata(index)); + + switch (functionName) { + case "auditReserves": + return ; + case "setAdjustment": + return ; + case "create": + return ; + default: + return ; + } +} + +export const getFunctionArguments = (functionName) => { + switch (functionName) { + case "auditReserves": + return null; + case "setAdjustment": + return SetAdjustmentSteps; + case "create": + return CreateBondSteps; + default: + return null; + } +} + +export const getFunctionCalldata = (functionName, chainId) => { + switch (functionName) { + case "auditReserves": + return prepareAuditReservesCalldata(chainId); + case "setAdjustment": + return prepareSetAdjustmentCalldata(chainId); + case "create": + return prepareCreateBondCalldata(chainId); + default: + return null; + } +} + +export const getFunctionDescription = (functionName) => { + switch (functionName) { + case "auditReserves": + return prepareAuditReservesDescription; + case "setAdjustment": + return prepareSetAdjustmentDescription; + case "create": + return prepareCreateBondDescription; + default: + return DEFAULT_DESCRIPTION; + } +} + +export const BooleanValue = ({ left, text, isSelected, setSelected }) => { + const theme = useTheme(); + return ( + setSelected()} + display="flex" + justifyContent="center" + alignItems="center" + sx={{ + cursor: "pointer", + borderRadius: left ? "12px 0 0 12px" : "0 12px 12px 0", + flex: 1, + border: "2px solid #fff", + borderLeft: left ? "2px solid #fff" : "none", + borderRight: left ? "none" : "2px solid #fff", + background: `${isSelected ? "#fff" : theme.colors.gray[600] }` + }} + > + + {text} + + + ) +} + +export const ArgumentsWrapper = ({ label, tooltip, children }) => { + return ( + + + {label} + + + {children} + + ) +} + +export const BooleanTrigger = ({ value, label, tooltip, leftText, rightText, setLeftValue, setRightValue }) => { + return ( + + + + + + + ) +} + +export const ArgumentInput = ({ + endString, + label, + tooltip, + value, + setValue, + disabled, + inputType = "number", + placeholder = "0", + maxWidth = "100%" +}) => { + const theme = useTheme(); + const ref = useRef(null); + + return ( + + + {label} + + + { + ref.current && ref.current.focus(); + }} + > + + setValue(e.target.value)} + sx={{ flex: 1 }} + /> + {endString && ( + + {endString} + + )} + + + + ) +} + +export const ParsedCell = (props) => { + const [isCopied, setIsCopied] = useState(false); + + const etherscanLink = useMemo(() => { + const client = config.getClient(); + let url = client?.chain?.blockExplorers?.default?.url; + if (url) { + url = url + `/address/${props.target}`; + } + return url; + }, [props, config]); + + const handleCalldataCopy = async () => { + try { + await navigator.clipboard.writeText(props.calldata); + setIsCopied(true); + toast.success("Calldata successfully copied to your clipboard.", { duration: 2000 }); + + setTimeout(() => setIsCopied(false), 2000); + } catch (err) { + toast.error("Could not copy calldata to clipboard, check logs for error details.", { duration: 5000 }); + console.error(err); + } + }; + + return ( + + + {props.label} + + + + + {shorten(props.target)} + + + + + + {shorten(props.calldata)} + + + + + {formatCurrency(props.value, 4, props.nativeCoin)} + + + + + {props.args && props.setIsOpened(!props.isOpened)}>View} + {props.remove && props.remove()}>Delete} + + + + ) +} diff --git a/src/containers/Governance/helpers.js b/src/containers/Governance/helpers.js new file mode 100644 index 0000000..a8003e8 --- /dev/null +++ b/src/containers/Governance/helpers.js @@ -0,0 +1,38 @@ +export const MY_PROPOSALS_PREFIX = "MY_PROPOSALS"; +export const VOTED_PROPOSALS_PREFIX = "VOTED_PROPOSALS"; + +export const convertStatusToTemplate = (status) => { + switch (status) { + case 7: + return 'darkGray'; + case 2: + return 'warning'; + case 4: + return 'success'; + case 3: + return 'error'; + default: + return 'darkGray'; + } +} + +export const convertStatusToLabel = (status) => { + switch (status) { + case 1: + return "Active"; + case 2: + return "Canceled"; + case 3: + return "Defeated"; + case 4: + return "Succeeded"; + case 5: + return "Queued"; + case 6: + return "Expired"; + case 7: + return "Executed"; + default: + return "Pending"; + } +} diff --git a/src/containers/Stake/StakeContainer.jsx b/src/containers/Stake/StakeContainer.jsx index 49e3f4a..e404bcc 100644 --- a/src/containers/Stake/StakeContainer.jsx +++ b/src/containers/Stake/StakeContainer.jsx @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction, useState, useEffect } from "react"; -import { Box, Container, Grid, Divider, Typography, Link, useMediaQuery, useTheme } from "@mui/material"; +import { Box, Container, Grid, Divider, Typography, useMediaQuery, useTheme } from "@mui/material"; import ReactGA from "react-ga4"; import Paper from "../../components/Paper/Paper"; @@ -47,7 +47,7 @@ export const StakeContainer = ({ chainId, address, connect }) => { } return ( - + { } return ( - + + + Farm Pools + + + } + > diff --git a/src/containers/WethWrapper/WethWrapper.jsx b/src/containers/WethWrapper/WethWrapper.jsx index 79cbdcd..8656d41 100644 --- a/src/containers/WethWrapper/WethWrapper.jsx +++ b/src/containers/WethWrapper/WethWrapper.jsx @@ -137,7 +137,7 @@ const WethWrapper = ({ chainId, address, config, connect }) => { - + { + const { data, error } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "voteOf", + args: [proposalId, who], + scopeKey: `voteOf-${chainId}-${proposalId?.toString()}-${who}`, + chainId: chainId, + }); + const voteOf = data ? BigInt(data) : 0n; + return { voteOf }; +} + +export const useProposalHash = (chainId, functions) => { + const { proposalCount } = useProposalCount(chainId); + const proposalDescription = `Proposal #${proposalCount}`; + const descriptionHash = keccak256(stringToBytes(proposalDescription)); + + const { data: proposalHash, refetch } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "hashProposal", + args: [ + functions.map(f => f.target), + functions.map(f => f.value), + functions.map(f => f.calldata), + descriptionHash + ], + scopeKey: `hashProposal-${chainId}-${functions.map(f => f.calldata)}`, + chainId: chainId, + }); + + return { proposalHash, proposalDescription }; +} + +export const useActiveProposedLock = (chainId) => { + const decimals = getTokenDecimals("GHST"); + + const { data: activeProposedLock, refetch } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "activeProposedLock", + scopeKey: `activeProposedLock-${chainId}`, + chainId: chainId, + }); + + const result = new DecimalBigNumber( + activeProposedLock ? activeProposedLock : 0n, + decimals + ); + + return result; +} + +export const useMinQuorum = (chainId) => { + const { data: quorumNumerator, refetch: quorumNumeratorRefetch } = useReadContract({ + abi: GovernorVotesQuorumFractionAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "quorumNumerator", + scopeKey: `quorumNumerator-${chainId}`, + chainId: chainId, + }); + + const { data: quorumDenominator, refetch: quorumDenominatorRefetch } = useReadContract({ + abi: GovernorVotesQuorumFractionAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "quorumDenominator", + scopeKey: `quorumDenominator-${chainId}`, + chainId: chainId, + }); + + 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 { data } = useReadContract({ + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalThreshold", + scopeKey: `proposalThreshold-${chainId}`, + chainId: chainId, + }); + + const { data: activeProposedLock } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "activeProposedLock", + scopeKey: `activeProposedLock-${chainId}`, + chainId: chainId, + }); + + let threshold = new DecimalBigNumber(data ?? 0n, decimals); + + const { proposalCount } = useProposalCount(chainId); + const { proposalId } = useProposalDetailsAt(chainId, proposalCount === 0n ? 0n : proposalCount - 1n); + const { state } = useProposalState(chainId, proposalId); + + if (state < 2) { + threshold = new DecimalBigNumber(activeProposedLock ?? 0n, decimals); + } + + return { threshold }; +} + +export const useProposalCount = (chainId) => { + 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 useProposalState = (chainId, proposalId) => { + const { data } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "state", + args: [proposalId], + scopeKey: `state-${chainId}`, + chainId: chainId, + }); + const state = data ?? 0; + return { state }; +} + +export const useProposalProposer = (chainId, proposalId) => { + const { data } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalProposer", + args: [proposalId], + scopeKey: `proposalProposer-${chainId}`, + chainId: chainId, + }); + const proposer = data ? data : "0x0000000000000000000000000000000000000000"; + return { proposer }; +} + +export const useProposalLocked = (chainId, proposalId) => { + const decimals = getTokenDecimals(name); + const { data } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "lockedAmounts", + args: [proposalId], + scopeKey: `lockedAmounts-${chainId}`, + chainId: chainId, + }); + const locked = new DecimalBigNumber(data ?? 0n, decimals); + return { locked } +} + +export const useProposalQuorum = (chainId, proposalId) => { + const decimals = getTokenDecimals(name); + const { snapshot } = useProposalSnapshot(chainId, proposalId); + const { data } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "quorum", + args: [snapshot], + scopeKey: `quorum-${chainId}`, + chainId: chainId, + }); + const quorum = new DecimalBigNumber(data ?? 0n, decimals); + return { quorum } +} + +export const useProposalVotes = (chainId, proposalId) => { + const decimals = getTokenDecimals(name); + + const { data } = useReadContract({ + abi: GovernorCountingAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalVotes", + args: [proposalId], + scopeKey: `proposalVotes-${chainId}`, + chainId: chainId, + }); + + const againstVotes = new DecimalBigNumber(data?.at(0) ?? 0n, decimals); + const forVotes = new DecimalBigNumber(data?.at(1) ?? 0n, decimals); + const totalVotes = new DecimalBigNumber( + (data?.at(0) ?? 0n) + (data?.at(1) ?? 0n), + decimals + ); + + return { forVotes, againstVotes, totalVotes } +} + +export const useProposalSnapshot = (chainId, proposalId) => { + const { data, error } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalSnapshot", + args: [proposalId], + scopeKey: `proposalSnapshot-${chainId}`, + chainId: chainId, + }); + const snapshot = data ?? 0n; + return { snapshot }; +} + +export const useProposalDetailsAt = (chainId, index) => { + const { data, error } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalDetailsAt", + args: [index], + scopeKey: `proposalDetailsAt-${chainId}-${index}`, + chainId: chainId, + }); + + const proposalId = data?.at(0) ?? 0n; + const proposalDetailsAt = data?.at(1)?.map((target, index) => ({ + target, + value: data?.at(2)?.at(index), + calldata: data?.at(3)?.at(index), + + })); + + return { proposalDetailsAt, proposalId }; +} + +export const useProposalDetails = (chainId, proposalId) => { + const { data } = useReadContract({ + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalDetails", + args: [proposalId], + scopeKey: `proposalDetails-${chainId}-${proposalId}`, + chainId: chainId, + }); + + const proposalDetails = data?.at(0)?.map((target, index) => ({ + target, + value: data?.at(1)?.at(index), + calldata: data?.at(2)?.at(index), + })); + + return { proposalDetails }; +} + +export const useProposalDeadline = (chainId, proposalId) => { + const { data, refetch } = useReadContract({ + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + args: [proposalId], + functionName: "proposalDeadline", + scopeKey: `proposalDeadline-${chainId}`, + chainId: chainId, + }); + const deadline = data ?? 0n; + return { deadline }; +} + +export const useProposalVotingDelay = (chainId) => { + const { data } = useReadContract({ + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "votingDelay", + scopeKey: `votingDelay-${chainId}`, + chainId: chainId, + }); + const delay = data ?? 0n; + return { delay }; +} + +export const useProposals = (chainId, depth, searchedIndexes) => { + const decimals = getTokenDecimals("GHST"); + const ghstAbi = getTokenAbi("GHST"); + const ghstAddress = getTokenAddress(chainId, "GHST"); + + const { proposalCount } = useProposalCount(chainId); + + const start = Number(proposalCount); + const end = Math.max(0, start - depth); + const indexes = searchedIndexes + ? searchedIndexes.map((_, i) => i) + : [...Array(start - end)].map((_, i) => start - 1 - i); + + const { data: proposalsDetailsAt } = useReadContracts({ + contracts: indexes?.map(index => { + return { + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalDetailsAt", + args: [index], + scopeKey: `proposalDetailsAt-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: proposalDetails } = useReadContracts({ + contracts: searchedIndexes?.map(index => { + return { + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalDetails", + args: [index], + scopeKey: `proposalDetails-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: proposalDeadlines } = useReadContracts({ + contracts: indexes?.map(index => { + const proposalId = searchedIndexes + ? searchedIndexes?.at(0) + : proposalsDetailsAt?.at(index)?.result?.at(0); + + return { + abi: GovernorStorageAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalDeadline", + args: [proposalId], + scopeKey: `proposalDeadline-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: proposalVotes } = useReadContracts({ + contracts: indexes?.map(index => { + const proposalId = searchedIndexes + ? searchedIndexes?.at(0) + : proposalsDetailsAt?.at(index)?.result?.at(0); + + return { + abi: GovernorCountingAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalVotes", + args: [proposalId], + scopeKey: `proposalVotes-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: proposalStates } = useReadContracts({ + contracts: indexes?.map(index => { + const proposalId = searchedIndexes + ? searchedIndexes?.at(0) + : proposalsDetailsAt?.at(index)?.result?.at(0); + + return { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "state", + args: [proposalId], + scopeKey: `state-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: proposalSnapshots } = useReadContracts({ + contracts: indexes?.map(index => { + const proposalId = searchedIndexes + ? searchedIndexes?.at(0) + : proposalsDetailsAt?.at(index)?.result?.at(0); + + return { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalSnapshot", + args: [proposalId], + scopeKey: `proposalSnapshot-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: proposalQuorums } = useReadContracts({ + contracts: indexes?.map(index => { + const timepoint = proposalSnapshots?.at(index)?.result; + return { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "quorum", + args: [timepoint], + scopeKey: `quorum-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: pastTotalSupplies } = useReadContracts({ + contracts: indexes?.map(index => { + const timepoint = proposalSnapshots?.at(index)?.result; + return { + abi: ghstAbi, + address: ghstAddress, + functionName: "getPastTotalSupply", + args: [timepoint], + scopeKey: `getPastTotalSupply-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const { data: proposalProposer } = useReadContracts({ + contracts: indexes?.map(index => { + const proposalId = searchedIndexes + ? searchedIndexes?.at(0) + : proposalsDetailsAt?.at(index)?.result?.at(0); + + return { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: "proposalProposer", + args: [proposalId], + scopeKey: `proposalProposer-${chainId}-${index}`, + chainId: chainId, + } + }) + }); + + const hashes = indexes?.map(index => { + let result = { short: index + 1, full: undefined }; + const proposalId = searchedIndexes + ? searchedIndexes?.at(0) + : proposalsDetailsAt?.at(index)?.result?.at(0); + + if (proposalId) { + const hash = "0x" + proposalId.toString(16); + result.short = hash.slice(-5); + result.full = hash; + } + return result; + }); + + const proposals = indexes?.map(index => ({ + hashes: hashes?.at(index), + proposer: proposalProposer?.at(index)?.result, + details: proposalsDetailsAt?.at(index)?.result, + deadline: proposalDeadlines?.at(index)?.result ?? 0n, + state: proposalStates?.at(index)?.result ?? 0, + pastTotalSupply: new DecimalBigNumber(pastTotalSupplies?.at(index)?.result ?? 0n, decimals), + quorum: new DecimalBigNumber(proposalQuorums?.at(index)?.result ?? 0n, decimals), + snapshot: new DecimalBigNumber(proposalSnapshots?.at(index)?.result ?? 0n, decimals), + votes: proposalVotes?.at(index)?.result?.map( + vote => new DecimalBigNumber(vote ?? 0n, decimals), + ), + })); + + return { proposals }; +} + +export const releaseLocked = async (chainId, account, proposalId) => { + try { + const { request } = await simulateContract(config, { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: 'releaseLocked', + args: [proposalId], + account: account, + chainId: chainId + }); + + const txHash = await writeContract(config, request); + await waitForTransactionReceipt(config, { + hash: txHash, + onReplaced: () => toast("Release locked transaction was replaced. Wait for inclusion please."), + chainId + }); + + toast.success("Successfully release locked funds from the governor."); + return true; + } catch (err) { + console.error(err); + toast.error("Release locked funds failed. Check logs for error detalization."); + return false; + } +} + +export const executeProposal = async (chainId, account, proposalId) => { + try { + const { request } = await simulateContract(config, { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: 'execute', + args: [proposalId], + account: account, + chainId: chainId + }); + + const txHash = await writeContract(config, request); + await waitForTransactionReceipt(config, { + hash: txHash, + onReplaced: () => toast("Proposal execution transaction was replaced. Wait for inclusion please."), + chainId + }); + + toast.success("Proposal execution was successful, wait for updates."); + return true; + } catch (err) { + console.error(err); + toast.error("Proposal execution failed. Check logs for error detalization."); + return false; + } +} + +export const castVote = async (chainId, account, proposalId, support) => { + try { + const { request } = await simulateContract(config, { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: 'castVote', + args: [proposalId, support], + account: account, + chainId: chainId + }); + + const txHash = await writeContract(config, request); + await waitForTransactionReceipt(config, { + hash: txHash, + onReplaced: () => toast("Cast vote transaction was replaced. Wait for inclusion please."), + chainId + }); + + toast.success("Successfully casted a vote, should be applied the proposal."); + return true; + } catch (err) { + console.error(err); + toast.error("Vote cast failed. Check logs for error detalization."); + return false; + } +} + +export const propose = async (chainId, account, functions, description) => { + const targets = functions.map(f => f.target); + const values = functions.map(f => f.value); + const calldatas = functions.map(f => f.calldata); + + try { + const { request } = await simulateContract(config, { + abi: GovernorAbi, + address: GHOST_GOVERNANCE_ADDRESSES[chainId], + functionName: 'propose', + args: [targets, values, calldatas, description], + account: account, + chainId: chainId + }); + + const txHash = await writeContract(config, request); + await waitForTransactionReceipt(config, { + hash: txHash, + onReplaced: () => toast("Proposal transaction was replaced. Wait for inclusion please."), + chainId + }); + + toast.success("Successfully proposed a set of functions to be executed."); + return true; + } catch (err) { + console.error(err); + toast.error("Proposal creation failed. Check logs for error detalization."); + return false; + } +} diff --git a/src/hooks/helpers.js b/src/hooks/helpers.js index 9eca45a..baddf59 100644 --- a/src/hooks/helpers.js +++ b/src/hooks/helpers.js @@ -48,6 +48,9 @@ export const getTokenAbi = (name) => { case "WETH": abi = WethAbi; break; + case "WMWETH": + abi = WethAbi; + break; } return abi; } @@ -86,6 +89,9 @@ export const getTokenDecimals = (name) => { case "WETH": decimals = 18; break; + case "WMWETH": + decimals = 18; + break; } return decimals; } @@ -133,6 +139,9 @@ export const getTokenAddress = (chainId, name) => { case "WETC": address = WETH_ADDRESSES[chainId]; break; + case "WMETC": + address = WETH_ADDRESSES[chainId]; + break; } return address; } @@ -141,7 +150,7 @@ export const getTokenIcons = (chainId, address) => { let icons = [""]; switch (address) { case RESERVE_ADDRESSES[chainId]: - icons = [chainId === 11155111 ? "GDAI" : "WETH"]; + icons = ["WETH"]; break; case FTSO_ADDRESSES[chainId]: icons = ["FTSO"]; @@ -153,7 +162,7 @@ export const getTokenIcons = (chainId, address) => { icons = ["GHST"]; break; case FTSO_DAI_LP_ADDRESSES[chainId]: - icons = ["FTSO", chainId === 11155111 ? "GDAI" : "WETH"]; + icons = ["FTSO", "WETH"]; break; } return icons; diff --git a/src/hooks/tokens/index.js b/src/hooks/tokens/index.js index a43c0db..4ff7cb5 100644 --- a/src/hooks/tokens/index.js +++ b/src/hooks/tokens/index.js @@ -9,6 +9,40 @@ import { shorten } from "../../helpers"; import { tokenNameConverter } from "../../helpers/tokenConverter"; import { config } from "../../config"; +export const usePastVotes = (chainId, name, timepoint, address) => { + const decimals = getTokenDecimals(name); + const contractAddress = getTokenAddress(chainId, name); + + const { data, refetch, error } = useReadContract({ + abi: getTokenAbi(name), + address: contractAddress, + functionName: "getPastVotes", + args: [address, timepoint], + scopeKey: `getPastVotes-${timepoint}-${chainId}`, + chainId: chainId, + }); + + const pastVotes = new DecimalBigNumber(data ? data : 0n, decimals); + return { pastVotes } +} + +export const usePastTotalSupply = (chainId, name, timepoint) => { + const decimals = getTokenDecimals(name); + const contractAddress = getTokenAddress(chainId, name); + + const { data, refetch, error } = useReadContract({ + abi: getTokenAbi(name), + address: contractAddress, + functionName: "getPastTotalSupply", + args: [timepoint], + scopeKey: `getPastTotalSupply-${timepoint}-${chainId}`, + chainId: chainId, + }); + + const pastTotalSupply = new DecimalBigNumber(data ? data : 0n, decimals); + return { pastTotalSupply, refetch }; +} + export const useTotalSupply = (chainId, name) => { const contractAddress = getTokenAddress(chainId, name); const { data, refetch } = useToken({ @@ -26,7 +60,7 @@ export const useTotalSupply = (chainId, name) => { export const useBalance = (chainId, name, address) => { const contractAddress = getTokenAddress(chainId, name); - const { data, refetch } = useInnerBalance({ + const { data, refetch, error } = useInnerBalance({ address, chainId, scopeKey: `balance-${contractAddress}-${address}-${chainId}`,