apply dao governance

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-02-18 18:48:51 +03:00
commit 133e911274
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
45 changed files with 3296 additions and 56 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "ghost-dao-interface", "name": "ghost-dao-interface",
"private": true, "private": true,
"version": "0.4.7", "version": "0.5.20",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -16,6 +16,7 @@
"@ethersproject/bignumber": "^5.8.0", "@ethersproject/bignumber": "^5.8.0",
"@ethersproject/units": "^5.8.0", "@ethersproject/units": "^5.8.0",
"@mui/icons-material": "^6.4.7", "@mui/icons-material": "^6.4.7",
"@mui/lab": "6.0.1-beta.36",
"@mui/material": "^6.4.7", "@mui/material": "^6.4.7",
"@mui/utils": "^6.4.6", "@mui/utils": "^6.4.6",
"@polkadot-api/metadata-builders": "0.13.0", "@polkadot-api/metadata-builders": "0.13.0",

View File

@ -26,6 +26,9 @@ importers:
'@mui/icons-material': '@mui/icons-material':
specifier: ^6.4.7 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) 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': '@mui/material':
specifier: ^6.4.7 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) 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': '@ethersproject/units@5.8.0':
resolution: {integrity: sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==} 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': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
@ -638,6 +656,18 @@ packages:
resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==}
engines: {node: '>=16.0.0'} 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': '@mui/core-downloads-tracker@6.4.7':
resolution: {integrity: sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==} resolution: {integrity: sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==}
@ -652,6 +682,27 @@ packages:
'@types/react': '@types/react':
optional: true 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': '@mui/material@6.4.7':
resolution: {integrity: sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==} resolution: {integrity: sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -682,6 +733,16 @@ packages:
'@types/react': '@types/react':
optional: true 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': '@mui/styled-engine@6.4.6':
resolution: {integrity: sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==} resolution: {integrity: sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -695,6 +756,19 @@ packages:
'@emotion/styled': '@emotion/styled':
optional: true 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': '@mui/system@6.4.7':
resolution: {integrity: sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==} resolution: {integrity: sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -711,6 +785,22 @@ packages:
'@types/react': '@types/react':
optional: true 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': '@mui/types@7.2.21':
resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==} resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==}
peerDependencies: peerDependencies:
@ -719,6 +809,14 @@ packages:
'@types/react': '@types/react':
optional: true 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': '@mui/utils@6.4.6':
resolution: {integrity: sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==} resolution: {integrity: sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -729,6 +827,16 @@ packages:
'@types/react': '@types/react':
optional: true 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': '@noble/ciphers@1.2.1':
resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==}
engines: {node: ^14.21.3 || >=16} engines: {node: ^14.21.3 || >=16}
@ -3423,6 +3531,23 @@ snapshots:
'@ethersproject/constants': 5.8.0 '@ethersproject/constants': 5.8.0
'@ethersproject/logger': 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/core@0.19.1': {}
'@humanfs/node@0.16.6': '@humanfs/node@0.16.6':
@ -3615,6 +3740,20 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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/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)': '@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: optionalDependencies:
'@types/react': 19.0.10 '@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)': '@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: dependencies:
'@babel/runtime': 7.26.9 '@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)': '@mui/private-theming@6.4.6(@types/react@19.0.10)(react@19.0.0)':
dependencies: 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) '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0)
prop-types: 15.8.1 prop-types: 15.8.1
react: 19.0.0 react: 19.0.0
optionalDependencies: optionalDependencies:
'@types/react': 19.0.10 '@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)': '@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: 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/cache': 11.14.0
'@emotion/serialize': 1.3.3 '@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0 '@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) '@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 '@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)': '@mui/types@7.2.21(@types/react@19.0.10)':
optionalDependencies: optionalDependencies:
'@types/react': 19.0.10 '@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)': '@mui/utils@6.4.6(@types/react@19.0.10)(react@19.0.0)':
dependencies: dependencies:
'@babel/runtime': 7.26.9 '@babel/runtime': 7.26.9
@ -3700,6 +3898,18 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.0.10 '@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.2.1': {}
'@noble/ciphers@1.3.0': {} '@noble/ciphers@1.3.0': {}
@ -5042,7 +5252,7 @@ snapshots:
babel-plugin-macros@3.1.0: babel-plugin-macros@3.1.0:
dependencies: dependencies:
'@babel/runtime': 7.26.9 '@babel/runtime': 7.27.6
cosmiconfig: 7.1.0 cosmiconfig: 7.1.0
resolve: 1.22.10 resolve: 1.22.10
@ -5238,7 +5448,7 @@ snapshots:
dom-helpers@5.2.1: dom-helpers@5.2.1:
dependencies: dependencies:
'@babel/runtime': 7.26.9 '@babel/runtime': 7.27.6
csstype: 3.1.3 csstype: 3.1.3
dot-case@3.0.4: dot-case@3.0.4:

View File

