apply dao governance
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
commit
133e911274
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ghost-dao-interface",
|
||||
"private": true,
|
||||
"version": "0.4.7",
|
||||
"version": "0.5.20",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -16,6 +16,7 @@
|
||||
"@ethersproject/bignumber": "^5.8.0",
|
||||
"@ethersproject/units": "^5.8.0",
|
||||
"@mui/icons-material": "^6.4.7",
|
||||
"@mui/lab": "6.0.1-beta.36",
|
||||
"@mui/material": "^6.4.7",
|
||||
"@mui/utils": "^6.4.6",
|
||||
"@polkadot-api/metadata-builders": "0.13.0",
|
||||
|
||||
218
pnpm-lock.yaml
218
pnpm-lock.yaml
@ -26,6 +26,9 @@ importers:
|
||||
'@mui/icons-material':
|
||||
specifier: ^6.4.7
|
||||
version: 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@mui/lab':
|
||||
specifier: 6.0.1-beta.36
|
||||
version: 6.0.1-beta.36(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@mui/material':
|
||||
specifier: ^6.4.7
|
||||
version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -525,6 +528,21 @@ packages:
|
||||
'@ethersproject/units@5.8.0':
|
||||
resolution: {integrity: sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==}
|
||||
|
||||
'@floating-ui/core@1.7.4':
|
||||
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
|
||||
|
||||
'@floating-ui/dom@1.7.5':
|
||||
resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
|
||||
|
||||
'@floating-ui/react-dom@2.1.7':
|
||||
resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@ -638,6 +656,18 @@ packages:
|
||||
resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
'@mui/base@5.0.0-beta.70':
|
||||
resolution: {integrity: sha512-Tb/BIhJzb0pa5zv/wu7OdokY9ZKEDqcu1BDFnohyvGCoHuSXbEr90rPq1qeNW3XvTBIbNWHEF7gqge+xpUo6tQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
deprecated: This package has been replaced by @base-ui/react
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/core-downloads-tracker@6.4.7':
|
||||
resolution: {integrity: sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==}
|
||||
|
||||
@ -652,6 +682,27 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/lab@6.0.1-beta.36':
|
||||
resolution: {integrity: sha512-af9lDmA9SZGEWF1XXk0EVBpfCITk9IKsvh9lLOZGdYaaHfQeCsqxGEDMvNO66j0P8EYoxpyry84LFCJYuLVtVw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.5.0
|
||||
'@emotion/styled': ^11.3.0
|
||||
'@mui/material': ^6.5.0
|
||||
'@mui/material-pigment-css': ^6.5.0
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
'@mui/material-pigment-css':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/material@6.4.7':
|
||||
resolution: {integrity: sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -682,6 +733,16 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/private-theming@6.4.9':
|
||||
resolution: {integrity: sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/styled-engine@6.4.6':
|
||||
resolution: {integrity: sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -695,6 +756,19 @@ packages:
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
|
||||
'@mui/styled-engine@6.5.0':
|
||||
resolution: {integrity: sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.4.1
|
||||
'@emotion/styled': ^11.3.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
|
||||
'@mui/system@6.4.7':
|
||||
resolution: {integrity: sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -711,6 +785,22 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/system@6.5.0':
|
||||
resolution: {integrity: sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.5.0
|
||||
'@emotion/styled': ^11.3.0
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/types@7.2.21':
|
||||
resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==}
|
||||
peerDependencies:
|
||||
@ -719,6 +809,14 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/types@7.2.24':
|
||||
resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/utils@6.4.6':
|
||||
resolution: {integrity: sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -729,6 +827,16 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/utils@6.4.9':
|
||||
resolution: {integrity: sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@noble/ciphers@1.2.1':
|
||||
resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
@ -3423,6 +3531,23 @@ snapshots:
|
||||
'@ethersproject/constants': 5.8.0
|
||||
'@ethersproject/logger': 5.8.0
|
||||
|
||||
'@floating-ui/core@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.7.5':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.4
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/react-dom@2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.5
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
@ -3615,6 +3740,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@mui/base@5.0.0-beta.70(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@floating-ui/react-dom': 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@mui/types': 7.2.24(@types/react@19.0.10)
|
||||
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
|
||||
'@popperjs/core': 2.11.8
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/core-downloads-tracker@6.4.7': {}
|
||||
|
||||
'@mui/icons-material@6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)':
|
||||
@ -3625,6 +3764,23 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/lab@6.0.1-beta.36(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@mui/base': 5.0.0-beta.70(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@mui/system': 6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@mui/types': 7.2.24(@types/react@19.0.10)
|
||||
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
@ -3648,16 +3804,38 @@ snapshots:
|
||||
|
||||
'@mui/private-theming@6.4.6(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
'@babel/runtime': 7.27.6
|
||||
'@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0)
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/private-theming@6.4.9(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/styled-engine@6.4.6(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
'@babel/runtime': 7.27.6
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
'@emotion/sheet': 1.4.0
|
||||
csstype: 3.1.3
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
|
||||
'@mui/styled-engine@6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
'@emotion/sheet': 1.4.0
|
||||
@ -3684,10 +3862,30 @@ snapshots:
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/system@6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@mui/private-theming': 6.4.9(@types/react@19.0.10)(react@19.0.0)
|
||||
'@mui/styled-engine': 6.5.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(react@19.0.0)
|
||||
'@mui/types': 7.2.24(@types/react@19.0.10)
|
||||
'@mui/utils': 6.4.9(@types/react@19.0.10)(react@19.0.0)
|
||||
clsx: 2.1.1
|
||||
csstype: 3.1.3
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.0.10)(react@19.0.0)
|
||||
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0)
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/types@7.2.21(@types/react@19.0.10)':
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/types@7.2.24(@types/react@19.0.10)':
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/utils@6.4.6(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
@ -3700,6 +3898,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@mui/utils@6.4.9(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@mui/types': 7.2.24(@types/react@19.0.10)
|
||||
'@types/prop-types': 15.7.14
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
react-is: 19.0.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
|
||||
'@noble/ciphers@1.2.1': {}
|
||||
|
||||
'@noble/ciphers@1.3.0': {}
|
||||
@ -5042,7 +5252,7 @@ snapshots:
|
||||
|
||||
babel-plugin-macros@3.1.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
'@babel/runtime': 7.27.6
|
||||
cosmiconfig: 7.1.0
|
||||
resolve: 1.22.10
|
||||
|
||||
@ -5238,7 +5448,7 @@ snapshots:
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
'@babel/runtime': 7.27.6
|
||||
csstype: 3.1.3
|
||||
|
||||
dot-case@3.0.4:
|
||||
|
||||
@ -14,7 +14,7 @@ import Sidebar from "./components/Sidebar/Sidebar";
|
||||
import TopBar from "./components/TopBar/TopBar";
|
||||
|
||||
import { shouldTriggerSafetyCheck } from "./helpers";
|
||||
import { isNetworkAvailable, isNetworkLegacy } from "./constants";
|
||||
import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "./constants";
|
||||
import useTheme from "./hooks/useTheme";
|
||||
import { useUnstableProvider } from "./hooks/ghost";
|
||||
import { dark as darkTheme } from "./themes/dark.js";
|
||||
@ -31,6 +31,9 @@ const Wrapper = lazy(() => import("./containers/WethWrapper/WethWrapper"));
|
||||
const Dex = lazy(() => import("./containers/Dex/Dex"));
|
||||
const Bridge = lazy(() => import("./containers/Bridge/Bridge"));
|
||||
const NotFound = lazy(() => import("./containers/NotFound/NotFound"));
|
||||
const Governance = lazy(() => import("./containers/Governance/Governance"));
|
||||
const ProposalDetails = lazy(() => import("./containers/Governance/ProposalDetails"));
|
||||
const NewProposal = lazy(() => import("./containers/Governance/NewProposal"));
|
||||
|
||||
const PREFIX = "App";
|
||||
|
||||
@ -213,6 +216,9 @@ function App() {
|
||||
}
|
||||
<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} />} />
|
||||
{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
|
||||
|
||||
1
src/abi/GhostDistributor.json
Normal file
1
src/abi/GhostDistributor.json
Normal file
File diff suppressed because one or more lines are too long
1
src/abi/GhostGovernor.json
Normal file
1
src/abi/GhostGovernor.json
Normal file
File diff suppressed because one or more lines are too long
1
src/abi/Governor.json
Normal file
1
src/abi/Governor.json
Normal file
File diff suppressed because one or more lines are too long
1
src/abi/GovernorGhostCounting.json
Normal file
1
src/abi/GovernorGhostCounting.json
Normal file
File diff suppressed because one or more lines are too long
1
src/abi/GovernorStorage.json
Normal file
1
src/abi/GovernorStorage.json
Normal file
File diff suppressed because one or more lines are too long
1
src/abi/GovernorVotesQuorumFraction.json
Normal file
1
src/abi/GovernorVotesQuorumFraction.json
Normal file
File diff suppressed because one or more lines are too long
@ -20,7 +20,7 @@ const StyledMuiChip = styled(MuiChip, {
|
||||
: template === "gray"
|
||||
? theme.colors.gray[500]
|
||||
: template === "darkGray"
|
||||
? theme.colors.gray[600]
|
||||
? theme.colors.primary[300]
|
||||
: theme.colors.feedback[template]
|
||||
: theme.palette.mode === "light"
|
||||
? theme.colors.gray[90]
|
||||
@ -34,7 +34,7 @@ const StyledMuiChip = styled(MuiChip, {
|
||||
: template === "gray"
|
||||
? theme.colors.gray[10]
|
||||
: template === "darkGray"
|
||||
? theme.colors.gray[90]
|
||||
? theme.colors.gray[800]
|
||||
: theme.colors.gray[600],
|
||||
fontWeight: strong ? 700 : 450,
|
||||
},
|
||||
|
||||
39
src/components/Progress/LinearProgressBar.jsx
Normal file
39
src/components/Progress/LinearProgressBar.jsx
Normal 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;
|
||||
74
src/components/Select/Select.jsx
Normal file
74
src/components/Select/Select.jsx
Normal 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;
|
||||
@ -24,6 +24,8 @@ import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import HowToVoteIcon from '@mui/icons-material/HowToVote';
|
||||
import HubIcon from '@mui/icons-material/Hub';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import GavelIcon from '@mui/icons-material/Gavel';
|
||||
import ForumIcon from '@mui/icons-material/Forum';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BookIcon from '@mui/icons-material/Book';
|
||||
@ -37,7 +39,7 @@ import BondIcon from "../Icon/BondIcon";
|
||||
import StakeIcon from "../Icon/StakeIcon";
|
||||
import WrapIcon from "../Icon/WrapIcon";
|
||||
|
||||
import { isNetworkAvailable, isNetworkLegacy } from "../../constants";
|
||||
import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "../../constants";
|
||||
import { AVAILABLE_DEXES } from "../../constants/dexes";
|
||||
import { ECOSYSTEM } from "../../constants/ecosystem";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
@ -177,7 +179,8 @@ const NavContent = ({ chainId, addressChainId }) => {
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
<Divider />
|
||||
</Box>
|
||||
|
||||
@ -7,7 +7,7 @@ import Token from "../Token/Token";
|
||||
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
|
||||
const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })(
|
||||
export const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })(
|
||||
({ inputWidth, inputFontSize }) => ({
|
||||
"& .MuiInputBase-input": {
|
||||
padding: 0,
|
||||
|
||||
@ -4,6 +4,19 @@ export enum NetworkId {
|
||||
TESTNET_MORDOR = 63,
|
||||
}
|
||||
|
||||
export const isGovernanceAvailable = (chainId, addressChainId) => {
|
||||
chainId = addressChainId ? addressChainId : chainId;
|
||||
let exists = false;
|
||||
switch (chainId) {
|
||||
case 11155111:
|
||||
exists = true
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export const isNetworkAvailable = (chainId, addressChainId) => {
|
||||
chainId = addressChainId ? addressChainId : chainId;
|
||||
let exists = false;
|
||||
@ -26,9 +39,6 @@ export const isNetworkAvailable = (chainId, addressChainId) => {
|
||||
export const isNetworkLegacy = (chainId) => {
|
||||
let exists = false;
|
||||
switch (chainId) {
|
||||
case 11155111:
|
||||
exists = true
|
||||
break;
|
||||
case 560048:
|
||||
exists = true
|
||||
break;
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
import { NetworkId } from "../constants";
|
||||
|
||||
export const STAKING_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xd90E63E88282596E1ea33765b41Ba3d650f4aD52",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||
[NetworkId.TESTNET_HOODI]: "0x25F62eDc6C89FF84E957C22336A35d2dfc861a86",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xC25C9C56a89ebd6ef291b415d00ACfa7913c55e7",
|
||||
};
|
||||
|
||||
export const BOND_DEPOSITORY_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xdcE486113280e49ca2fB200258E5Ee1B2D21D495",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||
[NetworkId.TESTNET_HOODI]: "0x6Ad50B1E293E68B2fC230c576220a93A9D311571",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x7C85cDEddBAd0f50453d373F7332BEa11ECa7BAf",
|
||||
};
|
||||
|
||||
export const DAO_TREASURY_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x93dd30f819403710de7933B79A74C4A42438458D",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||
[NetworkId.TESTNET_HOODI]: "0x1a1b29b18f714fac9dDabEf530dFc4f85b56A6e8",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x5883C8e2259556B534036c7fDF4555E09dE9f243",
|
||||
};
|
||||
|
||||
export const FTSO_DAI_LP_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x1394dC3f7bABaa2F0CA80353648087DAB1BF3fd6",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
|
||||
[NetworkId.TESTNET_HOODI]: "0xf7B2d44209E70782d93A70F7D8eC50010dF7ae50",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xE6546D12665dB5B22Cb92FB9e0221aE51A57aeaa",
|
||||
};
|
||||
@ -31,49 +31,47 @@ export const FTSO_STNK_LP_ADDRESSES = {
|
||||
}
|
||||
|
||||
export const RESERVE_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x5f63a27a9214a0352F2EF8dAF1eD4974d713192B",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
|
||||
[NetworkId.TESTNET_HOODI]: "0x80c6676c334BCcE60b3CC852085B72143379CE58",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC",
|
||||
};
|
||||
|
||||
export const WETH_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
|
||||
[NetworkId.TESTNET_HOODI]: "0xE69a5c6dd88cA798b93c3C92fc50c51Fd5305eB4",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC",
|
||||
};
|
||||
|
||||
export const GHST_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xdf2e5306A3dCcfA4e21bbF4226C17Ff5B008dDC4",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||
[NetworkId.TESTNET_HOODI]: "0xE98f7426457E6533B206e91B7EcA97aa8A258B46",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x14b5787F8a1E62786F50A7998A9b14aa24298423",
|
||||
};
|
||||
|
||||
export const STNK_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x02C296A27eA779d5a16F934337c12062C5E3c0D9",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||
[NetworkId.TESTNET_HOODI]: "0xF07e9303A9f16Afd82f4f57Fd6fca68Aa0AB6D7F",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x137bA9403885D8ECEa95AaFBb8734F5a16121bAC",
|
||||
};
|
||||
|
||||
export const FTSO_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xcFedFFEB3FdeCd2196820Ba3b71f3F84A1255f93",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||
[NetworkId.TESTNET_HOODI]: "0xb184e423811b644A1924334E63985c259F5D0033",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xeA170CC0faceC531a6a9e93a28C4330Ac50343a1",
|
||||
};
|
||||
|
||||
export const DISTRIBUTOR_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x8fbF8eB4Fcd451EF62Aee33508D46FE120963194",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||
[NetworkId.TESTNET_HOODI]: "0xdF49dC81c457c6f92e26cf6d686C7a8715255842",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xaf5e76706520db7fb01096E322940206bf3fce57",
|
||||
};
|
||||
|
||||
export const GHOST_GOVERNANCE_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xDab0c51918E6990d8763FAC8a04AE159e44e0c4f",
|
||||
[NetworkId.TESTNET_HOODI]: "0x1B96B792840d4d19d5097ee007392Ed4d851e64F",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x3dD438416D9593A58193fC52850E588efAa3D57E",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||
};
|
||||
|
||||
export const BONDING_CALCULATOR_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x4896bFc6256A57Df826d7144E48c9633d51d6319",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||
[NetworkId.TESTNET_HOODI]: "0x2635d526Ad24b98082563937f7b996075052c6Fd",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x0c4C7C49a173E2a3f9Eed93125F3F146D8e17bCb",
|
||||
}
|
||||
@ -111,6 +109,10 @@ export const NATIVE_TICKERS = {
|
||||
}
|
||||
|
||||
export const CEX_TICKERS = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: [
|
||||
"https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT",
|
||||
"https://api.coinbase.com/v2/prices/ETH-USDT/spot",
|
||||
],
|
||||
[NetworkId.TESTNET_MORDOR]: [
|
||||
"https://api.binance.com/api/v3/ticker/price?symbol=ETCUSDT",
|
||||
"https://api.coinbase.com/v2/prices/ETC-USDT/spot",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Box, Tab, Tabs, Container, useMediaQuery } from "@mui/material";
|
||||
import { Box, Tab, Tabs, Typography, Container, useMediaQuery } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import Paper from "../../components/Paper/Paper";
|
||||
@ -21,7 +20,6 @@ import { useTokenSymbol } from "../../hooks/tokens";
|
||||
|
||||
const Bonds = ({ chainId, address, connect }) => {
|
||||
const [isZoomed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [secondsTo, setSecondsTo] = useState(0);
|
||||
|
||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||
@ -29,7 +27,7 @@ const Bonds = ({ chainId, address, connect }) => {
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.send({ hitType: "pageview", page: "/bonds" });
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const { liveBonds } = useLiveBonds(chainId);
|
||||
const totalReserves = useTotalReserves(chainId);
|
||||
@ -59,7 +57,17 @@ const Bonds = ({ chainId, address, connect }) => {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Metric
|
||||
label={`Treasury Balance`}
|
||||
|
||||
@ -83,7 +83,17 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
|
||||
warmupLength={warmupInfo.expiry - epoch.number}
|
||||
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">
|
||||
<Typography variant="h4" align="center" color="textSecondary">
|
||||
Payout Options
|
||||
|
||||
@ -140,7 +140,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
|
||||
|
||||
const transactionApplaused = useMemo(() => {
|
||||
return transactionApplausedDirect || transactionApplausedIncremented;
|
||||
}, [transactionApplausedDirect, transactionApplausedIncremented])
|
||||
}, [transactionApplausedDirect, transactionApplausedIncremented]);
|
||||
|
||||
const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n);
|
||||
|
||||
@ -252,10 +252,10 @@ const Bridge = ({ chainId, address, config, connect }) => {
|
||||
if (commit.disabled || blockNumber < blocksInFourHours) {
|
||||
continue;
|
||||
}
|
||||
certainty += (commit?.lastStoredBlock ?? 0n) - (blockNumber - blocksInFourHours);
|
||||
certainty += (commit?.lastStoredBlock ?? 0n) + BigInt(finalityDelay) - (blockNumber - blocksInFourHours);
|
||||
}
|
||||
return Math.max(Number(certainty * 100n / (blocksInFourHours * BigInt(length))), 0);
|
||||
}, [latestCommits, blockNumber]);
|
||||
}, [latestCommits, blockNumber, finalityDelay]);
|
||||
|
||||
const timeToNextEpoch = useMemo(() => {
|
||||
if (!currentSession || !genesisSlot || !currentSlot) {
|
||||
@ -302,7 +302,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
style={{
|
||||
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||
@ -319,6 +319,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
|
||||
handleButtonProceed={handleButtonProceed}
|
||||
/>
|
||||
<BridgeModal
|
||||
providerDetail={providerDetail}
|
||||
currentRecord={currentRecord}
|
||||
activeTxIndex={activeTxIndex}
|
||||
setActiveTxIndex={setActiveTxIndex}
|
||||
@ -343,6 +344,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
|
||||
transactionEta={slowestEvmBlock ? Number(slowestEvmBlock) : undefined}
|
||||
timeToNextEpoch={timeToNextEpoch ? Number(timeToNextEpoch) : undefined}
|
||||
isSmallScreen={isSmallScreen}
|
||||
maxDelay={14400 + finalityDelay * Number(networkAvgBlockSpeed(chainId))}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
@ -358,7 +360,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
|
||||
onClick={() => setBridgeAction(!bridgeAction)}
|
||||
/>)}
|
||||
<Typography variant="h4">{
|
||||
bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction History"
|
||||
bridgeAction ? `Bridge-In $${ghstSymbol}` : "Transaction History"
|
||||
}</Typography>
|
||||
</Box>
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ export const BridgeHeader = ({
|
||||
bridgeStability,
|
||||
transactionEta,
|
||||
timeToNextEpoch,
|
||||
maxDelay,
|
||||
isSmallScreen
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@ -92,7 +93,7 @@ export const BridgeHeader = ({
|
||||
<Grid item xs={isSmallScreen ? 12 : 4}>
|
||||
<Metric
|
||||
isLoading={transactionEta === undefined}
|
||||
metric={transactionEta > 14400 ? "∞" : formatTime(transactionEta)}
|
||||
metric={transactionEta > maxDelay ? "∞" : formatTime(transactionEta)}
|
||||
label="Max Bridge ETA"
|
||||
tooltip="Maximum estimated time for finalizing bridge transactions based on the latest update."
|
||||
/>
|
||||
|
||||
@ -19,12 +19,13 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
||||
import Modal from "../../components/Modal/Modal";
|
||||
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||
import { PrimaryButton } from "../../components/Button";
|
||||
import { PrimaryButton, TertiaryButton } from "../../components/Button";
|
||||
|
||||
import { formatCurrency } from "../../helpers";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
|
||||
export const BridgeModal = ({
|
||||
providerDetail,
|
||||
currentRecord,
|
||||
activeTxIndex,
|
||||
setActiveTxIndex,
|
||||
@ -88,7 +89,15 @@ export const BridgeModal = ({
|
||||
minHeight={"100px"}
|
||||
>
|
||||
<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 && (
|
||||
<>
|
||||
<Box
|
||||
@ -276,7 +285,7 @@ export const BridgeModal = ({
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>}
|
||||
|
||||
<Box display="flex" flexDirection="column" gap="5px" padding="0.6rem 0">
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||
|
||||
@ -105,11 +105,11 @@ export const ValidatorTable = ({
|
||||
<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">
|
||||
<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="body2">Download GHOST Wallet 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="h6">GHOST Connect is not detected on your browser!</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 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')}>
|
||||
Get GHOST Extension
|
||||
Get GHOST Connect
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>}
|
||||
|
||||
@ -186,7 +186,7 @@ const Dex = ({ chainId, address, connect }) => {
|
||||
|
||||
<PageTitle
|
||||
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
|
||||
style={{
|
||||
|
||||
@ -46,6 +46,7 @@ const SwapContainer = ({
|
||||
refetch: balanceRefetchTop,
|
||||
contractAddress: addressTop,
|
||||
} = useBalance(chainId, tokenNameTop, address);
|
||||
|
||||
const {
|
||||
balance: balanceBottom,
|
||||
refetch: balanceRefetchBottom,
|
||||
|
||||
@ -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" />
|
||||
</Helmet>
|
||||
|
||||
<PageTitle name={`${reserveSymbol} Faucet`} subtitle={`Swap Sepolia ${nativeInfo.symbol} for ${reserveSymbol}.`} />
|
||||
<PageTitle name={`${reserveSymbol} Faucet`} subtitle={`Swap Sepolia ${nativeInfo.symbol} for ${reserveSymbol}`} />
|
||||
<Container
|
||||
style={{
|
||||
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||
|
||||
90
src/containers/Governance/Governance.jsx
Normal file
90
src/containers/Governance/Governance.jsx
Normal 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;
|
||||
226
src/containers/Governance/NewProposal.jsx
Normal file
226
src/containers/Governance/NewProposal.jsx
Normal 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;
|
||||
483
src/containers/Governance/ProposalDetails.jsx
Normal file
483
src/containers/Governance/ProposalDetails.jsx
Normal 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;
|
||||
23
src/containers/Governance/components/GovernanceInfoText.jsx
Normal file
23
src/containers/Governance/components/GovernanceInfoText.jsx
Normal 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"
|
||||
>
|
||||
ghostDAO’s adaptive governance system algorithmically sets minimum collateral based on activity.
|
||||
<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;
|
||||
65
src/containers/Governance/components/Metric.jsx
Normal file
65
src/containers/Governance/components/Metric.jsx
Normal 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} />;
|
||||
}
|
||||
23
src/containers/Governance/components/ProposalInfoText.jsx
Normal file
23
src/containers/Governance/components/ProposalInfoText.jsx
Normal 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.
|
||||
<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;
|
||||
115
src/containers/Governance/components/ProposalModal.jsx
Normal file
115
src/containers/Governance/components/ProposalModal.jsx
Normal 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;
|
||||
360
src/containers/Governance/components/ProposalsList.jsx
Normal file
360
src/containers/Governance/components/ProposalsList.jsx
Normal 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;
|
||||
@ -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} />
|
||||
}
|
||||
334
src/containers/Governance/components/functions/CreateBond.jsx
Normal file
334
src/containers/Governance/components/functions/CreateBond.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
112
src/containers/Governance/components/functions/SetAdjustment.jsx
Normal file
112
src/containers/Governance/components/functions/SetAdjustment.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
308
src/containers/Governance/components/functions/index.jsx
Normal file
308
src/containers/Governance/components/functions/index.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
38
src/containers/Governance/helpers.js
Normal file
38
src/containers/Governance/helpers.js
Normal 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";
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { Dispatch, SetStateAction, useState, useEffect } from "react";
|
||||
import { Box, Container, Grid, Divider, Typography, Link, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { Box, Container, Grid, Divider, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import Paper from "../../components/Paper/Paper";
|
||||
|
||||
@ -61,7 +61,17 @@ const FarmPools = ({ chainId }) => {
|
||||
}
|
||||
|
||||
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>
|
||||
<Table aria-label="Farm pools" style={{ tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
|
||||
@ -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" />
|
||||
</Helmet>
|
||||
|
||||
<PageTitle name={`${reserveSymbol} Wrapper`} subtitle={`Wrap ${nativeInfo.symbol} into ${reserveSymbol}.`} />
|
||||
<PageTitle name={`${reserveSymbol} Wrapper`} subtitle={`Wrap ${nativeInfo.symbol} into ${reserveSymbol}`} />
|
||||
<Container
|
||||
style={{
|
||||
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||
|
||||
589
src/hooks/governance/index.js
Normal file
589
src/hooks/governance/index.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -48,6 +48,9 @@ export const getTokenAbi = (name) => {
|
||||
case "WETH":
|
||||
abi = WethAbi;
|
||||
break;
|
||||
case "WMWETH":
|
||||
abi = WethAbi;
|
||||
break;
|
||||
}
|
||||
return abi;
|
||||
}
|
||||
@ -86,6 +89,9 @@ export const getTokenDecimals = (name) => {
|
||||
case "WETH":
|
||||
decimals = 18;
|
||||
break;
|
||||
case "WMWETH":
|
||||
decimals = 18;
|
||||
break;
|
||||
}
|
||||
return decimals;
|
||||
}
|
||||
@ -133,6 +139,9 @@ export const getTokenAddress = (chainId, name) => {
|
||||
case "WETC":
|
||||
address = WETH_ADDRESSES[chainId];
|
||||
break;
|
||||
case "WMETC":
|
||||
address = WETH_ADDRESSES[chainId];
|
||||
break;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
@ -141,7 +150,7 @@ export const getTokenIcons = (chainId, address) => {
|
||||
let icons = [""];
|
||||
switch (address) {
|
||||
case RESERVE_ADDRESSES[chainId]:
|
||||
icons = [chainId === 11155111 ? "GDAI" : "WETH"];
|
||||
icons = ["WETH"];
|
||||
break;
|
||||
case FTSO_ADDRESSES[chainId]:
|
||||
icons = ["FTSO"];
|
||||
@ -153,7 +162,7 @@ export const getTokenIcons = (chainId, address) => {
|
||||
icons = ["GHST"];
|
||||
break;
|
||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
||||
icons = ["FTSO", chainId === 11155111 ? "GDAI" : "WETH"];
|
||||
icons = ["FTSO", "WETH"];
|
||||
break;
|
||||
}
|
||||
return icons;
|
||||
|
||||
@ -9,6 +9,40 @@ import { shorten } from "../../helpers";
|
||||
import { tokenNameConverter } from "../../helpers/tokenConverter";
|
||||
import { config } from "../../config";
|
||||
|
||||
export const usePastVotes = (chainId, name, timepoint, address) => {
|
||||
const decimals = getTokenDecimals(name);
|
||||
const contractAddress = getTokenAddress(chainId, name);
|
||||
|
||||
const { data, refetch, error } = useReadContract({
|
||||
abi: getTokenAbi(name),
|
||||
address: contractAddress,
|
||||
functionName: "getPastVotes",
|
||||
args: [address, timepoint],
|
||||
scopeKey: `getPastVotes-${timepoint}-${chainId}`,
|
||||
chainId: chainId,
|
||||
});
|
||||
|
||||
const pastVotes = new DecimalBigNumber(data ? data : 0n, decimals);
|
||||
return { pastVotes }
|
||||
}
|
||||
|
||||
export const usePastTotalSupply = (chainId, name, timepoint) => {
|
||||
const decimals = getTokenDecimals(name);
|
||||
const contractAddress = getTokenAddress(chainId, name);
|
||||
|
||||
const { data, refetch, error } = useReadContract({
|
||||
abi: getTokenAbi(name),
|
||||
address: contractAddress,
|
||||
functionName: "getPastTotalSupply",
|
||||
args: [timepoint],
|
||||
scopeKey: `getPastTotalSupply-${timepoint}-${chainId}`,
|
||||
chainId: chainId,
|
||||
});
|
||||
|
||||
const pastTotalSupply = new DecimalBigNumber(data ? data : 0n, decimals);
|
||||
return { pastTotalSupply, refetch };
|
||||
}
|
||||
|
||||
export const useTotalSupply = (chainId, name) => {
|
||||
const contractAddress = getTokenAddress(chainId, name);
|
||||
const { data, refetch } = useToken({
|
||||
@ -26,7 +60,7 @@ export const useTotalSupply = (chainId, name) => {
|
||||
|
||||
export const useBalance = (chainId, name, address) => {
|
||||
const contractAddress = getTokenAddress(chainId, name);
|
||||
const { data, refetch } = useInnerBalance({
|
||||
const { data, refetch, error } = useInnerBalance({
|
||||
address,
|
||||
chainId,
|
||||
scopeKey: `balance-${contractAddress}-${address}-${chainId}`,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user