@ -14,7 +14,7 @@ import Sidebar from "./components/Sidebar/Sidebar";
import TopBar from "./components/TopBar/TopBar"; import TopBar from "./components/TopBar/TopBar";
import { shouldTriggerSafetyCheck } from "./helpers"; import { shouldTriggerSafetyCheck } from "./helpers";
import { isNetworkAvailable, isNetworkLegacy } from "./constants"; import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "./constants";
import useTheme from "./hooks/useTheme"; import useTheme from "./hooks/useTheme";
import { useUnstableProvider } from "./hooks/ghost"; import { useUnstableProvider } from "./hooks/ghost";
import { dark as darkTheme } from "./themes/dark.js"; 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 Dex = lazy(() => import("./containers/Dex/Dex"));
const Bridge = lazy(() => import("./containers/Bridge/Bridge")); const Bridge = lazy(() => import("./containers/Bridge/Bridge"));
const NotFound = lazy(() => import("./containers/NotFound/NotFound")); 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"; const PREFIX = "App";
@ -213,6 +216,9 @@ function App() {
} }
<Route path="/bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} /> <Route path="/bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/dex/:name" element={<Dex connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} /> <Route path="/dex/:name" element={<Dex connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
{isGovernanceAvailable(chainId, addressChainId) && <Route path="/governance" element={<Governance config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />}
{isGovernanceAvailable(chainId, addressChainId) && <Route path="/governance/:id" element={<ProposalDetails config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />}
{isGovernanceAvailable(chainId, addressChainId) && <Route path="/governance/create" element={<NewProposal config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />}
</> </>
} }
<Route path="/empty" element={<NotFound <Route path="/empty" element={<NotFound

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
src/abi/Governor.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -20,7 +20,7 @@ const StyledMuiChip = styled(MuiChip, {
: template === "gray" : template === "gray"
? theme.colors.gray[500] ? theme.colors.gray[500]
: template === "darkGray" : template === "darkGray"
? theme.colors.gray[600] ? theme.colors.primary[300]
: theme.colors.feedback[template] : theme.colors.feedback[template]
: theme.palette.mode === "light" : theme.palette.mode === "light"
? theme.colors.gray[90] ? theme.colors.gray[90]
@ -34,7 +34,7 @@ const StyledMuiChip = styled(MuiChip, {
: template === "gray" : template === "gray"
? theme.colors.gray[10] ? theme.colors.gray[10]
: template === "darkGray" : template === "darkGray"
? theme.colors.gray[90] ? theme.colors.gray[800]
: theme.colors.gray[600], : theme.colors.gray[600],
fontWeight: strong ? 700 : 450, fontWeight: strong ? 700 : 450,
}, },

View File

@ -0,0 +1,39 @@
import { Box, LinearProgress as MuiLinearProgress } from "@mui/material";
import { styled } from "@mui/material/styles";
const PREFIX = "MuiLinearProgress";
const classes = {
chip: `${PREFIX}-bar`,
};
const StyledMuiLinearProgress = styled(MuiLinearProgress, {
shouldForwardProp: (prop) => 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 (
<Box sx={{ position: 'relative', width: '100%', py: 1 }}>
<StyledMuiLinearProgress {...props} />
{props.target && <Box
sx={{
position: 'absolute',
left: `${props.target}%`,
top: props.targetTop || 0,
bottom: props.targetBottom || 0,
width: props.targetWidth || "2px",
backgroundColor: props.targetBackgroundColor || 'white',
}}
></Box>}
</Box>
)
}
export default LinearProgressBar;

View File

@ -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 (
<Box
display="flex"
flexDirection="column"
width={width}
sx={{ backgroundColor: theme.colors.gray[750] }}
borderRadius="12px"
padding="15px"
>
{label && (
<Typography color={theme.colors.gray[500]} fontSize="12px" marginBottom="8px">
{label}
</Typography>
)}
<Box display="flex" alignItems="center" justifyContent="space-between">
<StyledSelectInput
value={value}
onChange={onChange}
inputWidth={inputWidth}
IconComponent={KeyboardArrowDownIcon}
renderValue={renderValue}
displayEmpty
>
{options.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</StyledSelectInput>
</Box>
</Box>
);
};
export default Select;

View File

@ -24,6 +24,8 @@ import TelegramIcon from '@mui/icons-material/Telegram';
import HowToVoteIcon from '@mui/icons-material/HowToVote'; import HowToVoteIcon from '@mui/icons-material/HowToVote';
import HubIcon from '@mui/icons-material/Hub'; import HubIcon from '@mui/icons-material/Hub';
import PublicIcon from '@mui/icons-material/Public'; 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 ForumIcon from '@mui/icons-material/Forum';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import BookIcon from '@mui/icons-material/Book'; import BookIcon from '@mui/icons-material/Book';
@ -37,7 +39,7 @@ import BondIcon from "../Icon/BondIcon";
import StakeIcon from "../Icon/StakeIcon"; import StakeIcon from "../Icon/StakeIcon";
import WrapIcon from "../Icon/WrapIcon"; import WrapIcon from "../Icon/WrapIcon";
import { isNetworkAvailable, isNetworkLegacy } from "../../constants"; import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "../../constants";
import { AVAILABLE_DEXES } from "../../constants/dexes"; import { AVAILABLE_DEXES } from "../../constants/dexes";
import { ECOSYSTEM } from "../../constants/ecosystem"; import { ECOSYSTEM } from "../../constants/ecosystem";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
@ -177,7 +179,8 @@ const NavContent = ({ chainId, addressChainId }) => {
} }
/> />
<NavItem icon={StakeIcon} label={`Stake`} to="/stake" /> <NavItem icon={StakeIcon} label={`Stake`} to="/stake" />
<NavItem icon={PublicIcon} label={`Bridge`} to="/bridge" /> <NavItem icon={ForkRightIcon} label={`Bridge`} to="/bridge" />
{isGovernanceAvailable(chainId, addressChainId) && <NavItem icon={GavelIcon} label={`Governance`} to="/governance" />}
<Box className="menu-divider"> <Box className="menu-divider">
<Divider /> <Divider />
</Box> </Box>

View File

@ -7,7 +7,7 @@ import Token from "../Token/Token";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; 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 }) => ({ ({ inputWidth, inputFontSize }) => ({
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
padding: 0, padding: 0,

View File

@ -4,6 +4,19 @@ export enum NetworkId {
TESTNET_MORDOR = 63, 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) => { export const isNetworkAvailable = (chainId, addressChainId) => {
chainId = addressChainId ? addressChainId : chainId; chainId = addressChainId ? addressChainId : chainId;
let exists = false; let exists = false;
@ -26,9 +39,6 @@ export const isNetworkAvailable = (chainId, addressChainId) => {
export const isNetworkLegacy = (chainId) => { export const isNetworkLegacy = (chainId) => {
let exists = false; let exists = false;
switch (chainId) { switch (chainId) {
case 11155111:
exists = true
break;
case 560048: case 560048:
exists = true exists = true
break; break;

View File

@ -1,25 +1,25 @@
import { NetworkId } from "../constants"; import { NetworkId } from "../constants";
export const STAKING_ADDRESSES = { export const STAKING_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xd90E63E88282596E1ea33765b41Ba3d650f4aD52", [NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
[NetworkId.TESTNET_HOODI]: "0x25F62eDc6C89FF84E957C22336A35d2dfc861a86", [NetworkId.TESTNET_HOODI]: "0x25F62eDc6C89FF84E957C22336A35d2dfc861a86",
[NetworkId.TESTNET_MORDOR]: "0xC25C9C56a89ebd6ef291b415d00ACfa7913c55e7", [NetworkId.TESTNET_MORDOR]: "0xC25C9C56a89ebd6ef291b415d00ACfa7913c55e7",
}; };
export const BOND_DEPOSITORY_ADDRESSES = { export const BOND_DEPOSITORY_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xdcE486113280e49ca2fB200258E5Ee1B2D21D495", [NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
[NetworkId.TESTNET_HOODI]: "0x6Ad50B1E293E68B2fC230c576220a93A9D311571", [NetworkId.TESTNET_HOODI]: "0x6Ad50B1E293E68B2fC230c576220a93A9D311571",
[NetworkId.TESTNET_MORDOR]: "0x7C85cDEddBAd0f50453d373F7332BEa11ECa7BAf", [NetworkId.TESTNET_MORDOR]: "0x7C85cDEddBAd0f50453d373F7332BEa11ECa7BAf",
}; };
export const DAO_TREASURY_ADDRESSES = { export const DAO_TREASURY_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x93dd30f819403710de7933B79A74C4A42438458D", [NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
[NetworkId.TESTNET_HOODI]: "0x1a1b29b18f714fac9dDabEf530dFc4f85b56A6e8", [NetworkId.TESTNET_HOODI]: "0x1a1b29b18f714fac9dDabEf530dFc4f85b56A6e8",
[NetworkId.TESTNET_MORDOR]: "0x5883C8e2259556B534036c7fDF4555E09dE9f243", [NetworkId.TESTNET_MORDOR]: "0x5883C8e2259556B534036c7fDF4555E09dE9f243",
}; };
export const FTSO_DAI_LP_ADDRESSES = { export const FTSO_DAI_LP_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x1394dC3f7bABaa2F0CA80353648087DAB1BF3fd6", [NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
[NetworkId.TESTNET_HOODI]: "0xf7B2d44209E70782d93A70F7D8eC50010dF7ae50", [NetworkId.TESTNET_HOODI]: "0xf7B2d44209E70782d93A70F7D8eC50010dF7ae50",
[NetworkId.TESTNET_MORDOR]: "0xE6546D12665dB5B22Cb92FB9e0221aE51A57aeaa", [NetworkId.TESTNET_MORDOR]: "0xE6546D12665dB5B22Cb92FB9e0221aE51A57aeaa",
}; };
@ -31,49 +31,47 @@ export const FTSO_STNK_LP_ADDRESSES = {
} }
export const RESERVE_ADDRESSES = { export const RESERVE_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x5f63a27a9214a0352F2EF8dAF1eD4974d713192B", [NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
[NetworkId.TESTNET_HOODI]: "0x80c6676c334BCcE60b3CC852085B72143379CE58", [NetworkId.TESTNET_HOODI]: "0x80c6676c334BCcE60b3CC852085B72143379CE58",
[NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC", [NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC",
}; };
export const WETH_ADDRESSES = { export const WETH_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xfff9976782d46cc05630d1f6ebab18b2324d6b14", [NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
[NetworkId.TESTNET_HOODI]: "0xE69a5c6dd88cA798b93c3C92fc50c51Fd5305eB4", [NetworkId.TESTNET_HOODI]: "0xE69a5c6dd88cA798b93c3C92fc50c51Fd5305eB4",
[NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC", [NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC",
}; };
export const GHST_ADDRESSES = { export const GHST_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xdf2e5306A3dCcfA4e21bbF4226C17Ff5B008dDC4", [NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
[NetworkId.TESTNET_HOODI]: "0xE98f7426457E6533B206e91B7EcA97aa8A258B46", [NetworkId.TESTNET_HOODI]: "0xE98f7426457E6533B206e91B7EcA97aa8A258B46",
[NetworkId.TESTNET_MORDOR]: "0x14b5787F8a1E62786F50A7998A9b14aa24298423", [NetworkId.TESTNET_MORDOR]: "0x14b5787F8a1E62786F50A7998A9b14aa24298423",
}; };
export const STNK_ADDRESSES = { export const STNK_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x02C296A27eA779d5a16F934337c12062C5E3c0D9", [NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
[NetworkId.TESTNET_HOODI]: "0xF07e9303A9f16Afd82f4f57Fd6fca68Aa0AB6D7F", [NetworkId.TESTNET_HOODI]: "0xF07e9303A9f16Afd82f4f57Fd6fca68Aa0AB6D7F",
[NetworkId.TESTNET_MORDOR]: "0x137bA9403885D8ECEa95AaFBb8734F5a16121bAC", [NetworkId.TESTNET_MORDOR]: "0x137bA9403885D8ECEa95AaFBb8734F5a16121bAC",
}; };
export const FTSO_ADDRESSES = { export const FTSO_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xcFedFFEB3FdeCd2196820Ba3b71f3F84A1255f93", [NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
[NetworkId.TESTNET_HOODI]: "0xb184e423811b644A1924334E63985c259F5D0033", [NetworkId.TESTNET_HOODI]: "0xb184e423811b644A1924334E63985c259F5D0033",
[NetworkId.TESTNET_MORDOR]: "0xeA170CC0faceC531a6a9e93a28C4330Ac50343a1", [NetworkId.TESTNET_MORDOR]: "0xeA170CC0faceC531a6a9e93a28C4330Ac50343a1",
}; };
export const DISTRIBUTOR_ADDRESSES = { export const DISTRIBUTOR_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x8fbF8eB4Fcd451EF62Aee33508D46FE120963194", [NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
[NetworkId.TESTNET_HOODI]: "0xdF49dC81c457c6f92e26cf6d686C7a8715255842", [NetworkId.TESTNET_HOODI]: "0xdF49dC81c457c6f92e26cf6d686C7a8715255842",
[NetworkId.TESTNET_MORDOR]: "0xaf5e76706520db7fb01096E322940206bf3fce57", [NetworkId.TESTNET_MORDOR]: "0xaf5e76706520db7fb01096E322940206bf3fce57",
}; };
export const GHOST_GOVERNANCE_ADDRESSES = { export const GHOST_GOVERNANCE_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xDab0c51918E6990d8763FAC8a04AE159e44e0c4f", [NetworkId.TESTNET_SEPOLIA]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
[NetworkId.TESTNET_HOODI]: "0x1B96B792840d4d19d5097ee007392Ed4d851e64F",
[NetworkId.TESTNET_MORDOR]: "0x3dD438416D9593A58193fC52850E588efAa3D57E",
}; };
export const BONDING_CALCULATOR_ADDRESSES = { export const BONDING_CALCULATOR_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x4896bFc6256A57Df826d7144E48c9633d51d6319", [NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
[NetworkId.TESTNET_HOODI]: "0x2635d526Ad24b98082563937f7b996075052c6Fd", [NetworkId.TESTNET_HOODI]: "0x2635d526Ad24b98082563937f7b996075052c6Fd",
[NetworkId.TESTNET_MORDOR]: "0x0c4C7C49a173E2a3f9Eed93125F3F146D8e17bCb", [NetworkId.TESTNET_MORDOR]: "0x0c4C7C49a173E2a3f9Eed93125F3F146D8e17bCb",
} }
@ -111,6 +109,10 @@ export const NATIVE_TICKERS = {
} }
export const CEX_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]: [ [NetworkId.TESTNET_MORDOR]: [
"https://api.binance.com/api/v3/ticker/price?symbol=ETCUSDT", "https://api.binance.com/api/v3/ticker/price?symbol=ETCUSDT",
"https://api.coinbase.com/v2/prices/ETC-USDT/spot", "https://api.coinbase.com/v2/prices/ETC-USDT/spot",

View File

@ -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 { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import Paper from "../../components/Paper/Paper"; import Paper from "../../components/Paper/Paper";
@ -21,7 +20,6 @@ import { useTokenSymbol } from "../../hooks/tokens";
const Bonds = ({ chainId, address, connect }) => { const Bonds = ({ chainId, address, connect }) => {
const [isZoomed] = useState(false); const [isZoomed] = useState(false);
const navigate = useNavigate();
const [secondsTo, setSecondsTo] = useState(0); const [secondsTo, setSecondsTo] = useState(0);
const isSmallScreen = useMediaQuery("(max-width: 650px)"); const isSmallScreen = useMediaQuery("(max-width: 650px)");
@ -29,7 +27,7 @@ const Bonds = ({ chainId, address, connect }) => {
useEffect(() => { useEffect(() => {
ReactGA.send({ hitType: "pageview", page: "/bonds" }); ReactGA.send({ hitType: "pageview", page: "/bonds" });
}, []) }, []);
const { liveBonds } = useLiveBonds(chainId); const { liveBonds } = useLiveBonds(chainId);
const totalReserves = useTotalReserves(chainId); const totalReserves = useTotalReserves(chainId);
@ -59,7 +57,17 @@ const Bonds = ({ chainId, address, connect }) => {
}} }}
> >
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column"> <Box display="flex" alignItems="center" justifyContent="center" flexDirection="column">
<Paper headerText="Active bonds" fullWidth enableBackground> <Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Active bonds
</Typography>
</Box>
}
>
<MetricCollection> <MetricCollection>
<Metric <Metric
label={`Treasury Balance`} label={`Treasury Balance`}

View File

@ -83,7 +83,17 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
warmupLength={warmupInfo.expiry - epoch.number} warmupLength={warmupInfo.expiry - epoch.number}
setPreClaimConfirmed={() => setPreClaimConfirmed(true)} setPreClaimConfirmed={() => setPreClaimConfirmed(true)}
/> />
<Paper headerText="Your Bonds" fullWidth enableBackground> <Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Your Bonds
</Typography>
</Box>
}
>
<Box display="flex" flexDirection="column" alignItems="center"> <Box display="flex" flexDirection="column" alignItems="center">
<Typography variant="h4" align="center" color="textSecondary"> <Typography variant="h4" align="center" color="textSecondary">
Payout Options Payout Options

View File

@ -140,7 +140,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
const transactionApplaused = useMemo(() => { const transactionApplaused = useMemo(() => {
return transactionApplausedDirect || transactionApplausedIncremented; return transactionApplausedDirect || transactionApplausedIncremented;
}, [transactionApplausedDirect, transactionApplausedIncremented]) }, [transactionApplausedDirect, transactionApplausedIncremented]);
const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n); const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n);
@ -252,10 +252,10 @@ const Bridge = ({ chainId, address, config, connect }) => {
if (commit.disabled || blockNumber < blocksInFourHours) { if (commit.disabled || blockNumber < blocksInFourHours) {
continue; 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); return Math.max(Number(certainty * 100n / (blocksInFourHours * BigInt(length))), 0);
}, [latestCommits, blockNumber]); }, [latestCommits, blockNumber, finalityDelay]);
const timeToNextEpoch = useMemo(() => { const timeToNextEpoch = useMemo(() => {
if (!currentSession || !genesisSlot || !currentSlot) { if (!currentSession || !genesisSlot || !currentSlot) {
@ -302,7 +302,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
return ( return (
<> <>
<PageTitle name="GHOST Bridge" subtitle="The only pure Web3 decentralized bridge." /> <PageTitle name="GHOST Bridge" subtitle={`Bridge $${ghstSymbol} via the only pure Web3 protocol`} />
<Container <Container
style={{ style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem", paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
@ -319,6 +319,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
handleButtonProceed={handleButtonProceed} handleButtonProceed={handleButtonProceed}
/> />
<BridgeModal <BridgeModal
providerDetail={providerDetail}
currentRecord={currentRecord} currentRecord={currentRecord}
activeTxIndex={activeTxIndex} activeTxIndex={activeTxIndex}
setActiveTxIndex={setActiveTxIndex} setActiveTxIndex={setActiveTxIndex}
@ -343,6 +344,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
transactionEta={slowestEvmBlock ? Number(slowestEvmBlock) : undefined} transactionEta={slowestEvmBlock ? Number(slowestEvmBlock) : undefined}
timeToNextEpoch={timeToNextEpoch ? Number(timeToNextEpoch) : undefined} timeToNextEpoch={timeToNextEpoch ? Number(timeToNextEpoch) : undefined}
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
maxDelay={14400 + finalityDelay * Number(networkAvgBlockSpeed(chainId))}
/> />
</Paper> </Paper>
</Grid> </Grid>
@ -358,7 +360,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
onClick={() => setBridgeAction(!bridgeAction)} onClick={() => setBridgeAction(!bridgeAction)}
/>)} />)}
<Typography variant="h4">{ <Typography variant="h4">{
bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction History" bridgeAction ? `Bridge-In $${ghstSymbol}` : "Transaction History"
}</Typography> }</Typography>
</Box> </Box>
} }

View File

@ -12,6 +12,7 @@ export const BridgeHeader = ({
bridgeStability, bridgeStability,
transactionEta, transactionEta,
timeToNextEpoch, timeToNextEpoch,
maxDelay,
isSmallScreen isSmallScreen
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@ -92,7 +93,7 @@ export const BridgeHeader = ({
<Grid item xs={isSmallScreen ? 12 : 4}> <Grid item xs={isSmallScreen ? 12 : 4}>
<Metric <Metric
isLoading={transactionEta === undefined} isLoading={transactionEta === undefined}
metric={transactionEta > 14400 ? "∞" : formatTime(transactionEta)} metric={transactionEta > maxDelay ? "∞" : formatTime(transactionEta)}
label="Max Bridge ETA" label="Max Bridge ETA"
tooltip="Maximum estimated time for finalizing bridge transactions based on the latest update." tooltip="Maximum estimated time for finalizing bridge transactions based on the latest update."
/> />

View File

@ -19,12 +19,13 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import InfoTooltip from "../../components/Tooltip/InfoTooltip"; import InfoTooltip from "../../components/Tooltip/InfoTooltip";
import Modal from "../../components/Modal/Modal"; import Modal from "../../components/Modal/Modal";
import GhostStyledIcon from "../../components/Icon/GhostIcon"; import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { PrimaryButton } from "../../components/Button"; import { PrimaryButton, TertiaryButton } from "../../components/Button";
import { formatCurrency } from "../../helpers"; import { formatCurrency } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
export const BridgeModal = ({ export const BridgeModal = ({
providerDetail,
currentRecord, currentRecord,
activeTxIndex, activeTxIndex,
setActiveTxIndex, setActiveTxIndex,
@ -88,7 +89,15 @@ export const BridgeModal = ({
minHeight={"100px"} minHeight={"100px"}
> >
<Box display="flex" gap="1.5rem" flexDirection="column" marginTop=".8rem"> <Box display="flex" gap="1.5rem" flexDirection="column" marginTop=".8rem">
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center"> {!providerDetail && <Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<TertiaryButton
fullWidth
onClick={() => window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}
>
Get GHOST Connect
</TertiaryButton>
</Box>}
{providerDetail && <Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
{currentRecord?.finalization > 0 && ( {currentRecord?.finalization > 0 && (
<> <>
<Box <Box
@ -276,7 +285,7 @@ export const BridgeModal = ({
</Box> </Box>
</> </>
)} )}
</Box> </Box>}
<Box display="flex" flexDirection="column" gap="5px" padding="0.6rem 0"> <Box display="flex" flexDirection="column" gap="5px" padding="0.6rem 0">
<Box display="flex" flexDirection="row" justifyContent="space-between"> <Box display="flex" flexDirection="row" justifyContent="space-between">

View File

@ -105,11 +105,11 @@ export const ValidatorTable = ({
<Box width="100%" height="340px" display="flex" flexDirection="column" justifyContent="space-between" gap="10px"> <Box width="100%" height="340px" display="flex" flexDirection="column" justifyContent="space-between" gap="10px">
{!providerDetail && <Box sx={{ borderRadius: "15px", background: theme.colors.paper.background }} width="100%" height="100%" display="flex" justifyContent="center"> {!providerDetail && <Box sx={{ borderRadius: "15px", background: theme.colors.paper.background }} width="100%" height="100%" display="flex" justifyContent="center">
<Box padding="20px 30px" display="flex" flexDirection="column" justifyContent="space-around" alignItems="center"> <Box padding="20px 30px" display="flex" flexDirection="column" justifyContent="space-around" alignItems="center">
<Typography sx={{ textAlign: "center" }} variant="h6">GHOST Wallet is not detected on your browser!</Typography> <Typography sx={{ textAlign: "center" }} variant="h6">GHOST Connect is not detected on your browser!</Typography>
<Typography sx={{ textAlign: "center" }} variant="body2">Download GHOST Wallet Extension for real-time visibility into validator status and related transaction risks.</Typography> <Typography sx={{ textAlign: "center" }} variant="body2">Download GHOST Connect browser extension for real-time visibility into validator status and related transaction risks.</Typography>
<Typography sx={{ textAlign: "center" }} variant="body2"><b>Important:</b> 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.</Typography> <Typography sx={{ textAlign: "center" }} variant="body2"><b>Important:</b> The GHOST Connect is optional, but be aware that your bridge transaction will succeed or fail irreversibly based on the condition of the validators.</Typography>
<PrimaryButton onClick={() => window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}> <PrimaryButton onClick={() => window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}>
Get GHOST Extension Get GHOST Connect
</PrimaryButton> </PrimaryButton>
</Box> </Box>
</Box>} </Box>}

View File

@ -186,7 +186,7 @@ const Dex = ({ chainId, address, connect }) => {
<PageTitle <PageTitle
name={`${pathname.name.charAt(0).toUpperCase() + pathname.name.slice(1).toLowerCase()} V2 Classic`} name={`${pathname.name.charAt(0).toUpperCase() + pathname.name.slice(1).toLowerCase()} V2 Classic`}
subtitle="Classic interface to V2 smart contracts." subtitle="Swap via Uniswap V2 Fork"
/> />
<Container <Container
style={{ style={{

View File

@ -46,6 +46,7 @@ const SwapContainer = ({
refetch: balanceRefetchTop, refetch: balanceRefetchTop,
contractAddress: addressTop, contractAddress: addressTop,
} = useBalance(chainId, tokenNameTop, address); } = useBalance(chainId, tokenNameTop, address);
const { const {
balance: balanceBottom, balance: balanceBottom,
refetch: balanceRefetchBottom, refetch: balanceRefetchBottom,

View File

@ -159,7 +159,7 @@ const Faucet = ({ chainId, address, config, connect }) => {
<meta name="twitter:image" content="https://ghostchain.io/wp-content/uploads/2025/03/ghostFaucet-Featured_Image.png" /> <meta name="twitter:image" content="https://ghostchain.io/wp-content/uploads/2025/03/ghostFaucet-Featured_Image.png" />
</Helmet> </Helmet>
<PageTitle name={`${reserveSymbol} Faucet`} subtitle={`Swap Sepolia ${nativeInfo.symbol} for ${reserveSymbol}.`} /> <PageTitle name={`${reserveSymbol} Faucet`} subtitle={`Swap Sepolia ${nativeInfo.symbol} for ${reserveSymbol}`} />
<Container <Container
style={{ style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem", paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",

View File

@ -0,0 +1,90 @@
import { useEffect } from "react";
import ReactGA from "react-ga4";
import { useNavigate } from "react-router-dom";
import { Box, Container, Grid, Divider, Typography, useMediaQuery } from "@mui/material";
import Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
import { PrimaryButton } from "../../components/Button";
import GovernanceInfoText from "./components/GovernanceInfoText";
import ProposalsList from "./components/ProposalsList";
import { ProposalsCount, MinQuorumPercentage, ProposalThreshold } from "./components/Metric";
import { useTokenSymbol } from "../../hooks/tokens";
const Governance = ({ connect, config, address, chainId }) => {
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 (
<Box>
<PageTitle name="Protocol Governance" subtitle={`Cast $${ghstSymbol} to facilitate DAO decisions`} />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
minHeight: "calc(100vh - 128px)",
display: "flex",
flexDirection: "column",
justifyContent: "center"
}}
>
<Box sx={{ mt: "15px" }}>
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposal Requirements
</Typography>
</Box>
}
>
<Grid container spacing={1}>
<Grid item xs={isSmallScreen ? 12 : 4}>
<ProposalsCount chainId={chainId} />
</Grid>
<Grid item xs={isSmallScreen ? 12 : 4}>
<MinQuorumPercentage chainId={chainId} ghstSymbol={ghstSymbol} />
</Grid>
<Grid item xs={isSmallScreen ? 12 : 4}>
<ProposalThreshold chainId={chainId} ghstSymbol={ghstSymbol} />
</Grid>
</Grid>
<Box mt="45px" display="flex" flexDirection="column" alignItems="center" justifyContent="center">
<PrimaryButton
fullWidth
onClick={() => navigate(`/governance/create`)}
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
>
Create Proposal
</PrimaryButton>
<Box textAlign="center" mt="15px">
<GovernanceInfoText />
</Box>
</Box>
</Paper>
<ProposalsList address={address} config={config} chainId={chainId} />
</Box>
</Container>
</Box>
)
}
export default Governance;

View File

@ -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 (
<>
<ProposalModal
nativeCurrency={nativeCurrency}
ftsoSymbol={ftsoSymbol}
chainId={chainId}
addCalldata={addCalldata}
isOpened={isModalOpened}
closeModal={() => setIsModalOpened(false)}
/>
<Box>
<PageTitle name="Create Proposal" subtitle="Submit your proposal to strengthen the DAO" />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
minHeight: "calc(100vh - 128px)",
display: "flex",
flexDirection: "column",
justifyContent: "center"
}}
>
<Box sx={{ mt: "15px" }}>
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposal Functions
</Typography>
</Box>
}
topRight={
<PrimaryButton variant="text" href="http://ghostchain.io/governance">
Explore Governance
</PrimaryButton>
}
>
<Box>
<Box>
{proposalFunctions.length === 0 && <Box
width="100%"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
marginBottom="50px"
marginTop="25px"
>
<Typography variant="subtitle1">
Create new proposal by adding one or more of the functions below.
</Typography>
</Box>}
</Box>
<Box display="flex" flexDirection="column" alignItems="center">
<Box sx={{ width: isSemiSmallScreen ? "100%" : "350px" }}>
<TokenAllowanceGuard
spendAmount={threshold}
tokenName={ghstSymbol}
owner={address}
spender={GHOST_GOVERNANCE_ADDRESSES[chainId]}
decimals={ghstBalance._decimals}
approvalText={`Approve ${ghstSymbol}`}
approvalPendingText={"Approving..."}
connect={connect}
isVertical
>
<Box display="flex" flexDirection="column" alignItems="center">
<PrimaryButton
disabled={
proposalFunctions.length === 0 ||
ghstBalance.lt(threshold) ||
isPending
}
fullWidth
onClick={() => submitProposal()}
>
{isPending ? "Submitting..." : "Submit Proposal"}
</PrimaryButton>
<TertiaryButton
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
fullWidth
disabled={isPending}
onClick={() => setIsModalOpened(true)}
>
Add New
</TertiaryButton>
</Box>
</TokenAllowanceGuard>
</Box>
</Box>
</Box>
</Paper>
{proposalFunctions.length > 0 && <Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposal Functions
</Typography>
</Box>
}
>
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ padding: "8px 0" }}>Function</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Target</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Calldata</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalFunctions.map((metadata, index) => {
return parseFunctionCalldata(metadata, index, chainId, nativeCurrency, removeCalldata);
})}</TableBody>
</Table>
</TableContainer>
</Paper>}
</Box>
</Container>
</Box>
</>
)
}
export default NewProposal;

View File

@ -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 (
<Box>
<PageTitle
name={`GBP-${id.slice(-5)}`}
subtitle={
<Typography component="span">
Proposal details, need more in-depth description
</Typography>
}
/>
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
minHeight: "calc(100vh - 128px)",
display: "flex",
flexDirection: "column",
justifyContent: "center"
}}
>
<Box sx={{ mt: "15px" }}>
<Box display="flex" justifyContent="space-between" gap="20px">
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="10px">
<Typography variant="h6">
Progress
</Typography>
<Chip
sx={{ marginTop: "4px", width: "88px" }}
label={convertStatusToLabel(proposalState)}
template={convertStatusToTemplate(proposalState)}
/>
</Box>
}
topRight={
<PrimaryButton sx={{ padding: "0 !important"}} variant="text" href={"https://forum.ghostchain.io"} >
View Forum
</PrimaryButton>
}
>
<Box height="280px" display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
<Box display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between">
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body1" color={theme.colors.feedback.success}>
For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forPercentage?.toString(), 1)}%)
</Typography>
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body1" color={theme.colors.feedback.error}>
Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstPercentage?.toString(), 1)}%)
</Typography>
</Box>
<LinearProgressBar
barColor={theme.colors.feedback.success}
barBackground={theme.colors.feedback.error}
variant="determinate"
value={voteValue}
target={voteTarget}
/>
</Box>
<Box display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between">
<Typography>Proposer</Typography>
<Link href={etherscanLink} target="_blank" rel="noopener noreferrer">
{`${shorten(proposalProposer)}`}
</Link>
</Box>
<Box display="flex" justifyContent="space-between">
<Typography>Locked</Typography>
<Typography>{formatCurrency(proposalLocked, 4, ghstSymbol)}</Typography>
</Box>
<hr width="100%" />
<Box display="flex" justifyContent="space-between">
<Box display="flex" flexDirection="row">
<Typography>Quorum</Typography>
<InfoTooltip message={`Minimum $${ghstSymbol} turnout required for the proposal to become valid, as percentage of the total $${ghstSymbol} supply at the time when proposal was created`} />
</Box>
<Typography>{formatNumber(proposalQuorum.toString(), 4)} ({formatNumber(quorumPercentage, 1)}%)</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
<Box display="flex" flexDirection="row">
<Typography>Total</Typography>
<InfoTooltip message={`Total votes for the proposal, as percentage of the total $${ghstSymbol} supply at the time when proposal was created`}/>
</Box>
<Typography>{formatNumber(totalSupply.toString(), 4)} ({formatNumber(votePercentage, 1)}%)</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
<Box display="flex" flexDirection="row">
<Typography>Votes</Typography>
<InfoTooltip message={`Your voting power, as percentage of total $${ghstSymbol} at the time when proposal was created`} />
</Box>
<Typography>{formatNumber(pastVotes.toString(), 4)} ({formatNumber(voteWeightPercentage, 1)}%)</Typography>
</Box>
</Box>
<Box display="flex" gap="20px">
{address === undefined || address === ""
? <PrimaryButton fullWidth onClick={() => connect()}>Connect</PrimaryButton>
: voteOf === 0n
? <>
<SecondaryButton
fullWidth
disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending}
onClick={() => handleVote(1)}
>
For
</SecondaryButton>
<SecondaryButton
fullWidth
disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending}
onClick={() => handleVote(0)}
>
Against
</SecondaryButton>
</>
: <SecondaryButton
fullWidth
disabled
>
{`Voted ${voteOf === 1n ? "Against" : "For"}`}
</SecondaryButton>
}
</Box>
</Box>
</Paper>
<Paper
fullWidth
enableBackground
headerContent={
<Box height="48px" display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Timeline
</Typography>
</Box>
}
>
<VotingTimeline
proposalLocked={proposalLocked}
connect={connect}
handleExecute={handleExecute}
handleRelease={handleRelease}
state={proposalState}
address={address}
isProposer={proposalProposer === address}
chainId={chainId}
proposalId={id}
/>
</Paper>
</Box>
</Box>
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Executable Code
</Typography>
</Box>
}
>
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ padding: "8px 0" }}>Function</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Target</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Calldata</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalDetails?.map((metadata, index) => {
return parseFunctionCalldata(metadata, index, chainId, nativeCurrency);
})}</TableBody>
</Table>
</TableContainer>
</Paper>
</Container>
</Box>
)
}
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 (
<Box height="280px" display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
<Timeline sx={{ margin: 0, padding: 0 }}>
<VotingTimelineItem chainId={chainId} occured={voteStarted} message="Proposed on" isFirst />
<VotingTimelineItem chainId={chainId} occured={proposalSnapshot} message="Voting started" />
<VotingTimelineItem chainId={chainId} occured={proposalDeadline} message="Voting ends" />
</Timeline>
<Box width="100%" display="flex" gap="10px">
{isProposer && <SecondaryButton
fullWidth
disabled={(proposalLocked?._value ?? 0n) === 0n || state < 2}
onClick={() => address === "" ? connect() : handleRelease()}
>
{address === "" ? "Connect" : "Release"}
</SecondaryButton>}
<SecondaryButton
fullWidth
disabled={state !== 4}
onClick={() => address === "" ? connect() : handleExecute()}
>
{address === "" ? "Connect" : "Execute"}
</SecondaryButton>
</Box>
</Box>
)
}
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 (
<TimelineItem>
<TimelineOppositeContent
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 16px",
minWidth: "140px",
flex: 0,
}}
>
<Typography>{message}</Typography>
</TimelineOppositeContent>
<TimelineSeparator>
<TimelineConnector sx={{ background: isFirst ? "transparent" : "#fff" }} />
<TimelineDot sx={{ width: "15px", height: "15px", background: "#fff", margin: "12px 0" }} />
<TimelineConnector sx={{ background: isLast ? "transparent" : "#fff" }} />
</TimelineSeparator>
<TimelineContent
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
padding: "0 26px",
}}
>
<Typography>{timestamp}</Typography>
</TimelineContent>
</TimelineItem>
)
}
export default ProposalDetails;

View File

@ -0,0 +1,23 @@
import { Link, Typography, useTheme } from "@mui/material";
const GovernanceInfoText = () => {
const theme = useTheme();
return (
<Typography
variant="body2"
color="textSecondary"
fontSize="0.875em"
lineHeight="15px"
>
ghostDAOs adaptive governance system algorithmically sets minimum collateral based on activity.
&nbsp;<Link
color={theme.colors.primary[300]}
href="https://docs.dao.ghostchain.io/"
target="_blank"
rel="noopener noreferrer"
>Learn more here.</Link>
</Typography>
)
};
export default GovernanceInfoText;

View File

@ -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 <Metric {..._props} />;
};
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 <Metric {..._props} />;
}
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 <Metric {..._props} />;
}

View File

@ -0,0 +1,23 @@
import { Link, Typography, useTheme } from "@mui/material";
const ProposalInfoText = () => {
const theme = useTheme();
return (
<Typography
variant="body2"
color="textSecondary"
fontSize="0.875em"
lineHeight="15px"
>
Important: Only the 10 most recent proposals are displayed. Only one proposal can be active at a time.
&nbsp;<Link
color={theme.colors.primary[300]}
href="https://ghostchain.io/ghostdao_litepaper"
target="_blank"
rel="noopener noreferrer"
>Learn more here.</Link>
</Typography>
)
};
export default ProposalInfoText;

View File

@ -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 (
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">{headerLabel}</Typography>
</Box>
}
open={isOpened}
onClose={handleClose}
maxWidth="460px"
minHeight="200px"
>
<Box minHeight="220px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
{renderArguments
? <ArgumentsSteps
nativeCurrency={nativeCurrency}
ftsoSymbol={ftsoSymbol}
chainId={chainId}
addCalldata={handleAddCalldata}
toInitialStep={() => setRenderArguments(false)}
/>
: <InitialStep
selectedOption={selectedOption}
handleChange={handleChange}
handleCalldata={handleCalldata}
handleProceed={() => setRenderArguments(true)}
ready={ArgumentsSteps !== null}
/>
}
</Box>
</Modal>
)
}
const InitialStep = ({ selectedOption, handleChange, handleCalldata, handleProceed, ready }) => {
const theme = useTheme();
const functionDescription = useMemo(() => getFunctionDescription(selectedOption), [selectedOption]);
return (
<Box minHeight="220px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
<Select
value={selectedOption ?? ""}
onChange={handleChange}
options={allPossibleFunctions}
inputWidth="100%"
renderValue={(selected) => {
if (!selected || selected.length === 0) {
return (
<span style={{ color: theme.colors.gray[500], opacity: 0.7 }}>
Select function
</span>
);
}
return allPossibleFunctions.find(opt => opt.value === selected)?.label || selected;
}}
/>
<Typography align="center" variant="body2">{functionDescription}</Typography>
{ready
? <TertiaryButton disabled={!selectedOption} onClick={() => handleProceed()} fullWidth>Proceed</TertiaryButton>
: <PrimaryButton disabled={!selectedOption} onClick={() => handleCalldata()} fullWidth>Create Function</PrimaryButton>
}
</Box>
)
}
export default ProposalModal;

View File

@ -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 (
<Box display="flex" justifyContent="center">
<Typography variant="h4">No proposals yet</Typography>
</Box>
);
}
if (isSmallScreen) {
return (
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposals
</Typography>
</Box>
}
>
<Box display="flex" flexDirection="column" gap="40px">
{proposals?.map(proposal => (
<ProposalCard
key={proposal.hashes.short}
proposal={proposal}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`/governance/${proposal.hashes.full}`)}
/>
))}
</Box>
{proposalsFilter === "active" && <Box my="24px" textAlign="center">
<ProposalInfoText />
</Box>}
</Paper>
);
}
return (
<Paper
fullWidth
enableBackground
headerContent={
<Box
width="100%"
display="flex"
alignItems="center"
justifyContent="space-between"
flexDirection="row"
gap="5px"
>
<Typography variant="h6">
Proposals
</Typography>
<PrimaryButton
variant="text"
href="https://forum.ghostchain.io"
>
View Forum
</PrimaryButton>
</Box>
}
>
<ProposalFilterTrigger trigger={proposalsFilter} setTrigger={setProposalFilter} />
<ProposalTable>
{proposals?.map(proposal => (
<ProposalRow
key={proposal.hashes.short}
proposal={proposal}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`/governance/${proposal.hashes.full}`)}
/>
))}
</ProposalTable>
{proposalsFilter === "active" && <Box mt="24px" textAlign="center" width="70%" mx="auto">
<ProposalInfoText />
</Box>}
</Paper>
);
}
const ProposalTable = ({ children }) => (
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ width: "130px", padding: "8px 0" }}>Proposal ID</TableCell>
<TableCell align="center" style={{ width: "130px", padding: "8px 0" }}>Status</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Vote Ends</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Voting Stats</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}></TableCell>
</TableRow>
</TableHead>
<TableBody>{children}</TableBody>
</Table>
</TableContainer>
);
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 (
<TableRow id={proposal.hashes.short + `--proposal`} data-testid={proposal.hashes.short + `--proposal`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>GDP-{proposal.hashes.short}</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Chip
sx={{ width: "100px" }}
label={convertStatusToLabel(proposal.state)}
template={convertStatusToTemplate(proposal.state)}
/>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>
{convertDeadline(
proposal.deadline,
blockNumber,
chainId
)}
</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Box marginLeft="15px" marginRight="15px">
<LinearProgressBar
barColor={theme.colors.feedback.success}
barBackground={theme.colors.feedback.error}
variant="determinate"
value={voteValue}
target={voteTarget}
/>
</Box>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
{(proposal.state === "Active" || proposal.state === "Succeeded") && <PrimaryButton
fullWidth
onClick={() => openProposal()}
sx={{ maxWidth: "130px" }}
>
{proposal.state === "Succeeded" ? "Execute" : "Vote"}
</PrimaryButton>}
{(proposal.state !== "Active" && proposal.state !== "Succeeded") && <TertiaryButton
fullWidth
onClick={() => openProposal()}
sx={{ alignSelf: "right", maxWidth: "130px" }}
>
View
</TertiaryButton>}
</TableCell>
</TableRow>
);
}
const ProposalCard = ({ proposal, blockNumber, openProposal, chainId }) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery('(max-width: 450px)');
return (
<Box id={proposal.hashes.short + `--proposal`} data-testid={proposal.hashes.short + `--proposal`}>
<Box display="flex" flexDirection={isSmallScreen ? "column" : "row"} justifyContent="space-between">
<Box display="flex" flexDirection="column" width="100%">
<Box display="flex" flexDirection="row" alignItems="center" width="100%" gap="10px">
<Typography variant="h3">GIP-{proposal.hashes.short}</Typography>
<Chip
sx={{ width: "88px" }}
label={convertStatusToLabel(proposal.state)}
template={convertStatusToTemplate(proposal.state)}
/>
</Box>
<Typography>
{convertDeadline(
proposal.deadline,
blockNumber,
chainId
)}
</Typography>
</Box>
</Box>
<Box marginTop="15px" marginBottom="15px">
<LinearProgressBar
barColor={theme.colors.feedback.success}
barBackground={theme.colors.feedback.error}
variant="determinate"
value={69}
target={Math.floor(Math.random() * 101)}
/>
</Box>
<Box marginBottom="20px">
{(proposal.state === "Active" || proposal.state === "Succeeded") && <PrimaryButton
fullWidth
onClick={() => openProposal()}
>
{proposal.state === "Succeeded" ? "Execute" : "Vote"}
</PrimaryButton>}
{(proposal.state !== "Active" && proposal.state !== "Succeeded") && <TertiaryButton
fullWidth
onClick={() => openProposal()}
>
View
</TertiaryButton>}
</Box>
</Box>
);
};
const ProposalFilterTrigger = ({ trigger, setTrigger }) => {
return (
<Tabs
centered
textColor="primary"
indicatorColor="primary"
value={trigger}
aria-label="Proposal filter tabs"
onChange={(_, view) => setTrigger(view)}
TabIndicatorProps={{ style: { display: "none" } }}
>
<Tab aria-label="proposal-filter-active-button" value="active" label="Active" style={{ fontSize: "1rem" }} />
<Tab aria-label="proposal-filter-voted-button" value="voted" label="Voted" style={{ fontSize: "1rem" }} />
<Tab aria-label="proposal-filter-created-button" value="created" label="Created" style={{ fontSize: "1rem" }} />
</Tabs>
)
}
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;

View File

@ -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 && <AuditReservesParsedCell {...props} />}
</>
)
}
const AuditReservesParsedCell = (props) => {
return <ParsedCell {...props} />
}

View File

@ -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 (
<>
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">View create</Typography>
</Box>
}
open={isOpened}
onClose={() => setIsOpened(false)}
maxWidth="460px"
minHeight="200px"
>
<Box minHeight="220px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
<CreateBondSteps args={props.args} ftsoSymbol={ftsoSymbol} nativeCurrency={props.nativeCoin} {...props} />
</Box>
</Modal>
{props.isTable && <CreateBondParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props}/>}
</>
)
}
const CreateBondParsedCell = (props) => {
return <ParsedCell {...props} />
}
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 (
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent="space-between">
{step === 1 && <TokenAndBooleansArguments
tokenAddress={tokenAddress}
createMode={createMode}
possibleTokens={possibleTokens}
setTokenAddress={createMode ? setTokenAddress : empty}
nativeCurrency={nativeCurrency}
ftsoSymbol={ftsoSymbol}
capacityInQuote={capacityInQuote}
setCapacityInQuote={createMode ? setCapacityInQuote : empty}
fixedTerm={fixedTerm}
setFixedTerm={createMode ? setFixedTerm : empty}
/>}
{step === 2 && <MarketArguments
capacityInQuote={capacityInQuote}
currencySymbol={possibleTokens.find(token => 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 && <IntervalsArguments
depositInterval={depositInterval}
setDepositInterval={createMode ? setDepositInterval : empty}
tuneInterval={tuneInterval}
setTuneInterval={createMode ? setTuneInterval : empty}
/>}
{step === 4 && <TermsAgruments
fixedTerm={fixedTerm}
vestingLength={vestingLength}
setVestingLength={createMode ? setVestingLength : empty}
conclusionTimestamp={conclusionTimestamp}
setConclusionTimestamp={createMode ? setConclusionTimestamp : empty}
/>}
<Box width="100%" sx={{ marginTop: "20px" }} display="flex" flexDirection="column" gap="5px">
<Box display="flex" gap="10px">
<TertiaryButton onClick={() => decrementStep()} fullWidth>Back</TertiaryButton>
<TertiaryButton disabled={isNextDisabled} onClick={() => incrementStep()} fullWidth>Next</TertiaryButton>
</Box>
{(step === 4 && createMode) && <PrimaryButton onClick={() => handleProceed()} fullWidth>Create Function</PrimaryButton>}
</Box>
</Box>
);
}
const MarketArguments = ({
capacityInQuote,
currencySymbol,
ftsoSymbol,
capacity,
setCapacity,
initialPrice,
setInitialPrice,
debtBuffer,
setDebtBuffer
}) => {
return (
<Box>
<ArgumentInput
endString={capacityInQuote ? currencySymbol : ftsoSymbol}
label="market[0]"
value={capacity ?? ""}
setValue={setCapacity}
tooltip={`Total capacity limit of the bond market denominated in ${capacityInQuote ? currencySymbol : ftsoSymbol}, representing the maximum amount of bonds that can be sold in this market.`}
/>
<ArgumentInput
endString={currencySymbol}
label="market[1]"
value={initialPrice ?? ""}
setValue={setInitialPrice}
tooltip={`Initial bond price of 1 ${ftsoSymbol} denominated in ${currencySymbol}. The bond price adjusts dynamically via Dutch auction based on market demand.`}
/>
<ArgumentInput
endString="%"
label="market[2]"
value={debtBuffer ?? ""}
setValue={setDebtBuffer}
tooltip="Debt buffer in basis points (e.g. 30,000 = 30%). Determines how far bond price can fall from initial price before tuning is triggered."
/>
</Box>
)
}
const IntervalsArguments = ({
depositInterval,
setDepositInterval,
tuneInterval,
setTuneInterval,
}) => {
return (
<Box>
<ArgumentInput
endString="seconds"
label="_intervals[0]"
value={depositInterval ?? ""}
setValue={setDepositInterval}
tooltip="Time in seconds that determines per-transaction bond purchase limit based on time until expiration. Higher values = larger per-transaction purchases allowed."
/>
<ArgumentInput
endString="seconds"
label="_intervals[0]"
value={tuneInterval ?? ""}
setValue={setTuneInterval}
tooltip="Time in seconds between bond price tuning events that evaluate actual vs. expected bond sales, adjusting how aggressively price decays in the next interval."
/>
</Box>
)
}
const TermsAgruments = ({
fixedTerm,
vestingLength,
setVestingLength,
conclusionTimestamp,
setConclusionTimestamp,
}) => {
return (
<Box>
<ArgumentInput
endString="seconds"
label={"_terms[0]"}
value={vestingLength ?? ""}
setValue={setVestingLength}
tooltip={fixedTerm
? "Time in seconds representing bond vesting schedule. Bond becomes fully claimable after this time elapses from purchase date."
: "Absolute Unix timestamp when all bonds mature and become fully claimable, regardless of the purchase date."
}
/>
<ArgumentInput
endString="seconds"
label="_terms[1]"
value={conclusionTimestamp ?? ""}
setValue={setConclusionTimestamp}
tooltip="Unix timestamp when bond market stops accepting new purchases. Bond market closes after this time."
/>
</Box>
)
}
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 (
<Box>
<BooleanTrigger
value={capacityInQuote}
label="_booleans[0]"
leftText={"True"}
rightText={"False"}
setLeftValue={() => setCapacityInQuote(true)}
setRightValue={() => setCapacityInQuote(false)}
tooltip={`Determines how the bond market capacity is measured. True = measured in ${nativeCurrency}, False = measured in ${ftsoSymbol}.`}
/>
<BooleanTrigger
value={fixedTerm}
label="_booleans[1]"
leftText="True"
rightText="False"
setLeftValue={() => 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.`}
/>
<ArgumentsWrapper
label="_quoteToken"
tooltip={`ERC20 token that users pay with to purchase bonds (e.g. ${nativeCurrency} or ${nativeCurrency}-${ftsoSymbol} LP token).`}
>
<Select
value={selectedOption ?? ""}
onChange={handleChange}
options={possibleTokens}
inputWidth="100%"
renderValue={(selected) => {
if (!selected || selected.length === 0) {
return (
<span style={{ color: theme.colors.gray[500], opacity: 0.7 }}>
Select payment token
</span>
);
}
return possibleTokens.find(opt => opt.value === selected)?.label || selected;
}}
/>
</ArgumentsWrapper>
</Box>
)
}

View File

@ -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 (
<>
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">View setAdjustment</Typography>
</Box>
}
open={isOpened}
onClose={() => setIsOpened(false)}
maxWidth="460px"
minHeight="200px"
>
<Box minHeight="220px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
<SetAdjustmentSteps {...props} />
</Box>
</Modal>
{props.isTable && <SetAdjustmentParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />}
</>
)
}
const SetAdjustmentParsedCell = (props) => {
return <ParsedCell {...props} />
}
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 (
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent="space-between">
<Box>
<BooleanTrigger
value={increase}
leftText="True"
rightText="False"
setLeftValue={() => 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."
/>
<ArgumentInput
disabled={!createMode}
endString="%"
label="rate"
value={rate ?? ""}
setValue={createMode ? setRate : () => {}}
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)]."
/>
<ArgumentInput
disabled={!createMode}
endString="%"
label="target"
value={target ?? ""}
setValue={createMode ? setTarget : () => {}}
tooltip="The target staking rate to be achieved [e.g. 633 => APY = (1 + 633/1,000,000)^(365*3) 1 = 100%]."
/>
</Box>
{createMode && <Box width="100%" sx={{ marginTop: "20px" }} display="flex" flexDirection="column" gap="5px">
<Box display="flex" gap="10px">
<TertiaryButton onClick={toInitialStep} fullWidth>Back</TertiaryButton>
<TertiaryButton disabled fullWidth>Next</TertiaryButton>
</Box>
<PrimaryButton
disabled={!target || !rate}
onClick={() => handleProceed()}
fullWidth
>
Create Function
</PrimaryButton>
</Box>}
</Box>
);
}

View File

@ -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 <AuditReservesParsed
isTable
key={index}
calldata={calldata}
label={labelOrName}
chainId={chainId}
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
case "setAdjustment":
return <SetAdjustmentParsed
isTable
args={args}
key={index}
calldata={calldata}
label={labelOrName}
chainId={chainId}
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
case "create":
return <CreateBondParsed
isTable
args={args}
key={index}
calldata={calldata}
label={labelOrName}
chainId={chainId}
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
default:
return <ParsedCell
isTable
key={index}
calldata={calldata}
label="Unknown"
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
}
}
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 (
<Box
onClick={() => 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] }`
}}
>
<Typography
color={isSelected ? theme.colors.gray[600] : "#fff"}
align="center"
>
{text}
</Typography>
</Box>
)
}
export const ArgumentsWrapper = ({ label, tooltip, children }) => {
return (
<Box sx={{ marginBottom: "18px" }}>
<Box sx={{ marginLeft: "5px", marginBottom: "5px" }} display="flex" flexDirection="row" alignItems="center">
<Typography>{label}</Typography>
<InfoTooltip message={tooltip} />
</Box>
{children}
</Box>
)
}
export const BooleanTrigger = ({ value, label, tooltip, leftText, rightText, setLeftValue, setRightValue }) => {
return (
<ArgumentsWrapper label={label} tooltip={tooltip}>
<Box display="flex" width="100%" height="40px">
<BooleanValue setSelected={setLeftValue} left isSelected={value} text={leftText} />
<BooleanValue setSelected={setRightValue} isSelected={!value} text={rightText} />
</Box>
</ArgumentsWrapper>
)
}
export const ArgumentInput = ({
endString,
label,
tooltip,
value,
setValue,
disabled,
inputType = "number",
placeholder = "0",
maxWidth = "100%"
}) => {
const theme = useTheme();
const ref = useRef(null);
return (
<Box sx={{ marginBottom: "15px" }}>
<Box sx={{ marginLeft: "5px", marginBottom: "5px" }} display="flex" flexDirection="row" alignItems="center">
<Typography>{label}</Typography>
<InfoTooltip message={tooltip} />
</Box>
<Box
display="flex"
flexDirection="column"
maxWidth={maxWidth}
sx={{ backgroundColor: theme.colors.gray[750] }}
borderRadius="12px"
padding="6px"
onClick={() => {
ref.current && ref.current.focus();
}}
>
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<StyledInputBase
disabled={disabled}
placeholder={placeholder}
type={inputType}
fontSize="20px"
value={value}
onChange={(e) => setValue(e.target.value)}
sx={{ flex: 1 }}
/>
{endString && (
<Box sx={{ paddingRight: "10px", color: theme.colors.gray[500] }} fontSize="12px" lineHeight="15px">
{endString}
</Box>
)}
</Box>
</Box>
</Box>
)
}
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 (
<TableRow id={props.id + `--proposalFunction`} data-testid={props.id + `--proposalFunction`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.label}</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Link href={etherscanLink} target="_blank" rel="noopener noreferrer">
<Typography>{shorten(props.target)}</Typography>
</Link>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Link onClick={handleCalldataCopy} target="_blank" rel="noopener noreferrer">
<Typography>{shorten(props.calldata)}</Typography>
</Link>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{formatCurrency(props.value, 4, props.nativeCoin)}</Typography>
</TableCell>
<TableCell>
<div style={{ display: 'flex', gap: '8px' }}>
{props.args && <TertiaryButton fullWidth onClick={() => props.setIsOpened(!props.isOpened)}>View</TertiaryButton>}
{props.remove && <TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>}
</div>
</TableCell>
</TableRow>
)
}

View File

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

View File

@ -1,5 +1,5 @@
import { Dispatch, SetStateAction, useState, useEffect } from "react"; 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 ReactGA from "react-ga4";
import Paper from "../../components/Paper/Paper"; import Paper from "../../components/Paper/Paper";
@ -47,7 +47,7 @@ export const StakeContainer = ({ chainId, address, connect }) => {
} }
return ( return (
<Box > <Box>
<PageTitle name="Protocol Staking" subtitle={`Stake your ${ftsoSymbol} to earn rebase yields`} /> <PageTitle name="Protocol Staking" subtitle={`Stake your ${ftsoSymbol} to earn rebase yields`} />
<Container <Container
style={{ style={{

View File

@ -61,7 +61,17 @@ const FarmPools = ({ chainId }) => {
} }
return ( return (
<Paper headerText="Farm Pools" fullWidth enableBackground> <Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Farm Pools
</Typography>
</Box>
}
>
<TableContainer> <TableContainer>
<Table aria-label="Farm pools" style={{ tableLayout: "fixed" }}> <Table aria-label="Farm pools" style={{ tableLayout: "fixed" }}>
<TableHead> <TableHead>

View File

@ -137,7 +137,7 @@ const WethWrapper = ({ chainId, address, config, connect }) => {
<meta name="twitter:image" content="https://ghostchain.io/wp-content/uploads/2025/03/ghostFaucet-Featured_Image.png" /> <meta name="twitter:image" content="https://ghostchain.io/wp-content/uploads/2025/03/ghostFaucet-Featured_Image.png" />
</Helmet> </Helmet>
<PageTitle name={`${reserveSymbol} Wrapper`} subtitle={`Wrap ${nativeInfo.symbol} into ${reserveSymbol}.`} /> <PageTitle name={`${reserveSymbol} Wrapper`} subtitle={`Wrap ${nativeInfo.symbol} into ${reserveSymbol}`} />
<Container <Container
style={{ style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem", paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",

View File

@ -0,0 +1,589 @@
import { useReadContract, useReadContracts } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import { keccak256, stringToBytes } from 'viem'
import { config } from "../../config";
import {
GHOST_GOVERNANCE_ADDRESSES,
} from "../../constants/addresses";
import { abi as GovernorAbi } from "../../abi/GhostGovernor.json";
import { abi as GovernorCountingAbi } from "../../abi/GovernorGhostCounting.json";
import { abi as GovernorStorageAbi } from "../../abi/GovernorStorage.json";
import { abi as GovernorVotesQuorumFractionAbi } from "../../abi/GovernorVotesQuorumFraction.json";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { getTokenDecimals, getTokenAbi, getTokenAddress } from "../helpers";
export const useProposalVoteOf = (chainId, proposalId, who) => {
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;
}
}

View File

@ -48,6 +48,9 @@ export const getTokenAbi = (name) => {
case "WETH": case "WETH":
abi = WethAbi; abi = WethAbi;
break; break;
case "WMWETH":
abi = WethAbi;
break;
} }
return abi; return abi;
} }
@ -86,6 +89,9 @@ export const getTokenDecimals = (name) => {
case "WETH": case "WETH":
decimals = 18; decimals = 18;
break; break;
case "WMWETH":
decimals = 18;
break;
} }
return decimals; return decimals;
} }
@ -133,6 +139,9 @@ export const getTokenAddress = (chainId, name) => {
case "WETC": case "WETC":
address = WETH_ADDRESSES[chainId]; address = WETH_ADDRESSES[chainId];
break; break;
case "WMETC":
address = WETH_ADDRESSES[chainId];
break;
} }
return address; return address;
} }
@ -141,7 +150,7 @@ export const getTokenIcons = (chainId, address) => {
let icons = [""]; let icons = [""];
switch (address) { switch (address) {
case RESERVE_ADDRESSES[chainId]: case RESERVE_ADDRESSES[chainId]:
icons = [chainId === 11155111 ? "GDAI" : "WETH"]; icons = ["WETH"];
break; break;
case FTSO_ADDRESSES[chainId]: case FTSO_ADDRESSES[chainId]:
icons = ["FTSO"]; icons = ["FTSO"];
@ -153,7 +162,7 @@ export const getTokenIcons = (chainId, address) => {
icons = ["GHST"]; icons = ["GHST"];
break; break;
case FTSO_DAI_LP_ADDRESSES[chainId]: case FTSO_DAI_LP_ADDRESSES[chainId]:
icons = ["FTSO", chainId === 11155111 ? "GDAI" : "WETH"]; icons = ["FTSO", "WETH"];
break; break;
} }
return icons; return icons;

View File

@ -9,6 +9,40 @@ import { shorten } from "../../helpers";
import { tokenNameConverter } from "../../helpers/tokenConverter"; import { tokenNameConverter } from "../../helpers/tokenConverter";
import { config } from "../../config"; 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) => { export const useTotalSupply = (chainId, name) => {
const contractAddress = getTokenAddress(chainId, name); const contractAddress = getTokenAddress(chainId, name);
const { data, refetch } = useToken({ const { data, refetch } = useToken({
@ -26,7 +60,7 @@ export const useTotalSupply = (chainId, name) => {
export const useBalance = (chainId, name, address) => { export const useBalance = (chainId, name, address) => {
const contractAddress = getTokenAddress(chainId, name); const contractAddress = getTokenAddress(chainId, name);
const { data, refetch } = useInnerBalance({ const { data, refetch, error } = useInnerBalance({
address, address,
chainId, chainId,
scopeKey: `balance-${contractAddress}-${address}-${chainId}`, scopeKey: `balance-${contractAddress}-${address}-${chainId}`,