Compare commits
No commits in common. "main" and "extension-integration" have entirely different histories.
main
...
extension-
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ghost-dao-interface",
|
||||
"private": true,
|
||||
"version": "0.7.15",
|
||||
"version": "0.2.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -16,7 +16,6 @@
|
||||
"@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
@ -26,9 +26,6 @@ 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)
|
||||
@ -528,21 +525,6 @@ 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'}
|
||||
@ -656,18 +638,6 @@ 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==}
|
||||
|
||||
@ -682,27 +652,6 @@ 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'}
|
||||
@ -733,16 +682,6 @@ 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'}
|
||||
@ -756,19 +695,6 @@ 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'}
|
||||
@ -785,22 +711,6 @@ 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:
|
||||
@ -809,14 +719,6 @@ 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'}
|
||||
@ -827,16 +729,6 @@ 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}
|
||||
@ -3531,23 +3423,6 @@ 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':
|
||||
@ -3740,20 +3615,6 @@ 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)':
|
||||
@ -3764,23 +3625,6 @@ 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
|
||||
@ -3804,38 +3648,16 @@ snapshots:
|
||||
|
||||
'@mui/private-theming@6.4.6(@types/react@19.0.10)(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@babel/runtime': 7.26.9
|
||||
'@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.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
|
||||
'@babel/runtime': 7.26.9
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
'@emotion/sheet': 1.4.0
|
||||
@ -3862,30 +3684,10 @@ 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
|
||||
@ -3898,18 +3700,6 @@ 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': {}
|
||||
@ -5252,7 +5042,7 @@ snapshots:
|
||||
|
||||
babel-plugin-macros@3.1.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@babel/runtime': 7.26.9
|
||||
cosmiconfig: 7.1.0
|
||||
resolve: 1.22.10
|
||||
|
||||
@ -5448,7 +5238,7 @@ snapshots:
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
'@babel/runtime': 7.26.9
|
||||
csstype: 3.1.3
|
||||
|
||||
dot-case@3.0.4:
|
||||
|
||||
57
src/App.jsx
@ -5,16 +5,8 @@ import CssBaseline from "@mui/material/CssBaseline";
|
||||
import { styled, ThemeProvider } from "@mui/material/styles";
|
||||
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import { Outlet, Navigate, Route, Routes, useLocation, useParams } from "react-router-dom";
|
||||
import {
|
||||
useAccount,
|
||||
useConnect,
|
||||
useChainId,
|
||||
useConfig,
|
||||
usePublicClient,
|
||||
useSwitchChain,
|
||||
injected
|
||||
} from "wagmi";
|
||||
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { useAccount, useConnect, useChainId, useConfig, usePublicClient, injected } from "wagmi";
|
||||
|
||||
import Messages from "./components/Messages/Messages";
|
||||
import NavDrawer from "./components/Sidebar/NavDrawer";
|
||||
@ -22,7 +14,7 @@ import Sidebar from "./components/Sidebar/Sidebar";
|
||||
import TopBar from "./components/TopBar/TopBar";
|
||||
|
||||
import { shouldTriggerSafetyCheck } from "./helpers";
|
||||
import { isNetworkAvailable, isGovernanceAvailable } from "./constants";
|
||||
import { isNetworkAvailable } from "./constants";
|
||||
import useTheme from "./hooks/useTheme";
|
||||
import { useUnstableProvider } from "./hooks/ghost";
|
||||
import { dark as darkTheme } from "./themes/dark.js";
|
||||
@ -34,12 +26,10 @@ const Bonds = lazy(() => import("./containers/Bond/Bonds"));
|
||||
const BondModalContainer = lazy(() => import("./containers/Bond/BondModal"));
|
||||
const StakeContainer = lazy(() => import("./containers/Stake/StakeContainer"));
|
||||
const TreasuryDashboard = lazy(() => import("./containers/TreasuryDashboard/TreasuryDashboard"));
|
||||
const Faucet = lazy(() => import("./containers/Faucet/Faucet"));
|
||||
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";
|
||||
|
||||
@ -106,7 +96,6 @@ function App() {
|
||||
const [theme, toggleTheme] = useTheme();
|
||||
|
||||
const config = useConfig();
|
||||
const { chains } = useSwitchChain();
|
||||
const { connect, error: errorMessage } = useConnect();
|
||||
const tryConnectInjected = () => connect({ connector: injected() });
|
||||
|
||||
@ -116,12 +105,12 @@ function App() {
|
||||
const [isSidebarExpanded, setIsSidebarExpanded] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const { address = "", chainId: addressChainId, status, isConnected, isReconnecting } = useAccount();
|
||||
const { address = "", chainId: addressChainId, isConnected, isReconnecting } = useAccount();
|
||||
|
||||
const provider = usePublicClient();
|
||||
const chainId = useChainId();
|
||||
|
||||
const isSmallerScreen = useMediaQuery("(max-width: 1130px)");
|
||||
const isSmallerScreen = useMediaQuery("(max-width: 1047px)");
|
||||
const isSmallScreen = useMediaQuery("(max-width: 600px)");
|
||||
|
||||
const {
|
||||
@ -160,7 +149,7 @@ function App() {
|
||||
setWrongNetworkToastId(null);
|
||||
}
|
||||
}
|
||||
}, [chainId, addressChainId, isConnected, wrongNetworkToastId])
|
||||
}, [chainId, addressChainId, isConnected])
|
||||
|
||||
useEffect(() => {
|
||||
if (errorMessage) {
|
||||
@ -177,6 +166,7 @@ function App() {
|
||||
};
|
||||
|
||||
const themeMode = theme === "light" ? lightTheme : theme === "dark" ? darkTheme : gTheme;
|
||||
|
||||
const chainExists = isNetworkAvailable(chainId, addressChainId);
|
||||
|
||||
useEffect(() => {
|
||||
@ -193,7 +183,6 @@ function App() {
|
||||
wrongNetworkToastId={wrongNetworkToastId}
|
||||
setWrongNetworkToastId={setWrongNetworkToastId}
|
||||
address={address}
|
||||
status={status}
|
||||
chainId={addressChainId ? addressChainId : chainId}
|
||||
chainExists={chainExists}
|
||||
handleDrawerToggle={handleDrawerToggle}
|
||||
@ -210,19 +199,17 @@ function App() {
|
||||
<div className={`${classes.content} ${isSmallerScreen && classes.contentShift}`}>
|
||||
<Suspense fallback={<div></div>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to={chainExists ? `/${chains.at(0).name.toLowerCase()}/dashboard` : "/empty"} />} />
|
||||
<Route path="/" element={<Navigate to={chainExists ? "/dashboard" : "/empty"} />} />
|
||||
{chainExists &&
|
||||
<Route path="/:network" element={<AvailableNetworkGuard allowedNetworks={chains.map(chain => chain.name.toLowerCase())} /> }>
|
||||
<Route path="dashboard" element={<TreasuryDashboard chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="bonds/:id" element={<BondModalContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="stake" element={<StakeContainer connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="dex/:name" element={<Dex config={config} 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>
|
||||
<>
|
||||
<Route path="/dashboard" element={<TreasuryDashboard chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="/bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="/bonds/:id" element={<BondModalContainer connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="/stake" element={<StakeContainer connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId}/>} />
|
||||
<Route path="/faucet" element={<Faucet config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="/bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="/dex/:name" element={<Dex connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
</>
|
||||
}
|
||||
<Route path="/empty" element={<NotFound
|
||||
wrongNetworkToastId={wrongNetworkToastId}
|
||||
@ -245,12 +232,4 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
const AvailableNetworkGuard = ({ allowedNetworks }) => {
|
||||
const { network } = useParams();
|
||||
if (!allowedNetworks.includes(network)) {
|
||||
return <Navigate to="/empty" replace />;
|
||||
}
|
||||
return <Outlet />
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@ -1 +0,0 @@
|
||||
{"abi":[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"src","type":"address"},{"indexed":true,"internalType":"address","name":"guy","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"dst","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"src","type":"address"},{"indexed":true,"internalType":"address","name":"dst","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"src","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"guy","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]}
|
||||
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="uuid-61b3c585-44d6-4fd8-a0e7-6a5e76478eb1" data-name="uuid-70ba15fb-c44e-4945-a02a-8d07f94a9abb" xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 513 513">
|
||||
<defs>
|
||||
<style>
|
||||
.uuid-6437a6f0-42e0-4c9c-b0ef-b632253e989a {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.uuid-f6980b42-338a-476b-b4b3-c6f8474bfe6a {
|
||||
fill: #0b8311;
|
||||
}
|
||||
|
||||
.uuid-cee5d69f-ecfa-41d4-b22b-62a3555cf216 {
|
||||
fill: #3ab83a;
|
||||
}
|
||||
|
||||
.uuid-8c3bab1c-21e8-4834-aebc-42a28aaec68e {
|
||||
fill: #146714;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g fill="#222" data-name="uuid-3af91251-9afa-43cd-b331-1e6ceef8228b">
|
||||
<g id="uuid-38146a20-6896-4919-8709-3b8aa9b9b9de" data-name="uuid-bca144dc-fff0-427e-b9b1-33b0d4491804">
|
||||
<g id="uuid-01777163-9cb4-4f63-8018-462f63cda4ae" data-name="uuid-d1a1e453-da74-40e2-920d-7662cd9352f5">
|
||||
<path d="m245.2,485.4c-64.14,0-124.44-24.99-169.8-70.36C30.05,369.68,5.05,309.36,5,245.2c0-64.15,24.99-124.46,70.36-169.84C120.73,29.98,181.05,5,245.2,5s124.47,24.99,169.84,70.36c45.37,45.37,70.36,105.69,70.36,169.84s-24.99,124.47-70.36,169.84-105.69,70.36-169.84,70.36h0Z"/>
|
||||
<path class="uuid-6437a6f0-42e0-4c9c-b0ef-b632253e989a" d="m245.2,10c31.75,0,62.55,6.22,91.54,18.48,28.01,11.85,53.16,28.81,74.77,50.41,21.61,21.61,38.57,46.76,50.41,74.77,12.26,28.99,18.48,59.79,18.48,91.54s-6.22,62.55-18.48,91.54c-11.85,28.01-28.81,53.16-50.41,74.77-21.61,21.61-46.76,38.57-74.77,50.41-28.99,12.26-59.79,18.48-91.54,18.48s-62.54-6.22-91.52-18.48c-28-11.85-53.14-28.81-74.74-50.41s-38.56-46.76-50.41-74.77c-12.27-28.99-18.5-59.79-18.52-91.54,0-31.75,6.22-62.55,18.48-91.54,11.85-28.01,28.81-53.16,50.41-74.77,21.61-21.61,46.76-38.57,74.77-50.41,28.99-12.26,59.79-18.48,91.54-18.48M245.21,0C109.8,0,0,109.8,0,245.2c.1,135.4,109.8,245.2,245.2,245.2s245.2-109.8,245.2-245.2S380.6,0,245.2,0h0Z"/>
|
||||
</g>
|
||||
<g id="uuid-91c9d373-1cb9-4018-9a6a-66437f262f1c" data-name="uuid-863b3aee-71cf-488d-8b5a-a2d97fac93c4">
|
||||
<path id="uuid-538e9787-13a3-4271-8587-11f8527f9a71" data-name="uuid-10bd56be-0db7-4e24-bb20-a24d1c3cbcb7" class="uuid-cee5d69f-ecfa-41d4-b22b-62a3555cf216" d="m144.29,259.73c35.56,18.89,72.67,38.65,101.33,53.95l100.5-53.95c-36.39,54.06-66.71,99.06-100.5,148.87-33.85-49.7-71.23-104.54-101.33-148.87Zm3.87-14.91l97.57-52.07,96.3,51.69-96.25,52.12-97.63-51.74h.01Zm97.46-68.75l-101.33,53.34,100.89-147.71,100.94,148.04-100.5-53.67h0Z"/>
|
||||
<path id="uuid-4aa91baa-53f1-4f63-abbe-cf753f8525a0" data-name="uuid-4c5fd78b-71fb-4d56-83d4-a2022fef9f50" class="uuid-f6980b42-338a-476b-b4b3-c6f8474bfe6a" d="m245.61,313.68l100.5-53.95c-36.39,54.06-100.5,148.87-100.5,148.87v-94.92h0Zm.11-120.93l96.3,51.69-96.25,52.12-.05-103.81h0Zm-.11-16.68l-.44-94.37,100.94,148.04-100.5-53.67h0Z"/>
|
||||
<path id="uuid-3febb173-485e-4b4e-92b6-b05cc7b0412b" data-name="uuid-3f7cae2c-d7bb-44d6-a6e9-cd80adc2aeac" class="uuid-f6980b42-338a-476b-b4b3-c6f8474bfe6a" d="m148.15,244.82l97.58,8.01,96.3-8.34-96.25,52.13-97.63-51.8h0Z"/>
|
||||
<path id="uuid-df565d0e-a3cf-452f-80f6-2e6ea5ca4f56" data-name="uuid-e69b7837-4cf3-4793-a865-a26ad4121390" class="uuid-8c3bab1c-21e8-4834-aebc-42a28aaec68e" d="m245.72,252.83l96.3-8.34-96.25,52.13-.05-43.79h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@ -1,36 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="uuid-39ebc51d-db02-45d2-ad4b-e7ae31bb915b" data-name="Ethereum" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 254 254">
|
||||
<defs>
|
||||
<style>
|
||||
.uuid-e080659c-91a4-4965-9905-603394020b2c {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.uuid-ca9afedd-40fd-4006-99f9-d2c3cf46bc21 {
|
||||
fill: #627eea;
|
||||
}
|
||||
|
||||
.uuid-bb37a35d-b2ce-440e-86f6-0142a60ec718 {
|
||||
fill: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50 {
|
||||
fill: rgba(255, 255, 255, .6);
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="uuid-c49657cc-831e-4816-83c0-b3ceca5eead0" data-name="Ethereum">
|
||||
<g id="uuid-559a2235-ac8a-4942-be49-ac3193ebd3cd" data-name="EthereumBackground">
|
||||
<circle class="uuid-ca9afedd-40fd-4006-99f9-d2c3cf46bc21" cx="125" cy="125" r="122.5"/>
|
||||
<path class="uuid-e080659c-91a4-4965-9905-603394020b2c" d="m125,5c32.05,0,62.19,12.48,84.85,35.15,22.67,22.66,35.15,52.8,35.15,84.85s-12.48,62.19-35.15,84.85-52.8,35.15-84.85,35.15-62.19-12.48-84.85-35.15C17.48,187.19,5,157.05,5,125s12.48-62.19,35.15-84.85C62.81,17.48,92.95,5,125,5m0-5C55.96,0,0,55.96,0,125s55.96,125,125,125,125-55.96,125-125S194.04,0,125,0h0Z"/>
|
||||
</g>
|
||||
<g id="uuid-0bd28738-f2c4-4934-8365-8668de7b64ac" data-name="EthereumIcon">
|
||||
<path class="uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50" d="m128.89,31.25v69.3l58.57,26.17-58.57-95.47Z"/>
|
||||
<path class="uuid-e080659c-91a4-4965-9905-603394020b2c" d="m128.89,31.25l-58.58,95.47,58.58-26.17V31.25Z"/>
|
||||
<path class="uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50" d="m128.89,171.62v47.09l58.61-81.09-58.61,34Z"/>
|
||||
<path class="uuid-e080659c-91a4-4965-9905-603394020b2c" d="m128.89,218.71v-47.09l-58.58-33.99,58.58,81.09Z"/>
|
||||
<path class="uuid-bb37a35d-b2ce-440e-86f6-0142a60ec718" d="m128.89,160.73l58.57-34.01-58.57-26.16v60.16Z"/>
|
||||
<path class="uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50" d="m70.31,126.72l58.58,34.01v-60.16l-58.58,26.16Z"/>
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 32 32">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<circle cx="16" cy="16" r="16" fill="#627EEA"/>
|
||||
<g fill="#FFF" fill-rule="nonzero">
|
||||
<path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/>
|
||||
<path d="M16.498 4L9 16.22l7.498-3.35z"/>
|
||||
<path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/>
|
||||
<path d="M16.498 27.995v-6.028L9 17.616z"/>
|
||||
<path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/>
|
||||
<path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 617 B |
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="uuid-cf283373-2eb4-44d0-88d2-93533a7f4b1d" data-name="eGHST" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 254 254">
|
||||
<svg id="uuid-cf283373-2eb4-44d0-88d2-93533a7f4b1d" data-name="eGHST" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||
<defs>
|
||||
<style>
|
||||
.uuid-26b0e53c-73c9-4369-9725-1cee728298be {
|
||||
@ -21,4 +21,4 @@
|
||||
<path class="uuid-26b0e53c-73c9-4369-9725-1cee728298be" d="m66.78,163.74c2.41,0,4.76,1.17,6.19,3.3,2.27,3.4,5.12,6.55,8.46,9.36,3.84,3.27,8.17,6.14,12.9,8.57,9.33,4.8,19.72,7.51,30.06,7.86,5.23.17,10.45-.28,15.5-1.35,5.01-1.07,9.96-2.8,14.69-5.14,4.55-2.25,9.03-5.12,13.31-8.55,3.83-3.17,7.58-6.89,11.4-11.33l.39-.41c2.02-1.87,5.18-1.9,7.34-.06,2.12,1.8,2.65,4.77,1.28,7.06l-.19.31-.23.28c-4.12,5.06-8.21,9.34-12.51,13.1-4.9,4.16-10.02,7.64-15.28,10.42-5.57,2.95-11.43,5.18-17.42,6.63-6.02,1.45-12.27,2.16-18.55,2.14-12.31-.08-24.77-2.97-36.04-8.37-5.79-2.77-11.14-6.12-15.91-9.96-4.46-3.56-8.33-7.62-11.48-12.06-1.21-1.7-1.65-3.84-1.22-5.88.43-2.03,1.7-3.81,3.49-4.87,1.19-.71,2.51-1.05,3.81-1.05Z"/>
|
||||
<path class="uuid-26b0e53c-73c9-4369-9725-1cee728298be" d="m105.35,44.06c1.3,0,2.58.33,3.72.98,1.77,1.01,3.06,2.73,3.54,4.71.95,3.9-1.42,7.92-5.29,8.95-8.06,2.16-16.05,5.98-23.1,11.04-7.76,5.57-14.37,12.59-19.13,20.31-4.9,7.92-8.02,16.99-9.02,26.22-1.01,8.71-.13,18.21,2.63,28.27l.09.4c.51,2.71-1.12,5.41-3.78,6.28-2.64.87-5.47-.28-6.73-2.72l-.2-.4-.13-.43c-3.5-11.4-4.83-22.35-3.97-32.56.85-11.07,4.26-22.07,9.86-31.77,5.38-9.38,13-17.99,22.02-24.89,8.36-6.39,17.55-11.13,27.31-14.08.71-.21,1.44-.32,2.17-.32Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 258.751 258.751">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 254.751 254.751">
|
||||
<defs>
|
||||
<style>
|
||||
.uuid-67f42f03-098b-49dc-a7ed-dc13bd713387 {
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="uuid-0fc887e6-719b-4e51-bef8-e5c269508f6b" data-name="sGHST" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 254 254">
|
||||
<svg id="uuid-0fc887e6-719b-4e51-bef8-e5c269508f6b" data-name="sGHST" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||
<defs>
|
||||
<style>
|
||||
.uuid-a205ebe2-7c8f-434c-9bde-29ac218716be {
|
||||
@ -84,4 +84,4 @@
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
36
src/assets/tokens/wETH.svg
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="uuid-39ebc51d-db02-45d2-ad4b-e7ae31bb915b" data-name="Ethereum" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||
<defs>
|
||||
<style>
|
||||
.uuid-e080659c-91a4-4965-9905-603394020b2c {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.uuid-ca9afedd-40fd-4006-99f9-d2c3cf46bc21 {
|
||||
fill: #627eea;
|
||||
}
|
||||
|
||||
.uuid-bb37a35d-b2ce-440e-86f6-0142a60ec718 {
|
||||
fill: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50 {
|
||||
fill: rgba(255, 255, 255, .6);
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="uuid-c49657cc-831e-4816-83c0-b3ceca5eead0" data-name="Ethereum">
|
||||
<g id="uuid-559a2235-ac8a-4942-be49-ac3193ebd3cd" data-name="EthereumBackground">
|
||||
<circle class="uuid-ca9afedd-40fd-4006-99f9-d2c3cf46bc21" cx="125" cy="125" r="122.5"/>
|
||||
<path class="uuid-e080659c-91a4-4965-9905-603394020b2c" d="m125,5c32.05,0,62.19,12.48,84.85,35.15,22.67,22.66,35.15,52.8,35.15,84.85s-12.48,62.19-35.15,84.85-52.8,35.15-84.85,35.15-62.19-12.48-84.85-35.15C17.48,187.19,5,157.05,5,125s12.48-62.19,35.15-84.85C62.81,17.48,92.95,5,125,5m0-5C55.96,0,0,55.96,0,125s55.96,125,125,125,125-55.96,125-125S194.04,0,125,0h0Z"/>
|
||||
</g>
|
||||
<g id="uuid-0bd28738-f2c4-4934-8365-8668de7b64ac" data-name="EthereumIcon">
|
||||
<path class="uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50" d="m128.89,31.25v69.3l58.57,26.17-58.57-95.47Z"/>
|
||||
<path class="uuid-e080659c-91a4-4965-9905-603394020b2c" d="m128.89,31.25l-58.58,95.47,58.58-26.17V31.25Z"/>
|
||||
<path class="uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50" d="m128.89,171.62v47.09l58.61-81.09-58.61,34Z"/>
|
||||
<path class="uuid-e080659c-91a4-4965-9905-603394020b2c" d="m128.89,218.71v-47.09l-58.58-33.99,58.58,81.09Z"/>
|
||||
<path class="uuid-bb37a35d-b2ce-440e-86f6-0142a60ec718" d="m128.89,160.73l58.57-34.01-58.57-26.16v60.16Z"/>
|
||||
<path class="uuid-933ff958-c1b5-4023-94a9-0cca9b4aab50" d="m70.31,126.72l58.58,34.01v-60.16l-58.58,26.16Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -9,18 +9,18 @@ const classes = {
|
||||
|
||||
const StyledMuiChip = styled(MuiChip, {
|
||||
shouldForwardProp: prop => prop !== "template" && prop !== "strong",
|
||||
})(({ theme, template, background, strong }) => {
|
||||
})(({ theme, template, strong }) => {
|
||||
return {
|
||||
[`&.${classes.chip}`]: {
|
||||
height: "21px",
|
||||
borderRadius: "16px",
|
||||
backgroundColor: background ? background : template
|
||||
backgroundColor: template
|
||||
? template === "purple"
|
||||
? theme.colors.special["olyZaps"]
|
||||
: template === "gray"
|
||||
? theme.colors.gray[500]
|
||||
: template === "darkGray"
|
||||
? theme.colors.primary[300]
|
||||
? theme.colors.gray[600]
|
||||
: 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[800]
|
||||
? theme.colors.gray[90]
|
||||
: theme.colors.gray[600],
|
||||
fontWeight: strong ? 700 : 450,
|
||||
},
|
||||
@ -42,8 +42,8 @@ const StyledMuiChip = styled(MuiChip, {
|
||||
};
|
||||
});
|
||||
|
||||
const Chip = ({ background, template, strong = false, ...props }) => {
|
||||
return <StyledMuiChip className={classes.chip} background={background} template={template} strong={strong} {...props} />;
|
||||
const Chip = ({ template, strong = false, ...props }) => {
|
||||
return <StyledMuiChip className={classes.chip} template={template} strong={strong} {...props} />;
|
||||
};
|
||||
|
||||
export default Chip;
|
||||
|
||||
@ -118,19 +118,19 @@ const NavItem = ({
|
||||
const match = currentLocation.pathname === to || currentLocation.pathname === `/${to}`;
|
||||
|
||||
const linkProps = props.href
|
||||
? {
|
||||
href: props.href,
|
||||
target: "_blank",
|
||||
className: `external-site-link ${className}`,
|
||||
}
|
||||
: {
|
||||
component: NavLink,
|
||||
to: to,
|
||||
className: `button-dapp-menu ${className}`,
|
||||
};
|
||||
? {
|
||||
href: props.href,
|
||||
target: "_blank",
|
||||
className: `external-site-link ${className}`,
|
||||
}
|
||||
: {
|
||||
component: NavLink,
|
||||
to: to,
|
||||
className: `button-dapp-menu ${className}`,
|
||||
};
|
||||
|
||||
const LinkItem = () => (
|
||||
<Link {...linkProps} {...props} underline="hover" onClick={(e) => e.stopPropagation()}>
|
||||
<Link {...linkProps} {...props} underline="hover">
|
||||
<Box
|
||||
sx={{ fontFamily: "Ubuntu" }}
|
||||
display="flex"
|
||||
|
||||
@ -2,7 +2,7 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
|
||||
const PageTitle = ({ name, subtitle, noMargin }) => {
|
||||
const theme = useTheme();
|
||||
const mobile = useMediaQuery(theme.breakpoints.down("900"));
|
||||
const mobile = useMediaQuery(theme.breakpoints.down("700"));
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
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: `${Math.min(Math.max(props.target, 0), 100)}%`,
|
||||
top: props.targetTop || 0,
|
||||
bottom: props.targetBottom || 0,
|
||||
width: props.targetWidth || "2px",
|
||||
backgroundColor: props.targetBackgroundColor || 'white',
|
||||
}}
|
||||
></Box>}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinearProgressBar;
|
||||
@ -1,85 +0,0 @@
|
||||
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%",
|
||||
maxWidth = "100%"
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
width={width}
|
||||
maxWidth={maxWidth}
|
||||
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
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
maxHeight: 160,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</StyledSelectInput>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import React from "react";
|
||||
|
||||
import "./Sidebar.scss";
|
||||
|
||||
@ -16,7 +16,6 @@ import {
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { useSwitchChain } from "wagmi";
|
||||
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
@ -25,9 +24,7 @@ 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 CasinoIcon from '@mui/icons-material/Casino';
|
||||
import ForumIcon from '@mui/icons-material/Forum';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BookIcon from '@mui/icons-material/Book';
|
||||
import CurrencyExchangeIcon from '@mui/icons-material/CurrencyExchange';
|
||||
@ -40,20 +37,18 @@ import BondIcon from "../Icon/BondIcon";
|
||||
import StakeIcon from "../Icon/StakeIcon";
|
||||
import WrapIcon from "../Icon/WrapIcon";
|
||||
|
||||
import { isNetworkAvailable, isGovernanceAvailable } from "../../constants";
|
||||
import { isNetworkAvailable } from "../../constants";
|
||||
import { AVAILABLE_DEXES } from "../../constants/dexes";
|
||||
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
|
||||
import { ECOSYSTEM } from "../../constants/ecosystem";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { sortBondsByDiscount, formatCurrency } from "../../helpers";
|
||||
import BondDiscount from "../../containers/Bond/components/BondDiscount";
|
||||
import Chip from "../Chip/Chip";
|
||||
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import ShowerIcon from '@mui/icons-material/Shower';
|
||||
import WifiProtectedSetupIcon from '@mui/icons-material/WifiProtectedSetup';
|
||||
|
||||
import { useTokenSymbol } from "../../hooks/tokens";
|
||||
import { useFtsoPrice, useGhstPrice, useGhostedSupplyPrice } from "../../hooks/prices";
|
||||
import { useLiveBonds } from "../../hooks/bonds/index";
|
||||
import pckg from "../../../package.json"
|
||||
|
||||
@ -70,21 +65,14 @@ const StyledBox = styled(Box)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const NavContent = ({ chainId, addressChainId }) => {
|
||||
const { chains } = useSwitchChain();
|
||||
const chainName = useMemo(() => {
|
||||
return chains.find(chain => chain.id === chainId).name.toLowerCase();
|
||||
}, [chains, chainId, addressChainId])
|
||||
|
||||
const { liveBonds: ghostBonds } = useLiveBonds(chainId);
|
||||
const ftsoPrice = useFtsoPrice(chainId);
|
||||
const ghstPrice = useGhstPrice(chainId);
|
||||
const ghostedSupplyPrice = useGhostedSupplyPrice(chainId);
|
||||
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const bridgeNumbers = useMemo(() => {
|
||||
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||
const number = 1 + connectedNetworks * 3;
|
||||
return `(${number}, ${number})`;
|
||||
}, [chainId]);
|
||||
|
||||
return (
|
||||
<Paper className="dapp-sidebar">
|
||||
<Box className="dapp-sidebar-inner" display="flex" justifyContent="space-between" flexDirection="column">
|
||||
@ -101,6 +89,17 @@ const NavContent = ({ chainId, addressChainId }) => {
|
||||
Version {pckg.version}
|
||||
</Box>
|
||||
</Link>
|
||||
<Box display="flex" flexDirection="column" mt="10px">
|
||||
<Box fontSize="12px" fontWeight="500" lineHeight={"15px"}>
|
||||
{ftsoSymbol} Price: {formatCurrency(ftsoPrice, 2)}
|
||||
</Box>
|
||||
<Box fontSize="12px" fontWeight="500" lineHeight="15px">
|
||||
{ghstSymbol} Price: {formatCurrency(ghstPrice, 2)}
|
||||
</Box>
|
||||
<Box fontSize="12px" fontWeight="500" lineHeight={"15px"}>
|
||||
GHOSTed Supply: {formatCurrency(ghostedSupplyPrice, 2)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="menu-divider">
|
||||
@ -111,18 +110,57 @@ const NavContent = ({ chainId, addressChainId }) => {
|
||||
<div className="dapp-nav" id="navbarNav">
|
||||
{isNetworkAvailable(chainId, addressChainId) &&
|
||||
<>
|
||||
<NavItem icon={DashboardIcon} label={`Dashboard`} to={`/${chainName}/dashboard`} />
|
||||
<NavItem icon={DashboardIcon} label={`Dashboard`} to="/dashboard" />
|
||||
<NavItem
|
||||
defaultExpanded
|
||||
icon={BondIcon}
|
||||
label={`Bond`}
|
||||
to="/bonds"
|
||||
children={
|
||||
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{sortBondsByDiscount(ghostBonds).map((bond, index) => {
|
||||
return (
|
||||
<Link
|
||||
component={NavLink}
|
||||
to={`/bonds/${bond.id}`}
|
||||
key={index}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<Box mb="10px" display="flex" justifyContent="end">
|
||||
<Typography
|
||||
style={{
|
||||
width: "180px",
|
||||
justifyContent: "space-between",
|
||||
display: "flex",
|
||||
gap: "10px"
|
||||
}}
|
||||
component="span"
|
||||
variant="body2"
|
||||
>
|
||||
{bond.displayName}
|
||||
<BondDiscount textOnly discount={bond.discount} />
|
||||
</Typography>
|
||||
</Box>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</AccordionDetails>
|
||||
}
|
||||
/>
|
||||
<NavItem icon={StakeIcon} label={`Stake`} to="/stake" />
|
||||
<NavItem icon={ShowerIcon} label={`Faucet`} to="/faucet" />
|
||||
<NavItem icon={PublicIcon} label={`Bridge`} to="/bridge" />
|
||||
<NavItem
|
||||
icon={CurrencyExchangeIcon}
|
||||
label={`(3, 3) Swap`}
|
||||
to={`/${chainName}/dex/uniswap`}
|
||||
label={`Dex`}
|
||||
to={'/dex/uniswap'}
|
||||
children={
|
||||
AVAILABLE_DEXES[chainId].length > 1 && <AccordionDetails style={{ margin: "0 0 -10px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<AccordionDetails style={{ margin: "0 0 -10px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{AVAILABLE_DEXES[chainId].map((dex, index) => {
|
||||
return (
|
||||
<Link
|
||||
component={NavLink}
|
||||
to={`/${chainName}/dex/${dex.name.toLowerCase()}`}
|
||||
to={`/dex/${dex.name.toLowerCase()}`}
|
||||
key={index}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
@ -136,65 +174,50 @@ const NavContent = ({ chainId, addressChainId }) => {
|
||||
</AccordionDetails>
|
||||
}
|
||||
/>
|
||||
<NavItem icon={StakeIcon} label={`(3, 3) Stake`} to={`/${chainName}/stake`} />
|
||||
<NavItem
|
||||
defaultExpanded
|
||||
icon={BondIcon}
|
||||
label={`(1, 1) Bond`}
|
||||
to={`/${chainName}/bonds`}
|
||||
children={
|
||||
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{ghostBonds.length > 0 && <Box width="180px" mb="10px" ml="auto">
|
||||
<Typography component="span" variant="body2">Bond Discounts</Typography>
|
||||
</Box>}
|
||||
{sortBondsByDiscount(ghostBonds).map((bond, index) => {
|
||||
return (
|
||||
<Link
|
||||
component={NavLink}
|
||||
to={`/${chainName}/bonds/${bond.id}`}
|
||||
key={index}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<Box mb="10px" display="flex" justifyContent="end">
|
||||
<Typography
|
||||
style={{
|
||||
width: "180px",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
gap: "10px"
|
||||
}}
|
||||
component="span"
|
||||
variant="body2"
|
||||
>
|
||||
{bond.displayName}
|
||||
{bond.soldOut
|
||||
? <Chip label="Sold Out" template="darkGray" />
|
||||
: <BondDiscount discount={bond.discount} />
|
||||
}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</AccordionDetails>
|
||||
}
|
||||
/>
|
||||
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
|
||||
{isGovernanceAvailable(chainId, addressChainId) && <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />}
|
||||
<Box className="menu-divider">
|
||||
<Divider />
|
||||
<Divider />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
|
||||
<NavItem
|
||||
to=''
|
||||
icon={PublicIcon}
|
||||
label={`Ecosystem`}
|
||||
children={
|
||||
<AccordionDetails style={{ margin: "0 0 -10px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{ECOSYSTEM.map((item, index) => {
|
||||
return (
|
||||
<Link
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key={index}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<Box display="flex" justifyContent="end">
|
||||
<Typography style={{ display: "flex", alignItems: "center" }} >
|
||||
{item.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</AccordionDetails>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Box className="menu-divider">
|
||||
<Divider />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<NavItem href="https://ghostchain.io/game-theory-3-3" icon={CasinoIcon} label={`${bridgeNumbers} Game Theory`} />
|
||||
<NavItem href="http://ecosystem.ghostchain.io" icon={PublicIcon} label={`Ecosystem`} />
|
||||
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Documentation`} />
|
||||
<NavItem href="http://ghostchain.io/builders" icon={ForumIcon} label={`Forum`} />
|
||||
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Docs`} />
|
||||
<NavItem href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts/issues" icon={ErrorOutlineIcon} label={`Git Issues`} />
|
||||
<StyledBox display="flex" justifyContent="space-around" paddingY="24px">
|
||||
<Link href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts" target="_blank" rel="noopener noreferrer">
|
||||
<GhostStyledIcon viewBox="0 0 24 24" component={GitHubIcon} className={classes.gray} />
|
||||
|
||||
@ -7,7 +7,7 @@ import Token from "../Token/Token";
|
||||
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
|
||||
export const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })(
|
||||
const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })(
|
||||
({ inputWidth, inputFontSize }) => ({
|
||||
"& .MuiInputBase-input": {
|
||||
padding: 0,
|
||||
@ -90,7 +90,7 @@ const SwapCard = ({
|
||||
</Box>
|
||||
)}
|
||||
<Box display="flex" flexDirection="row" marginTop="12px" justifyContent="space-between" alignItems="center">
|
||||
<Box display="flex" flexDirection="row" alignItems="center" width={!info && !endString && !usdValue ? "100%" : inputWidth ? inputWidth : "136px"}>
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<StyledInputBase
|
||||
id={id}
|
||||
sx={{
|
||||
@ -99,7 +99,6 @@ const SwapCard = ({
|
||||
padding: 0,
|
||||
height: "24px",
|
||||
maxWidth: inputWidth || "136px",
|
||||
width: !info && !endString && !usdValue ? "100%" : inputWidth ? inputWidth : "136px",
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
type={inputType}
|
||||
|
||||
@ -28,10 +28,11 @@ const StyledArrow = styled(Box)(
|
||||
},
|
||||
);
|
||||
|
||||
const SwapCollection = ({ UpperSwapCard, LowerSwapCard, arrowOnClick, iconNotNeeded, maxWidth}) => {
|
||||
const SwapCollection = ({ UpperSwapCard, LowerSwapCard, arrowOnClick, iconNotNeeded }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" maxWidth={maxWidth ? maxWidth : "476px"}>
|
||||
<Box display="flex" flexDirection="column" maxWidth="476px">
|
||||
{UpperSwapCard}
|
||||
<Box display="flex" flexDirection="row" justifyContent="center">
|
||||
{!iconNotNeeded && (<StyledArrow
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { SvgIcon, Box } from "@mui/material";
|
||||
import { SvgIcon } from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
import FtsoIcon from "../../assets/tokens/FTSO.svg?react";
|
||||
import StnkIcon from "../../assets/tokens/STNK.svg?react";
|
||||
import GhstIcon from "../../assets/tokens/GHST.svg?react";
|
||||
import DaiIcon from "../../assets/tokens/DAI.svg?react";
|
||||
import EthIcon from "../../assets/tokens/ETH.svg?react";
|
||||
import EtcIcon from "../../assets/tokens/ETC.svg?react";
|
||||
import WethIcon from "../../assets/tokens/wETH.svg?react";
|
||||
import UnknownIcon from "../../assets/tokens/Unknown.svg?react";
|
||||
|
||||
const PREFIX = "Token";
|
||||
@ -24,76 +23,53 @@ const StyledSvgIcon = styled(SvgIcon)(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const parseKnownToken = (name) => {
|
||||
let icon;
|
||||
switch (name?.toUpperCase()) {
|
||||
case "FTSO":
|
||||
icon = FtsoIcon;
|
||||
break;
|
||||
case "ECSPR":
|
||||
icon = FtsoIcon;
|
||||
break;
|
||||
case "STNK":
|
||||
icon = StnkIcon;
|
||||
break;
|
||||
case "SCSPR":
|
||||
icon = StnkIcon;
|
||||
break;
|
||||
case "GHST":
|
||||
icon = GhstIcon;
|
||||
break;
|
||||
case "CSPR":
|
||||
icon = GhstIcon;
|
||||
break;
|
||||
case "GDAI":
|
||||
icon = DaiIcon;
|
||||
break;
|
||||
case "DAI":
|
||||
icon = DaiIcon;
|
||||
break;
|
||||
case "ETH":
|
||||
icon = EthIcon;
|
||||
break;
|
||||
case "WETH":
|
||||
icon = EthIcon;
|
||||
break;
|
||||
case "METC":
|
||||
icon = EtcIcon;
|
||||
break;
|
||||
case "WMETC":
|
||||
icon = EtcIcon;
|
||||
break;
|
||||
default:
|
||||
icon = UnknownIcon;
|
||||
const Token = ({ name, viewBox = "0 0 260 260", fontSize = "large", ...props }) => {
|
||||
const parseKnownToken = (name) => {
|
||||
let icon;
|
||||
switch (name?.toUpperCase()) {
|
||||
case "FTSO":
|
||||
icon = FtsoIcon;
|
||||
break;
|
||||
case "ECSPR":
|
||||
icon = FtsoIcon;
|
||||
break;
|
||||
case "STNK":
|
||||
icon = StnkIcon;
|
||||
break;
|
||||
case "SCSPR":
|
||||
icon = StnkIcon;
|
||||
break;
|
||||
case "GHST":
|
||||
icon = GhstIcon;
|
||||
break;
|
||||
case "CSPR":
|
||||
icon = GhstIcon;
|
||||
break;
|
||||
case "GDAI":
|
||||
icon = DaiIcon;
|
||||
break;
|
||||
case "DAI":
|
||||
icon = DaiIcon;
|
||||
break;
|
||||
case "ETH":
|
||||
icon = WethIcon;
|
||||
break;
|
||||
case "WETH":
|
||||
icon = WethIcon;
|
||||
break;
|
||||
default:
|
||||
icon = UnknownIcon;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
const Token = ({ chainTokenName, name, viewBox = "0 0 260 260", fontSize = "large", ...props }) => {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" position="relative">
|
||||
<StyledSvgIcon
|
||||
inheritViewBox
|
||||
fontSize={fontSize}
|
||||
component={parseKnownToken(name)}
|
||||
{...props}
|
||||
></StyledSvgIcon>
|
||||
{chainTokenName && (
|
||||
<StyledSvgIcon
|
||||
inheritViewBox
|
||||
component={parseKnownToken(chainTokenName)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
marginLeft: "70%",
|
||||
marginTop: "45%",
|
||||
width: "65%",
|
||||
height: "65%",
|
||||
border: "1px solid #fff",
|
||||
borderRadius: "100%"
|
||||
}}
|
||||
></StyledSvgIcon>
|
||||
)}
|
||||
</Box>
|
||||
<StyledSvgIcon
|
||||
viewBox={viewBox}
|
||||
fontSize={fontSize}
|
||||
component={parseKnownToken(name)}
|
||||
{...props}
|
||||
></StyledSvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -58,7 +58,6 @@ export const TokenAllowanceGuard = ({
|
||||
height = "auto",
|
||||
spendAmount,
|
||||
tokenName,
|
||||
isNative,
|
||||
owner,
|
||||
spender,
|
||||
decimals,
|
||||
@ -89,7 +88,7 @@ export const TokenAllowanceGuard = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNative && allowance && spendAmount && allowance.lt(spendAmount))
|
||||
if (allowance && spendAmount && allowance.lt(spendAmount))
|
||||
return (
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={12} sm={isVertical ? 12 : 8}>
|
||||
|
||||
@ -38,8 +38,8 @@ const Tooltip = ({ message, children }) => {
|
||||
onResizeCapture={undefined}
|
||||
slotProps={undefined}
|
||||
slots={undefined}
|
||||
>
|
||||
<Paper style={{ boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px" }} className="info-tooltip">
|
||||
>
|
||||
<Paper className="info-tooltip">
|
||||
{typeof message === "string" ? (
|
||||
<Typography variant="body1" className="info-tooltip-text">
|
||||
{message}
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
import { Box, Typography, SvgIcon, useTheme } from "@mui/material";
|
||||
|
||||
import { parseKnownToken } from "../../components/Token/Token";
|
||||
import { useUnstableProvider } from "../../hooks/ghost";
|
||||
import { PrimaryButton } from "../Button"
|
||||
|
||||
const GHOST_CONNECT = 'https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases';
|
||||
|
||||
function GhostChainSelect({ small }) {
|
||||
const { providerDetail, isConnected } = useUnstableProvider();
|
||||
const theme = useTheme();
|
||||
|
||||
if (providerDetail) {
|
||||
return (
|
||||
<Box
|
||||
height="39px"
|
||||
width={small ? "100px" : "155px"}
|
||||
borderRadius="4px"
|
||||
padding="0 14px"
|
||||
border="1px solid #50759e"
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<Box display="flex" flexDirection="row">
|
||||
<SvgIcon component={parseKnownToken("CSPR")} inheritViewBox />
|
||||
{!small && <Typography sx={{ paddingLeft: "6px"}}>CASPER</Typography>}
|
||||
</Box>
|
||||
<Box
|
||||
width="21px"
|
||||
height="21px"
|
||||
backgroundColor={isConnected ? theme.colors.feedback.success : theme.colors.feedback.error}
|
||||
borderRadius="50%"
|
||||
></Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box height="39px" width={small ? "100px" : "155px"}>
|
||||
<PrimaryButton
|
||||
sx={{ margin: "0 !important", padding: "0 !important" }}
|
||||
onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}
|
||||
fullWidth
|
||||
>
|
||||
Get GHOST {small ? "" : "Connect"}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default GhostChainSelect;
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { Box, Typography, SvgIcon, useTheme } from "@mui/material";
|
||||
|
||||
@ -9,74 +9,39 @@ import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
|
||||
import { isNetworkAvailable } from "../../constants";
|
||||
import { parseKnownToken } from "../../components/Token/Token";
|
||||
import EthIcon from "../../assets/tokens/ETH.svg?react";
|
||||
|
||||
import { useSwitchChain } from 'wagmi';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, small, status }) {
|
||||
function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, small }) {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const hasAttemptedSwitch = useRef(false);
|
||||
|
||||
const { chains, switchChain } = useSwitchChain();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [networkId, setNetworkId] = useState(chainId);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'reconnecting' || status === 'connecting') return;
|
||||
if (chainId !== networkId) setNetworkId(chainId);
|
||||
}, [chainId])
|
||||
|
||||
const parts = pathname.split('/');
|
||||
if (!hasAttemptedSwitch.current && parts.length > 2) {
|
||||
let targetChain = chains.at(0);
|
||||
const chainName = parts[1].toLowerCase();
|
||||
const chain = chains.find(chain => chain.name.toLowerCase() === chainName);
|
||||
if (chain && targetChain.name !== chain.name) {
|
||||
targetChain = chain;
|
||||
}
|
||||
|
||||
hasAttemptedSwitch.current = true;
|
||||
changeNetworkId(targetChain.name, targetChain.id);
|
||||
}
|
||||
}, [status, chains, pathname]);
|
||||
|
||||
const changeNetworkId = (chainName, newNetworkId) => {
|
||||
const handleChange = (event) => {
|
||||
const chainName = chains.find((chain) => chain.id === event.target.value).name;
|
||||
toast.dismiss(wrongNetworkToastId);
|
||||
const toastId = toast.loading(`Trying to connect to ${chainName} network... Wait please`, {
|
||||
position: 'bottom-right'
|
||||
});
|
||||
setWrongNetworkToastId(toastId);
|
||||
|
||||
setNetworkId(newNetworkId);
|
||||
switchChain({
|
||||
chainId: newNetworkId
|
||||
}, {
|
||||
onSuccess: (data) => console.log("Network switched to", data.name),
|
||||
onError: (error) => console.log("Error during network switching", error),
|
||||
});
|
||||
}
|
||||
|
||||
const handleChange = (event) => {
|
||||
const chainName = chains.find((chain) => chain.id === event.target.value).name;
|
||||
changeNetworkId(chainName, event.target.value);
|
||||
|
||||
const parts = pathname.split('/');
|
||||
if (parts.length > 2) {
|
||||
parts[1] = chainName.toLowerCase();
|
||||
const newPath = parts.join("/");
|
||||
navigate(newPath, { replace: true });
|
||||
}
|
||||
setNetworkId(event.target.value);
|
||||
switchChain({ chainId: event.target.value });
|
||||
}
|
||||
|
||||
return(
|
||||
<FormControl sx={{ width: small ? "auto" : "155px" }}>
|
||||
<FormControl sx={{ width: small ? "100px" : "155px" }}>
|
||||
<Select
|
||||
labelId="network-select-helper-label"
|
||||
id="network-select-helper"
|
||||
value={networkId}
|
||||
disabled={!hasAttemptedSwitch.current}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
boxShadow: 'none',
|
||||
@ -88,11 +53,11 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
|
||||
}
|
||||
}}
|
||||
>
|
||||
{chains.map((chain, i) => {
|
||||
{chains.map(chain => {
|
||||
return (
|
||||
<MenuItem key={chain.name} value={chain.id}>
|
||||
<Box gap="10px" display="flex" flexDirection="row" alignItems="center">
|
||||
<SvgIcon sx={{ width: 22 }} component={parseKnownToken(chain?.nativeCurrency?.symbol)} inheritViewBox />
|
||||
<SvgIcon component={EthIcon} viewBox="0 0 32 32" />
|
||||
{!small && <Typography>{chain.name}</Typography>}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
@ -2,7 +2,7 @@ import { Box, Button, SvgIcon, useMediaQuery, useTheme } from "@mui/material";
|
||||
import MenuIcon from "../../assets/icons/hamburger.svg?react";
|
||||
import Wallet from "./Wallet"
|
||||
import SelectNetwork from "./SelectNetwork";
|
||||
import GhostChainSelect from "./GhostChainSelect";
|
||||
|
||||
|
||||
const PREFIX = "TopBar";
|
||||
|
||||
@ -18,13 +18,12 @@ function TopBar({
|
||||
address,
|
||||
wrongNetworkToastId,
|
||||
connect,
|
||||
status,
|
||||
handleDrawerToggle,
|
||||
setWrongNetworkToastId
|
||||
}) {
|
||||
const themeColor = useTheme();
|
||||
const desktop = useMediaQuery(themeColor.breakpoints.up(1130));
|
||||
const small = useMediaQuery(themeColor.breakpoints.down(600));
|
||||
const desktop = useMediaQuery(themeColor.breakpoints.up(1048));
|
||||
const small = useMediaQuery(themeColor.breakpoints.down(400));
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
@ -34,20 +33,12 @@ function TopBar({
|
||||
marginRight={desktop ? "33px" : "0px"}
|
||||
>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
width={small ? "calc(100vw - 78px)" : "500px"}
|
||||
height="40px"
|
||||
>
|
||||
<GhostChainSelect small={small} />
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" width={small ? "calc(100vw - 78px)" : "320px"}>
|
||||
<SelectNetwork
|
||||
wrongNetworkToastId={wrongNetworkToastId}
|
||||
setWrongNetworkToastId={setWrongNetworkToastId}
|
||||
chainId={chainId}
|
||||
small={small}
|
||||
status={status}
|
||||
/>
|
||||
<Wallet address={address} connect={connect} chainId={chainId} />
|
||||
</Box>
|
||||
|
||||
@ -22,9 +22,9 @@ import { PrimaryButton, SecondaryButton } from "../../Button";
|
||||
import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
|
||||
import { formatCurrency, shorten } from "../../../helpers";
|
||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
import { RESERVE_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses";
|
||||
import { DAI_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses";
|
||||
|
||||
import { useAccount, useDisconnect, useSwitchChain } from "wagmi";
|
||||
import { useAccount, useDisconnect } from "wagmi";
|
||||
|
||||
const DisconnectButton = ({ onClose }) => {
|
||||
const { disconnect } = useDisconnect();
|
||||
@ -109,11 +109,10 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const tokens = useWallet(chainId, address);
|
||||
const { chains } = useSwitchChain();
|
||||
|
||||
const onBtnClick = (dexName, from, to) => {
|
||||
navigate({
|
||||
pathname: `${chains.find(chain => chain.id === chainId).name.toLowerCase()}/dex/` + dexName,
|
||||
pathname: "/dex/" + dexName,
|
||||
search: createSearchParams({
|
||||
pool: true,
|
||||
from: from,
|
||||
@ -151,6 +150,18 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
|
||||
<Divider />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{ display: "flex", flexDirection: "column" }}
|
||||
style={{ gap: theme.spacing(1.5) }}
|
||||
>
|
||||
<SecondaryButton
|
||||
fullWidth
|
||||
onClick={() => onBtnClick("uniswap", DAI_ADDRESSES[chainId], FTSO_ADDRESSES[chainId])}
|
||||
>
|
||||
<Typography>{`${tokens?.ftso?.symbol}-${tokens?.dai?.symbol} on Uniswap`}</Typography>
|
||||
</SecondaryButton>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: "100%", marginTop: "auto", marginX: "auto", padding: theme.spacing(2, 0) }}>
|
||||
<DisconnectButton onClose={onClose} />
|
||||
</Box>
|
||||
|
||||
@ -9,29 +9,19 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { ChangeEvent, useState, useMemo, useEffect } from "react";
|
||||
import { ChangeEvent, useState, useEffect } from "react";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
import { useQuery } from "react-query";
|
||||
import { formatCurrency, formatNumber } from "../../../helpers";
|
||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber"
|
||||
import { tokenNameConverter } from "../../../helpers/tokenConverter";
|
||||
import { EMPTY_ADDRESS } from "../../../constants/addresses";
|
||||
|
||||
import GhostStyledIcon from "../../Icon/GhostIcon";
|
||||
import TokenStack from "../../TokenStack/TokenStack";
|
||||
import { PrimaryButton, SecondaryButton } from "../../Button";
|
||||
|
||||
import { useBalance, useTokenSymbol } from "../../../hooks/tokens";
|
||||
import {
|
||||
useNativePrice,
|
||||
useReservePrice,
|
||||
useFtsoPrice,
|
||||
useGhstPrice
|
||||
} from "../../../hooks/prices";
|
||||
import { useDaiPrice, useFtsoPrice, useStnkPrice, useGhstPrice } from "../../../hooks/prices";
|
||||
import { useLpValuation } from "../../../hooks/treasury";
|
||||
import { useUniswapV2PairReserves } from "../../../hooks/uniswapv2";
|
||||
import { getTokenIcons } from "../../../hooks/helpers";
|
||||
import { useAccount, useBalance as useNativeBalance, useConfig, useSwitchChain, useChainId } from "wagmi";
|
||||
import { useAccount } from "wagmi";
|
||||
|
||||
const addTokenToWallet = async (token, userAddress) => {
|
||||
if (!window.ethereum) return;
|
||||
@ -70,9 +60,7 @@ const BalanceValue = ({
|
||||
|
||||
export const Token = (props) => {
|
||||
const {
|
||||
isNative,
|
||||
symbol,
|
||||
faucetPath,
|
||||
icons,
|
||||
address,
|
||||
price = 0,
|
||||
@ -80,26 +68,19 @@ export const Token = (props) => {
|
||||
onAddTokenToWallet,
|
||||
expanded,
|
||||
onChangeExpanded,
|
||||
reserveAddress,
|
||||
daiAddress,
|
||||
onClose,
|
||||
isPool
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const chainId = useChainId();
|
||||
|
||||
const { chains } = useSwitchChain();
|
||||
|
||||
const chainName = useMemo(() => {
|
||||
return chains.find(chain => chain.id === chainId).name.toLowerCase();
|
||||
}, [chains, chainId])
|
||||
|
||||
const useLink = (symbol, fromAddress, toAddress, isPool) => {
|
||||
if (faucetPath) {
|
||||
navigate({ pathname: faucetPath })
|
||||
if (symbol.toUpperCase() === "GDAI") {
|
||||
navigate({ pathname: "/faucet" })
|
||||
} else {
|
||||
navigate({
|
||||
pathname: `${chainName}/dex/uniswap`,
|
||||
pathname: "/dex/uniswap",
|
||||
search: isPool ?
|
||||
createSearchParams({
|
||||
pool: "true",
|
||||
@ -117,11 +98,8 @@ export const Token = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion expanded={isNative ? false : expanded} onChange={onChangeExpanded}>
|
||||
<AccordionSummary
|
||||
sx={{ paddingRight: isNative ? "37.43px" : "" }}
|
||||
expandIcon={isNative ? null : <GhostStyledIcon component={ExpandMoreIcon} color="disabled" />}
|
||||
>
|
||||
<Accordion expanded={expanded} onChange={onChangeExpanded}>
|
||||
<AccordionSummary expandIcon={<GhostStyledIcon component={ExpandMoreIcon} color="disabled" />}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", width: "100%", marginRight: "10px" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
|
||||
<TokenStack
|
||||
@ -139,7 +117,7 @@ export const Token = (props) => {
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
{!isNative && <AccordionDetails style={{ margin: "auto", padding: theme.spacing(1, 0) }}>
|
||||
<AccordionDetails style={{ margin: "auto", padding: theme.spacing(1, 0) }}>
|
||||
<Box
|
||||
sx={{ display: "flex", flexDirection: "column", flex: 1, mx: "32px", justifyContent: "center" }}
|
||||
style={{ gap: theme.spacing(1) }}
|
||||
@ -152,14 +130,14 @@ export const Token = (props) => {
|
||||
<Typography>Add to Wallet</Typography>
|
||||
</PrimaryButton>
|
||||
<SecondaryButton
|
||||
onClick={() => useLink(symbol, reserveAddress, address, isPool)}
|
||||
onClick={() => useLink(symbol, daiAddress, address, isPool)}
|
||||
fullWidth
|
||||
>
|
||||
<Typography>Get {faucetPath ? "for Free" : "on DEX"}</Typography>
|
||||
<Typography>Get on {symbol.toUpperCase() === "GDAI" ? "Faucet" : "Uniswap"}</Typography>
|
||||
</SecondaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
@ -169,79 +147,52 @@ const sumObjValues = (obj: Record<string, string> = {}) =>
|
||||
|
||||
export const useWallet = (chainId, userAddress) => {
|
||||
const {
|
||||
data: nativeBalanceRaw,
|
||||
refetch: nativeBalanceRefetch
|
||||
} = useNativeBalance({ address: userAddress });
|
||||
const nativeBalance = new DecimalBigNumber(nativeBalanceRaw?.value ?? 0n, 18);
|
||||
const {
|
||||
balance: reserveBalance,
|
||||
refetch: reserveRefetch,
|
||||
contractAddress: reserveAddress,
|
||||
} = useBalance(chainId, "RESERVE", userAddress);
|
||||
balance: daiBalance,
|
||||
refetch: daiRefetch,
|
||||
contractAddress: daiAddress,
|
||||
} = useBalance(chainId, "GDAI", userAddress);
|
||||
const {
|
||||
balance: ftsoBalance,
|
||||
refetch: ftsoRefetch,
|
||||
contractAddress: ftsoAddress,
|
||||
} = useBalance(chainId, "FTSO", userAddress);
|
||||
const {
|
||||
balance: stnkBalance,
|
||||
refetch: stnkRefetch,
|
||||
contractAddress: stnkAddress,
|
||||
} = useBalance(chainId, "STNK", userAddress);
|
||||
const {
|
||||
balance: ghstBalance,
|
||||
refetch: ghstRefetch,
|
||||
contractAddress: ghstAddress,
|
||||
} = useBalance(chainId, "GHST", userAddress);
|
||||
const {
|
||||
balance: lpReserveFtsoBalance,
|
||||
refetch: lpReserveFtsoRefetch,
|
||||
contractAddress: lpReserveFtsoBalanceAddress,
|
||||
} = useBalance(chainId, "RESERVE_FTSO", userAddress);
|
||||
const {
|
||||
tokens: lpReserveFtsoTokens,
|
||||
} = useUniswapV2PairReserves(chainId, "RESERVE_FTSO");
|
||||
balance: lpDaiFtsoBalance,
|
||||
refetch: lpDaiFtsoRefetch,
|
||||
contractAddress: lpDaiFtsoBalanceAddress,
|
||||
} = useBalance(chainId, "GDAI_FTSO", userAddress);
|
||||
|
||||
const nativePrice = useNativePrice(chainId);
|
||||
const reservePrice = useReservePrice(chainId);
|
||||
const daiPrice = useDaiPrice(chainId);
|
||||
const ftsoPrice = useFtsoPrice(chainId);
|
||||
const stnkPrice = useStnkPrice(chainId);
|
||||
const ghstPrice = useGhstPrice(chainId);
|
||||
const lpReserveFtsoPrice = useLpValuation(chainId, "RESERVE_FTSO", 1000000000000000000n);
|
||||
const lpDaiFtsoPrice = useLpValuation(chainId, "GDAI_FTSO", 1000000000000000000n);
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
const nativeSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
|
||||
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
|
||||
const { symbol: daiSymbol } = useTokenSymbol(chainId, "GDAI");
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
const { symbol: lpReserveFtsoSymbol } = useTokenSymbol(chainId, "RESERVE_FTSO");
|
||||
|
||||
const lpReserveFtsoTokenNames = useMemo(() => {
|
||||
const token0 = getTokenIcons(chainId, lpReserveFtsoTokens?.token0 ?? []);
|
||||
const token1 = getTokenIcons(chainId, lpReserveFtsoTokens?.token1 ?? []);
|
||||
|
||||
let tokenAddresses = [lpReserveFtsoTokens?.token1, lpReserveFtsoTokens?.token0];
|
||||
let tokenNames = [...token1, ...token0];
|
||||
|
||||
if (token0?.at(0) === reserveSymbol) {
|
||||
tokenAddresses = [lpReserveFtsoTokens?.token0, lpReserveFtsoTokens?.token1];
|
||||
let tokenNames = [...token0, ...token1];
|
||||
}
|
||||
|
||||
return { tokenAddresses, tokenNames }
|
||||
}, [chainId, reserveSymbol, lpReserveFtsoTokens]);
|
||||
const { symbol: lpDaiFtsoSymbol } = useTokenSymbol(chainId, "GDAI_FTSO");
|
||||
|
||||
const tokens = {
|
||||
native: {
|
||||
symbol: nativeSymbol,
|
||||
icons: [nativeSymbol],
|
||||
balance: nativeBalance,
|
||||
price: nativePrice,
|
||||
refetch: nativeBalanceRefetch,
|
||||
},
|
||||
reserve: {
|
||||
symbol: reserveSymbol,
|
||||
address: reserveAddress,
|
||||
balance: reserveBalance,
|
||||
price: reservePrice,
|
||||
icons: [tokenNameConverter(chainId, reserveSymbol)],
|
||||
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/11/6A-Classic-ETC-Token.svg",
|
||||
refetch: reserveRefetch,
|
||||
dai: {
|
||||
symbol: daiSymbol,
|
||||
address: daiAddress,
|
||||
balance: daiBalance,
|
||||
price: daiPrice,
|
||||
icons: ["GDAI"],
|
||||
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/gDAI.svg",
|
||||
refetch: daiRefetch,
|
||||
},
|
||||
ftso: {
|
||||
symbol: ftsoSymbol,
|
||||
@ -252,6 +203,15 @@ export const useWallet = (chainId, userAddress) => {
|
||||
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/eGHST.svg",
|
||||
refetch: ftsoRefetch,
|
||||
},
|
||||
stnk: {
|
||||
symbol: stnkSymbol,
|
||||
address: stnkAddress,
|
||||
balance: stnkBalance,
|
||||
price: stnkPrice,
|
||||
icons: ["STNK"],
|
||||
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/sGHST.svg",
|
||||
refetch: stnkRefetch,
|
||||
},
|
||||
ghst: {
|
||||
symbol: ghstSymbol,
|
||||
address: ghstAddress,
|
||||
@ -261,15 +221,15 @@ export const useWallet = (chainId, userAddress) => {
|
||||
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/GHST.svg",
|
||||
refetch: ghstRefetch,
|
||||
},
|
||||
reserveFtso: {
|
||||
daiFtso: {
|
||||
isPool: true,
|
||||
symbol: lpReserveFtsoSymbol,
|
||||
address: lpReserveFtsoTokenNames?.tokenAddresses.at(1) ?? "",
|
||||
balance: lpReserveFtsoBalance,
|
||||
price: lpReserveFtsoPrice,
|
||||
icons: lpReserveFtsoTokenNames?.tokenNames,
|
||||
symbol: lpDaiFtsoSymbol,
|
||||
address: lpDaiFtsoBalanceAddress,
|
||||
balance: lpDaiFtsoBalance,
|
||||
price: lpDaiFtsoPrice,
|
||||
icons: ["GDAI", "FTSO"],
|
||||
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/uni-v2.svg",
|
||||
refetch: lpReserveFtsoRefetch,
|
||||
refetch: lpDaiFtsoRefetch,
|
||||
}
|
||||
};
|
||||
|
||||
@ -286,12 +246,12 @@ export const useWallet = (chainId, userAddress) => {
|
||||
|
||||
export const Tokens = ({ address, tokens, onClose }) => {
|
||||
const [expanded, setExpanded] = useState(null);
|
||||
const alwaysShowTokens = [tokens.native, tokens.reserve, tokens.ftso, tokens.ghst, tokens.reserveFtso];
|
||||
const alwaysShowTokens = [tokens.dai, tokens.ftso, tokens.stnk, tokens.ghst, tokens.daiFtso];
|
||||
|
||||
const tokenProps = (token) => ({
|
||||
...token,
|
||||
expanded: expanded === token.symbol,
|
||||
reserveAddress: EMPTY_ADDRESS,
|
||||
daiAddress: tokens.dai.address,
|
||||
onChangeExpanded: (e, isExpanded) => setExpanded(isExpanded ? token.symbol : null),
|
||||
onAddTokenToWallet: () => addTokenToWallet(token, address),
|
||||
onClose: () => onClose(),
|
||||
@ -300,7 +260,7 @@ export const Tokens = ({ address, tokens, onClose }) => {
|
||||
return (
|
||||
<>
|
||||
{alwaysShowTokens.map((token, i) => (
|
||||
<Token key={i} isNative={i === 0} {...tokenProps(token)} />
|
||||
<Token key={i} {...tokenProps(token)} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -11,7 +11,7 @@ const WalletButton = ({ openWallet, connect }) => {
|
||||
const { isConnected, chain } = useAccount();
|
||||
const theme = useTheme();
|
||||
const onClick = isConnected ? openWallet : connect;
|
||||
const label = isConnected ? "Wallet" : `Connect`;
|
||||
const label = isConnected ? "Open Wallet" : `Connect Wallet`;
|
||||
return (
|
||||
<Button
|
||||
id="fatso-menu-button"
|
||||
@ -76,12 +76,7 @@ export function Wallet({ address, chainId, connect }) {
|
||||
onOpen={openWallet}
|
||||
onClose={closeWallet}
|
||||
>
|
||||
<InitialWalletView
|
||||
isWalletOpen={isWalletOpen}
|
||||
address={address}
|
||||
chainId={chainId}
|
||||
onClose={closeWallet}
|
||||
/>
|
||||
<InitialWalletView isWalletOpen={isWalletOpen} address={address} chainId={chainId} onClose={closeWallet} />
|
||||
</StyledSwipeableDrawer>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,28 +1,8 @@
|
||||
import { defineChain } from 'viem'
|
||||
import { http, fallback, createConfig } from 'wagmi'
|
||||
import { sepolia, hoodi } from 'wagmi/chains'
|
||||
|
||||
const mordor = defineChain({
|
||||
id: 63,
|
||||
name: 'Mordor',
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: 'METC',
|
||||
symbol: 'mETC',
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ['https://rpc.mordor.etccooperative.org'] },
|
||||
},
|
||||
blockExplorers: {
|
||||
default: {
|
||||
name: 'Blockscout',
|
||||
url: 'https://etc-mordor.blockscout.com/'
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const config = createConfig({
|
||||
chains: [sepolia, hoodi, mordor],
|
||||
chains: [sepolia, hoodi],
|
||||
transports: {
|
||||
[sepolia.id]: fallback([
|
||||
http('https://ethereum-sepolia-rpc.publicnode.com'),
|
||||
@ -36,10 +16,6 @@ export const config = createConfig({
|
||||
[hoodi.id]: fallback([
|
||||
http('https://rpc.hoodi.ethpandaops.io'),
|
||||
http('https://0xrpc.io/hoodi'),
|
||||
]),
|
||||
[mordor.id]: fallback([
|
||||
http('https://rpc.mordor.etccooperative.org'),
|
||||
http('https://geth-mordor.etc-network.info'),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,23 +1,6 @@
|
||||
export enum NetworkId {
|
||||
TESTNET_SEPOLIA = 11155111,
|
||||
TESTNET_HOODI = 560048,
|
||||
TESTNET_MORDOR = 63,
|
||||
}
|
||||
|
||||
export const isGovernanceAvailable = (chainId, addressChainId) => {
|
||||
chainId = addressChainId ? addressChainId : chainId;
|
||||
let exists = false;
|
||||
switch (chainId) {
|
||||
case 11155111:
|
||||
exists = true
|
||||
break;
|
||||
case 63:
|
||||
exists = true
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export const isNetworkAvailable = (chainId, addressChainId) => {
|
||||
@ -30,56 +13,8 @@ export const isNetworkAvailable = (chainId, addressChainId) => {
|
||||
case 560048:
|
||||
exists = true
|
||||
break;
|
||||
case 63:
|
||||
exists = true
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export const isNetworkLegacy = (chainId) => {
|
||||
let isLegacy = false;
|
||||
switch (chainId) {
|
||||
case 11155111:
|
||||
isLegacy = true
|
||||
break;
|
||||
case 63:
|
||||
isLegacy = true
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return isLegacy;
|
||||
}
|
||||
|
||||
export const isNetworkLegacyType = (chainId) => {
|
||||
let isLegacyType = false;
|
||||
switch (chainId) {
|
||||
case 63:
|
||||
isLegacyType = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return isLegacyType
|
||||
}
|
||||
|
||||
export const networkAvgBlockSpeed = (chainId) => {
|
||||
let blockSpeed = 12n;
|
||||
switch (chainId) {
|
||||
case 11155111:
|
||||
blockSpeed = 12n
|
||||
break;
|
||||
case 560048:
|
||||
blockSpeed = 12n
|
||||
break;
|
||||
case 63:
|
||||
blockSpeed = 13n
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return blockSpeed
|
||||
}
|
||||
|
||||
@ -1,129 +1,80 @@
|
||||
import { NetworkId } from "../constants";
|
||||
|
||||
export const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000";
|
||||
|
||||
export const STAKING_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||
[NetworkId.TESTNET_HOODI]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xd90E63E88282596E1ea33765b41Ba3d650f4aD52",
|
||||
[NetworkId.TESTNET_HOODI]: "0x25F62eDc6C89FF84E957C22336A35d2dfc861a86",
|
||||
};
|
||||
|
||||
export const BOND_DEPOSITORY_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||
[NetworkId.TESTNET_HOODI]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xdcE486113280e49ca2fB200258E5Ee1B2D21D495",
|
||||
[NetworkId.TESTNET_HOODI]: "0x6Ad50B1E293E68B2fC230c576220a93A9D311571",
|
||||
};
|
||||
|
||||
export const DAO_TREASURY_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||
[NetworkId.TESTNET_HOODI]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x93dd30f819403710de7933B79A74C4A42438458D",
|
||||
[NetworkId.TESTNET_HOODI]: "0x1a1b29b18f714fac9dDabEf530dFc4f85b56A6e8",
|
||||
};
|
||||
|
||||
export const FTSO_DAI_LP_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
|
||||
[NetworkId.TESTNET_HOODI]: "0x32388605b5E83Ea79CDdC479AA9939DBCF98f59D",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x53B13C4722081c405ce25c7A7629fC326A49a469",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x1394dC3f7bABaa2F0CA80353648087DAB1BF3fd6",
|
||||
[NetworkId.TESTNET_HOODI]: "0xf7B2d44209E70782d93A70F7D8eC50010dF7ae50",
|
||||
};
|
||||
|
||||
export const FTSO_STNK_LP_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x0000000000000000000000000000000000000000", // TBD
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x0000000000000000000000000000000000000000",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x0000000000000000000000000000000000000000",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x0000000000000000000000000000000000000000",
|
||||
}
|
||||
|
||||
export const RESERVE_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
|
||||
[NetworkId.TESTNET_HOODI]: "0xE69a5c6dd88cA798b93c3C92fc50c51Fd5305eB4",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC",
|
||||
export const DAI_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x5f63a27a9214a0352F2EF8dAF1eD4974d713192B",
|
||||
[NetworkId.TESTNET_HOODI]: "0x80c6676c334BCcE60b3CC852085B72143379CE58",
|
||||
};
|
||||
|
||||
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]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||
[NetworkId.TESTNET_HOODI]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xdf2e5306A3dCcfA4e21bbF4226C17Ff5B008dDC4",
|
||||
[NetworkId.TESTNET_HOODI]: "0xE98f7426457E6533B206e91B7EcA97aa8A258B46",
|
||||
};
|
||||
|
||||
export const STNK_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||
[NetworkId.TESTNET_HOODI]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x02C296A27eA779d5a16F934337c12062C5E3c0D9",
|
||||
[NetworkId.TESTNET_HOODI]: "0xF07e9303A9f16Afd82f4f57Fd6fca68Aa0AB6D7F",
|
||||
};
|
||||
|
||||
export const FTSO_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||
[NetworkId.TESTNET_HOODI]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xcFedFFEB3FdeCd2196820Ba3b71f3F84A1255f93",
|
||||
[NetworkId.TESTNET_HOODI]: "0xb184e423811b644A1924334E63985c259F5D0033",
|
||||
};
|
||||
|
||||
export const DISTRIBUTOR_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||
[NetworkId.TESTNET_HOODI]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x8fbF8eB4Fcd451EF62Aee33508D46FE120963194",
|
||||
[NetworkId.TESTNET_HOODI]: "0xdF49dC81c457c6f92e26cf6d686C7a8715255842",
|
||||
};
|
||||
|
||||
export const GHOST_GOVERNANCE_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1",
|
||||
[NetworkId.TESTNET_HOODI]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xDab0c51918E6990d8763FAC8a04AE159e44e0c4f",
|
||||
[NetworkId.TESTNET_HOODI]: "0x1B96B792840d4d19d5097ee007392Ed4d851e64F",
|
||||
};
|
||||
|
||||
export const BONDING_CALCULATOR_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||
[NetworkId.TESTNET_HOODI]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x4896bFc6256A57Df826d7144E48c9633d51d6319",
|
||||
[NetworkId.TESTNET_HOODI]: "0x2635d526Ad24b98082563937f7b996075052c6Fd",
|
||||
}
|
||||
|
||||
export const GATEKEEPER_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xd735cA07984a16911222c08411A80e24EB38869B",
|
||||
[NetworkId.TESTNET_HOODI]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xc85129A097773B7F8970a7364c928C05f265E6A1",
|
||||
}
|
||||
|
||||
export const UNISWAP_V2_ROUTER = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xee567fe1712faf6149d80da1e6934e354124cfe3",
|
||||
[NetworkId.TESTNET_HOODI]: "0xD41daF947c6FFEf344754B99ad09466FBCBb7583",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x90ecf6a29798E3cf31EB7DCE64a372AC40d83F83",
|
||||
};
|
||||
|
||||
export const UNISWAP_V2_FACTORY = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xF62c03E08ada871A0bEb309762E260a7a6a880E6",
|
||||
[NetworkId.TESTNET_HOODI]: "0xF140342cB5C29C1468d91Aee408d7b7271C48b5A",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x909f96C1a436B3386E9962e30f3Ce753070ff524",
|
||||
};
|
||||
|
||||
export const NATIVE_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_HOODI]: [
|
||||
"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",
|
||||
],
|
||||
}
|
||||
|
||||
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_HOODI]: [
|
||||
"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",
|
||||
],
|
||||
}
|
||||
|
||||
@ -18,11 +18,4 @@ export const AVAILABLE_DEXES = {
|
||||
viewBox: "0 0 195 230",
|
||||
},
|
||||
],
|
||||
[NetworkId.TESTNET_MORDOR]: [
|
||||
{
|
||||
name: "Uniswap",
|
||||
icon: UniswapIcon,
|
||||
viewBox: "0 0 195 230",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { ArrowBack } from "@mui/icons-material";
|
||||
import { Box, Link, Skeleton, Typography } from "@mui/material";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { Link as RouterLink, useLocation, useParams, useNavigate } from "react-router-dom";
|
||||
import { useAccount, useSwitchChain } from "wagmi";
|
||||
import { isAddress } from "viem";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link as RouterLink, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useAccount, useChainId } from "wagmi";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import PageTitle from "../../components/PageTitle/PageTitle";
|
||||
@ -18,92 +17,64 @@ import BondSettingsModal from "./components/BondSettingsModal";
|
||||
import NotFound from "../NotFound/NotFound";
|
||||
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { isNetworkLegacy } from "../../constants";
|
||||
import { NetworkId } from "../../constants";
|
||||
import { formatCurrency } from "../../helpers";
|
||||
|
||||
import { useLocalStorage } from "../../hooks/localstorage";
|
||||
import { useLiveBonds } from "../../hooks/bonds";
|
||||
import { useFtsoPrice } from "../../hooks/prices";
|
||||
|
||||
const BondModalContainer = ({ chainId, address, config, connect }) => {
|
||||
const { id, network } = useParams();
|
||||
const { liveBonds } = useLiveBonds(chainId, network);
|
||||
const BondModalContainer = ({ chainId, address, connect }) => {
|
||||
const { id } = useParams();
|
||||
const { liveBonds } = useLiveBonds(chainId);
|
||||
const bond = liveBonds.find(bond => bond.id === Number(id));
|
||||
|
||||
if (!bond) return <NotFound />;
|
||||
|
||||
return <BondModal config={config} chainId={chainId} bond={bond} address={address} connect={connect} />;
|
||||
return <BondModal chainId={chainId} bond={bond} address={address} connect={connect} />;
|
||||
};
|
||||
|
||||
export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
export const BondModal = ({ bond, chainId, address, connect }) => {
|
||||
const navigate = useNavigate();
|
||||
const { network } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [slippage, setSlippage] = useState(localStorage.getItem("bond-slippage") || "10");
|
||||
const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("bond-decimals") || "5");
|
||||
const [isSettingsOpen, setSettingsOpen] = useState(false);
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "bond-slippage", "10"));
|
||||
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "bond-decimals", "5"));
|
||||
const [recipientAddress, setRecipientAddressInner] = useState(() => getStorageValue(chainId, address, "bond-recipient", address));
|
||||
const [recipientAddress, setRecipientAddress] = useState(address);
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.send({ hitType: "pageview", page: pathname });
|
||||
}, [])
|
||||
|
||||
const setRecipientAddress = useCallback((value) => {
|
||||
setRecipientAddressInner(value);
|
||||
if (isAddress(value)) {
|
||||
setStorageValue(chainId, address, "bond-recipient", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
|
||||
const setSlippageInner = useCallback((value) => {
|
||||
const setSlippageInner = (value) => {
|
||||
const maybeValue = parseFloat(value);
|
||||
if (!maybeValue || parseFloat(value) <= 100) {
|
||||
setSlippage(value);
|
||||
setStorageValue(chainId, address, "bond-slippage", value);
|
||||
localStorage.setItem("bond-slippage", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
}
|
||||
|
||||
const setFormatDecimalsInner = useCallback((value) => {
|
||||
const setFormatDecimalsInner = (value) => {
|
||||
if (Number(value) <= 17) {
|
||||
setFormatDecimals(value);
|
||||
setStorageValue(chainId, address, "bond-decimals", value);
|
||||
localStorage.setItem("bond-decimals", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Escape") isSettingsOpen ? setSettingsOpen(false) : navigate(`/${network}/bonds`);
|
||||
if (event.key === "Escape") isSettingsOpen ? setSettingsOpen(false) : navigate("/bonds");
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [network, navigate, isSettingsOpen]);
|
||||
|
||||
const chainSymbol = useMemo(() => {
|
||||
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
|
||||
if (chainSymbol) return chainSymbol;
|
||||
return "WTF";
|
||||
}, [config]);
|
||||
|
||||
const preparedQuoteToken = useMemo(() => {
|
||||
if (isNetworkLegacy(chainId)) {
|
||||
return {
|
||||
address: bond.quoteToken.quoteTokenAddress,
|
||||
icons: bond.quoteToken.icons,
|
||||
name: bond.quoteToken.name,
|
||||
}
|
||||
}
|
||||
return { address: undefined, icons: [chainSymbol], name: chainSymbol };
|
||||
}, [bond, chainSymbol, chainId])
|
||||
}, [navigate, isSettingsOpen]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<PageTitle
|
||||
name={
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<Link component={RouterLink} to={`/${network}/bonds`}>
|
||||
<Link component={RouterLink} to="/bonds">
|
||||
<Box display="flex" flexDirection="row">
|
||||
<ArrowBack />
|
||||
<Typography fontWeight="500" marginLeft="9.5px" marginRight="18px">
|
||||
@ -112,10 +83,10 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
</Box>
|
||||
</Link>
|
||||
|
||||
<TokenStack tokens={preparedQuoteToken.icons} sx={{ fontSize: "27px" }} />
|
||||
<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "27px" }} />
|
||||
<Box display="flex" flexDirection="column" ml={1} justifyContent="center" alignItems="center">
|
||||
<Typography variant="h4" fontWeight={500}>
|
||||
{preparedQuoteToken.name}
|
||||
{bond.quoteToken.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -161,12 +132,10 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
<BondInputArea
|
||||
chainId={chainId}
|
||||
bond={bond}
|
||||
config={config}
|
||||
connect={connect}
|
||||
address={address}
|
||||
slippage={slippage}
|
||||
recipientAddress={recipientAddress}
|
||||
preparedQuoteToken={preparedQuoteToken}
|
||||
handleSettingsOpen={() => setSettingsOpen(true)}
|
||||
formatDecimals={formatDecimals}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Box, Tab, Tabs, Typography, Container, useMediaQuery } from "@mui/material";
|
||||
import { Box, Tab, Tabs, Container, useMediaQuery } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import Paper from "../../components/Paper/Paper";
|
||||
@ -20,8 +20,8 @@ import { useFtsoPrice } from "../../hooks/prices";
|
||||
import { useTokenSymbol } from "../../hooks/tokens";
|
||||
|
||||
const Bonds = ({ chainId, address, connect }) => {
|
||||
const { network } = useParams();
|
||||
const [isZoomed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [secondsTo, setSecondsTo] = useState(0);
|
||||
|
||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||
@ -29,9 +29,9 @@ const Bonds = ({ chainId, address, connect }) => {
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.send({ hitType: "pageview", page: "/bonds" });
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const { liveBonds } = useLiveBonds(chainId, network);
|
||||
const { liveBonds } = useLiveBonds(chainId);
|
||||
const totalReserves = useTotalReserves(chainId);
|
||||
const ftsoPrice = useFtsoPrice(chainId);
|
||||
|
||||
@ -59,17 +59,7 @@ const Bonds = ({ chainId, address, connect }) => {
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column">
|
||||
<Paper
|
||||
fullWidth
|
||||
enableBackground
|
||||
headerContent={
|
||||
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
||||
<Typography variant="h6">
|
||||
Active bonds
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper headerText="Active bonds" fullWidth enableBackground>
|
||||
<MetricCollection>
|
||||
<Metric
|
||||
label={`Treasury Balance`}
|
||||
|
||||
@ -37,9 +37,6 @@ const BondConfirmModal = ({
|
||||
sender,
|
||||
handleSettingsOpen,
|
||||
isOpen,
|
||||
isNative,
|
||||
bondQuoteTokenName,
|
||||
bondQuoteTokenIcons,
|
||||
handleConfirmClose
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@ -56,16 +53,15 @@ const BondConfirmModal = ({
|
||||
const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one;
|
||||
const referral = import.meta.env.VITE_APP_REFERRAL_ADDRESS;
|
||||
|
||||
await purchaseBond({
|
||||
await purchaseBond(
|
||||
chainId,
|
||||
bondId: bond.id,
|
||||
amount: spendAmountValue._value.toBigInt(),
|
||||
bond.id,
|
||||
spendAmountValue._value.toBigInt(),
|
||||
maxPrice,
|
||||
user: recipientAddress,
|
||||
recipientAddress,
|
||||
sender,
|
||||
referral,
|
||||
isNative
|
||||
});
|
||||
referral
|
||||
);
|
||||
|
||||
setIsPending(false);
|
||||
handleConfirmClose();
|
||||
@ -78,9 +74,9 @@ const BondConfirmModal = ({
|
||||
open={isOpen}
|
||||
headerContent={
|
||||
<Box display="flex" flexDirection="row">
|
||||
<TokenStack tokens={bondQuoteTokenIcons} sx={{ fontSize: "27px" }} />
|
||||
<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "27px" }} />
|
||||
<Typography variant="h4" sx={{ marginLeft: "6px" }}>
|
||||
{bondQuoteTokenName}
|
||||
{bond.quoteToken.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
@ -95,7 +91,7 @@ const BondConfirmModal = ({
|
||||
metric={spendAmount}
|
||||
/>
|
||||
<Box display="flex" flexDirection="row" justifyContent="center">
|
||||
<Typography>{bondQuoteTokenName}</Typography>
|
||||
<Typography>{bond.quoteToken.name}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<GhostStyledIcon sx={{ transform: "rotate(-90deg)" }} component={ArrowDropDownIcon} />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
|
||||
import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material";
|
||||
import { Box, Checkbox, FormControlLabel } from "@mui/material";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
@ -30,15 +30,12 @@ const BondInputArea = ({
|
||||
formatDecimals,
|
||||
handleSettingsOpen,
|
||||
address,
|
||||
preparedQuoteToken,
|
||||
connect
|
||||
}) => {
|
||||
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
|
||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { currentIndex } = useCurrentIndex(chainId);
|
||||
const { balance, refetch } = useBalance(chainId, preparedQuoteToken.address, address);
|
||||
const { balance, refetch } = useBalance(chainId, bond.quoteToken.quoteTokenAddress, address);
|
||||
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
@ -98,21 +95,22 @@ const BondInputArea = ({
|
||||
UpperSwapCard={
|
||||
<SwapCard
|
||||
maxWidth="476px"
|
||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||
inputWidth="280px"
|
||||
id="from"
|
||||
token={<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "21px" }} />}
|
||||
tokenName={preparedQuoteToken.name}
|
||||
info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)}
|
||||
endString={preparedQuoteToken.address && "Max"}
|
||||
tokenName={bond.quoteToken.name}
|
||||
info={formatCurrency(balance, formatDecimals, bond.quoteToken.name)}
|
||||
endString="Max"
|
||||
endStringOnClick={setMax}
|
||||
value={amount}
|
||||
onChange={event => setAmount(event.currentTarget.value)}
|
||||
inputProps={{ "data-testid": "fromInput" }}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
LowerSwapCard={
|
||||
<SwapCard
|
||||
maxWidth="476px"
|
||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||
inputWidth="280px"
|
||||
id="to"
|
||||
token={<TokenStack tokens={bond.baseToken.icons} sx={{ fontSize: "21px" }} />}
|
||||
tokenName={bond.baseToken.name}
|
||||
@ -134,7 +132,6 @@ const BondInputArea = ({
|
||||
connect={connect}
|
||||
width="100%"
|
||||
height="60px"
|
||||
isNative={preparedQuoteToken.address === undefined}
|
||||
isVertical
|
||||
message={
|
||||
<>
|
||||
@ -180,12 +177,12 @@ const BondInputArea = ({
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
tooltip={`Total amount of ${ftsoSymbol} you will receive from this bond purchase`}
|
||||
tooltip={`The total amount of payout asset you will receive from this bond purchase`}
|
||||
/>
|
||||
|
||||
<DataRow
|
||||
title="Max You Can Buy"
|
||||
tooltip={`Maximum ${ftsoSymbol} that can be purchased in a single transaction. Prevents whales from buying entire bond supply at once.`}
|
||||
tooltip={`The maximum quantity of payout token offered via bonds at this moment in time`}
|
||||
balance={
|
||||
<span>
|
||||
{bond.baseToken.tokenAddress.toUpperCase() === bond.quoteToken.quoteTokenAddress.toUpperCase()
|
||||
@ -198,14 +195,14 @@ const BondInputArea = ({
|
||||
<DataRow
|
||||
title="Discount"
|
||||
balance={<BondDiscount discount={bond.discount} textOnly />}
|
||||
tooltip={`The discount (or premium) between ${ftsoSymbol} market price and bond price. Higher discount = better deal for bond buyers.`}
|
||||
/>
|
||||
tooltip={`The bond discount is the percentage difference between ${ftsoSymbol} market value and the bond's price`}
|
||||
/>
|
||||
|
||||
<DataRow
|
||||
title={`Vesting Term`}
|
||||
balance={<BondVesting vesting={bond.vesting} />}
|
||||
tooltip={"Time until bond fully vests and becomes claimable. Vesting period aligns bond buyer with the protocol by encouraging longer-term holding."}
|
||||
/>
|
||||
tooltip={"The duration of the Bond whereby the bond can be claimed in its entirety"}
|
||||
/>
|
||||
|
||||
{recipientAddress !== address && (
|
||||
<DataRow title={`Recipient`} balance={shorten(recipientAddress)} />
|
||||
@ -222,9 +219,6 @@ const BondInputArea = ({
|
||||
spendAmountValue={parsedAmount}
|
||||
spendAmount={formatNumber(parsedAmount, formatDecimals)}
|
||||
receiveAmount={formatNumber(amountInBaseToken, formatDecimals)}
|
||||
bondQuoteTokenName={preparedQuoteToken.name}
|
||||
bondQuoteTokenIcons={preparedQuoteToken.icons}
|
||||
isNative={preparedQuoteToken.address === undefined}
|
||||
handleSettingsOpen={handleSettingsOpen}
|
||||
isOpen={confirmOpen}
|
||||
sender={address}
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
Typography,
|
||||
useMediaQuery
|
||||
} from "@mui/material";
|
||||
import { NavLink, useParams } from "react-router-dom";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import ArrowUp from "../../../assets/icons/arrow-up.svg?react";
|
||||
|
||||
import BondDiscount from "./BondDiscount";
|
||||
@ -26,7 +26,6 @@ import { TertiaryButton } from "../../../components/Button";
|
||||
|
||||
export const BondList = ({ bonds, secondsTo, chainId }) => {
|
||||
const isSmallScreen = useScreenSize("md");
|
||||
const { network } = useParams();
|
||||
|
||||
if (bonds.length === 0) {
|
||||
return (
|
||||
@ -44,7 +43,7 @@ export const BondList = ({ bonds, secondsTo, chainId }) => {
|
||||
</Box>
|
||||
|
||||
{sortBondsByDiscount(bonds).map(bond => (
|
||||
<BondCard key={bond.id} secondsTo={secondsTo} bond={bond} chainName={network} />
|
||||
<BondCard key={bond.id} secondsTo={secondsTo} bond={bond} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@ -54,7 +53,7 @@ export const BondList = ({ bonds, secondsTo, chainId }) => {
|
||||
<>
|
||||
<BondTable>
|
||||
{sortBondsByDiscount(bonds).map(bond => (
|
||||
<BondRow key={bond.id} bond={bond} secondsTo={secondsTo} chainName={network} />
|
||||
<BondRow key={bond.id} bond={bond} secondsTo={secondsTo} />
|
||||
))}
|
||||
</BondTable>
|
||||
|
||||
@ -65,7 +64,7 @@ export const BondList = ({ bonds, secondsTo, chainId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const BondCard = ({ bond, secondsTo, chainName }) => {
|
||||
const BondCard = ({ bond, secondsTo }) => {
|
||||
const quoteTokenName = bond.quoteToken.name;
|
||||
const baseTokenName = bond.baseToken.name;
|
||||
|
||||
@ -131,11 +130,10 @@ const BondCard = ({ bond, secondsTo, chainName }) => {
|
||||
<Box mt="16px">
|
||||
<Link
|
||||
component={NavLink}
|
||||
to={`/${chainName}/bonds/${bond.id}`}
|
||||
sx={{ pointerEvents: bond.isSoldOut ? 'none' : 'auto' }}
|
||||
to={`/bonds/${bond.id}`}
|
||||
>
|
||||
<TertiaryButton fullWidth disabled={bond.isSoldOut}>
|
||||
{bond.isSoldOut ? "Sold Out" : `Bond`}
|
||||
<TertiaryButton fullWidth>
|
||||
Bond
|
||||
</TertiaryButton>
|
||||
</Link>
|
||||
</Box>
|
||||
@ -175,7 +173,7 @@ const payoutTokenCapacity = (bond) => {
|
||||
return `${formatNumber(payoutTokenCapacity, 4)} ${bond.baseToken.name}`;
|
||||
};
|
||||
|
||||
const BondRow = ({ bond, secondsTo, chainName }) => {
|
||||
const BondRow = ({ bond, secondsTo }) => {
|
||||
const quoteTokenName = bond.quoteToken.name;
|
||||
const baseTokenName = bond.baseToken.name;
|
||||
|
||||
@ -223,8 +221,7 @@ const BondRow = ({ bond, secondsTo, chainName }) => {
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Link
|
||||
component={NavLink}
|
||||
to={`/${chainName}/bonds/${bond.id}`}
|
||||
sx={{ pointerEvents: bond.isSoldOut ? 'none' : 'auto' }}
|
||||
to={`/bonds/${bond.id}`}
|
||||
>
|
||||
<TertiaryButton fullWidth disabled={bond.isSoldOut}>
|
||||
{bond.isSoldOut ? "Sold Out" : `Bond`}
|
||||
|
||||
@ -20,16 +20,17 @@ import { formatCurrency } from "../../../helpers";
|
||||
import { useCurrentIndex, useEpoch, useWarmupLength, useWarmupInfo } from "../../../hooks/staking";
|
||||
import { useNotes, redeem } from "../../../hooks/bonds";
|
||||
import { useTokenSymbol } from "../../../hooks/tokens";
|
||||
import { useGhstPrice } from "../../../hooks/prices";
|
||||
|
||||
export const ClaimBonds = ({ chainId, address, secondsTo }) => {
|
||||
const isSmallScreen = useScreenSize("md");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [isWarmup, setIsWapmup] = useState(false);
|
||||
const [isPreClaimConfirmed, setPreClaimConfirmed] = useState(false);
|
||||
const [isPayoutGhst, _] = useState(true);
|
||||
const [isPayoutGhst, setIsPayoutGhst] = useState(localStorage.getItem("bond-isGhstPayout")
|
||||
? localStorage.getItem("bond-isGhstPayout") === "true"
|
||||
: true
|
||||
);
|
||||
|
||||
const ghstPrice = useGhstPrice(chainId);
|
||||
const { epoch } = useEpoch(chainId);
|
||||
const { warmupExists } = useWarmupLength(chainId);
|
||||
const { warmupInfo } = useWarmupInfo(chainId, BOND_DEPOSITORY_ADDRESSES[chainId]);
|
||||
@ -38,6 +39,7 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
|
||||
const { notes, refetch: notesRefetch } = useNotes(chainId, address);
|
||||
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
if (!notes || notes.length === 0) return null;
|
||||
@ -50,18 +52,23 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
|
||||
18
|
||||
);
|
||||
|
||||
const setIsPayoutGhstInner = (value) => {
|
||||
setIsPayoutGhst(value);
|
||||
localStorage.setItem("bond-isGhstPayout", value);
|
||||
}
|
||||
|
||||
const onSubmit = async (indexes) => {
|
||||
const isFundsInWarmup = warmupInfo.deposit._value > 0n;
|
||||
if (warmupExists && isFundsInWarmup && !isPreClaimConfirmed) {
|
||||
setIsWapmup(true);
|
||||
} else {
|
||||
setIsPending(true);
|
||||
await redeem({
|
||||
await redeem(
|
||||
chainId,
|
||||
user: address,
|
||||
isGhst: isPayoutGhst,
|
||||
address,
|
||||
isPayoutGhst,
|
||||
indexes
|
||||
});
|
||||
);
|
||||
await notesRefetch();
|
||||
setIsPending(false);
|
||||
}
|
||||
@ -76,17 +83,25 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
|
||||
warmupLength={warmupInfo.expiry - epoch.number}
|
||||
setPreClaimConfirmed={() => setPreClaimConfirmed(true)}
|
||||
/>
|
||||
<Paper
|
||||
fullWidth
|
||||
enableBackground
|
||||
headerContent={
|
||||
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
||||
<Typography variant="h6">
|
||||
Your Bonds
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper headerText="Your Bonds" fullWidth enableBackground>
|
||||
<Box display="flex" flexDirection="column" alignItems="center">
|
||||
<Typography variant="h4" align="center" color="textSecondary">
|
||||
Payout Options
|
||||
</Typography>
|
||||
<Tabs
|
||||
centered
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
value={isPayoutGhst ? 1 : 0}
|
||||
aria-label="Payout token tabs"
|
||||
onChange={(_, view) => setIsPayoutGhstInner(view === 1)}
|
||||
TabIndicatorProps={{ style: { display: "none" } }}
|
||||
>
|
||||
<Tab aria-label="payout-stnk-button" label={stnkSymbol} style={{ fontSize: "1rem" }} />
|
||||
<Tab aria-label="payout-ghst-button" label={ghstSymbol} style={{ fontSize: "1rem" }} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="center">
|
||||
<Box display="flex" flexDirection="column" alignItems="center" mt="24px" width={isSmallScreen ? "100%" : "50%"}>
|
||||
<Typography variant="h5" align="center" color="textSecondary" style={{ fontSize: "1.2rem" }}>
|
||||
@ -99,137 +114,128 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
|
||||
? formatCurrency(totalClaimableBalance, 5, ghstSymbol)
|
||||
: formatCurrency(currentIndex.mul(totalClaimableBalance), 5, stnkSymbol)
|
||||
}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" align="center">
|
||||
{formatCurrency(totalClaimableBalance * ghstPrice, 2)}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<PrimaryButton
|
||||
disabled={isPending || totalClaimableBalance._value === 0n}
|
||||
fullWidth
|
||||
className=""
|
||||
onClick={() => onSubmit(notes.filter((note) => secondsTo > note.matured).map(note => note.id))}
|
||||
>
|
||||
Claim All
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
|
||||
<PrimaryButton
|
||||
disabled={isPending || totalClaimableBalance._value === 0n}
|
||||
fullWidth
|
||||
className=""
|
||||
onClick={() => onSubmit(notes.filter((note) => secondsTo > note.matured).map(note => note.id))}
|
||||
>
|
||||
Claim All
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt="48px">
|
||||
{isSmallScreen ? (
|
||||
<>
|
||||
{notes.map((note, index) => (
|
||||
<Box key={index} mt="32px">
|
||||
<Box display="flex" alignItems="center">
|
||||
<TokenStack tokens={note.quoteToken.icons} />
|
||||
<Box ml="8px">
|
||||
<Typography>{note.quoteToken.name}</Typography>
|
||||
<Box mt="48px">
|
||||
{isSmallScreen ? (
|
||||
<>
|
||||
{notes.map((note, index) => (
|
||||
<Box key={index} mt="32px">
|
||||
<Box display="flex" alignItems="center">
|
||||
<TokenStack tokens={note.quoteToken.icons} />
|
||||
<Box ml="8px">
|
||||
<Typography>{note.quoteToken.name}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="space-between" mt="16px">
|
||||
<Typography>Duration</Typography>
|
||||
<Typography>
|
||||
<BondVesting vesting={note.vesting} />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mt="16px">
|
||||
<Typography>Duration</Typography>
|
||||
<Typography>
|
||||
<BondVesting vesting={note.vesting} />
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="space-between" mt="8px">
|
||||
<Typography>Remaining</Typography>
|
||||
<Typography>
|
||||
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mt="8px">
|
||||
<Typography>Remaining</Typography>
|
||||
<Typography>
|
||||
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="space-between" mt="8px">
|
||||
<Typography>Payout</Typography>
|
||||
<Box display="flex" flexDirection="column" alignItems="flex-end">
|
||||
<Box display="flex" justifyContent="space-between" mt="8px">
|
||||
<Typography>Payout</Typography>
|
||||
<Typography>
|
||||
{isPayoutGhst
|
||||
? formatCurrency(note.payout, 5, ghstSymbol)
|
||||
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
|
||||
}
|
||||
</Typography>
|
||||
<Typography>{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box mt="16px">
|
||||
<TertiaryButton
|
||||
fullWidth
|
||||
disabled={isPending || secondsTo < note.matured}
|
||||
onClick={() => onSubmit([note.id])}
|
||||
>
|
||||
Claim
|
||||
</TertiaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt="16px">
|
||||
<TertiaryButton
|
||||
fullWidth
|
||||
disabled={isPending || secondsTo < note.matured}
|
||||
onClick={() => onSubmit([note.id])}
|
||||
>
|
||||
Claim
|
||||
</TertiaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{ width: "180px", padding: "8px 0" }}>Bond</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>Duration</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>Remaining</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>Payout</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{notes.map((note, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<TokenStack tokens={note.quoteToken.icons} />
|
||||
<Box display="flex" flexDirection="column" ml="16px">
|
||||
<Typography>{note.quoteToken.name}</Typography>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{ width: "180px", padding: "8px 0" }}>Bond</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>Duration</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>Remaining</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>Payout</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{notes.map((note, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<TokenStack tokens={note.quoteToken.icons} />
|
||||
<Box display="flex" flexDirection="column" ml="16px">
|
||||
<Typography>{note.quoteToken.name}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Typography>
|
||||
<BondVesting vesting={note.vesting} />
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Typography>
|
||||
<BondVesting vesting={note.vesting} />
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Typography>
|
||||
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Typography>
|
||||
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Box display="flex" flexDirection="column" alignItems="flex-start">
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<Typography>
|
||||
{isPayoutGhst
|
||||
? formatCurrency(note.payout, 5, ghstSymbol)
|
||||
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
|
||||
}
|
||||
</Typography>
|
||||
<Typography variant="body2">{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<TertiaryButton
|
||||
fullWidth
|
||||
disabled={isPending || secondsTo < note.matured}
|
||||
onClick={() => onSubmit([note.id])}
|
||||
>
|
||||
Claim
|
||||
</TertiaryButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
<TableCell style={{ padding: "8px 0" }}>
|
||||
<TertiaryButton
|
||||
fullWidth
|
||||
disabled={isPending || secondsTo < note.matured}
|
||||
onClick={() => onSubmit([note.id])}
|
||||
>
|
||||
Claim
|
||||
</TertiaryButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -44,7 +44,7 @@ const WarmupConfirmModal = ({
|
||||
minHeight="150px"
|
||||
open={isOpen}
|
||||
headerText={
|
||||
warmupLength <= 0
|
||||
warmupLength < 0
|
||||
? "Bond Notification"
|
||||
: "Bond in Warm-up"
|
||||
}
|
||||
@ -52,7 +52,7 @@ const WarmupConfirmModal = ({
|
||||
>
|
||||
<Box gap="20px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
|
||||
<Box display="flex" flexDirection="column">
|
||||
{warmupLength <= 0
|
||||
{warmupLength < 0
|
||||
? <FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
@ -69,7 +69,7 @@ const WarmupConfirmModal = ({
|
||||
}
|
||||
</Box>
|
||||
|
||||
{warmupLength <= 0 && <PrimaryButton fullWidth disabled={!isChecked} onClick={onSubmit}>
|
||||
{warmupLength < 0 && <PrimaryButton fullWidth disabled={!isChecked} onClick={onSubmit}>
|
||||
Confirm
|
||||
</PrimaryButton>}
|
||||
</Box>
|
||||
|
||||
@ -1,375 +0,0 @@
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Link,
|
||||
Skeleton,
|
||||
TableContainer,
|
||||
Table,
|
||||
Paper,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Modal,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useConfig } from "wagmi";
|
||||
import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
|
||||
import { toHex } from "@polkadot-api/utils";
|
||||
|
||||
import { PrimaryButton } from "../../components/Button";
|
||||
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||
import SwapCard from "../../components/Swap/SwapCard";
|
||||
import SwapCollection from "../../components/Swap/SwapCollection";
|
||||
|
||||
import { ghost } from "../../hooks/staking";
|
||||
import { useBalance } from "../../hooks/tokens";
|
||||
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { formatNumber, formatCurrency, timeConverter } from "../../helpers";
|
||||
|
||||
import { BridgeRoute } from "./BridgeRoute";
|
||||
|
||||
const sliceString = (string, first, second) => {
|
||||
if (!string) return "";
|
||||
return string.slice(0, first) + "..." + string.slice(second);
|
||||
}
|
||||
|
||||
export const BridgeCardAction = ({
|
||||
isVerySmallScreen,
|
||||
isSemiSmallScreen,
|
||||
chainId,
|
||||
address,
|
||||
ghstSymbol,
|
||||
gatekeeperAddressEmpty,
|
||||
gatekeeperAddress,
|
||||
evmNetwork,
|
||||
connect,
|
||||
isConfirmed,
|
||||
setIsConfirmed,
|
||||
openBridgeModal,
|
||||
storeTransactionHash,
|
||||
}) => {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [receiver, setReceiver] = useState("");
|
||||
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
|
||||
const [amount, setAmount] = useState("");
|
||||
|
||||
const config = useConfig();
|
||||
const incomingFee = Number(evmNetwork?.incoming_fee ?? 0n) / 10000000;
|
||||
|
||||
const {
|
||||
balance: ghstBalance,
|
||||
refetch: ghstBalanceRefetch
|
||||
} = useBalance(chainId, "GHST", address);
|
||||
|
||||
const chainName = useMemo(() => {
|
||||
const client = config?.getClient();
|
||||
return client?.chain?.name;
|
||||
}, [config]);
|
||||
|
||||
const chainNativeCurrency = useMemo(() => {
|
||||
const client = config?.getClient();
|
||||
return client?.chain?.nativeCurrency?.symbol;
|
||||
}, [config]);
|
||||
|
||||
const chainExplorerUrl = useMemo(() => {
|
||||
const client = config?.getClient();
|
||||
return client?.chain?.blockExplorers?.default?.url;
|
||||
}, [config]);
|
||||
|
||||
const preparedAmount = useMemo(() => {
|
||||
try {
|
||||
const result = BigInt(parseFloat(amount) * Math.pow(10, 18));
|
||||
if (result > ghstBalance._value) {
|
||||
return ghstBalance._value;
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return 0n;
|
||||
}
|
||||
}, [amount]);
|
||||
|
||||
const amountAfterFee = useMemo(() => {
|
||||
const convertedAmount = parseFloat(amount);
|
||||
if (!convertedAmount) {
|
||||
return 0;
|
||||
}
|
||||
return convertedAmount * (1 - incomingFee / 100);
|
||||
}, [amount, incomingFee]);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
let isDisabled = isPending || gatekeeperAddressEmpty;
|
||||
if (address !== "") {
|
||||
isDisabled = isDisabled
|
||||
|| !convertedReceiver
|
||||
|| preparedAmount === 0n
|
||||
|| ghstBalance._value < preparedAmount;
|
||||
}
|
||||
return isDisabled;
|
||||
}, [isPending, gatekeeperAddressEmpty, address, convertedReceiver, preparedAmount, ghstBalance]);
|
||||
|
||||
const ghostFunds = async () => {
|
||||
setIsPending(true);
|
||||
try {
|
||||
const txHash = await ghost(chainId, address, convertedReceiver, preparedAmount);
|
||||
if (txHash) {
|
||||
storeTransactionHash(txHash, receiver, preparedAmount.toString());
|
||||
}
|
||||
} finally {
|
||||
await ghstBalanceRefetch();
|
||||
setReceiver("");
|
||||
setAmount("");
|
||||
setIsPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isConfirmed) {
|
||||
setIsConfirmed(false);
|
||||
ghostFunds();
|
||||
}
|
||||
}, [isConfirmed]);
|
||||
|
||||
const ghostOrConnect = async () => {
|
||||
if (address === "") {
|
||||
connect();
|
||||
} else if (!isConfirmed) {
|
||||
openBridgeModal();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const [publicKey, prefix] = ss58Decode(receiver);
|
||||
if (prefix !== 1995 && prefix !== 1996) {
|
||||
throw new Error("bad prefix");
|
||||
}
|
||||
setConvertedReceiver(toHex(publicKey));
|
||||
} catch {
|
||||
setConvertedReceiver(undefined);
|
||||
}
|
||||
}, [receiver])
|
||||
|
||||
return (
|
||||
<Box width="100%" height="340px" display="flex" flexDirection="column" justifyContent="space-between">
|
||||
<SwapCollection
|
||||
iconNotNeeded
|
||||
maxWidth="100%"
|
||||
UpperSwapCard={<SwapCard
|
||||
id={`bridge-token-receiver`}
|
||||
inputWidth={"100%"}
|
||||
value={convertedReceiver ? sliceString(receiver, 15, -10) : receiver}
|
||||
onChange={event => setReceiver(convertedReceiver ? "" : event.currentTarget.value)}
|
||||
inputProps={{ "data-testid": "fromInput" }}
|
||||
placeholder="GHOST address (sf prefixed)"
|
||||
endString={convertedReceiver
|
||||
? <GhostStyledIcon color="success" viewBox="0 0 25 25" component={CheckCircleIcon} />
|
||||
: undefined
|
||||
}
|
||||
type="text"
|
||||
maxWidth="100%"
|
||||
/>}
|
||||
LowerSwapCard={<SwapCard
|
||||
id={`bridge-token-amount`}
|
||||
inputWidth={"100%"}
|
||||
info={`${formatCurrency(ghstBalance.toString(), 4, ghstSymbol)}`}
|
||||
value={amount}
|
||||
onChange={event => setAmount(event.currentTarget.value)}
|
||||
inputProps={{ "data-testid": "fromInput" }}
|
||||
endString={"Max"}
|
||||
endStringOnClick={() => setAmount(ghstBalance.toString())}
|
||||
maxWidth="100%"
|
||||
/>}
|
||||
/>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
display="flex"
|
||||
gap="10px"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box width="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{gatekeeperAddressEmpty && (
|
||||
<Box display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
<Typography align="justify" mr="10px" variant="body2" color="textSecondary">
|
||||
<em>
|
||||
There is no connected gatekeeper on {chainName} network yet.
|
||||
</em>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{!gatekeeperAddressEmpty && (
|
||||
<Box width="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Gatekeeper:</Typography>}
|
||||
<Link
|
||||
fontSize="12px"
|
||||
lineHeight="15px"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`${chainExplorerUrl}/token/${gatekeeperAddress}`}
|
||||
>
|
||||
{sliceString(gatekeeperAddress, 10, -8)}
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box width="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
<Box width="100%" display="flex" flexDirection="column" gap="0px">
|
||||
<Box maxWidth="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Bridge Fee:</Typography>}
|
||||
{incomingFee
|
||||
? <Typography fontSize="12px" lineHeight="15px">{`${incomingFee.toFixed(4)}%`}</Typography>
|
||||
: <Skeleton height={15} width="80px" />
|
||||
}
|
||||
</Box>
|
||||
<Box maxWidth="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">You will get:</Typography>}
|
||||
{incomingFee
|
||||
? <Typography fontSize="12px" lineHeight="15px">{amountAfterFee.toFixed(4)} {ghstSymbol}</Typography>
|
||||
: <Skeleton height={15} width="80px" />
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<BridgeRoute coinName={ghstSymbol} chainTokenName={chainNativeCurrency} tokens={[ghstSymbol]} />
|
||||
</Box>
|
||||
<PrimaryButton
|
||||
fullWidth
|
||||
disabled={isDisabled}
|
||||
loading={isPending}
|
||||
onClick={() => ghostOrConnect()}
|
||||
>
|
||||
{address === "" ? "Connect" : "Bridge" }
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const BridgeCardHistory = ({
|
||||
isSemiSmallScreen,
|
||||
isBigScreen,
|
||||
filteredStoredTransactions,
|
||||
ghstSymbol,
|
||||
blockNumber,
|
||||
finalityDelay,
|
||||
setActiveTxIndex
|
||||
}) => {
|
||||
const isVeryBigScreen = useMediaQuery("(max-width: 1360px)");
|
||||
|
||||
const theme = useTheme();
|
||||
const background = (index) => {
|
||||
return index % 2 === 1 ? "" : theme.colors.gray[750];
|
||||
}
|
||||
|
||||
return (
|
||||
<Box height="320px">
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
className="custom-scrollbar"
|
||||
sx={{
|
||||
height: "320px",
|
||||
overflowY: 'scroll',
|
||||
msOverflowStyle: "thin !important",
|
||||
scrollbarWidth: "thin !important",
|
||||
}}
|
||||
>
|
||||
<Table sx={{ marginTop: "0px" }} stickyHeader aria-label="sticky available transactions">
|
||||
<TableHead>
|
||||
<TableRow sx={{ height: "40px" }}>
|
||||
{!(isBigScreen ^ isVeryBigScreen) && <TableCell align="left" style={{ padding: "0px", paddingLeft: "16px", fontSize: "12px", borderTopLeftRadius: "3px", background: theme.colors.paper.cardHover }}>
|
||||
Transaction
|
||||
</TableCell>}
|
||||
<TableCell align="center" style={{ padding: "0px", fontSize: "12px", background: theme.colors.paper.cardHover }}>
|
||||
Datetime
|
||||
</TableCell>
|
||||
<TableCell align="center" style={{ padding: "0px", fontSize: "12px", borderTopRightRadius: "3px", background: theme.colors.paper.cardHover }}>
|
||||
Status
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredStoredTransactions?.map((obj, idx) => (
|
||||
<TableRow
|
||||
key={idx}
|
||||
sx={{ cursor: "pointer", height: "30px" }}
|
||||
id={idx + `--tx-history`}
|
||||
data-testid={idx + `--tx-history`}
|
||||
onClick={() => setActiveTxIndex(idx)}
|
||||
>
|
||||
{!(isBigScreen ^ isVeryBigScreen) && <TableCell style={{ background: background(idx) }}>
|
||||
<Box display="flex" flexDirection="column" justifyContent="center">
|
||||
<Typography variant="caption">
|
||||
{formatCurrency(
|
||||
new DecimalBigNumber(BigInt(obj.amount), 18).toString(),
|
||||
isSemiSmallScreen ? 3 : 8,
|
||||
ghstSymbol
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
{sliceString(
|
||||
obj.receiverAddress,
|
||||
isSemiSmallScreen ? 5 : 10,
|
||||
isSemiSmallScreen ? -3 : -8
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>}
|
||||
|
||||
<TableCell style={{ background: background(idx) }}>
|
||||
<Box display="flex" flexDirection="column" alignItems="center" paddingLeft="5px">
|
||||
<Typography variant="caption">
|
||||
{new Date(obj.timestamp).toLocaleDateString('en-US')}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
{new Date(obj.timestamp).toLocaleTimeString('en-US')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ background: background(idx) }}>
|
||||
<Box display="flex" justifyContent="center" alignItems="center">
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
background: Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0
|
||||
? theme.colors.feedback.warning
|
||||
: theme.colors.feedback.success,
|
||||
borderRadius: "100%",
|
||||
boxShadow: "0px 0px 1px black"
|
||||
}}
|
||||
>
|
||||
{Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0 ?
|
||||
<GhostStyledIcon
|
||||
sx={{ width: "15px", height: "15px" }}
|
||||
viewBox="0 0 25 25"
|
||||
component={HourglassBottomIcon}
|
||||
/>
|
||||
:
|
||||
<GhostStyledIcon
|
||||
sx={{ width: "15px", height: "15px" }}
|
||||
viewBox="0 0 25 25"
|
||||
component={CheckIcon}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -1,144 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { Box, Paper, Grid, Typography, LinearProgress, useTheme } from "@mui/material"
|
||||
|
||||
import Metric from "../../components/Metric/Metric";
|
||||
import Countdown from "../../components/Countdown/Countdown";
|
||||
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
||||
import { formatNumber } from "../../helpers";
|
||||
|
||||
export const BridgeHeader = ({
|
||||
totalValidators,
|
||||
disabledValidators,
|
||||
bridgeStability,
|
||||
transactionEta,
|
||||
timeToNextEpoch,
|
||||
maxDelay,
|
||||
isSmallScreen
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const disabledPercentage = useMemo(() => {
|
||||
if (totalValidators === undefined || disabledValidators === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return ((totalValidators - disabledValidators) / totalValidators) * 100;
|
||||
}, [totalValidators, disabledValidators]);
|
||||
|
||||
const validatorsColor = useMemo(() => {
|
||||
if (disabledPercentage < 50) {
|
||||
return theme.colors.validatorsColor.red;
|
||||
}
|
||||
return theme.colors.validatorsColor.green;
|
||||
}, [disabledPercentage, theme]);
|
||||
|
||||
const stabilityColor = useMemo(() => {
|
||||
if (bridgeStability > 80) {
|
||||
return theme.colors.bridgeProgress.success;
|
||||
} else if (bridgeStability > 50) {
|
||||
return theme.colors.bridgeProgress.warning;
|
||||
} else {
|
||||
return theme.colors.bridgeProgress.error;
|
||||
}
|
||||
}, [bridgeStability, theme]);
|
||||
|
||||
const progressBarPostfix = useMemo(() => {
|
||||
if (bridgeStability > 90) {
|
||||
return "✅ Safe";
|
||||
} else if (bridgeStability > 80) {
|
||||
return "✅ Moderate Risk";
|
||||
} else if (bridgeStability > 70) {
|
||||
return "⚠️ High Risk";
|
||||
} else if (bridgeStability > 50) {
|
||||
return "⚠️ Critical Risk";
|
||||
} else {
|
||||
return "❌ Do NOT Bridge";
|
||||
}
|
||||
}, [bridgeStability]);
|
||||
|
||||
const formatTime = (totalSeconds) => {
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const secs = Math.floor(totalSeconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours} hours ${minutes} mins`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes} mins`;
|
||||
} else {
|
||||
return `${secs} secs`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={isSmallScreen ? 4 : 1}>
|
||||
<Grid item xs={isSmallScreen ? 12 : 4}>
|
||||
<Metric
|
||||
isLoading={totalValidators === undefined || disabledValidators === undefined}
|
||||
metric={
|
||||
<Typography color={validatorsColor} fontSize="24px" fontWeight="700" lineHeight="33px">
|
||||
{totalValidators} ({formatNumber(disabledPercentage, 0)}% active)
|
||||
</Typography>
|
||||
}
|
||||
label="Total Validators"
|
||||
tooltip="Active and disabled GHOST Validators in the current GHOST Epoch."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={isSmallScreen ? 12 : 4}>
|
||||
<Metric
|
||||
isLoading={timeToNextEpoch === undefined}
|
||||
metric={timeToNextEpoch < 30 ? "Rotating..." : formatTime(timeToNextEpoch)}
|
||||
label="Rotation in"
|
||||
tooltip="Bridge Stability Index refreshes every 10 minutes with new validator blocks; resets each Era when the validator set is updated."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={isSmallScreen ? 12 : 4}>
|
||||
<Metric
|
||||
isLoading={transactionEta === undefined}
|
||||
metric={transactionEta > maxDelay ? "∞" : formatTime(transactionEta)}
|
||||
label="Max Bridge ETA"
|
||||
tooltip="Maximum estimated time for finalizing bridge transactions based on the latest update."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item gap={2} xs={12} style={{ paddingTop: "0" }} >
|
||||
<Box position="relative" margin="4.5px 0">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={bridgeStability ?? 0}
|
||||
sx={{
|
||||
borderRadius: "5px",
|
||||
height: "40px",
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: stabilityColor,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
|
||||
Bridge Stability {bridgeStability
|
||||
? `${formatNumber(bridgeStability, 0)}% ${progressBarPostfix}`
|
||||
: "N/A"
|
||||
}
|
||||
</Typography>
|
||||
<InfoTooltip message={
|
||||
<Box width="220px">
|
||||
<Typography>0% - 50%: ❌ Do NOT Bridge</Typography>
|
||||
<Typography>50% - 70%: ⚠️ Critical Risk</Typography>
|
||||
<Typography>70% - 80%: ⚠️ High Risk</Typography>
|
||||
<Typography>80% - 90%: ✅ Moderate Risk</Typography>
|
||||
<Typography>90% - 100%: ✅ Safe</Typography>
|
||||
</Box>
|
||||
} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@ -1,508 +0,0 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
import { Box, Typography, Link, FormControlLabel, Checkbox, useTheme } from "@mui/material";
|
||||
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
|
||||
|
||||
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
||||
import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import AssuredWorkloadIcon from '@mui/icons-material/AssuredWorkload';
|
||||
import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
|
||||
|
||||
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
|
||||
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
|
||||
import HandshakeIcon from '@mui/icons-material/Handshake';
|
||||
import PendingIcon from '@mui/icons-material/Pending';
|
||||
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, TertiaryButton, SecondaryButton } from "../../components/Button";
|
||||
|
||||
import { formatCurrency } from "../../helpers";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
|
||||
|
||||
export const BridgeModal = ({
|
||||
providerDetail,
|
||||
currentRecord,
|
||||
activeTxIndex,
|
||||
setActiveTxIndex,
|
||||
authorities,
|
||||
ghstSymbol,
|
||||
hashedArguments,
|
||||
chainExplorerUrl,
|
||||
removeStoredRecord,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [copiedIndex, setCopiedIndex] = useState(null);
|
||||
|
||||
const sliceString = (string, first, second) => {
|
||||
if (!string) return "";
|
||||
return string.slice(0, first) + "..." + string.slice(second);
|
||||
}
|
||||
|
||||
const copyToClipboard = (text, index) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedIndex(index);
|
||||
setTimeout(() => setCopiedIndex(null) , 800);
|
||||
});
|
||||
};
|
||||
|
||||
const bridgeNumbers = () => {
|
||||
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||
const number = 1 + connectedNetworks * 3;
|
||||
return `(${number}, ${number})`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
data-testid="transaction-details-modal"
|
||||
maxWidth="476px"
|
||||
headerContent={
|
||||
<Box display="flex" flexDirection="row">
|
||||
<Typography variant="h5">
|
||||
TX Hash
|
||||
<Link
|
||||
sx={{
|
||||
margin: "0px",
|
||||
font: "inherit",
|
||||
letterSpacing: "inherit",
|
||||
textDecoration: "underline",
|
||||
color: theme.colors.gray[10],
|
||||
textUnderlineOffset: "0.23rem",
|
||||
cursor: "pointer",
|
||||
textDecorationThickness: "3px",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
}
|
||||
}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={currentRecord
|
||||
? `${chainExplorerUrl}/tx/${currentRecord ? currentRecord.transactionHash : ""}`
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : ""}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
open={activeTxIndex >= 0}
|
||||
onClose={() => setActiveTxIndex(-1)}
|
||||
minHeight={"100px"}
|
||||
>
|
||||
<Box display="flex" gap="1.5rem" flexDirection="column" marginTop=".8rem">
|
||||
{!providerDetail && <Box
|
||||
width="90%"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
backgroundColor="#1f4771"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '130px',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<SecondaryButton
|
||||
fullWidth
|
||||
sx={{
|
||||
marginTop: "0 !important",
|
||||
marginBottom: "0 !important"
|
||||
}}
|
||||
onClick={() => window.open(
|
||||
'https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases',
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)}
|
||||
>
|
||||
Get GHOST Connect
|
||||
</SecondaryButton>
|
||||
</Box>}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ filter: providerDetail ? '' : 'blur(5px)' }}
|
||||
>
|
||||
{currentRecord?.finalization > 0 && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
transition: "all 0.8s ease",
|
||||
transform: currentRecord?.finalization > 0 && "scale(1.2)",
|
||||
color: currentRecord?.finalization === 0 && theme.colors.primary[300]
|
||||
}}
|
||||
width="120px"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<GhostStyledIcon
|
||||
sx={{
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
animation: currentRecord?.finalization > 0 && 'rotateHourGlass 2s ease-in-out infinite',
|
||||
'@keyframes rotateHourGlass': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'15%': { transform: 'rotate(0deg)' },
|
||||
'85%': { transform: 'rotate(180deg)' },
|
||||
'100%': { transform: 'rotate(180deg)' },
|
||||
},
|
||||
}}
|
||||
viewBox="0 0 25 25"
|
||||
component={HourglassBottomIcon}
|
||||
/>
|
||||
<Typography variant="caption">Finalization</Typography>
|
||||
<Typography variant="caption">
|
||||
{(currentRecord?.finalization ?? 0).toString()} blocks left
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<GhostStyledIcon
|
||||
sx={{
|
||||
transition: "all 0.3s ease",
|
||||
opacity: currentRecord?.finalization > 0 && "0.2"
|
||||
}}
|
||||
component={ArrowRightIcon}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
transition: "all 0.3s ease",
|
||||
opacity: currentRecord?.finalization > 0 && "0.2",
|
||||
transform: currentRecord?.finalization === 0 && currentRecord?.clappedPercentage < 50n && "scale(1.2)",
|
||||
color: currentRecord?.clappedPercentage > 50n && theme.colors.primary[300]
|
||||
}}
|
||||
width="120px"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center">
|
||||
{currentRecord?.clappedPercentage < 50n
|
||||
? <GhostStyledIcon
|
||||
sx={{
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
animation: currentRecord?.finalization === 0 && 'bounce 2s ease-in-out infinite',
|
||||
'@keyframes bounce': {
|
||||
'0%, 20%, 50%, 80%, 75%, 100%': { transform: 'translateY(0)' },
|
||||
'40%': { transform: 'translateY(-4px)' },
|
||||
'60%': { transform: 'translateY(-2px)' },
|
||||
},
|
||||
}}
|
||||
viewBox="0 0 25 25"
|
||||
component={AccountBalanceIcon}
|
||||
/>
|
||||
: <GhostStyledIcon sx={{ width: "35px", height: "35px" }} viewBox="0 0 25 25" component={AssuredWorkloadIcon} />
|
||||
}
|
||||
<Typography variant="caption">Capital Backed</Typography>
|
||||
<Typography variant="caption">
|
||||
{(currentRecord?.clappedAmount ?? 0n) / 10n**18n} {ghstSymbol} ({currentRecord?.clappedPercentage ?? 0}%)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<GhostStyledIcon
|
||||
sx={{ transition: "all 0.3s ease", opacity: currentRecord?.clappedPercentage < 50n && "0.2" }}
|
||||
component={ArrowRightIcon}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
transition: "all 0.3s ease",
|
||||
opacity: currentRecord?.finalization > 0 && "0.2",
|
||||
transform: currentRecord?.finalization === 0 && currentRecord?.clapsPercentage < 50 && "scale(1.2)",
|
||||
color: currentRecord?.clapsPercentage > 50 && theme.colors.primary[300]
|
||||
}}
|
||||
width="120px"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center">
|
||||
{currentRecord?.clapsPercentage < 50
|
||||
? (
|
||||
<>
|
||||
<GhostStyledIcon
|
||||
sx={{
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
animation: currentRecord?.finalization === 0 && 'rotateRightHand 2s ease-in-out infinite',
|
||||
'@keyframes rotateRightHand': {
|
||||
'0%': { transform: 'rotateX(360deg)' },
|
||||
'15%': { transform: 'rotateX(360deg)' },
|
||||
'50%': { transform: 'rotateX(180deg)' },
|
||||
'85%': { transform: 'rotateX(0deg)' },
|
||||
'100%': { transform: 'rotateX(0deg)' },
|
||||
},
|
||||
}}
|
||||
viewBox="0 0 25 25"
|
||||
component={ThumbUpIcon}
|
||||
/>
|
||||
<GhostStyledIcon
|
||||
sx={{
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
animation: currentRecord?.finalization === 0 && 'rotateRightHand 2s ease-in-out infinite',
|
||||
'@keyframes rotateRightHand': {
|
||||
'0%': { transform: 'rotateX(0deg)' },
|
||||
'15%': { transform: 'rotateX(0deg)' },
|
||||
'50%': { transform: 'rotateX(180deg)' },
|
||||
'85%': { transform: 'rotateX(360deg)' },
|
||||
'100%': { transform: 'rotateX(360deg)' },
|
||||
},
|
||||
}}
|
||||
viewBox="0 0 25 25"
|
||||
component={ThumbDownAltIcon}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<GhostStyledIcon
|
||||
sx={{
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
}}
|
||||
viewBox="0 0 25 25"
|
||||
component={HandshakeIcon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center">
|
||||
<Typography variant="caption">Slow Claps</Typography>
|
||||
<Typography variant="caption">{currentRecord?.numberOfClaps ?? 0} / {authorities?.length ?? 0}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{currentRecord?.finalization === 0 && (
|
||||
<>
|
||||
<GhostStyledIcon
|
||||
sx={{ transition: "all 0.3s ease", opacity: !currentRecord?.applaused && "0.2" }}
|
||||
component={ArrowRightIcon}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
transition: "all 0.3s ease",
|
||||
opacity: !currentRecord?.applaused && "0.2",
|
||||
transform: currentRecord?.applaused && "scale(1.2)",
|
||||
color: currentRecord?.applaused && theme.colors.primary[300]
|
||||
}}
|
||||
width="120px"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="start"
|
||||
alignItems="center"
|
||||
>
|
||||
<GhostStyledIcon
|
||||
sx={{ width: "35px", height: "35px" }}
|
||||
viewBox="0 0 25 25"
|
||||
component={CheckCircleIcon}
|
||||
/>
|
||||
<Typography variant="caption">Applaused</Typography>
|
||||
<Typography variant="caption">Check Receiver</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap="5px" padding="0.6rem 0">
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||
<Typography variant="body2">GHOST Epoch:</Typography>
|
||||
<Typography variant="body2">{currentRecord?.sessionIndex}</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||
<Typography variant="body2">Accepted Bridge Risk:</Typography>
|
||||
<Typography variant="body2">{currentRecord?.bridgeStability}%</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||
<Box display="flex" flexDirection="row">
|
||||
<Typography variant="body2">Arguments Hash:</Typography>
|
||||
<InfoTooltip message="A unique identifier for transaction parameters, represented as a hash generated by keccak256(receiver, amount, blockNumber, chainId)." />
|
||||
</Box>
|
||||
<Link
|
||||
style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center" }}
|
||||
onClick={() => copyToClipboard(hashedArguments ? hashedArguments : "", 2)}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
{hashedArguments ? sliceString(hashedArguments, 10, -8) : "0x"}
|
||||
</Typography>
|
||||
<GhostStyledIcon
|
||||
sx={{ marginLeft: "5px", width: "12px", height: "12px" }}
|
||||
viewBox="0 0 25 25"
|
||||
component={copiedIndex === 2 ? CheckIcon : ContentPasteIcon}
|
||||
/>
|
||||
</Link>
|
||||
</Box>
|
||||
<hr style={{ width: "100%" }} />
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography variant="body2">Receiver Address:</Typography>
|
||||
<Link
|
||||
style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center" }}
|
||||
onClick={() => copyToClipboard(currentRecord ? currentRecord.receiverAddress : "", 0)}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
{currentRecord ? sliceString(currentRecord.receiverAddress, 14, -5) : ""}
|
||||
</Typography>
|
||||
<GhostStyledIcon
|
||||
sx={{ marginLeft: "5px", width: "12px", height: "12px" }}
|
||||
viewBox="0 0 25 25"
|
||||
component={copiedIndex === 0 ? CheckIcon : ContentPasteIcon}
|
||||
/>
|
||||
</Link>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||
<Typography variant="body2">Bridged Amount:</Typography>
|
||||
<Typography variant="body2">{formatCurrency(
|
||||
new DecimalBigNumber(
|
||||
BigInt(currentRecord ? currentRecord.amount : "0"),
|
||||
18
|
||||
).toString(), 9, ghstSymbol)
|
||||
}</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||
<Box display="flex" flexDirection="row">
|
||||
<Typography variant="body2">Executed at:</Typography>
|
||||
<InfoTooltip message="The timestamp when the bridge transaction was submitted." />
|
||||
</Box>
|
||||
<Typography variant="body2">{
|
||||
new Date(currentRecord ? currentRecord.timestamp : 0).toLocaleString('en-US')
|
||||
}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap="5px">
|
||||
{currentRecord && currentRecord.finalization < 1 && currentRecord.applaused && <PrimaryButton
|
||||
fullWidth
|
||||
loading={false}
|
||||
onClick={() => removeStoredRecord()}
|
||||
>
|
||||
{`Get ${bridgeNumbers()} Stake\u00B2`}
|
||||
</PrimaryButton>}
|
||||
|
||||
<TertiaryButton
|
||||
fullWidth
|
||||
loading={false}
|
||||
onClick={() => removeStoredRecord()}
|
||||
>
|
||||
Erase Record
|
||||
</TertiaryButton>
|
||||
|
||||
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
|
||||
This will permanently remove the bridge transaction record from the session storage, but it will not cancel the bridge transaction.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export const BridgeConfirmModal = ({
|
||||
bridgeStability,
|
||||
isOpen,
|
||||
setClose,
|
||||
handleButtonProceed
|
||||
}) => {
|
||||
const [isBridgingRiskChecked, setIsBridgingRiskChecked] = useState(false);
|
||||
const [isBridgingRecipientChecked, setIsBridgingRecipientChecked] = useState(false);
|
||||
|
||||
const handleProceed = () => {
|
||||
setIsBridgingRiskChecked(false);
|
||||
setIsBridgingRecipientChecked(false);
|
||||
handleButtonProceed();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
maxWidth="450px"
|
||||
minHeight="150px"
|
||||
headerText="Bridge Confirmation"
|
||||
open={isOpen}
|
||||
onClose={setClose}
|
||||
>
|
||||
<Box gap="20px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
|
||||
<Box width="100%" display="flex" flexDirection="column" alignItems="start">
|
||||
<Box>
|
||||
<Typography variant="subtitle1">
|
||||
You are bridging to GHOST Chain. We will guide you towards (10, 10) Stake<sup>2</sup> rewards right after that.
|
||||
</Typography>
|
||||
</Box>
|
||||
<hr style={{ margin: "10px 0", width: "100%" }} />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
data-testid="acknowledge-bridge-stability"
|
||||
checked={isBridgingRiskChecked}
|
||||
onChange={event => setIsBridgingRiskChecked(event.target.checked)}
|
||||
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
|
||||
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<span>
|
||||
{`I acknowledge bridging risk at ${bridgeStability}%.`}
|
||||
<Link
|
||||
sx={{
|
||||
margin: "0px",
|
||||
font: "inherit",
|
||||
letterSpacing: "inherit",
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: "0.23rem",
|
||||
cursor: "pointer",
|
||||
textDecorationThickness: "1px",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
}
|
||||
}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://ghostchain.io/bridge-disclaimer"
|
||||
>
|
||||
Learn more.
|
||||
</Link>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<hr style={{ margin: "10px 0", width: "100%" }} />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
data-testid="acknowledge-bridge-stability"
|
||||
checked={isBridgingRecipientChecked}
|
||||
onChange={event => setIsBridgingRecipientChecked(event.target.checked)}
|
||||
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
|
||||
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
|
||||
/>
|
||||
}
|
||||
label="I confirm that recipient address is a self-custodial wallet, not an exchange, third party service, or smart-contract."
|
||||
sx={{ '& .MuiFormControlLabel-label': { textAlign: "justify" } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<PrimaryButton fullWidth disabled={!isBridgingRiskChecked || !isBridgingRecipientChecked} onClick={handleProceed}>
|
||||
Proceed Bridge
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
import React from "react";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
|
||||
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||
import Token from "../../components/Token/Token";
|
||||
|
||||
export const BridgeRoute = ({ coinName, chainTokenName, tokens }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box width="100%" display="flex" alignItems="center" flexDirection="row" flexWrap="wrap">
|
||||
<Typography>Route:</Typography>
|
||||
<Box display="flex" marginLeft="20px" gap="5px" alignItems="center">
|
||||
{tokens?.map((token, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<RouteHop key={index} theme={theme} token={token} chainTokenName={chainTokenName} />
|
||||
<GhostStyledIcon sx={{ width: "12px", height: "12px" }} component={ArrowForwardIosIcon} />
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<RouteHop theme={theme} token={coinName} />
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const RouteHop = ({ theme, token, chainTokenName, arrowNeeded }) => {
|
||||
return (
|
||||
<Box
|
||||
display="inline-flex"
|
||||
sx={{ backgroundColor: theme.colors.gray[600] }}
|
||||
borderRadius="6px"
|
||||
paddingX="9px"
|
||||
paddingY="6.5px"
|
||||
alignItems="center"
|
||||
>
|
||||
<Token chainTokenName={chainTokenName} name={token} sx={{ fontSize: "21px" }} />
|
||||
<Typography fontSize="15px" lineHeight="24px" marginLeft="9px">
|
||||
{token}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -1,331 +0,0 @@
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
Link,
|
||||
LinearProgress,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
|
||||
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
||||
import { PrimaryButton } from "../../components/Button";
|
||||
|
||||
export const ValidatorTable = ({
|
||||
currentTime,
|
||||
currentBlock,
|
||||
latestCommits,
|
||||
isVerySmallScreen,
|
||||
bridgeStability,
|
||||
providerDetail,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [sortedBy, setsortedBy] = useState(undefined);
|
||||
|
||||
const stabilityColor = useMemo(() => {
|
||||
const red = Math.round(255 * (1 - bridgeStability / 100));
|
||||
const green = Math.round(255 * (bridgeStability / 100));
|
||||
return `rgb(${red}, ${green}, 0)`;
|
||||
}, [bridgeStability]);
|
||||
|
||||
const sortedCommits = useMemo(() => {
|
||||
return latestCommits?.sort((a, b) => {
|
||||
let one = 0;
|
||||
let two = 0;
|
||||
switch (sortedBy) {
|
||||
case -1:
|
||||
one = Number(b.lastUpdated ?? 0n);
|
||||
two = Number(a.lastUpdated ?? 0n);
|
||||
break;
|
||||
case 1:
|
||||
one = Number(a.lastUpdated ?? 0n);
|
||||
two = Number(b.lastUpdated ?? 0n);
|
||||
break;
|
||||
case -2:
|
||||
one = Number(b.lastStoredBlock ?? 0n);
|
||||
two = Number(a.lastStoredBlock ?? 0n);
|
||||
break;
|
||||
case 2:
|
||||
one = Number(a.lastStoredBlock ?? 0n);
|
||||
two = Number(b.lastStoredBlock ?? 0n);
|
||||
break;
|
||||
case -3:
|
||||
one = Number(b.lastStoredBlock ?? 0n);
|
||||
two = Number(a.lastStoredBlock ?? 0n);
|
||||
break;
|
||||
case 3:
|
||||
one = Number(a.lastStoredBlock ?? 0n);
|
||||
two = Number(b.lastStoredBlock ?? 0n);
|
||||
break;
|
||||
case -4:
|
||||
one = b.disabled ? 1 : 0;
|
||||
two = a.disabled ? 1 : 0;
|
||||
break;
|
||||
case 4:
|
||||
one = a.disabled ? 1 : 0;
|
||||
two = b.disabled ? 1 : 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return one - two;
|
||||
}) ?? [];
|
||||
}, [latestCommits, sortedBy]);
|
||||
|
||||
const changeSort = (prevValue, newValue) => {
|
||||
if (prevValue === undefined) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
const prevValueAbs = Math.abs(prevValue);
|
||||
const newValueAbs = Math.abs(newValue);
|
||||
|
||||
if (prevValueAbs === newValueAbs) {
|
||||
return prevValue * -1;
|
||||
} else {
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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 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 Connect
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>}
|
||||
{providerDetail && <TableContainer
|
||||
component={Paper}
|
||||
className="custom-scrollbar"
|
||||
sx={{
|
||||
height: "310px",
|
||||
overflowY: 'scroll',
|
||||
msOverflowStyle: "thin !important",
|
||||
scrollbarWidth: "thin !important",
|
||||
}}
|
||||
>
|
||||
<Table sx={{ marginTop: "0px" }} stickyHeader aria-label="sticky available validators">
|
||||
<TableHead>
|
||||
<TableRow sx={{ height: "40px" }}>
|
||||
<BridgeHeaderTableCell index={0} changeSort={() => setsortedBy(undefined)} value="Validator" background={theme.colors.paper.cardHover} borderTopLeftRadius="3px" />
|
||||
<BridgeHeaderTableCell index={1} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 1))} value="Last Acitve" background={theme.colors.paper.cardHover} tooltip="GHOST Validators must submit block commitments every 10 minutes to remain active." />
|
||||
<BridgeHeaderTableCell index={2} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 2))} value="Block Height" background={theme.colors.paper.cardHover} tooltip="The latest EVM block height reported by the Validator." />
|
||||
<BridgeHeaderTableCell index={3} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 3))} value="Block Delayed" background={theme.colors.paper.cardHover} tooltip="Block delays under 4 hours are safe. Block delays over 4 hours risk bridge transaction failure." />
|
||||
<BridgeHeaderTableCell index={4} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 4))} value="Status" background={theme.colors.paper.cardHover} borderTopRightRadius="3px" tooltip="Active and disabled validators fore the current GHOST Epoch." />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{latestCommits?.map((commit, index) => {
|
||||
return (
|
||||
<ValidatorRow
|
||||
key={index}
|
||||
colors={theme.colors}
|
||||
currentTime={currentTime}
|
||||
currentBlock={currentBlock}
|
||||
index={index}
|
||||
commit={commit}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>}
|
||||
<Box marginTop="5px" display="flex" flexDirection="row" justifyContent="center" alignItems="center">
|
||||
<Link
|
||||
underline="always"
|
||||
fontSize="12px"
|
||||
lineHeight="15px"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://telemetry.ghostchain.io"
|
||||
>
|
||||
See Validator Telemetry
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const BridgeHeaderTableCell = ({
|
||||
index=0,
|
||||
sortedBy=undefined,
|
||||
changeSort,
|
||||
align="center",
|
||||
borderTopRightRadius="0px",
|
||||
borderTopLeftRadius="0px",
|
||||
borderBottomRightRadius="0px",
|
||||
borderBottomLeftRadius="0px",
|
||||
background="transparent",
|
||||
fontSize="12px",
|
||||
padding="0px",
|
||||
tooltip,
|
||||
value
|
||||
}) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={align}
|
||||
style={{
|
||||
borderTopRightRadius,
|
||||
borderTopLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
background,
|
||||
fontSize,
|
||||
padding,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Box onClick={changeSort} display="flex" justifyContent="center" alignItems="center">
|
||||
{value}
|
||||
{tooltip
|
||||
? Math.abs(sortedBy) !== index
|
||||
? <InfoTooltip message={tooltip} />
|
||||
: <GhostStyledIcon
|
||||
sx={{ width: "12px", height: "12px" }}
|
||||
viewBox="0 0 25 25"
|
||||
component={sortedBy > 0 ? KeyboardArrowDownIcon : KeyboardArrowUpIcon}
|
||||
/>
|
||||
: <></>
|
||||
}
|
||||
</Box>
|
||||
</TableCell>
|
||||
)
|
||||
}
|
||||
|
||||
const BridgeTableCell = ({
|
||||
align="center",
|
||||
borderTopRightRadius="0px",
|
||||
borderTopLeftRadius="0px",
|
||||
borderBottomRightRadius="0px",
|
||||
borderBottomLeftRadius="0px",
|
||||
background="transparent",
|
||||
fontSize="10px",
|
||||
padding="0px",
|
||||
value
|
||||
}) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={align}
|
||||
style={{
|
||||
borderTopRightRadius,
|
||||
borderTopLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
background,
|
||||
fontSize,
|
||||
padding,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</TableCell>
|
||||
)
|
||||
}
|
||||
|
||||
const ValidatorRow = ({
|
||||
colors,
|
||||
index,
|
||||
currentTime,
|
||||
currentBlock,
|
||||
commit,
|
||||
}) => {
|
||||
const background = index % 2 === 1 ? "" : colors.gray[750];
|
||||
return (
|
||||
<TableRow
|
||||
sx={{ height: "30px" }}
|
||||
id={index + `--vaidator`}
|
||||
data-testid={index + `--vaidator`}
|
||||
>
|
||||
<BridgeTableCell align="center" value={sliceString(commit.validator, 10, 45)} background={background} />
|
||||
<BridgeTableCell align="center" value={getTimeAgo(currentTime - (Number(commit?.lastUpdated) ?? 0))} background={background} />
|
||||
<BridgeTableCell align="center" value={(commit?.lastStoredBlock ?? 0n).toLocaleString('en-US')} background={background} />
|
||||
<BridgeTableCell align="center" value={blockDelayIcon(colors, currentBlock - (commit?.lastStoredBlock ?? 0n))} background={background} />
|
||||
<BridgeTableCell align="center" value={statusIcon(colors, commit?.disabled)} background={background} />
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
const sliceString = (string, first, second) => {
|
||||
if (!string) return "";
|
||||
return string.slice(0, first) + "..." + string.slice(second);
|
||||
}
|
||||
|
||||
const getTimeAgo = (timestampDiff) => {
|
||||
const seconds = Math.floor(timestampDiff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
|
||||
return "long ago";
|
||||
}
|
||||
|
||||
const blockDelayIcon = (colors, timestampDiff) => {
|
||||
let color = colors.feedback.error;
|
||||
let icon = CancelIcon;
|
||||
|
||||
if (timestampDiff < 900000n) {
|
||||
color = colors.feedback.success;
|
||||
icon = CheckCircleIcon;
|
||||
} else if (timestampDiff < 2700000n) {
|
||||
color = colors.feedback.warning;
|
||||
icon = WarningIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<GhostStyledIcon
|
||||
sx={{
|
||||
marginLeft: "5px",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
fill: color
|
||||
}}
|
||||
viewBox="0 0 25 25"
|
||||
component={icon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const statusIcon = (colors, disabled) => {
|
||||
let color = colors.feedback.success;
|
||||
let icon = CheckCircleIcon;
|
||||
|
||||
if (disabled === true) {
|
||||
color = colors.feedback.error;
|
||||
icon = CancelIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<GhostStyledIcon
|
||||
sx={{
|
||||
marginLeft: "5px",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
fill: color
|
||||
}}
|
||||
viewBox="0 0 25 25"
|
||||
component={icon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -10,10 +10,9 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { useEffect, useMemo, useState, useCallback } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useLocation, useSearchParams } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { isAddress } from "viem";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
||||
@ -27,25 +26,20 @@ import { Tab, Tabs } from "../../components/Tabs/Tabs";
|
||||
import {
|
||||
UNISWAP_V2_ROUTER,
|
||||
UNISWAP_V2_FACTORY,
|
||||
RESERVE_ADDRESSES,
|
||||
DAI_ADDRESSES,
|
||||
FTSO_ADDRESSES,
|
||||
EMPTY_ADDRESS,
|
||||
WETH_ADDRESSES,
|
||||
} from "../../constants/addresses";
|
||||
import { useLocalStorage } from "../../hooks/localstorage";
|
||||
import { useTokenSymbol } from "../../hooks/tokens";
|
||||
import { getTokenAddress } from "../../hooks/helpers";
|
||||
|
||||
import PoolContainer from "./PoolContainer";
|
||||
import SwapContainer from "./SwapContainer";
|
||||
import TokenModal from "./TokenModal";
|
||||
|
||||
const Dex = ({ chainId, address, connect, config }) => {
|
||||
const Dex = ({ chainId, address, connect }) => {
|
||||
const location = useLocation();
|
||||
const pathname = useParams();
|
||||
|
||||
const theme = useTheme();
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||
@ -58,50 +52,15 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
const [topTokenListOpen, setTopTokenListOpen] = useState(false);
|
||||
const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false);
|
||||
|
||||
const [secondsToWait, setSecondsToWait] = useState(() => getStorageValue(chainId, address, "dex-deadline", "60"));
|
||||
const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "dex-slippage", "5"));
|
||||
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "dex-decimals", "5"));
|
||||
const [actualDestinationAddress, setActualDestinationAddress] = useState(() => getStorageValue(chainId, address, "dex-destination", address));
|
||||
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
|
||||
const [secondsToWait, setSecondsToWait] = useState(localStorage.getItem("dex-deadline") || "60");
|
||||
const [slippage, setSlippage] = useState(localStorage.getItem("dex-slippage") || "5");
|
||||
const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("dex-decimals") || "5");
|
||||
|
||||
const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS);
|
||||
const [tokenAddressTop, setTokenAddressTop] = useState(DAI_ADDRESSES[chainId]);
|
||||
const [tokenAddressBottom, setTokenAddressBottom] = useState(FTSO_ADDRESSES[chainId]);
|
||||
|
||||
const { symbol: tokenNameTopInner } = useTokenSymbol(chainId, tokenAddressTop);
|
||||
const { symbol: tokenNameBottomInner } = useTokenSymbol(chainId, tokenAddressBottom);
|
||||
|
||||
const chainSymbol = useMemo(() => {
|
||||
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
|
||||
if (chainSymbol) return chainSymbol;
|
||||
return "WTF";
|
||||
}, [config])
|
||||
|
||||
const tokenNameTop = useMemo(() => {
|
||||
if (chainSymbol && tokenAddressTop === EMPTY_ADDRESS) {
|
||||
return chainSymbol;
|
||||
}
|
||||
return tokenNameTopInner;
|
||||
}, [tokenAddressTop, tokenNameTopInner, chainSymbol]);
|
||||
|
||||
const tokenNameBottom = useMemo(() => {
|
||||
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
|
||||
if (chainSymbol && tokenAddressBottom === EMPTY_ADDRESS) {
|
||||
return chainSymbol;
|
||||
}
|
||||
return tokenNameBottomInner;
|
||||
}, [tokenAddressBottom, tokenNameBottomInner, config]);
|
||||
|
||||
const isWrapping = useMemo(() => {
|
||||
const isNative = tokenAddressTop === EMPTY_ADDRESS;
|
||||
const isWrappedNative = tokenAddressBottom === WETH_ADDRESSES[chainId];
|
||||
return isNative && isWrappedNative;
|
||||
}, [chainId, tokenAddressTop, tokenAddressBottom]);
|
||||
|
||||
const isUnwrapping = useMemo(() => {
|
||||
const isWrappedNative = tokenAddressTop === WETH_ADDRESSES[chainId];
|
||||
const isNative = tokenAddressBottom === EMPTY_ADDRESS;
|
||||
return isNative && isWrappedNative;
|
||||
}, [chainId, tokenAddressTop, tokenAddressBottom]);
|
||||
const { symbol: tokenNameTop } = useTokenSymbol(chainId, tokenAddressTop);
|
||||
const { symbol: tokenNameBottom } = useTokenSymbol(chainId, tokenAddressBottom);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentQueryParameters.has("pool")) {
|
||||
@ -116,8 +75,8 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
setTokenAddressTop(currentQueryParameters.get("from"));
|
||||
newQueryParameters.set("from", currentQueryParameters.get("from"));
|
||||
} else {
|
||||
setTokenAddressTop(EMPTY_ADDRESS);
|
||||
newQueryParameters.set("from", EMPTY_ADDRESS);
|
||||
setTokenAddressTop(DAI_ADDRESSES[chainId]);
|
||||
newQueryParameters.set("from", DAI_ADDRESSES[chainId]);
|
||||
}
|
||||
|
||||
if (currentQueryParameters.has("to")) {
|
||||
@ -133,7 +92,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.send({ hitType: "pageview", page: location.pathname + location.search });
|
||||
}, [location]);
|
||||
}, [location])
|
||||
|
||||
const dexAddresses = {
|
||||
router: UNISWAP_V2_ROUTER[chainId],
|
||||
@ -154,7 +113,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
}
|
||||
|
||||
const changeSwapTab = (swap) => {
|
||||
if (swap || (isWrapping || isUnwrapping)) newQueryParameters.delete("pool");
|
||||
if (swap) newQueryParameters.delete("pool");
|
||||
else newQueryParameters.set("pool", true);
|
||||
newQueryParameters.set("from", currentQueryParameters.get("from"));
|
||||
newQueryParameters.set("to", currentQueryParameters.get("to"));
|
||||
@ -183,38 +142,24 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
setSearchParams(newQueryParameters);
|
||||
}
|
||||
|
||||
const setSlippageInner = useCallback((value) => {
|
||||
const setSlippageInner = (value) => {
|
||||
const maybeValue = parseFloat(value);
|
||||
if (!maybeValue || parseFloat(value) <= 100) {
|
||||
setSlippage(value);
|
||||
setStorageValue(chainId, address, "dex-slippage", value);
|
||||
localStorage.setItem("dex-slippage", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
}
|
||||
|
||||
const setSecondsToWaitInner = useCallback((value) => {
|
||||
const setSecondsToWaitInner = (value) => {
|
||||
localStorage.setItem("dex-deadline", value);
|
||||
setSecondsToWait(value);
|
||||
setStorageValue(chainId, address, "dex-deadline", value);
|
||||
}, [chainId, address]);
|
||||
}
|
||||
|
||||
const setFormatDecimalsInner = useCallback((value) => {
|
||||
const setFormatDecimalsInner = (value) => {
|
||||
if (Number(value) <= 17) {
|
||||
localStorage.setItem("dex-decimals", value);
|
||||
setFormatDecimals(value);
|
||||
setStorageValue(chainId, address, "dex-decimals", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
|
||||
const setDestinationAddressInner = useCallback((value) => {
|
||||
const cleanedValue = value.trim();
|
||||
setDestinationAddress(value);
|
||||
if (isAddress(cleanedValue)) {
|
||||
setActualDestinationAddress(value);
|
||||
setStorageValue(chainId, address, "dex-destination", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
|
||||
const handleCloseSetting = () => {
|
||||
setDestinationAddress(undefined);
|
||||
handleSettingsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -241,7 +186,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
|
||||
<PageTitle
|
||||
name={`${pathname.name.charAt(0).toUpperCase() + pathname.name.slice(1).toLowerCase()} V2 Classic`}
|
||||
subtitle="Swap via Uniswap V2 Fork"
|
||||
subtitle="Classic interface to V2 smart contracts."
|
||||
/>
|
||||
<Container
|
||||
style={{
|
||||
@ -254,11 +199,11 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
}}
|
||||
>
|
||||
<Modal
|
||||
maxWidth="450px"
|
||||
maxWidth="376px"
|
||||
minHeight="200px"
|
||||
open={settingsOpen}
|
||||
headerText={"Settings"}
|
||||
onClose={() => handleCloseSetting()}
|
||||
onClose={() => handleSettingsOpen(false)}
|
||||
>
|
||||
<Box>
|
||||
<InputLabel htmlFor="slippage">Slippage</InputLabel>
|
||||
@ -322,35 +267,9 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt="32px">
|
||||
<InputLabel htmlFor="recipient">
|
||||
{`${actualDestinationAddress ? "Custom" : "Default"} destination address`}
|
||||
</InputLabel>
|
||||
<Box mt="8px">
|
||||
<FormControl variant="outlined" color="primary" fullWidth>
|
||||
<OutlinedInput
|
||||
inputProps={{ "data-testid": "decimals-to-wait" }}
|
||||
type="text"
|
||||
id="destination-to-wait"
|
||||
value={destinationAddress
|
||||
? destinationAddress
|
||||
: actualDestinationAddress ?? address
|
||||
}
|
||||
onChange={event => setDestinationAddressInner(event.currentTarget.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box mt="8px">
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Recipient address of swapped assets and liquidity tokens
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
<TokenModal
|
||||
chainSymbol={chainSymbol}
|
||||
account={address}
|
||||
chainId={chainId}
|
||||
listOpen={topTokenListOpen}
|
||||
@ -358,7 +277,6 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
setTokenAddress={setInnerTokenAddressTop}
|
||||
/>
|
||||
<TokenModal
|
||||
chainSymbol={chainSymbol}
|
||||
account={address}
|
||||
chainId={chainId}
|
||||
listOpen={bottomTokenListOpen}
|
||||
@ -404,14 +322,11 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
dexAddresses={dexAddresses}
|
||||
connect={connect}
|
||||
slippage={slippage}
|
||||
destination={actualDestinationAddress ? actualDestinationAddress : address}
|
||||
secondsToWait={secondsToWait}
|
||||
setTopTokenListOpen={setTopTokenListOpen}
|
||||
setBottomTokenListOpen={setBottomTokenListOpen}
|
||||
setIsSwap={setIsSwap}
|
||||
formatDecimals={formatDecimals}
|
||||
isWrapping={isWrapping}
|
||||
isUnwrapping={isUnwrapping}
|
||||
/>
|
||||
:
|
||||
<PoolContainer
|
||||
@ -423,7 +338,6 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
dexAddresses={dexAddresses}
|
||||
connect={connect}
|
||||
slippage={slippage}
|
||||
destination={actualDestinationAddress ? actualDestinationAddress : address}
|
||||
secondsToWait={secondsToWait}
|
||||
setTopTokenListOpen={setTopTokenListOpen}
|
||||
setBottomTokenListOpen={setBottomTokenListOpen}
|
||||
|
||||
@ -9,15 +9,10 @@ import { TokenAllowanceGuard } from "../../components/TokenAllowanceGuard/TokenA
|
||||
import { SecondaryButton } from "../../components/Button";
|
||||
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { formatNumber, formatCurrency, bigIntSqrt } from "../../helpers";
|
||||
import { formatNumber, formatCurrency } from "../../helpers";
|
||||
|
||||
import { useBalance, useTotalSupply } from "../../hooks/tokens";
|
||||
import {
|
||||
useUniswapV2Pair,
|
||||
useUniswapV2PairReserves,
|
||||
addLiquidity,
|
||||
addLiquidityETH,
|
||||
} from "../../hooks/uniswapv2";
|
||||
import { useUniswapV2Pair, useUniswapV2PairReserves, addLiquidity } from "../../hooks/uniswapv2";
|
||||
|
||||
const PoolContainer = ({
|
||||
tokenNameTop,
|
||||
@ -28,11 +23,10 @@ const PoolContainer = ({
|
||||
dexAddresses,
|
||||
connect,
|
||||
slippage,
|
||||
destination,
|
||||
secondsToWait,
|
||||
setTopTokenListOpen,
|
||||
setBottomTokenListOpen,
|
||||
formatDecimals,
|
||||
formatDecimals
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
||||
@ -45,13 +39,11 @@ const PoolContainer = ({
|
||||
balance: balanceTop,
|
||||
refetch: balanceRefetchTop,
|
||||
contractAddress: addressTop,
|
||||
isNative: topIsNative,
|
||||
} = useBalance(chainId, tokenNameTop, address);
|
||||
const {
|
||||
balance: balanceBottom,
|
||||
refetch: balanceRefetchBottom,
|
||||
contractAddress: addressBottom,
|
||||
isNative: bottomIsNative,
|
||||
} = useBalance(chainId, tokenNameBottom, address);
|
||||
|
||||
const {
|
||||
@ -83,6 +75,34 @@ const PoolContainer = ({
|
||||
const setMaxTop = () => setAmountTop(balanceTop.toString());
|
||||
const setMaxBottom = () => setAmountBottom(balanceBottom.toString());
|
||||
|
||||
const bigIntSqrt = (n) => {
|
||||
if (n < 0n) {
|
||||
throw new Error("Cannot compute the square root of a negative number.");
|
||||
}
|
||||
if (n < 2n) {
|
||||
return n; // The square root of 0 or 1 is the number itself
|
||||
}
|
||||
|
||||
let low = 0n;
|
||||
let high = n;
|
||||
let mid;
|
||||
|
||||
while (low <= high) {
|
||||
mid = (low + high) / 2n;
|
||||
const midSquared = mid * mid;
|
||||
|
||||
if (midSquared === n) {
|
||||
return mid; // Found the exact square root
|
||||
} else if (midSquared < n) {
|
||||
low = mid + 1n; // Move to the right half
|
||||
} else {
|
||||
high = mid - 1n; // Move to the left half
|
||||
}
|
||||
}
|
||||
|
||||
return high; // The integer part of the square root
|
||||
}
|
||||
|
||||
const estimatedAmountOut = useMemo(() => {
|
||||
const pairReserves0 = addressTop.toUpperCase() === tokenAddresses.token0.toUpperCase()
|
||||
? pairReserves.reserve0
|
||||
@ -131,40 +151,14 @@ const PoolContainer = ({
|
||||
}
|
||||
}
|
||||
|
||||
}, [addressBottom, balanceTop, balanceBottom, amountTop, amountBottom, tokenAddresses, pairReserves]);
|
||||
|
||||
const poolShares = useMemo(() => {
|
||||
const hundred = new DecimalBigNumber(1n, 2);
|
||||
if (lpTotalSupply?._value == 0n) {
|
||||
return { currentShares: lpTotalSupply, nextShares: lpTotalSupply };
|
||||
}
|
||||
const currentShares = lpBalance.div(lpTotalSupply).div(hundred);
|
||||
const nextShares = (lpBalance.add(estimatedAmountOut)).div(lpTotalSupply.add(estimatedAmountOut)).div(hundred);
|
||||
return { currentShares, nextShares };
|
||||
}, [lpBalance, lpTotalSupply, estimatedAmountOut]);
|
||||
|
||||
const poolPrices = useMemo(() => {
|
||||
const amountIn = addressTop.toUpperCase() === tokenAddresses.token0.toUpperCase()
|
||||
? pairReserves.reserve0
|
||||
: pairReserves.reserve1;
|
||||
|
||||
const amountOut = addressBottom.toUpperCase() === tokenAddresses.token1.toUpperCase()
|
||||
? pairReserves.reserve1
|
||||
: pairReserves.reserve0;
|
||||
|
||||
let priceIn = "0";
|
||||
let priceOut = "0";
|
||||
|
||||
if (amountIn?._value > 0n) priceIn = (amountOut.div(amountIn)).toString();
|
||||
if (amountOut?._value > 0n) priceOut = (amountIn.div(amountOut)).toString();
|
||||
|
||||
return { priceIn , priceOut }
|
||||
}, [addressTop, addressBottom, balanceTop, tokenAddresses, pairReserves]);
|
||||
}, [addressBottom, balanceTop, balanceBottom, amountTop, amountBottom, tokenAddresses, pairReserves])
|
||||
|
||||
const addLiquidityInner = async () => {
|
||||
setIsPending(true);
|
||||
|
||||
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
|
||||
const destination = address;
|
||||
|
||||
const shares = 100000;
|
||||
const one = BigInt(shares * 100);
|
||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||
@ -177,7 +171,7 @@ const PoolContainer = ({
|
||||
const amountAMin = amountADesired * bigIntSlippage / one;
|
||||
const amountBMin = amountBDesired * bigIntSlippage / one;
|
||||
|
||||
const params = {
|
||||
await addLiquidity(
|
||||
chainId,
|
||||
tokenNameTop,
|
||||
tokenNameBottom,
|
||||
@ -185,16 +179,9 @@ const PoolContainer = ({
|
||||
amountBDesired,
|
||||
amountAMin,
|
||||
amountBMin,
|
||||
address,
|
||||
destination,
|
||||
deadline,
|
||||
}
|
||||
|
||||
if (topIsNative || bottomIsNative) {
|
||||
await addLiquidityETH(params)
|
||||
} else {
|
||||
await addLiquidity(params);
|
||||
}
|
||||
);
|
||||
|
||||
await balanceRefetchTop();
|
||||
await balanceRefetchBottom();
|
||||
@ -246,7 +233,7 @@ const PoolContainer = ({
|
||||
}
|
||||
arrowOnClick={onSwap}
|
||||
/>
|
||||
<Box
|
||||
{!isSmallScreen && <Box
|
||||
m="10px 0"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
@ -257,29 +244,24 @@ const PoolContainer = ({
|
||||
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
||||
>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameTop}`}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(poolPrices.priceIn, formatDecimals, tokenNameBottom)}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">Current Balance:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatNumber(lpBalance, formatDecimals)} LP</Typography>
|
||||
</Box>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom}`}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(poolPrices.priceOut, formatDecimals, tokenNameTop)}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">Total Supply:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatNumber(lpTotalSupply, formatDecimals)} LP</Typography>
|
||||
</Box>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">Current Pool Share:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatNumber(poolShares.currentShares, formatDecimals)}%</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">Extra Balance:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">~{formatNumber(estimatedAmountOut, formatDecimals)} LP</Typography>
|
||||
</Box>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">Next Pool Share:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatNumber(poolShares.nextShares, formatDecimals)}%</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>}
|
||||
<TokenAllowanceGuard
|
||||
spendAmount={new DecimalBigNumber(amountTop, balanceTop._decimals)}
|
||||
tokenName={tokenNameTop}
|
||||
owner={address}
|
||||
spender={dexAddresses.router}
|
||||
decimals={balanceTop._decimals}
|
||||
isNative={topIsNative}
|
||||
approvalText={"Approve " + tokenNameTop}
|
||||
approvalPendingText={"Approving..."}
|
||||
connect={connect}
|
||||
@ -292,7 +274,6 @@ const PoolContainer = ({
|
||||
owner={address}
|
||||
spender={dexAddresses.router}
|
||||
decimals={balanceBottom._decimals}
|
||||
isNative={bottomIsNative}
|
||||
approvalText={"Approve " + tokenNameBottom}
|
||||
approvalPendingText={"Approving..."}
|
||||
connect={connect}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
@ -13,16 +13,8 @@ import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { prettifySecondsInDays } from "../../helpers/timeUtil";
|
||||
|
||||
import { getTokenAddress } from "../../hooks/helpers";
|
||||
import { useBalance, depositNative, withdrawWeth } from "../../hooks/tokens";
|
||||
import {
|
||||
useUniswapV2Pair,
|
||||
useUniswapV2PairReserves,
|
||||
swapExactTokensForTokens,
|
||||
swapExactETHForTokens,
|
||||
swapExactTokensForETH,
|
||||
} from "../../hooks/uniswapv2";
|
||||
|
||||
import { EMPTY_ADDRESS } from "../../constants/addresses";
|
||||
import { useBalance } from "../../hooks/tokens";
|
||||
import { useUniswapV2Pair, useUniswapV2PairReserves, swapExactTokensForTokens } from "../../hooks/uniswapv2";
|
||||
|
||||
const SwapContainer = ({
|
||||
tokenNameTop,
|
||||
@ -35,12 +27,9 @@ const SwapContainer = ({
|
||||
setTopTokenListOpen,
|
||||
setBottomTokenListOpen,
|
||||
slippage,
|
||||
destination,
|
||||
secondsToWait,
|
||||
setIsSwap,
|
||||
formatDecimals,
|
||||
isWrapping,
|
||||
isUnwrapping,
|
||||
formatDecimals
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
||||
@ -56,14 +45,11 @@ const SwapContainer = ({
|
||||
balance: balanceTop,
|
||||
refetch: balanceRefetchTop,
|
||||
contractAddress: addressTop,
|
||||
isNative: topIsNative,
|
||||
} = useBalance(chainId, tokenNameTop, address);
|
||||
|
||||
const {
|
||||
balance: balanceBottom,
|
||||
refetch: balanceRefetchBottom,
|
||||
contractAddress: addressBottom,
|
||||
isNative: bottomIsNative,
|
||||
} = useBalance(chainId, tokenNameBottom, address);
|
||||
|
||||
const {
|
||||
@ -87,73 +73,42 @@ const SwapContainer = ({
|
||||
const setMax = () => setAmountTop(balanceTop.toString());
|
||||
|
||||
useEffect(() => {
|
||||
if (isWrapping || isUnwrapping) {
|
||||
setAmountBottom(amountTop.toString());
|
||||
setNextPrice("1");
|
||||
setCurrentPrice("1");
|
||||
return;
|
||||
}
|
||||
|
||||
const zero = new DecimalBigNumber(0n, 0);
|
||||
const raw = new DecimalBigNumber(amountTop, balanceTop._decimals);
|
||||
const amountInRaw = new DecimalBigNumber(raw._value.toBigInt(), balanceTop._decimals);
|
||||
const amountInWithFee = amountInRaw.mul(new DecimalBigNumber(997n, 3));
|
||||
|
||||
const topAddress = getTokenAddress(chainId, tokenNameTop);
|
||||
const bottomAddress = getTokenAddress(chainId, tokenNameBottom);
|
||||
|
||||
const amountIn = addressTop.toUpperCase() === tokenAddresses.token0.toUpperCase() ? pairReserves.reserve0 : pairReserves.reserve1;
|
||||
const amountOut = addressBottom.toUpperCase() === tokenAddresses.token1.toUpperCase() ? pairReserves.reserve1 : pairReserves.reserve0;
|
||||
|
||||
if (amountOut.eq(zero)) {
|
||||
if (amountIn.eq(zero)) {
|
||||
setCurrentPrice("");
|
||||
} else {
|
||||
setCurrentPrice(amountIn.div(amountOut).toString());
|
||||
}
|
||||
|
||||
if (amountOut.eq(zero) || amountInWithFee.eq(zero)) {
|
||||
if (amountIn.eq(zero) || amountInWithFee.eq(zero)) {
|
||||
setAmountBottom("");
|
||||
setNextPrice("");
|
||||
} else {
|
||||
const nominator = amountOut.mul(amountInWithFee);
|
||||
const nominator = amountOut.mul(amountIn);
|
||||
const denominator = amountIn.add(amountInWithFee);
|
||||
const newAmountOut = nominator.div(denominator);
|
||||
|
||||
const newReserveIn = amountIn.add(amountInWithFee);
|
||||
const newReserveOut = amountOut.sub(newAmountOut);
|
||||
const nextPrice = newReserveIn.div(newReserveOut);
|
||||
|
||||
setAmountBottom(newAmountOut.toString());
|
||||
setNextPrice(nextPrice.toString());
|
||||
setAmountBottom(amountOut.sub(newAmountOut).toString());
|
||||
setNextPrice(denominator.div(newAmountOut).toString())
|
||||
}
|
||||
}, [pairReserves, addressBottom, amountTop, addressTop, isWrapping, isUnwrapping]);
|
||||
|
||||
const minReceived = useMemo(() => {
|
||||
const decimals = 7;
|
||||
const shares = Math.pow(10, decimals);
|
||||
const one = BigInt(shares * 100);
|
||||
|
||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||
const bigIntSlippage = one - BigInt(Math.round(floatSlippage * shares));
|
||||
const slippageDecimalBigNumber = new DecimalBigNumber(bigIntSlippage, 2);
|
||||
|
||||
const bigIntAmount = BigInt(Math.round(amountBottom * shares));
|
||||
const amountDecimalBigNumber = new DecimalBigNumber(bigIntAmount, decimals);
|
||||
|
||||
const tmpResult = amountDecimalBigNumber.mul(slippageDecimalBigNumber);
|
||||
const result = new DecimalBigNumber(tmpResult?._value, tmpResult?._decimals + decimals);
|
||||
return result?.toString();
|
||||
}, [amountBottom, amountBottom, slippage, balanceBottom]);
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
let text = "Swap";
|
||||
if (isWrapping) text = "Wrap";
|
||||
else if (isUnwrapping) text = "Unwrap";
|
||||
else if (pairAddress === EMPTY_ADDRESS) text = "Create Pool";
|
||||
return text;
|
||||
}, [isWrapping, isUnwrapping, pairAddress]);
|
||||
}, [amountTop, addressTop]);
|
||||
|
||||
const swapTokens = async () => {
|
||||
setIsPending(true);
|
||||
|
||||
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
|
||||
const destination = address;
|
||||
|
||||
const shares = 100000;
|
||||
const one = BigInt(shares * 100);
|
||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||
@ -165,30 +120,14 @@ const SwapContainer = ({
|
||||
const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals)));
|
||||
const amountBMin = amountBDesired * bigIntSlippage / one;
|
||||
|
||||
if (isWrapping) {
|
||||
await depositNative(chainId, address, amountADesired);
|
||||
} else if (isUnwrapping) {
|
||||
await withdrawWeth(chainId, address, amountADesired);
|
||||
} else {
|
||||
const params = {
|
||||
chainId,
|
||||
amountADesired,
|
||||
amountBMin,
|
||||
tokenNameTop,
|
||||
tokenNameBottom,
|
||||
destination,
|
||||
address,
|
||||
deadline
|
||||
};
|
||||
|
||||
if (topIsNative) {
|
||||
await swapExactETHForTokens(params)
|
||||
} else if (bottomIsNative) {
|
||||
await swapExactTokensForETH(params)
|
||||
} else {
|
||||
await swapExactTokensForTokens(params);
|
||||
}
|
||||
}
|
||||
await swapExactTokensForTokens(
|
||||
chainId,
|
||||
amountADesired,
|
||||
amountBMin,
|
||||
[tokenNameTop, tokenNameBottom],
|
||||
destination,
|
||||
deadline
|
||||
);
|
||||
|
||||
await balanceRefetchTop();
|
||||
await balanceRefetchBottom();
|
||||
@ -236,7 +175,7 @@ const SwapContainer = ({
|
||||
}
|
||||
arrowOnClick={onSwap}
|
||||
/>
|
||||
<Box
|
||||
{!isSmallScreen && <Box
|
||||
m="10px 0"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
@ -247,29 +186,24 @@ const SwapContainer = ({
|
||||
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
||||
>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom} (Current)`}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(currentPrice, formatDecimals, tokenNameTop)}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">Current price:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(currentPrice, formatDecimals)}</Typography>
|
||||
</Box>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom} (Next)`}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(nextPrice === "" ? currentPrice : nextPrice, formatDecimals, tokenNameTop)}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">Next price:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(nextPrice === "" ? currentPrice : nextPrice, formatDecimals)}</Typography>
|
||||
</Box>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">Min. Receive:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(minReceived, formatDecimals, tokenNameBottom)}</Typography>
|
||||
</Box>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
<Typography fontSize="12px" lineHeight="15px">Tx. Deadline:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">Transaction deadline:</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">~{prettifySecondsInDays(secondsToWait)}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>}
|
||||
<TokenAllowanceGuard
|
||||
spendAmount={new DecimalBigNumber(amountTop, balanceTop._decimals)}
|
||||
tokenName={tokenNameTop}
|
||||
owner={address}
|
||||
spender={dexAddresses.router}
|
||||
decimals={balanceTop._decimals}
|
||||
isNative={topIsNative}
|
||||
approvalText={"Approve " + tokenNameTop}
|
||||
approvalPendingText={"Approving..."}
|
||||
connect={connect}
|
||||
@ -289,10 +223,17 @@ const SwapContainer = ({
|
||||
onClick={() => address === "" ?
|
||||
connect()
|
||||
:
|
||||
(!isWrapping && !isUnwrapping) && pairAddress === EMPTY_ADDRESS ? setIsSwap(false) : swapTokens()
|
||||
pairAddress === "0x0000000000000000000000000000000000000000" ? setIsSwap(false) : swapTokens()
|
||||
}
|
||||
>
|
||||
{address === "" ? "Connect" : buttonText }
|
||||
{address === "" ?
|
||||
"Connect"
|
||||
:
|
||||
pairAddress === "0x0000000000000000000000000000000000000000" ?
|
||||
"Create Pool"
|
||||
:
|
||||
"Swap"
|
||||
}
|
||||
</SecondaryButton>
|
||||
</TokenAllowanceGuard>
|
||||
</Box>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useConfig } from "wagmi";
|
||||
import {
|
||||
Divider,
|
||||
Typography,
|
||||
@ -20,14 +19,9 @@ import TokenStack from "../../components/TokenStack/TokenStack";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { formatNumber } from "../../helpers/";
|
||||
import { useBalance, useTokenSymbol } from "../../hooks/tokens";
|
||||
import {
|
||||
RESERVE_ADDRESSES,
|
||||
FTSO_ADDRESSES,
|
||||
GHST_ADDRESSES,
|
||||
EMPTY_ADDRESS
|
||||
} from "../../constants/addresses";
|
||||
import { DAI_ADDRESSES, FTSO_ADDRESSES, STNK_ADDRESSES, GHST_ADDRESSES } from "../../constants/addresses";
|
||||
|
||||
const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setTokenAddress }) => {
|
||||
const TokenModal = ({ chainId, account, listOpen, setListOpen, setTokenAddress }) => {
|
||||
const isSmallScreen = useMediaQuery("(max-width: 599px)");
|
||||
const isVerySmallScreen = useMediaQuery("(max-width: 425px)");
|
||||
|
||||
@ -43,13 +37,14 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
|
||||
const { symbol: searchSymbol } = useTokenSymbol(chainId, address);
|
||||
const { balance: searchBalance } = useBalance(chainId, address, account);
|
||||
|
||||
const { balance: nativeBalance } = useBalance(chainId, chainSymbol, account);
|
||||
const { balance: reserveBalance } = useBalance(chainId, "RESERVE", account);
|
||||
const { balance: daiBalance } = useBalance(chainId, "GDAI", account);
|
||||
const { balance: ftsoBalance } = useBalance(chainId, "FTSO", account);
|
||||
const { balance: stnkBalance } = useBalance(chainId, "STNK", account);
|
||||
const { balance: ghstBalance } = useBalance(chainId, "GHST", account);
|
||||
|
||||
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
|
||||
const { symbol: daiSymbol } = useTokenSymbol(chainId, "GDAI");
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const searchToken = useMemo(() => {
|
||||
@ -64,16 +59,10 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
|
||||
const knownTokens = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: chainSymbol,
|
||||
icons: [chainSymbol],
|
||||
balance: nativeBalance,
|
||||
address: EMPTY_ADDRESS,
|
||||
},
|
||||
{
|
||||
name: reserveSymbol,
|
||||
icons: [chainSymbol],
|
||||
balance: reserveBalance,
|
||||
address: RESERVE_ADDRESSES[chainId]
|
||||
name: daiSymbol,
|
||||
icons: ["GDAI"],
|
||||
balance: daiBalance,
|
||||
address: DAI_ADDRESSES[chainId]
|
||||
},
|
||||
{
|
||||
name: ftsoSymbol,
|
||||
@ -81,6 +70,12 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
|
||||
balance: ftsoBalance,
|
||||
address: FTSO_ADDRESSES[chainId]
|
||||
},
|
||||
{
|
||||
name: stnkSymbol,
|
||||
icons: ["STNK"],
|
||||
balance: stnkBalance,
|
||||
address: STNK_ADDRESSES[chainId]
|
||||
},
|
||||
{
|
||||
name: ghstSymbol,
|
||||
icons: ["GHST"],
|
||||
@ -88,7 +83,7 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
|
||||
address: GHST_ADDRESSES[chainId]
|
||||
}
|
||||
]
|
||||
}, [reserveSymbol, ftsoSymbol, ghstSymbol, reserveBalance, ftsoBalance, ghstBalance]);
|
||||
}, [daiSymbol, ftsoSymbol, stnkSymbol, ghstSymbol, daiBalance, ftsoBalance, stnkBalance, ghstBalance]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddress(userInput)) {
|
||||
|
||||
@ -11,7 +11,7 @@ import TokenStack from "../../components/TokenStack/TokenStack";
|
||||
import { PrimaryButton } from "../../components/Button";
|
||||
import { Tab, Tabs } from "../../components/Tabs/Tabs";
|
||||
|
||||
import { RESERVE_ADDRESSES } from "../../constants/addresses";
|
||||
import { DAI_ADDRESSES } from "../../constants/addresses";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { formatCurrency, formatNumber } from "../../helpers";
|
||||
|
||||
@ -44,14 +44,14 @@ const Faucet = ({ chainId, address, config, connect }) => {
|
||||
symbol: "",
|
||||
})
|
||||
|
||||
const reserveConversionRate = useConversionRate(chainId, "RESERVE");
|
||||
const accumulatedDonation = useAccumulatedDonation(chainId, "RESERVE");
|
||||
const daiConversionRate = useConversionRate(chainId, "GDAI");
|
||||
const accumulatedDonation = useAccumulatedDonation(chainId, "GDAI");
|
||||
|
||||
const { balance: reserveBalance, refetch: reserveBalanceRefetch } = useTokenBalance(chainId, "RESERVE", address);
|
||||
const { balance: daiBalance, refetch: daiBalanceRefetch } = useTokenBalance(chainId, "GDAI", address);
|
||||
const { data: nativeBalance, refetch: balanceRefetch } = useBalance({ address });
|
||||
const { data: contractBalance, refetch: contractBalanceRefetch } = useBalance({ address: RESERVE_ADDRESSES[chainId] });
|
||||
const { totalSupply: reserveTotalSupply, refetch: refetchReserveTotalSupply } = useTotalSupply(chainId, "RESERVE");
|
||||
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
|
||||
const { data: contractBalance, refetch: contractBalanceRefetch } = useBalance({ address: DAI_ADDRESSES[chainId] });
|
||||
const { totalSupply: reserveTotalSupply, refetch: refetchReserveTotalSupply } = useTotalSupply(chainId, "GDAI");
|
||||
const { symbol: faucetSymbol } = useTokenSymbol(chainId, "GDAI");
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.send({ hitType: "pageview", page: "/faucet" });
|
||||
@ -93,10 +93,10 @@ const Faucet = ({ chainId, address, config, connect }) => {
|
||||
}, [amount, balance, nativeInfo])
|
||||
|
||||
const estimatedAmountIn = useMemo(() => {
|
||||
const rate = new DecimalBigNumber(reserveConversionRate.toString(), 0);
|
||||
const rate = new DecimalBigNumber(daiConversionRate.toString(), 0);
|
||||
const value = new DecimalBigNumber(amount, nativeInfo.decimals);
|
||||
return value.mul(rate);
|
||||
}, [amount, reserveConversionRate, nativeInfo]);
|
||||
}, [amount, daiConversionRate, nativeInfo]);
|
||||
|
||||
const contractBalanceFree = useMemo(() => {
|
||||
const realContractBalance = contractBalance ? contractBalance.value : 0n;
|
||||
@ -129,7 +129,7 @@ const Faucet = ({ chainId, address, config, connect }) => {
|
||||
}
|
||||
|
||||
await balanceRefetch();
|
||||
await reserveBalanceRefetch();
|
||||
await daiBalanceRefetch();
|
||||
await contractBalanceRefetch();
|
||||
await refetchReserveTotalSupply();
|
||||
setAmount("");
|
||||
@ -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={`${faucetSymbol} Faucet`} subtitle={`Swap Sepolia ${nativeInfo.symbol} for ${faucetSymbol}.`} />
|
||||
<Container
|
||||
style={{
|
||||
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||
@ -188,7 +188,7 @@ const Faucet = ({ chainId, address, config, connect }) => {
|
||||
</Tabs>
|
||||
{!isSemiSmallScreen && <PrimaryButton
|
||||
variant="text"
|
||||
href={`${scanInfo.url}/token/${RESERVE_ADDRESSES[chainId]}`}
|
||||
href={`${scanInfo.url}/token/${DAI_ADDRESSES[chainId]}`}
|
||||
>
|
||||
Check on {scanInfo.name}
|
||||
</PrimaryButton>}
|
||||
@ -211,14 +211,14 @@ const Faucet = ({ chainId, address, config, connect }) => {
|
||||
{!isMint && <SwapCard
|
||||
id={`faucet-sepolia-eth`}
|
||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||
tokenName={reserveSymbol}
|
||||
token={<TokenStack tokens={[reserveSymbol]} sx={{ fontSize: "21px" }} />}
|
||||
info={`${formatCurrency(reserveBalance.toString(), 4, reserveSymbol)}`}
|
||||
tokenName={faucetSymbol}
|
||||
token={<TokenStack tokens={[faucetSymbol]} sx={{ fontSize: "21px" }} />}
|
||||
info={`${formatCurrency(daiBalance.toString(), 4, faucetSymbol)}`}
|
||||
value={amount}
|
||||
onChange={event => setAmount(event.currentTarget.value)}
|
||||
inputProps={{ "data-testid": "fromInput" }}
|
||||
endString={"Max"}
|
||||
endStringOnClick={() => setAmount(reserveBalance.toString())}
|
||||
endStringOnClick={() => setAmount(daiBalance.toString())}
|
||||
/>}
|
||||
<Box
|
||||
mb="20px"
|
||||
@ -231,15 +231,15 @@ const Faucet = ({ chainId, address, config, connect }) => {
|
||||
<>
|
||||
<Box maxWidth="416px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">{nativeInfo.symbol} multiplier:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatNumber(reserveConversionRate, 2)}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatNumber(daiConversionRate, 2)}</Typography>
|
||||
</Box>
|
||||
<Box maxWidth="416px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">You will get:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(estimatedAmountIn, 5, reserveSymbol)}</Typography>
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(estimatedAmountIn, 5, faucetSymbol)}</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Your {reserveSymbol} balance:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(reserveBalance, 5, reserveSymbol)}</Typography>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Your {faucetSymbol} balance:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(daiBalance, 5, faucetSymbol)}</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
@ -267,7 +267,7 @@ const Faucet = ({ chainId, address, config, connect }) => {
|
||||
preparedAmount?._value === 0n ||
|
||||
isPending ||
|
||||
(isMint && balance?.lt(preparedAmount)) ||
|
||||
(!isMint && reserveBalance?.lt(preparedAmount))
|
||||
(!isMint && daiBalance?.lt(preparedAmount))
|
||||
)
|
||||
}
|
||||
loading={isPending}
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import { useNavigate, useParams } 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 { network } = useParams();
|
||||
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
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(`/${network}/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;
|
||||
@ -1,246 +0,0 @@
|
||||
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 { useLocalStorage } from "../../hooks/localstorage";
|
||||
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 { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
const myStoredProposals = getStorageValue(chainId, address, MY_PROPOSALS_PREFIX, []);
|
||||
const [myProposals, setMyProposalsInner] = useState(() => 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);
|
||||
|
||||
const setMyProposals = useCallback((proposals) => {
|
||||
setMyProposalsInner(proposals);
|
||||
setStorageValue(chainId, address, MY_PROPOSALS_PREFIX, proposals.map(id => id.toString()))
|
||||
}, [chainId, address]);
|
||||
|
||||
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">
|
||||
<TertiaryButton
|
||||
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
|
||||
fullWidth
|
||||
disabled={isPending}
|
||||
onClick={() => setIsModalOpened(true)}
|
||||
>
|
||||
Add New Function
|
||||
</TertiaryButton>
|
||||
<PrimaryButton
|
||||
disabled={
|
||||
proposalFunctions.length === 0 ||
|
||||
ghstBalance.lt(threshold) ||
|
||||
isPending
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => submitProposal()}
|
||||
>
|
||||
{isPending ? "Submitting..." : "Submit Proposal"}
|
||||
</PrimaryButton>
|
||||
</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>
|
||||
}
|
||||
>
|
||||
{isSmallScreen
|
||||
? <Box display="flex" flexDirection="column">
|
||||
{proposalFunctions?.map((metadata, index) => {
|
||||
return parseFunctionCalldata({
|
||||
metadata,
|
||||
index,
|
||||
chainId,
|
||||
removeCalldata,
|
||||
nativeCoin: nativeCurrency,
|
||||
isTable: false,
|
||||
});
|
||||
})}
|
||||
</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,
|
||||
removeCalldata,
|
||||
nativeCoin: nativeCurrency,
|
||||
});
|
||||
})}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
</Paper>}
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewProposal;
|
||||
@ -1,497 +0,0 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
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 { convertStatusToColor, convertStatusToLabel } from "./helpers";
|
||||
import { parseFunctionCalldata } from "./components/functions";
|
||||
|
||||
import { networkAvgBlockSpeed } from "../../constants";
|
||||
|
||||
import { useLocalStorage } from "../../hooks/localstorage";
|
||||
import { useTokenSymbol, usePastTotalSupply, usePastVotes, useBalance } from "../../hooks/tokens";
|
||||
import {
|
||||
getVoteValue,
|
||||
getVoteTarget,
|
||||
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 queryClient = useQueryClient();
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
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;
|
||||
return totalVotes * HUNDRED / totalSupply;
|
||||
}, [totalVotes, totalSupply]);
|
||||
|
||||
const forPercentage = useMemo(() => {
|
||||
if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2);
|
||||
return forVotes * HUNDRED / totalSupply;
|
||||
}, [forVotes, totalSupply]);
|
||||
|
||||
const againstPercentage= useMemo(() => {
|
||||
if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2);
|
||||
return againstVotes * HUNDRED / totalSupply;
|
||||
}, [againstVotes, totalSupply]);
|
||||
|
||||
const voteWeightPercentage = useMemo(() => {
|
||||
if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2);
|
||||
return pastVotes * HUNDRED / totalSupply;
|
||||
}, [pastVotes, totalSupply]);
|
||||
|
||||
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 = useCallback(async (support) => {
|
||||
setIsPending(true);
|
||||
const result = await castVote(chainId, address, proposalId, support);
|
||||
|
||||
if (result) {
|
||||
const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []);
|
||||
const proposals = JSON.parse(storedVotedProposals || "[]").map(id => BigInt(id));
|
||||
proposals.push(proposalId);
|
||||
setStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, proposals.map(id => id.toString()));
|
||||
await queryClient.invalidateQueries();
|
||||
}
|
||||
setIsPending(false);
|
||||
}, [chainId, address, proposalId]);
|
||||
|
||||
const handleExecute = async () => {
|
||||
setIsPending(true);
|
||||
await executeProposal(chainId, address, proposalId);
|
||||
await queryClient.invalidateQueries();
|
||||
setIsPending(false);
|
||||
}
|
||||
|
||||
const handleRelease = async () => {
|
||||
setIsPending(true);
|
||||
await releaseLocked(chainId, address, proposalId);
|
||||
await queryClient.invalidateQueries();
|
||||
setIsPending(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<PageTitle
|
||||
name={`GDP-${id.slice(-5)}`}
|
||||
subtitle={
|
||||
<Typography component="span">
|
||||
{`Cast $${ghstSymbol} to shape this proposal's outcome`}
|
||||
</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
|
||||
gap="20px"
|
||||
display="flex"
|
||||
justifyContent={"space-between"}
|
||||
flexDirection={isSmallScreen ? "column" : "row"}
|
||||
>
|
||||
<Paper
|
||||
fullWidth
|
||||
enableBackground
|
||||
headerContent={
|
||||
<Box display="flex" alignItems="center" flexDirection="row" gap="10px">
|
||||
{!isSmallScreen &&<Typography variant="h6">
|
||||
Progress
|
||||
</Typography>}
|
||||
<Chip
|
||||
sx={{ marginTop: "4px", width: "88px" }}
|
||||
label={convertStatusToLabel(proposalState)}
|
||||
background={convertStatusToColor(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={getVoteValue(forVotes?._value ?? 0n, totalVotes?._value ?? 0n)}
|
||||
target={getVoteTarget(totalVotes?._value ?? 0n, totalSupply?._value ?? 0n)}
|
||||
/>
|
||||
</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)}
|
||||
>
|
||||
{isPending ? "Voting..." : "For"}
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
fullWidth
|
||||
disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending}
|
||||
onClick={() => handleVote(0)}
|
||||
>
|
||||
{isPending ? "Voting..." : "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}
|
||||
isPending={isPending}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Paper
|
||||
fullWidth
|
||||
enableBackground
|
||||
headerContent={
|
||||
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
||||
<Typography variant="h6">
|
||||
Executable Code
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{isSmallScreen
|
||||
? <Box display="flex" flexDirection="column">
|
||||
{proposalDetails?.map((metadata, index) => {
|
||||
return parseFunctionCalldata({
|
||||
metadata,
|
||||
index,
|
||||
chainId,
|
||||
nativeCoin: nativeCurrency,
|
||||
isTable: false,
|
||||
});
|
||||
})}
|
||||
</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,
|
||||
nativeCoin: nativeCurrency,
|
||||
});
|
||||
})}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked, proposalId, chainId, state, address, isProposer, isPending }) => {
|
||||
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 && (proposalLocked?._value ?? 0n) > 0n) && <SecondaryButton
|
||||
fullWidth
|
||||
disabled={isPending || state < 2}
|
||||
onClick={() => address === "" ? connect() : handleRelease()}
|
||||
>
|
||||
{address === "" ? "Connect" : "Release"}
|
||||
</SecondaryButton>}
|
||||
<SecondaryButton
|
||||
fullWidth
|
||||
disabled={isPending || state !== 4}
|
||||
onClick={() => address === "" ? connect() : handleExecute()}
|
||||
>
|
||||
{address === ""
|
||||
? "Connect"
|
||||
: state !== 4
|
||||
? convertStatusToLabel(state)
|
||||
: isPending ? "Executing..." : "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;
|
||||
@ -1,23 +0,0 @@
|
||||
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="http://forum.ghostchain.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Learn more here.</Link>
|
||||
</Typography>
|
||||
)
|
||||
};
|
||||
|
||||
export default GovernanceInfoText;
|
||||
@ -1,65 +0,0 @@
|
||||
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} />;
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
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;
|
||||
@ -1,119 +0,0 @@
|
||||
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">
|
||||
<b>{selectedOption}</b>
|
||||
{" "}
|
||||
{functionDescription}
|
||||
</Typography>
|
||||
{ready
|
||||
? <TertiaryButton disabled={!selectedOption} onClick={() => handleProceed()} fullWidth>Proceed</TertiaryButton>
|
||||
: <PrimaryButton disabled={!selectedOption} onClick={() => handleCalldata()} fullWidth>Create Function</PrimaryButton>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProposalModal;
|
||||
@ -1,334 +0,0 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useNavigate, useParams } 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 { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
|
||||
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 {
|
||||
convertStatusToColor,
|
||||
convertStatusToLabel,
|
||||
MY_PROPOSALS_PREFIX,
|
||||
VOTED_PROPOSALS_PREFIX
|
||||
} from "../helpers";
|
||||
|
||||
import { useScreenSize } from "../../../hooks/useScreenSize";
|
||||
|
||||
import { useProposals } from "../../../hooks/governance";
|
||||
import { useLocalStorage } from "../../../hooks/localstorage";
|
||||
|
||||
const MAX_PROPOSALS_TO_SHOW = 10;
|
||||
|
||||
const ProposalsList = ({ chainId, address, config }) => {
|
||||
const isSmallScreen = useScreenSize("md");
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const { network } = useParams();
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
const [proposalsFilter, setProposalFilter] = useState("active");
|
||||
|
||||
const myStoredProposals = getStorageValue(chainId, address, MY_PROPOSALS_PREFIX, []);
|
||||
const [myProposals, setMyProposals] = useState(() => myStoredProposals.map(id => BigInt(id)));
|
||||
|
||||
const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []);
|
||||
const [votedProposals, setVotedProposals] = useState(() => 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(`${network}/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(`/${network}/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={{ width: "180px", padding: "8px 0" }}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>{children}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
|
||||
const theme = useTheme();
|
||||
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)}
|
||||
background={convertStatusToColor(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={proposal?.voteValue ?? 0}
|
||||
target={proposal?.voteTarget ?? 50}
|
||||
/>
|
||||
</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)}
|
||||
background={convertStatusToColor(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={proposal?.voteValue ?? 0}
|
||||
target={proposal?.voteTarget ?? 50}
|
||||
/>
|
||||
</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 alreadyHappened = blockNumber > deadline;
|
||||
const diff = alreadyHappened ? blockNumber - deadline : deadline - blockNumber;
|
||||
const voteSeconds = Number(diff * networkAvgBlockSpeed(chainId));
|
||||
|
||||
const result = prettifySeconds(voteSeconds, "min");
|
||||
if (result === "now") {
|
||||
return new Date(Date.now()).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
const prefix = alreadyHappened ? "" : "in ";
|
||||
const postfix = alreadyHappened ? " ago" : "";
|
||||
|
||||
return `${prefix}${result}${postfix}`;
|
||||
}
|
||||
|
||||
export default ProposalsList;
|
||||
@ -1,31 +0,0 @@
|
||||
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 = "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 <ParsedCell {...props} />
|
||||
}
|
||||
@ -1,331 +0,0 @@
|
||||
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 = "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>
|
||||
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...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}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} 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={reserveSymbol}
|
||||
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[1]"
|
||||
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 _quoteToken, 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}
|
||||
maxWidth="calc(100vw - 70px)"
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
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 { GHOST_GOVERNANCE_ADDRESSES } from "../../../../constants/addresses";
|
||||
import { abi as GovernorAbi } from "../../../../abi/GhostGovernor.json";
|
||||
|
||||
import { ArgumentInput, ParsedCell } from "./index";
|
||||
|
||||
export const prepareSetLateQuorumExtensionCalldata = (chainId, lateQuorumVoteExtension) => {
|
||||
const value = 0n;
|
||||
const label = "setLateQuorumVoteExtension";
|
||||
const target = GHOST_GOVERNANCE_ADDRESSES[chainId];
|
||||
const calldata = encodeFunctionData({
|
||||
abi: GovernorAbi,
|
||||
functionName: 'setLateQuorumVoteExtension',
|
||||
args: [lateQuorumVoteExtension]
|
||||
});
|
||||
return { label, target, calldata, value };
|
||||
}
|
||||
|
||||
export const prepareSetLateQuorumExtensionDescription = "updates the current value of the vote extension parameter: the number of blocks that are required to pass from the time a proposal reaches quorum until its voting period ends.";
|
||||
|
||||
export const SetLateQuorumExtensionParsed = (props) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
headerContent={
|
||||
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
|
||||
<Typography variant="h4">View setLateQuorumVoteExtension</Typography>
|
||||
</Box>
|
||||
}
|
||||
open={isOpened}
|
||||
onClose={() => setIsOpened(false)}
|
||||
maxWidth="460px"
|
||||
minHeight="80px"
|
||||
>
|
||||
<Box minHeight="100px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
|
||||
<SetLateQuorumExtensionSteps {...props} />
|
||||
</Box>
|
||||
</Modal>
|
||||
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SetLateQuorumExtensionSteps = ({ chainId, toInitialStep, addCalldata, args }) => {
|
||||
const createMode = args === undefined;
|
||||
|
||||
const [lateQuorumVoteExtension, setlateQuorumVoteExtension] = useState(args?.at(0));
|
||||
|
||||
const handleProceed = () => {
|
||||
addCalldata(prepareSetLateQuorumExtensionCalldata(chainId, lateQuorumVoteExtension));
|
||||
}
|
||||
|
||||
return (
|
||||
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent={"space-between"}>
|
||||
<ArgumentInput
|
||||
disabled={!createMode}
|
||||
endString="blocks"
|
||||
label="lateQuorumVoteExtension"
|
||||
value={lateQuorumVoteExtension ?? ""}
|
||||
setValue={createMode ? setlateQuorumVoteExtension : () => {}}
|
||||
tooltip="Vote extension defines extension in blocks if a vote causes quorum to be reached, the proposal's voting period may be extended so that it does not end before at least a specified time has passed"
|
||||
/>
|
||||
{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={!lateQuorumVoteExtension}
|
||||
onClick={() => handleProceed()}
|
||||
fullWidth
|
||||
>
|
||||
Create Function
|
||||
</PrimaryButton>
|
||||
</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
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 { GHOST_GOVERNANCE_ADDRESSES } from "../../../../constants/addresses";
|
||||
import { abi as GovernorAbi } from "../../../../abi/GhostGovernor.json";
|
||||
|
||||
import { ArgumentInput, ParsedCell } from "./index";
|
||||
|
||||
export const prepareSetProposalThresholdCalldata = (chainId, newThreshold) => {
|
||||
const value = 0n;
|
||||
const label = "setProposalThreshold";
|
||||
const target = GHOST_GOVERNANCE_ADDRESSES[chainId];
|
||||
const calldata = encodeFunctionData({
|
||||
abi: GovernorAbi,
|
||||
functionName: 'setProposalThreshold',
|
||||
args: [newThreshold]
|
||||
});
|
||||
return { label, target, calldata, value };
|
||||
}
|
||||
|
||||
export const prepareSetProposalThresholdDescription = "updates the minimum amount required to create a proposal. This threshold applies only when there are no Active or Pending proposals; otherwise, a higher threshold is enforced.";
|
||||
|
||||
export const SetProposalThresholdParsed = (props) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
headerContent={
|
||||
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
|
||||
<Typography variant="h4">View setProposalThreshold</Typography>
|
||||
</Box>
|
||||
}
|
||||
open={isOpened}
|
||||
onClose={() => setIsOpened(false)}
|
||||
maxWidth="460px"
|
||||
minHeight="80px"
|
||||
>
|
||||
<Box minHeight="100px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
|
||||
<SetProposalThresholdSteps {...props} />
|
||||
</Box>
|
||||
</Modal>
|
||||
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SetProposalThresholdSteps = ({ chainId, toInitialStep, addCalldata, args }) => {
|
||||
const createMode = args === undefined;
|
||||
|
||||
const [newThreshold, setNewThreshold] = useState(args?.at(0));
|
||||
|
||||
const handleProceed = () => {
|
||||
addCalldata(prepareSetProposalThresholdCalldata(chainId, newThreshold));
|
||||
}
|
||||
|
||||
return (
|
||||
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent={"space-between"}>
|
||||
<ArgumentInput
|
||||
disabled={!createMode}
|
||||
endString="!CSPR units"
|
||||
label="newProposalThreshold"
|
||||
value={newThreshold ?? ""}
|
||||
setValue={createMode ? setNewThreshold : () => {}}
|
||||
tooltip="The new proposal threshold defines the minimum amount each proposer must lock for the duration of the proposal. This locked amount serves as the required deposit to create a proposal."
|
||||
/>
|
||||
{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={!newThreshold}
|
||||
onClick={() => handleProceed()}
|
||||
fullWidth
|
||||
>
|
||||
Create Function
|
||||
</PrimaryButton>
|
||||
</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
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 = "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>
|
||||
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...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 === undefined ? true : args.at(2));
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,353 +0,0 @@
|
||||
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 { abi as GovernorAbi } from "../../../../abi/GhostGovernor.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";
|
||||
import { prepareSetProposalThresholdDescription, prepareSetProposalThresholdCalldata, SetProposalThresholdSteps, SetProposalThresholdParsed } from "./ProposalThreshold";
|
||||
import { prepareSetLateQuorumExtensionDescription, prepareSetLateQuorumExtensionCalldata, SetLateQuorumExtensionSteps, SetLateQuorumExtensionParsed } from "./LateQuorumExtension";
|
||||
|
||||
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" },
|
||||
{ value: "setProposalThreshold", label: "setProposalThreshold" },
|
||||
{ value: "setLateQuorumVoteExtension", label: "setLateQuorumVoteExtension" },
|
||||
];
|
||||
|
||||
const allAbis = [TreasuryAbi, DistributorAbi, DepositoryAbi, GovernorAbi];
|
||||
|
||||
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,
|
||||
isTable = true,
|
||||
}) => {
|
||||
const { label, calldata, target, value } = metadata;
|
||||
const { functionName, args } = identifyAction(calldata);
|
||||
const labelOrName = label ?? (functionName ?? "Unknown");
|
||||
|
||||
const remove = removeCalldata && (() => removeCalldata(index));
|
||||
const props = {
|
||||
isTable,
|
||||
calldata,
|
||||
label: labelOrName,
|
||||
chainId: chainId,
|
||||
remove: removeCalldata,
|
||||
nativeCoin,
|
||||
value,
|
||||
target,
|
||||
args,
|
||||
id: index,
|
||||
};
|
||||
|
||||
switch (functionName) {
|
||||
case allPossibleFunctions[0].value:
|
||||
return <AuditReservesParsed key={index} {...props} />;
|
||||
case allPossibleFunctions[1].value:
|
||||
return <SetAdjustmentParsed key={index} {...props} />;
|
||||
case allPossibleFunctions[2].value:
|
||||
return <CreateBondParsed {...props} />;
|
||||
case allPossibleFunctions[3].value:
|
||||
return <SetProposalThresholdParsed key={index} {...props} />;
|
||||
case allPossibleFunctions[4].value:
|
||||
return <SetLateQuorumExtensionParsed key={index} {...props} />;
|
||||
default:
|
||||
return <ParsedCell key={index} {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export const getFunctionArguments = (functionName) => {
|
||||
switch (functionName) {
|
||||
case allPossibleFunctions[1].value:
|
||||
return SetAdjustmentSteps;
|
||||
case allPossibleFunctions[2].value:
|
||||
return CreateBondSteps;
|
||||
case allPossibleFunctions[3].value:
|
||||
return SetProposalThresholdSteps;
|
||||
case allPossibleFunctions[4].value:
|
||||
return SetLateQuorumExtensionSteps;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getFunctionCalldata = (functionName, chainId) => {
|
||||
switch (functionName) {
|
||||
case allPossibleFunctions[0].value:
|
||||
return prepareAuditReservesCalldata(chainId);
|
||||
case allPossibleFunctions[1].value:
|
||||
return prepareSetAdjustmentCalldata(chainId);
|
||||
case allPossibleFunctions[2].value:
|
||||
return prepareCreateBondCalldata(chainId);
|
||||
case allPossibleFunctions[3].value:
|
||||
return prepareSetProposalThresholdCalldata(chainId);
|
||||
case allPossibleFunctions[4].value:
|
||||
return prepareSetLateQuorumExtensionCalldata(chainId);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getFunctionDescription = (functionName) => {
|
||||
switch (functionName) {
|
||||
case allPossibleFunctions[0].value:
|
||||
return prepareAuditReservesDescription;
|
||||
case allPossibleFunctions[1].value:
|
||||
return prepareSetAdjustmentDescription;
|
||||
case allPossibleFunctions[2].value:
|
||||
return prepareCreateBondDescription;
|
||||
case allPossibleFunctions[3].value:
|
||||
return prepareSetProposalThresholdDescription;
|
||||
case allPossibleFunctions[4].value:
|
||||
return prepareSetLateQuorumExtensionDescription;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
if (props.isTable) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
id={props.id + `--proposalFunction`}
|
||||
data-testid={props.id + `--proposalFunction`}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>{`Function ${props.id + 1}`}</Typography>
|
||||
<Typography>{props.label}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Target</Typography>
|
||||
<Link href={etherscanLink} target="_blank" rel="noopener noreferrer">
|
||||
<Typography>{shorten(props.target)}</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Calldata</Typography>
|
||||
<Link onClick={handleCalldataCopy} target="_blank" rel="noopener noreferrer">
|
||||
<Typography>{shorten(props.calldata)}</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Value</Typography>
|
||||
<Link onClick={handleCalldataCopy} target="_blank" rel="noopener noreferrer">
|
||||
<Typography>{formatCurrency(props.value, 4, props.nativeCoin)}</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
{(props.args || props.remove) && <Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
gap="10px"
|
||||
marginTop="10px"
|
||||
>
|
||||
{props.args && <TertiaryButton fullWidth onClick={() => props.setIsOpened(!props.isOpened)}>View</TertiaryButton>}
|
||||
{props.remove && <TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>}
|
||||
</Box>}
|
||||
|
||||
<hr width="100%" style={{ margin: "20px 0" }} />
|
||||
|
||||
</Box>
|
||||
)
|
||||
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
export const MY_PROPOSALS_PREFIX = "MY_PROPOSALS";
|
||||
export const VOTED_PROPOSALS_PREFIX = "VOTED_PROPOSALS";
|
||||
|
||||
export const convertStatusToColor = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "#f2e370";
|
||||
case 2:
|
||||
return "#f06f73";
|
||||
case 3:
|
||||
return "#f06f73";
|
||||
case 4:
|
||||
return "#60c45b";
|
||||
case 5:
|
||||
return "#60c45b";
|
||||
case 6:
|
||||
return "#f06f73";
|
||||
case 7:
|
||||
return "#f2e370";
|
||||
default:
|
||||
return "#a2b7ce";
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,6 @@ import Paper from "../../components/Paper/Paper";
|
||||
|
||||
import GhostIcon from "../../assets/icons/ghost-nav-header.svg?react";
|
||||
import EthIcon from "../../assets/tokens/ETH.svg?react";
|
||||
import { parseKnownToken } from "../../components/Token/Token"
|
||||
|
||||
import { useSwitchChain } from 'wagmi';
|
||||
import toast from "react-hot-toast";
|
||||
@ -31,6 +30,7 @@ export default function NotFound({
|
||||
});
|
||||
setWrongNetworkToastId(toastId);
|
||||
|
||||
|
||||
setNetworkId(newChainId);
|
||||
switchChain({ chainId: newChainId });
|
||||
}
|
||||
@ -92,7 +92,7 @@ export default function NotFound({
|
||||
alignItems="center"
|
||||
gap="15px"
|
||||
>
|
||||
<SvgIcon component={parseKnownToken(chain.nativeCurrency.symbol)} inheritViewBox style={{ height: "25px" }} />
|
||||
<SvgIcon component={EthIcon} viewBox="0 0 32 32" />
|
||||
<Typography fontSize="1.2em">{chain.name}</Typography>
|
||||
</Box>
|
||||
</Button>
|
||||
|
||||
@ -19,6 +19,7 @@ const Stake = ({ chainId, address, isOpened, closeModal, connect }) => {
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const [upperToken, setUpperToken] = useState(ftsoSymbol);
|
||||
@ -30,16 +31,28 @@ const Stake = ({ chainId, address, isOpened, closeModal, connect }) => {
|
||||
|
||||
useEffect(() => {
|
||||
switch (true) {
|
||||
case (upperToken === ftsoSymbol && bottomToken === stnkSymbol):
|
||||
setAction("STAKE")
|
||||
break;
|
||||
case (upperToken === ftsoSymbol && bottomToken === ghstSymbol):
|
||||
setAction("STAKE")
|
||||
break;
|
||||
case (upperToken === stnkSymbol && bottomToken === ftsoSymbol):
|
||||
setAction("UNSTAKE")
|
||||
break;
|
||||
case (upperToken === ghstSymbol && bottomToken === ftsoSymbol):
|
||||
setAction("UNSTAKE")
|
||||
break;
|
||||
case (upperToken === stnkSymbol && bottomToken === ghstSymbol):
|
||||
setAction("WRAP")
|
||||
break;
|
||||
case (upperToken === ghstSymbol && bottomToken === stnkSymbol):
|
||||
setAction("UNWRAP")
|
||||
break;
|
||||
default:
|
||||
setAction("STAKE")
|
||||
}
|
||||
}, [ftsoSymbol, ghstSymbol, upperToken, bottomToken])
|
||||
}, [upperToken, bottomToken])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Dispatch, SetStateAction, useState, useEffect } from "react";
|
||||
import { Box, Container, Grid, Divider, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { Box, Container, Grid, Divider, Typography, Link, useMediaQuery, useTheme } from "@mui/material";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import Paper from "../../components/Paper/Paper";
|
||||
@ -47,7 +47,7 @@ export const StakeContainer = ({ chainId, address, connect }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box >
|
||||
<PageTitle name="Protocol Staking" subtitle={`Stake your ${ftsoSymbol} to earn rebase yields`} />
|
||||
<Container
|
||||
style={{
|
||||
|
||||
@ -21,15 +21,14 @@ import InfoTooltip from "../../../components/Tooltip/InfoTooltip";
|
||||
import Paper from "../../../components/Paper/Paper";
|
||||
import Token from "../../../components/Token/Token";
|
||||
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
|
||||
import { Tab, Tabs } from "../../../components/Tabs/Tabs";
|
||||
|
||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
import { prettifySecondsInDays } from "../../../helpers/timeUtil";
|
||||
import { formatNumber, formatCurrency } from "../../../helpers";
|
||||
import { formatNumber } from "../../../helpers";
|
||||
import { STAKING_ADDRESSES } from "../../../constants/addresses";
|
||||
import { useCurrentIndex, useWarmupInfo } from "../../../hooks/staking";
|
||||
import { useBalanceForShares, useTokenSymbol } from "../../../hooks/tokens";
|
||||
import { useGhstPrice, useStnkPrice } from "../../../hooks/prices";
|
||||
import { isNetworkLegacy } from "../../../constants";
|
||||
|
||||
import ClaimConfirmationModal from "./ClaimConfirmationModal";
|
||||
|
||||
@ -53,10 +52,10 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
|
||||
const isSmallScreen = useMediaQuery("(max-width: 745px)");
|
||||
|
||||
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
|
||||
const [isPayoutGhst, _] = useState(true);
|
||||
|
||||
const ghstPrice = useGhstPrice(chainId);
|
||||
const stnkPrice = useStnkPrice(chainId);
|
||||
const [isPayoutGhst, setIsPayoutGhst] = useState(localStorage.getItem("stake-isGhstPayout")
|
||||
? localStorage.getItem("stake-isGhstPayout")
|
||||
: false
|
||||
);
|
||||
|
||||
const { warmupInfo: claim, refetch: claimRefetch } = useWarmupInfo(chainId, address);
|
||||
const { currentIndex, refetch: currentIndexRefetch } = useCurrentIndex(chainId);
|
||||
@ -66,14 +65,6 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const claimableBalance = useMemo(() => {
|
||||
if (isNetworkLegacy(chainId)) {
|
||||
return isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares;
|
||||
}
|
||||
const toClaim = new DecimalBigNumber(claim.shares, 18);
|
||||
return isPayoutGhst ? toClaim : toClaim.mul(currentIndex);
|
||||
}, [chainId, claim, currentIndex, balanceForShares]);
|
||||
|
||||
const closeConfirmationModal = () => {
|
||||
setConfirmationModalOpen(false);
|
||||
claimRefetch();
|
||||
@ -82,6 +73,11 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
|
||||
|
||||
if (claim.shares === 0n) return <></>;
|
||||
|
||||
const setIsPayoutGhstInner = (value) => {
|
||||
setIsPayoutGhst(value);
|
||||
localStorage.setitem("bond-isGhstPayout", value);
|
||||
}
|
||||
|
||||
const warmupTooltip = `Your claim earns rebases during warmup. You can emergency withdraw, but this forfeits the rebases`;
|
||||
|
||||
return (
|
||||
@ -91,7 +87,7 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
|
||||
onClose={() => closeConfirmationModal()}
|
||||
chainid={chainId}
|
||||
receiver={address}
|
||||
receiveAmount={claim.expiry > epoch.number ? claim.deposit : claimableBalance}
|
||||
receiveAmount={claim.expiry > epoch.number ? claim.deposit : isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
|
||||
outputToken={claim.expiry > epoch.number ? ftsoSymbol : isPayoutGhst ? ghstSymbol : stnkSymbol}
|
||||
stnkSymbol={stnkSymbol}
|
||||
ghstSymbol={ghstSymbol}
|
||||
@ -110,6 +106,18 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
|
||||
flexDirection={isSmallScreen ? "column" : "row"}
|
||||
>
|
||||
<Typography variant="h6">Your active {isPayoutGhst ? ghstSymbol : stnkSymbol} claim</Typography>
|
||||
<Tabs
|
||||
centered
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
value={isPayoutGhst ? 1 : 0}
|
||||
aria-label="Claim token tabs"
|
||||
onChange={(_, view) => setIsPayoutGhstInner(view === 1)}
|
||||
TabIndicatorProps={{ style: { display: "none" } }}
|
||||
>
|
||||
<Tab aria-label="payout-stnk-button" label={stnkSymbol} style={{ fontSize: "1rem" }} />
|
||||
<Tab aria-label="payout-ghst-button" label={ghstSymbol} style={{ fontSize: "1rem" }} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
}
|
||||
tooltip={warmupTooltip}
|
||||
@ -118,35 +126,33 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
|
||||
{isSmallScreen ? (
|
||||
<MobileClaimInfo
|
||||
setConfirmationModalOpen={setConfirmationModalOpen}
|
||||
prepareBalance={claimableBalance}
|
||||
prepareBalance={isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
|
||||
isPayoutGhst={isPayoutGhst}
|
||||
claim={claim}
|
||||
epoch={epoch}
|
||||
isClaimable={claim.expiry > epoch.number}
|
||||
stnkSymbol={stnkSymbol}
|
||||
ghstSymbol={ghstSymbol}
|
||||
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<StyledTableHeader className={classes.stakePoolHeaderText}>
|
||||
<TableRow>
|
||||
<TableCell style={{ width: "200px", padding: "8px 0" }}>Asset</TableCell>
|
||||
<TableCell style={{ width: "200px", padding: "8px 0" }}>Staked Amount</TableCell>
|
||||
<TableCell style={{ width: "200px", padding: "8px 0" }}>Amount</TableCell>
|
||||
<TableCell style={{ width: "150px", padding: "8px 0" }}>Claimable In</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</StyledTableHeader>
|
||||
<ClaimInfo
|
||||
setConfirmationModalOpen={setConfirmationModalOpen}
|
||||
prepareBalance={claimableBalance}
|
||||
prepareBalance={isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
|
||||
isPayoutGhst={isPayoutGhst}
|
||||
claim={claim}
|
||||
epoch={epoch}
|
||||
isClaimable={claim.expiry > epoch.number}
|
||||
stnkSymbol={stnkSymbol}
|
||||
ghstSymbol={ghstSymbol}
|
||||
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
|
||||
/>
|
||||
</Table>
|
||||
|
||||
@ -156,17 +162,7 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ClaimInfo = ({
|
||||
setConfirmationModalOpen,
|
||||
prepareBalance,
|
||||
claim,
|
||||
epoch,
|
||||
isClaimable,
|
||||
isPayoutGhst,
|
||||
stnkSymbol,
|
||||
ghstSymbol,
|
||||
tokenPrice
|
||||
}) => {
|
||||
const ClaimInfo = ({ setConfirmationModalOpen, prepareBalance, claim, epoch, isClaimable, isPayoutGhst, stnkSymbol, ghstSymbol }) => {
|
||||
return (
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
@ -180,10 +176,7 @@ const ClaimInfo = ({
|
||||
</TableCell>
|
||||
<TableCell style={{ padding: "8px 8px 8px 0" }}>
|
||||
<Typography gutterBottom={false} style={{ lineHeight: 1.4 }}>
|
||||
{formatCurrency(prepareBalance, 5, isPayoutGhst ? ghstSymbol : stnkSymbol)}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom={false} style={{ lineHeight: 1.4 }}>
|
||||
{formatCurrency(prepareBalance * tokenPrice, 2)}
|
||||
{`${formatNumber(prepareBalance, 5)} ${isPayoutGhst ? ghstSymbol : stnkSymbol}`}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell style={{ padding: "8px 8px 8px 0" }}>
|
||||
@ -200,17 +193,7 @@ const ClaimInfo = ({
|
||||
);
|
||||
};
|
||||
|
||||
const MobileClaimInfo = ({
|
||||
setConfirmationModalOpen,
|
||||
prepareBalance,
|
||||
epoch,
|
||||
claim,
|
||||
isPayoutGhst,
|
||||
isClaimable,
|
||||
ghstSymbol,
|
||||
stnkSymbol,
|
||||
tokenPrice,
|
||||
}) => {
|
||||
const MobileClaimInfo = ({ setConfirmationModalOpen, prepareBalance, epoch, claim, isPayoutGhst, isClaimable, ghstSymbol, stnkSymbol }) => {
|
||||
return (
|
||||
<Box mt="10px">
|
||||
<Box display="flex" flexDirection="row" alignItems="center" style={{ whiteSpace: "nowrap" }}>
|
||||
@ -221,15 +204,10 @@ const MobileClaimInfo = ({
|
||||
</Box>
|
||||
|
||||
<DataRow
|
||||
title="Staked Amount"
|
||||
title="Amount"
|
||||
balance={formatNumber(prepareBalance, 7)}
|
||||
/>
|
||||
|
||||
<DataRow
|
||||
title="Price Estimation"
|
||||
balance={formatCurrency(prepareBalance * tokenPrice, 2)}
|
||||
/>
|
||||
|
||||
<DataRow
|
||||
title="Claimed in"
|
||||
balance={prettifySecondsInDays(epoch.length * (claim.expiry - epoch.number))}
|
||||
|
||||
@ -9,40 +9,41 @@ import {
|
||||
Box,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import { useNavigate, useParams, createSearchParams } from "react-router-dom";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
|
||||
import Paper from "../../../components/Paper/Paper";
|
||||
import { SecondaryButton } from "../../../components/Button";
|
||||
import TokenStack from "../../../components/TokenStack/TokenStack";
|
||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
import { formatCurrency } from "../../../helpers";
|
||||
import { tokenNameConverter } from "../../../helpers/tokenConverter";
|
||||
|
||||
import { useLpValuation } from "../../../hooks/treasury";
|
||||
import { useTotalSupply, useTokenSymbol } from "../../../hooks/tokens";
|
||||
|
||||
import { EMPTY_ADDRESS, FTSO_ADDRESSES } from "../../../constants/addresses";
|
||||
import {
|
||||
DAI_ADDRESSES,
|
||||
FTSO_ADDRESSES,
|
||||
} from "../../../constants/addresses";
|
||||
|
||||
const FarmPools = ({ chainId }) => {
|
||||
const isSmallScreen = useMediaQuery("(max-width: 775px)");
|
||||
const { network } = useParams();
|
||||
|
||||
const { totalSupply: reserveFtsoUniTotalSupply } = useTotalSupply(chainId, "RESERVE_FTSO");
|
||||
const reserveFtsoUniValuation = useLpValuation(chainId, "RESERVE_FTSO", reserveFtsoUniTotalSupply._value);
|
||||
const { totalSupply: daiFtsoUniTotalSupply } = useTotalSupply(chainId, "GDAI_FTSO");
|
||||
const daiFtsoUniValuation = useLpValuation(chainId, "GDAI_FTSO", daiFtsoUniTotalSupply._value);
|
||||
|
||||
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
|
||||
const { symbol: daiSymbol } = useTokenSymbol(chainId, "GDAI");
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
|
||||
const pools = [
|
||||
{
|
||||
icons: ["FTSO", tokenNameConverter(chainId, reserveSymbol)],
|
||||
name: `${ftsoSymbol}-${reserveSymbol}`,
|
||||
icons: ["FTSO", "GDAI"],
|
||||
name: `${ftsoSymbol}-${daiSymbol}`,
|
||||
dex: "Uniswap V2",
|
||||
url: `/${network}/dex/uniswap`,
|
||||
tvl: reserveFtsoUniValuation,
|
||||
url: "/dex/uniswap",
|
||||
tvl: daiFtsoUniValuation,
|
||||
params: createSearchParams({
|
||||
pool: "true",
|
||||
from: `${EMPTY_ADDRESS}`,
|
||||
from: `${DAI_ADDRESSES[chainId]}`,
|
||||
to: `${FTSO_ADDRESSES[chainId]}`,
|
||||
})
|
||||
},
|
||||
@ -61,17 +62,7 @@ const FarmPools = ({ chainId }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
fullWidth
|
||||
enableBackground
|
||||
headerContent={
|
||||
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
||||
<Typography variant="h6">
|
||||
Farm Pools
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper headerText="Farm Pools" fullWidth enableBackground>
|
||||
<TableContainer>
|
||||
<Table aria-label="Farm pools" style={{ tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
|
||||
@ -52,7 +52,7 @@ export const TotalDeposit = props => {
|
||||
const _props = {
|
||||
...props,
|
||||
label: "Total Deposit",
|
||||
tooltip: `Total reserves of native coins, LP tokens, and other assets in the ghostDAO treasury backing the entire circulating supply of ${props.stnkSymbol}.`,
|
||||
tooltip: `The total stablecoin reserves in the ghostDAO treasury backing the entire circulating supply of ${props.stnkSymbol}.`,
|
||||
};
|
||||
|
||||
if (deposit) _props.metric = `${formatCurrency(deposit, 2)}`;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Avatar, Box, Link } from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
@ -23,7 +23,6 @@ import {
|
||||
STAKING_ADDRESSES,
|
||||
} from "../../../constants/addresses";
|
||||
import { useCurrentIndex } from "../../../hooks/staking";
|
||||
import { useLocalStorage } from "../../../hooks/localstorage";
|
||||
import { useBalance, useTokenSymbol } from "../../../hooks/tokens";
|
||||
import { formatNumber } from "../../../helpers";
|
||||
|
||||
@ -83,11 +82,20 @@ export const StakeInputArea = ({
|
||||
const [bottomTokenModalOpen, setBottomTokenModalOpen] = useState(false);
|
||||
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
|
||||
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("stake-decimals")
|
||||
? localStorage.getItem("stake-decimals")
|
||||
: "5"
|
||||
);
|
||||
|
||||
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "stake-decimals", "5"));
|
||||
const [isClaim, setIsClaim] = useState(() => getStorageValue(chainId, address, "stake-isClaim", true));
|
||||
const [isTrigger, setIsTrigger] = useState(() => getStorageValue(chainId, address, "stake-isTrigger", true));
|
||||
const [isClaim, setIsClaim] = useState(localStorage.getItem("stake-isClaim")
|
||||
? localStorage.getItem("stake-isClaim") === 'true'
|
||||
: true
|
||||
);
|
||||
|
||||
const [isTrigger, setIsTrigger] = useState(localStorage.getItem("stake-isTrigger")
|
||||
? localStorage.getItem("stake-isTrigger") === 'true'
|
||||
: true
|
||||
);
|
||||
|
||||
const [amount, setAmount] = useState("");
|
||||
const [receiveAmount, setReceiveAmount] = useState("");
|
||||
@ -102,22 +110,23 @@ export const StakeInputArea = ({
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const setIsClaimInner = useCallback((value) => {
|
||||
const setIsClaimInner = (value) => {
|
||||
setIsClaim(value);
|
||||
setStorageValue(chainId, address, "stake-isClaim", value);
|
||||
}, [chainId, address]);
|
||||
localStorage.setItem("stake-isClaim", value);
|
||||
|
||||
const setIsTriggerInner = useCallback((value) => {
|
||||
}
|
||||
|
||||
const setIsTriggerInner = (value) => {
|
||||
setIsTrigger(value);
|
||||
setStorageValue(chainId, address, "stake-isTrigger", value);
|
||||
}, [chainId, address]);
|
||||
localStorage.setItem("stake-isTrigger", value);
|
||||
}
|
||||
|
||||
const setFormatDecimalsInner = useCallback((value) => {
|
||||
const setFormatDecimalsInner = (value) => {
|
||||
if (Number(value) <= 17) {
|
||||
setFormatDecimals(value);
|
||||
setStorageValue(chainId, address, "stake-decimals", value);
|
||||
localStorage.setItem("staking-decimals", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const innerBalance = upperToken === ghstSymbol ?
|
||||
@ -233,6 +242,36 @@ export const StakeInputArea = ({
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{upperTokenModalOpen && (
|
||||
<TokenModal
|
||||
open={upperTokenModalOpen}
|
||||
handleSelect={data => handleTokenModalInput(data.token, data.isUpper)}
|
||||
handleClose={() => setUpperTokenModalOpen(false)}
|
||||
ftsoBalance={formatNumber(ftsoBalance, formatDecimals)}
|
||||
stnkBalance={formatNumber(stnkBalance, formatDecimals)}
|
||||
ghstBalance={formatNumber(ghstBalance, formatDecimals)}
|
||||
isUpper={true}
|
||||
ftsoSymbol={ftsoSymbol}
|
||||
stnkSymbol={stnkSymbol}
|
||||
ghstSymbol={ghstSymbol}
|
||||
/>
|
||||
)}
|
||||
{bottomTokenModalOpen && (
|
||||
<TokenModal
|
||||
open={bottomTokenModalOpen}
|
||||
handleSelect={data => handleTokenModalInput(data.token, data.isUpper)}
|
||||
handleClose={() => setBottomTokenModalOpen(false)}
|
||||
ftsoBalance={formatNumber(ftsoBalance, formatDecimals)}
|
||||
stnkBalance={formatNumber(stnkBalance, formatDecimals)}
|
||||
ghstBalance={formatNumber(ghstBalance, formatDecimals)}
|
||||
tokenToExclude={upperToken}
|
||||
isUpper={false}
|
||||
ftsoSymbol={ftsoSymbol}
|
||||
stnkSymbol={stnkSymbol}
|
||||
ghstSymbol={ghstSymbol}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<PrimaryButton
|
||||
fullWidth
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
CurrentIndex,
|
||||
} from "./components/Metric";
|
||||
import TokenInfo from "./components/TokenInfo";
|
||||
import ProtocolDetails from "./components/ProtocolDetails";
|
||||
|
||||
import Paper from "../../components/Paper/Paper";
|
||||
import PageTitle from "../../components/PageTitle/PageTitle";
|
||||
@ -61,7 +60,7 @@ const MetricsDashboard = ({ chainId }) => {
|
||||
</Paper>
|
||||
|
||||
<Paper style={{ padding: "0px" }} fullWidth={true} >
|
||||
<ProtocolDetails theme={theme} isMobileScreen={isMobileScreen} chainId={chainId} />
|
||||
<TokenInfo isMobileScreen={isMobileScreen} chainId={chainId} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@ -84,7 +84,7 @@ export const FatsoBacking = props => {
|
||||
const _props = {
|
||||
...props,
|
||||
label: `Backing per ${props.ftsoSymbol}`,
|
||||
tooltip: `The total amount of native coins, LP tokens, and other assets held by the ghostDAO treasury to support the value of each ${props.ftsoSymbol} in circulation.`
|
||||
tooltip: `The total amount of stablecoins held by the ghostDAO treasury to support the value of each ${props.ftsoSymbol} in circulation.`
|
||||
};
|
||||
|
||||
if (backing) _props.metric = formatCurrency(backing, 2);
|
||||
|
||||
@ -1,137 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { Grid, Box, Typography } from "@mui/material";
|
||||
import { useConfig } from "wagmi";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
|
||||
import { formatNumber } from "../../../helpers";
|
||||
import { useEpoch, useGatekeeperApy } from "../../../hooks/staking";
|
||||
import { useTokenSymbol, useBalance, useCirculatingSupply } from "../../../hooks/tokens";
|
||||
import { SecondaryButton } from "../../../components/Button";
|
||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
|
||||
import { GATEKEEPER_ADDRESSES, EMPTY_ADDRESS } from "../../../constants/addresses";
|
||||
|
||||
const ProtocolDetail = ({ isMobileScreen, theme, name, sideName, url, urlParams, description }) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Box position="relative" width={`${isMobileScreen ? "100%" : "48%"}`}>
|
||||
<Box
|
||||
borderRadius="9px"
|
||||
padding="12px"
|
||||
sx={{ backgroundColor: theme.colors.paper.card }}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box display="flex" gap={"3px"} alignItems="center" justifyContent="space-between">
|
||||
<Typography fontSize="20px" fontWeight="bold" lineHeight="33px">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography color={theme.colors.primary[300]} fontSize="20px" fontWeight="bold" lineHeight="33px">
|
||||
{sideName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<>
|
||||
<Box my="18px">
|
||||
<Typography color={theme.colors.gray[40]} mt="9px">
|
||||
{description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="center" width="100%">
|
||||
<SecondaryButton
|
||||
onClick={() => urlParams
|
||||
? navigate({
|
||||
pathname: url,
|
||||
search: urlParams.toString()
|
||||
})
|
||||
: navigate(url, { replace: true })
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
{name}
|
||||
</SecondaryButton>
|
||||
</Box>
|
||||
</>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const ProtocolDetails = ({ chainId, isMobileScreen, theme, }) => {
|
||||
const config = useConfig();
|
||||
const nativeSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
|
||||
const networkName = config?.getClient()?.chain?.name;
|
||||
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: csprSymbol } = useTokenSymbol(chainId, "CSPR");
|
||||
const { contractAddress: ftsoAddress } = useBalance(chainId, "FTSO", EMPTY_ADDRESS);
|
||||
|
||||
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
|
||||
const gatekeepedApy = useGatekeeperApy(chainId);
|
||||
const { epoch } = useEpoch(chainId);
|
||||
|
||||
const apyInner = useMemo(() => {
|
||||
let apy = Infinity;
|
||||
if (circulatingSupply._value > 0n) {
|
||||
const value = epoch.distribute.div(circulatingSupply);
|
||||
apy = 100 * (Math.pow(1 + parseFloat(value.toString()), 1095) - 1);
|
||||
if (apy === 0) apy = Infinity;
|
||||
}
|
||||
return apy;
|
||||
}, [circulatingSupply, epoch]);
|
||||
|
||||
const bridgeNumbers = useMemo(() => {
|
||||
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||
const number = 1 + connectedNetworks * 3;
|
||||
return `(${number}, ${number})`;
|
||||
}, [chainId]);
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} justifyContent={"center"}>
|
||||
<Box display="flex" flexWrap="wrap" justifyContent="space-between" mt="10px" gap="25px">
|
||||
<ProtocolDetail
|
||||
isMobileScreen={isMobileScreen}
|
||||
theme={theme}
|
||||
url={`/${networkName.toLowerCase()}/dex/uniswap`}
|
||||
urlParams={createSearchParams({
|
||||
from: EMPTY_ADDRESS,
|
||||
to: `${ftsoAddress}`,
|
||||
})}
|
||||
name="(3, 3) Swap"
|
||||
sideName="Unlock Magic"
|
||||
description={`Buying strategy expands bond capacity triggering a positive loop to strengthen the Treasury and attract more stakers. Swap ${nativeSymbol} for ${ftsoSymbol} to unlock (3, 3) Stake.`}
|
||||
/>
|
||||
|
||||
<ProtocolDetail
|
||||
isMobileScreen={isMobileScreen}
|
||||
theme={theme}
|
||||
url={`/${networkName.toLowerCase()}/bonds`}
|
||||
name="(1, 1) Bond"
|
||||
sideName="Up to 40% Discount"
|
||||
description={`Bonding strategy grows Treasury and builds Protocol Owned Liquidity (POL) by allowing usersto acquire ${csprSymbol} at a discount. Take advantage of the next available bond.`}
|
||||
/>
|
||||
|
||||
<ProtocolDetail
|
||||
isMobileScreen={isMobileScreen}
|
||||
theme={theme}
|
||||
url={`/${networkName.toLowerCase()}/stake`}
|
||||
name="(3, 3) Stake"
|
||||
sideName={`${formatNumber(apyInner, 0)}% APY`}
|
||||
description={`Staking enables (3, 3) coordination by aligning long-term incentives, sustainably backed (1, 1) Bonds, LP fees, and Farming. Stake ${ftsoSymbol} to earn rewards and unlock ${bridgeNumbers} Stake\u00B2.`}
|
||||
/>
|
||||
|
||||
<ProtocolDetail
|
||||
isMobileScreen={isMobileScreen}
|
||||
theme={theme}
|
||||
url={`/${networkName.toLowerCase()}/bridge`}
|
||||
name={`${bridgeNumbers} Stake\u00B2`}
|
||||
sideName={`${formatNumber(apyInner * gatekeepedApy, 0)}% APY`}
|
||||
description={`Staking\u00B2 strategy further deepens long-term incentives powered by sustainable cross-chain bridging revenue. ${bridgeNumbers} Stake\u00B2 your ${csprSymbol} to receive organic APY with no warmup and dillution.`}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProtocolDetails;
|
||||
@ -1,6 +1,6 @@
|
||||
import { Grid, Box, Typography, useTheme } from "@mui/material";
|
||||
import { useAccount, useConfig, useBalance as useBalanceNative } from "wagmi";
|
||||
import { useNavigate, useParams, createSearchParams } from "react-router-dom";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
|
||||
import Token from "../../../components/Token/Token";
|
||||
import { SecondaryButton } from "../../../components/Button";
|
||||
@ -10,13 +10,10 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
import { useBalance, useTokenSymbol } from "../../../hooks/tokens";
|
||||
import {
|
||||
useFtsoPrice,
|
||||
useStnkPrice,
|
||||
useGhstPrice,
|
||||
useReservePrice,
|
||||
useNativePrice,
|
||||
useDaiPrice,
|
||||
} from "../../../hooks/prices";
|
||||
import { tokenNameConverter } from "../../../helpers/tokenConverter";
|
||||
|
||||
import { EMPTY_ADDRESS, WETH_ADDRESSES } from "../../../constants/addresses";
|
||||
|
||||
const TokenTab = ({ isMobileScreen, theme, tokenName, tokenUrl, tokenUrlParams, balance, price, description }) => {
|
||||
const navigate = useNavigate();
|
||||
@ -58,13 +55,10 @@ const TokenTab = ({ isMobileScreen, theme, tokenName, tokenUrl, tokenUrlParams,
|
||||
|
||||
<Box display="flex" justifyContent="center" width="100%">
|
||||
<SecondaryButton
|
||||
onClick={() => tokenUrlParams
|
||||
? navigate({
|
||||
pathname: tokenUrl,
|
||||
search: tokenUrlParams.toString()
|
||||
})
|
||||
: window.open(tokenUrl, '_blank')
|
||||
}
|
||||
onClick={() => navigate({
|
||||
pathname: tokenUrl,
|
||||
search: tokenUrlParams.toString()
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
Get {tokenName}
|
||||
@ -78,35 +72,31 @@ const TokenTab = ({ isMobileScreen, theme, tokenName, tokenUrl, tokenUrlParams,
|
||||
|
||||
const TokenInfo = ({ chainId, isMobileScreen }) => {
|
||||
const theme = useTheme();
|
||||
const { network } = useParams();
|
||||
const { address } = useAccount();
|
||||
|
||||
const config = useConfig();
|
||||
const nativeSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
|
||||
const networkName = config?.getClient()?.chain?.name;
|
||||
|
||||
const nativePrice = useNativePrice(chainId);
|
||||
const ftsoPrice = useFtsoPrice(chainId);
|
||||
const stnkPrice = useStnkPrice(chainId);
|
||||
const ghstPrice = useGhstPrice(chainId);
|
||||
const reservePrice = useReservePrice(chainId);
|
||||
const daiPrice = useDaiPrice(chainId);
|
||||
|
||||
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
|
||||
const { symbol: daiSymbol } = useTokenSymbol(chainId, "GDAI");
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const { data: nativeBalance } = useBalanceNative({ address });
|
||||
const { balance: ftsoBalance, contractAddress: ftsoAddress } = useBalance(chainId, "FTSO", address);
|
||||
const { balance: stnkBalance, contractAddress: stnkAddress } = useBalance(chainId, "STNK", address);
|
||||
const { balance: ghstBalance, contractAddress: ghstAddress } = useBalance(chainId, "GHST", address);
|
||||
const { balance: reserveBalance, contractAddress: reserveAddress } = useBalance(chainId, "RESERVE", address);
|
||||
const { balance: daiBalance, contractAddress: daiAddress } = useBalance(chainId, "GDAI", address);
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} justifyContent={"center"}>
|
||||
<Box display="flex" flexWrap="wrap" justifyContent="space-between" mt="10px" gap="25px">
|
||||
<TokenTab
|
||||
isMobileScreen={isMobileScreen}
|
||||
tokenUrl={`/${network}/dex/uniswap`}
|
||||
tokenUrl="/dex/uniswap"
|
||||
tokenUrlParams={createSearchParams({
|
||||
from: `${reserveAddress}`,
|
||||
from: `${daiAddress}`,
|
||||
to: `${ftsoAddress}`,
|
||||
})}
|
||||
theme={theme}
|
||||
@ -117,38 +107,39 @@ const TokenInfo = ({ chainId, isMobileScreen }) => {
|
||||
/>
|
||||
<TokenTab
|
||||
isMobileScreen={isMobileScreen}
|
||||
tokenUrl={`/${network}/dex/uniswap`}
|
||||
tokenUrl="/dex/uniswap"
|
||||
tokenUrlParams={createSearchParams({
|
||||
from: `${reserveAddress}`,
|
||||
from: `${daiAddress}`,
|
||||
to: `${stnkAddress}`,
|
||||
})}
|
||||
theme={theme}
|
||||
tokenName={stnkSymbol}
|
||||
balance={stnkBalance}
|
||||
price={stnkPrice}
|
||||
description={`${stnkSymbol} is a receipt for staked ${ftsoSymbol}, growing with staking rewards. When unstaked, it’s burned for ${ftsoSymbol} at a 1:1 ratio.`}
|
||||
/>
|
||||
<TokenTab
|
||||
isMobileScreen={isMobileScreen}
|
||||
tokenUrl="/dex/uniswap"
|
||||
tokenUrlParams={createSearchParams({
|
||||
from: `${daiAddress}`,
|
||||
to: `${ghstAddress}`,
|
||||
})}
|
||||
theme={theme}
|
||||
tokenName={ghstSymbol}
|
||||
balance={ghstBalance}
|
||||
price={ghstPrice}
|
||||
description={`${ghstSymbol} is the governance token enabling pure Web3 cross-chain magic. 1 ${ghstSymbol} = 1 ${ftsoSymbol} x Current Index.`}
|
||||
description={`${ghstSymbol} enables ghostDAO to have on-chain governance and to be truly cross-chain. ${ghstSymbol} Price = ${ftsoSymbol} Price x Current Index.`}
|
||||
/>
|
||||
<TokenTab
|
||||
isMobileScreen={isMobileScreen}
|
||||
tokenUrl={"https://ghostchain.io/faucet/"}
|
||||
tokenUrl="/faucet"
|
||||
tokenUrlParams=""
|
||||
theme={theme}
|
||||
tokenName={nativeSymbol}
|
||||
balance={new DecimalBigNumber(nativeBalance?.value ?? 0n, 18)}
|
||||
price={reservePrice}
|
||||
description={`${nativeSymbol} is the native currency of the ${networkName} Network, functioning as the backing asset for the ghostDAO.`}
|
||||
/>
|
||||
<TokenTab
|
||||
isMobileScreen={isMobileScreen}
|
||||
tokenUrl={`/${network}/dex/uniswap`}
|
||||
tokenUrlParams={createSearchParams({
|
||||
from: `${EMPTY_ADDRESS}`,
|
||||
to: `${WETH_ADDRESSES[chainId]}`,
|
||||
})}
|
||||
theme={theme}
|
||||
tokenName={reserveSymbol}
|
||||
balance={reserveBalance}
|
||||
price={reservePrice}
|
||||
description={`${ftsoSymbol} is backed by a treasury reserve of crypto assets, with ${reserveSymbol} being the primary and most liquid asset.`}
|
||||
tokenName={daiSymbol}
|
||||
balance={daiBalance}
|
||||
price={daiPrice}
|
||||
description={`${ftsoSymbol} is backed by a treasury reserve of crypto assets, with ${daiSymbol} being the primary and most liquid asset.`}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@ -1,260 +0,0 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Box, Container, Typography, useMediaQuery } from "@mui/material";
|
||||
import { useConfig, useBalance } from "wagmi";
|
||||
import { Helmet } from "react-helmet";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import PageTitle from "../../components/PageTitle/PageTitle";
|
||||
import Paper from "../../components/Paper/Paper";
|
||||
import SwapCard from "../../components/Swap/SwapCard";
|
||||
import TokenStack from "../../components/TokenStack/TokenStack";
|
||||
import { PrimaryButton } from "../../components/Button";
|
||||
import { Tab, Tabs } from "../../components/Tabs/Tabs";
|
||||
|
||||
import { RESERVE_ADDRESSES } from "../../constants/addresses";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { formatCurrency, formatNumber } from "../../helpers";
|
||||
|
||||
import {
|
||||
useBalance as useTokenBalance,
|
||||
useTokenSymbol,
|
||||
useTotalSupply,
|
||||
useConversionRate,
|
||||
useAccumulatedDonation,
|
||||
depositNative,
|
||||
withdrawWeth
|
||||
} from "../../hooks/tokens";
|
||||
|
||||
const WethWrapper = ({ chainId, address, config, connect }) => {
|
||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
|
||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||
|
||||
const [chainName, setChainName] = useState("");
|
||||
const [isMint, setIsMint] = useState(true);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [balance, setBalance] = useState(new DecimalBigNumber(0, 0));
|
||||
const [amount, setAmount] = useState("");
|
||||
const [scanInfo, setScanInfo] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
});
|
||||
const [nativeInfo, setNativeInfo] = useState({
|
||||
decimals: 18,
|
||||
name: "",
|
||||
symbol: "",
|
||||
})
|
||||
|
||||
const { balance: reserveBalance, refetch: reserveBalanceRefetch } = useTokenBalance(chainId, "RESERVE", address);
|
||||
const { data: nativeBalance, refetch: balanceRefetch } = useBalance({ address });
|
||||
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.send({ hitType: "pageview", page: "/wrapper" });
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const value = nativeBalance ? nativeBalance.value : 0n;
|
||||
const decimals = nativeBalance ? nativeBalance.decimals : 18;
|
||||
setBalance(new DecimalBigNumber(value, decimals));
|
||||
}, [nativeBalance]);
|
||||
|
||||
useEffect(() => {
|
||||
let scanName = "";
|
||||
let scanUrl = "";
|
||||
|
||||
const client = config?.getClient();
|
||||
scanName = client?.chain?.blockExplorers?.default?.name;
|
||||
scanUrl = client?.chain?.blockExplorers?.default?.url;
|
||||
|
||||
setScanInfo({
|
||||
name: scanName,
|
||||
url: scanUrl,
|
||||
})
|
||||
|
||||
setChainName(client?.chain?.name);
|
||||
|
||||
setNativeInfo(client?.chain?.nativeCurrency)
|
||||
}, [chainId]);
|
||||
|
||||
const changeIsMinted = (value) => {
|
||||
setAmount("");
|
||||
setIsMint(value);
|
||||
}
|
||||
|
||||
const preparedAmount = useMemo(() => {
|
||||
if (address === "") new DecimalBigNumber("0", 0);
|
||||
const decimals = isMint ? nativeInfo.decimals : 18;
|
||||
return new DecimalBigNumber(amount, decimals);
|
||||
}, [amount, balance, nativeInfo])
|
||||
|
||||
const estimatedAmountIn = useMemo(() => {
|
||||
return new DecimalBigNumber(amount, nativeInfo.decimals);
|
||||
}, [amount, nativeInfo]);
|
||||
|
||||
const estimatedAmountOut = useMemo(() => {
|
||||
return new DecimalBigNumber(amount, nativeInfo.decimals);
|
||||
}, [amount, nativeInfo]);
|
||||
|
||||
const actionOrConnect = async () => {
|
||||
if (address === "") {
|
||||
connect();
|
||||
} else {
|
||||
setIsPending(true);
|
||||
|
||||
if (isMint) {
|
||||
await depositNative(chainId, address, preparedAmount._value.toString());
|
||||
} else {
|
||||
await withdrawWeth(chainId, address, preparedAmount._value.toString());
|
||||
}
|
||||
|
||||
await balanceRefetch();
|
||||
await reserveBalanceRefetch();
|
||||
setAmount("");
|
||||
setIsPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box height="calc(100vh - 43px)">
|
||||
<Helmet>
|
||||
<title>ghostWrapper | WETH9</title>
|
||||
<meta name="description" content="Standard WETH9 wrapper. Convert native coin to WETH9 representation directly to your wallet." />
|
||||
<meta name="keywords" content="weth, weth9, ghostFaucet, web3 faucet, ethereum, sepolia, polygon, bnb, bsc, AVAX" />
|
||||
|
||||
<meta property="og:image" content="https://ghostchain.io/wp-content/uploads/2025/03/ghostFaucet-Featured_Image.png" />
|
||||
<meta property="og:title" content="ghostDAO | The DeFi 2.0 cross-chain reserve currency" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:description" content="Standard WETH9 wrapper. Convert native coin to WETH9 representation directly to your wallet." />
|
||||
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@realGhostChain" />
|
||||
<meta name="twitter:title" content="ghostDAO | The DeFi 2.0 cross-chain reserve currency" />
|
||||
<meta name="twitter:description" content="Standard WETH9 wrapper. Convert native coin to WETH9 representation directly to your wallet." />
|
||||
<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}`} />
|
||||
<Container
|
||||
style={{
|
||||
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "calc(100vh - 153px)"
|
||||
}}
|
||||
>
|
||||
<Box width="100%" maxWidth="476px" display="flex" alignItems="center" justifyContent="center" flexDirection="column">
|
||||
<Paper
|
||||
headerContent={
|
||||
<Box alignItems="center" justifyContent="space-between" display="flex" width="100%">
|
||||
<Tabs
|
||||
centered
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
value={isMint ? 0 : 1}
|
||||
aria-label="Faucet menu"
|
||||
onChange={(_, view) => changeIsMinted(view === 0)}
|
||||
TabIndicatorProps={{ style: { display: "none" } }}
|
||||
>
|
||||
<Tab aria-label="faucet-mint-button" label="Wrap" style={{ fontSize: "1.5rem" }} />
|
||||
<Tab aria-label="faucet-burn-button" label="Unwrap" style={{ fontSize: "1.5rem" }} />
|
||||
</Tabs>
|
||||
{!isSemiSmallScreen && <PrimaryButton
|
||||
variant="text"
|
||||
href={`${scanInfo.url}/token/${RESERVE_ADDRESSES[chainId]}`}
|
||||
>
|
||||
Check on {scanInfo.name}
|
||||
</PrimaryButton>}
|
||||
</Box>
|
||||
}
|
||||
enableBackground
|
||||
fullWidth
|
||||
>
|
||||
<Box>
|
||||
{isMint && <SwapCard
|
||||
id={`faucet-sepolia-eth`}
|
||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||
tokenName={nativeInfo.symbol}
|
||||
token={<TokenStack tokens={[nativeInfo.symbol]} sx={{ fontSize: "21px" }} />}
|
||||
info={`${isSemiSmallScreen ? "" : "Balance: "}${formatCurrency(balance.toString(), 4, nativeInfo.symbol)}`}
|
||||
value={amount}
|
||||
onChange={event => setAmount(event.currentTarget.value)}
|
||||
inputProps={{ "data-testid": "fromInput" }}
|
||||
/>}
|
||||
{!isMint && <SwapCard
|
||||
id={`faucet-sepolia-eth`}
|
||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||
tokenName={reserveSymbol}
|
||||
token={<TokenStack tokens={[reserveSymbol]} sx={{ fontSize: "21px" }} />}
|
||||
info={`${formatCurrency(reserveBalance.toString(), 4, reserveSymbol)}`}
|
||||
value={amount}
|
||||
onChange={event => setAmount(event.currentTarget.value)}
|
||||
inputProps={{ "data-testid": "fromInput" }}
|
||||
endString={"Max"}
|
||||
endStringOnClick={() => setAmount(reserveBalance.toString())}
|
||||
/>}
|
||||
<Box
|
||||
mb="20px"
|
||||
mt="20px"
|
||||
flexDirection="column"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{isMint && (
|
||||
<>
|
||||
<Box maxWidth="416px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">You will get:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(estimatedAmountIn, 5, reserveSymbol)}</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Your {reserveSymbol} balance:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(reserveBalance, 5, reserveSymbol)}</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{!isMint && (
|
||||
<>
|
||||
<Box maxWidth="416px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">You will get:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(estimatedAmountOut, 5, nativeInfo.symbol)}</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Your {nativeInfo.symbol} balance:</Typography>}
|
||||
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(balance, 5, nativeInfo.symbol)}</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<PrimaryButton
|
||||
fullWidth
|
||||
disabled={
|
||||
address !== "" && (
|
||||
preparedAmount?._value === 0n ||
|
||||
isPending ||
|
||||
(isMint && balance?.lt(preparedAmount)) ||
|
||||
(!isMint && reserveBalance?.lt(preparedAmount))
|
||||
)
|
||||
}
|
||||
loading={isPending}
|
||||
onClick={() => actionOrConnect()}
|
||||
>
|
||||
{address === "" ?
|
||||
"Connect"
|
||||
:
|
||||
isMint ? "Wrap" : "Unwrap"
|
||||
}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default WethWrapper;
|
||||
@ -41,27 +41,3 @@ export const formatNumber = (number, precision = 0) => {
|
||||
export const sortBondsByDiscount = (bonds) => {
|
||||
return Array.from(bonds).filter((bond) => !bond.isSoldOut).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1));
|
||||
};
|
||||
|
||||
export const timeConverter = (time, max = 7200, maxText = "long ago") => {
|
||||
const seconds = Number(time);
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
if (mins > max) {
|
||||
return maxText;
|
||||
} else {
|
||||
return `${mins}m ${secs < 10 ? '0' : ''}${secs}s`;
|
||||
}
|
||||
}
|
||||
|
||||
export const bigIntSqrt = (n) => {
|
||||
if (n < 0n) return 0n;
|
||||
if (n < 2n) return n;
|
||||
|
||||
let x = n / 2n + 1n;
|
||||
let y = (x + n / x) / 2n;
|
||||
while (y < x) {
|
||||
x = y;
|
||||
y = (x + n / x) / 2n;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
export const tokenNameConverter = (chainId, name, address) => {
|
||||
if (name?.toUpperCase() === "WETH") {
|
||||
switch (chainId) {
|
||||
case 63:
|
||||
name = "wmETC"
|
||||
break;
|
||||
default:
|
||||
name = "wETH";
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
@ -2,7 +2,6 @@ import { useReadContract, useReadContracts } from "wagmi";
|
||||
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { isNetworkLegacyType } from "../../constants";
|
||||
import { config } from "../../config";
|
||||
|
||||
import {
|
||||
@ -14,18 +13,14 @@ import { abi as BondAbi } from "../../abi/GhostBondDepository.json";
|
||||
import { abi as TreasuryAbi } from "../../abi/GhostTreasury.json";
|
||||
import { abi as BondingCalculatorAbi } from "../../abi/GhostBondingCalculator.json";
|
||||
|
||||
import { useReservePrice, useFtsoPrice } from "../prices";
|
||||
import { useOrinalCoefficient } from "../treasury";
|
||||
import { useFtsoPrice } from "../prices";
|
||||
import { useTokenSymbol, useTokenSymbols } from "../tokens";
|
||||
import { getTokenAddress, getTokenIcons, getBondNameDisplayName, getTokenPurchaseLink } from "../helpers";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { shorten } from "../../helpers";
|
||||
import { tokenNameConverter } from "../../helpers/tokenConverter";
|
||||
|
||||
export const useLiveBonds = (chainId, chainName) => {
|
||||
const ftsoPrice = useFtsoPrice(chainId);
|
||||
const baseTokenPerUsd = useReservePrice(chainId);
|
||||
const originalCoefficient = useOrinalCoefficient(chainId);
|
||||
export const useLiveBonds = (chainId) => {
|
||||
const baseTokenPerUsd = useFtsoPrice(chainId);
|
||||
|
||||
const { data: liveIndexesRaw, refetch } = useReadContract({
|
||||
abi: BondAbi,
|
||||
@ -131,11 +126,10 @@ export const useLiveBonds = (chainId, chainName) => {
|
||||
const quoteTokenSymbol = quoteTokenSymbols?.at(index).result ? quoteTokenSymbols.at(index).result : "";
|
||||
|
||||
const quoteTokenPerBaseToken = new DecimalBigNumber(marketPrice, 9);
|
||||
const priceInUsd = quoteTokenPerBaseToken.mul(baseTokenPerUsd).mul(quoteTokenPerUsd).mul(markdown).div(originalCoefficient);
|
||||
|
||||
const discount = ftsoPrice._value > 0n
|
||||
? ftsoPrice.sub(priceInUsd).div(ftsoPrice)
|
||||
: new DecimalBigNumber("0", ftsoPrice._decimals);
|
||||
const priceInUsd = quoteTokenPerUsd.mul(quoteTokenPerBaseToken).mul(markdown);
|
||||
const discount = baseTokenPerUsd._value > 0n
|
||||
? baseTokenPerUsd.sub(priceInUsd).div(baseTokenPerUsd)
|
||||
: new DecimalBigNumber("0", baseTokenPerUsd._decimals);
|
||||
|
||||
const capacityInBaseToken = capacityInQuote
|
||||
? new DecimalBigNumber(marketPrice > 0n ? marketCapacity / marketPrice : 0n, 9)
|
||||
@ -151,20 +145,21 @@ export const useLiveBonds = (chainId, chainName) => {
|
||||
);
|
||||
|
||||
const zero = new DecimalBigNumber(0n, 0);
|
||||
const bondName = `${baseTokenSymbol}/${quoteTokenSymbol}`;
|
||||
|
||||
return {
|
||||
id,
|
||||
discount,
|
||||
displayName: getBondNameDisplayName(chainId, quoteTokenAddress, baseTokenSymbol),
|
||||
displayName: getBondNameDisplayName(chainId, bondName, quoteTokenAddress),
|
||||
baseToken: {
|
||||
name: baseTokenSymbol,
|
||||
purchaseUrl: getTokenPurchaseLink(chainId, "", chainName),
|
||||
purchaseUrl: getTokenPurchaseLink(chainId, ""),
|
||||
icons: ["FTSO"],
|
||||
tokenAddress: getTokenAddress(chainId, "FTSO")
|
||||
},
|
||||
quoteToken: {
|
||||
name: tokenNameConverter(chainId, quoteTokenSymbol),
|
||||
purchaseUrl: getTokenPurchaseLink(chainId, quoteTokenAddress, chainName),
|
||||
name: quoteTokenSymbol,
|
||||
purchaseUrl: getTokenPurchaseLink(chainId, quoteTokenAddress),
|
||||
icons: getTokenIcons(chainId, quoteTokenAddress),
|
||||
decimals: quoteTokenDecimals,
|
||||
quoteTokenAddress,
|
||||
@ -172,11 +167,11 @@ export const useLiveBonds = (chainId, chainName) => {
|
||||
duration: terms?.at(index).result?.at(3) ? terms.at(index).result.at(3) : 0,
|
||||
vesting: terms?.at(index).result?.at(2) ? terms.at(index).result.at(2) : 0,
|
||||
isFixedTerm: terms?.at(index).result?.at(0) ? terms.at(index).result.at(0) : true,
|
||||
isSoldOut: capacityInBaseToken.eq(zero),
|
||||
isSoldOut: capacityInBaseToken.eq(zero) || capacityInBaseToken.eq(zero),
|
||||
price: {
|
||||
inUsd: priceInUsd,
|
||||
inBaseToken: quoteTokenPerBaseToken,
|
||||
marketPriceInUsd: ftsoPrice
|
||||
marketPriceInUsd: baseTokenPerUsd
|
||||
},
|
||||
capacity: {
|
||||
inBaseToken: capacityInBaseToken,
|
||||
@ -250,7 +245,7 @@ export const useNotes = (chainId, address) => {
|
||||
return {
|
||||
id,
|
||||
quoteToken: {
|
||||
name: tokenNameConverter(chainId, quoteTokenSymbol),
|
||||
name: quoteTokenSymbol,
|
||||
icons: getTokenIcons(chainId, quoteTokenAddress),
|
||||
},
|
||||
vesting: terms?.at(index).result?.at(2) ? terms.at(index).result.at(2) : 0,
|
||||
@ -266,7 +261,7 @@ export const useNotes = (chainId, address) => {
|
||||
return { notes, refetch };
|
||||
}
|
||||
|
||||
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => {
|
||||
export const purchaseBond = async (chainId, bondId, amount, maxPrice, user, sender, referral) => {
|
||||
const args = [
|
||||
bondId,
|
||||
amount,
|
||||
@ -284,12 +279,11 @@ export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, se
|
||||
"deposit",
|
||||
args,
|
||||
sender,
|
||||
messages,
|
||||
isNative ? amount : undefined
|
||||
messages
|
||||
);
|
||||
}
|
||||
|
||||
export const redeem = async ({ chainId, user, isGhst, indexes }) => {
|
||||
export const redeem = async (chainId, user, isGhst, indexes) => {
|
||||
const args = [
|
||||
user,
|
||||
isGhst,
|
||||
@ -314,8 +308,7 @@ const executeOnChainTransaction = async (
|
||||
functionName,
|
||||
args,
|
||||
account,
|
||||
messages,
|
||||
value
|
||||
messages
|
||||
) => {
|
||||
try {
|
||||
const { request } = await simulateContract(config, {
|
||||
@ -324,9 +317,7 @@ const executeOnChainTransaction = async (
|
||||
functionName,
|
||||
args,
|
||||
account,
|
||||
chainId,
|
||||
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
|
||||
value: value,
|
||||
chainId
|
||||
});
|
||||
|
||||
const txHash = await writeContract(config, request);
|
||||
@ -8,24 +8,15 @@ import { useUnstableProvider } from "./UnstableProvider"
|
||||
const MetadataProvider = createContext(null)
|
||||
export const useMetadata = () => useContext(MetadataProvider)
|
||||
|
||||
const CACHE_VERSION = "v2"
|
||||
|
||||
export const MetadataProviderProvider = ({ children }) => {
|
||||
const { client, chainId } = useUnstableProvider()
|
||||
const { data: metadata } = useSWR(
|
||||
client && chainId ? ["metadata", client, chainId] : null,
|
||||
async ([_, client]) => {
|
||||
const storageKey = `metadata-${chainId}-${CACHE_VERSION}`
|
||||
const storageKey = `metadata-${chainId}`
|
||||
const storedMetadata = sessionStorage.getItem(storageKey)
|
||||
|
||||
if (storedMetadata) return unifyMetadata(decAnyMetadata(storedMetadata))
|
||||
|
||||
Object.keys(sessionStorage).forEach(key => {
|
||||
if (key.startsWith("metadata-")) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
const metadata = await new Promise((resolve, reject) =>
|
||||
client._request("state_getMetadata", [], {
|
||||
onSuccess: resolve,
|
||||
|
||||
@ -1,19 +1,14 @@
|
||||
import { createContext, useEffect, useContext, useState, useMemo, useCallback, useRef } from "react"
|
||||
import { createContext, useContext, useState, useMemo } from "react"
|
||||
import { Unstable } from "@substrate/connect-discovery"
|
||||
import { createClient } from "@polkadot-api/substrate-client"
|
||||
import { getObservableClient } from "@polkadot-api/observable-client"
|
||||
import useSWR from "swr"
|
||||
|
||||
const MAX_BLOCK_TIMEOUT = 15000
|
||||
const DEFAULT_CHAIN_ID = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3"
|
||||
const DEFAULT_CHAIN_ID = "0xa217f4ee58a944470e9633ca5bd6d28a428ed64cd9b6f3e413565f359f89af90"
|
||||
const UnstableProvider = createContext(null)
|
||||
export const useUnstableProvider = () => useContext(UnstableProvider)
|
||||
|
||||
export const UnstableProviderProvider = ({ children }) => {
|
||||
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [reconnectTicket, setReconnectTicket] = useState(0);
|
||||
|
||||
const { data: providerDetails } = useSWR("getGhostProviders", () =>
|
||||
Unstable.getSubstrateConnectExtensionProviders()
|
||||
);
|
||||
@ -24,66 +19,36 @@ export const UnstableProviderProvider = ({ children }) => {
|
||||
() => providerDetail ? providerDetail.provider : null
|
||||
);
|
||||
|
||||
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
|
||||
|
||||
const client = useMemo(() => {
|
||||
if (!provider || !chainId) return undefined;
|
||||
|
||||
const chain = provider.getChains()[chainId];
|
||||
if (!chain) return undefined;
|
||||
return createClient(chain.connect);
|
||||
}, [provider, chainId]);
|
||||
|
||||
return createClient(chain.connect)
|
||||
}, [provider, chainId, reconnectTicket]);
|
||||
const observableClient = useMemo(() => {
|
||||
return client ? getObservableClient(client) : undefined;
|
||||
}, [client]);
|
||||
|
||||
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
|
||||
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
|
||||
|
||||
const lastBlockNumber = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainHead$) return;
|
||||
|
||||
lastBlockNumber.current = 0;
|
||||
let timeoutId;
|
||||
|
||||
const sub = chainHead$.bestBlocks$.subscribe({
|
||||
next: (blocks) => {
|
||||
const currentHeight = blocks.at(0)?.number ?? -1;
|
||||
|
||||
if (currentHeight > lastBlockNumber.current) {
|
||||
lastBlockNumber.current = currentHeight;
|
||||
setIsConnected(true);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
setIsConnected(false);
|
||||
setReconnectTicket(t => t + 1);
|
||||
}, MAX_BLOCK_TIMEOUT);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
setIsConnected(false);
|
||||
setTimeout(() => setReconnectTicket(t => t + 1), 1000);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [chainHead$]);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
isConnected,
|
||||
providerDetails,
|
||||
providerDetail,
|
||||
connectProviderDetail: setProviderDetail,
|
||||
chainId,
|
||||
client,
|
||||
setChainId,
|
||||
chainHead$
|
||||
}), [isConnected, providerDetails, providerDetail, chainId, client, chainHead$]);
|
||||
const chainHead$ = useMemo(() => {
|
||||
return observableClient ? observableClient.chainHead$() : undefined;
|
||||
}, [observableClient]);
|
||||
|
||||
return (
|
||||
<UnstableProvider.Provider value={value}>
|
||||
<UnstableProvider.Provider
|
||||
value={{
|
||||
providerDetails,
|
||||
providerDetail,
|
||||
connectProviderDetail: setProviderDetail,
|
||||
provider,
|
||||
chainId,
|
||||
client,
|
||||
setChainId,
|
||||
chainHead$
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UnstableProvider.Provider>
|
||||
);
|
||||
|
||||
@ -6,11 +6,4 @@ export * from "./useClapsInSession";
|
||||
export * from "./useApplauseThreshold";
|
||||
export * from "./useReceivedClaps";
|
||||
export * from "./useAuthorities";
|
||||
export * from "./useValidators";
|
||||
export * from "./useDisabledValidators";
|
||||
export * from "./useBlockCommitments";
|
||||
export * from "./useApplauseDetails";
|
||||
export * from "./useBabeSlots";
|
||||
export * from "./useErasTotalStaked";
|
||||
export * from "./useLatestBlockNumber";
|
||||
export * from "./useEraIndex";
|
||||
export * from "./useApplausesForTransaction";
|
||||
|
||||
@ -6,41 +6,42 @@ import { fromHex } from "@polkadot-api/utils";
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
import { useMetadata } from "./MetadataProvider"
|
||||
|
||||
export const useApplauseDetails = ({ currentSession, argsHash }) => {
|
||||
export const useApplausesForTransaction = ({ currentSession, txHash, argsHash }) => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: applauseDetails } = useSWRSubscription(
|
||||
chainHead$ && argsHash && currentSession && chainId && metadata
|
||||
? ["applauseDetails", chainHead$, argsHash, currentSession, chainId, metadata]
|
||||
const { data: applausesForTransaction } = useSWRSubscription(
|
||||
chainHead$ && txHash && argsHash && currentSession && chainId && metadata
|
||||
? ["applausesForTransaction", chainHead$, txHash, argsHash, currentSession, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, argsHash, currentSession, chainId, metadata], { next }) => {
|
||||
([_, chainHead$, txHash, argsHash, currentSession, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const applauseDetails = builder.buildStorage("GhostSlowClaps", "ApplauseDetails")
|
||||
const applausesForTransaction = builder.buildStorage("GhostSlowClaps", "ApplausesForTransaction")
|
||||
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
applauseDetails?.keys.enc(
|
||||
applausesForTransaction?.keys.enc(
|
||||
currentSession,
|
||||
{ asBytes: () => fromHex(txHash) },
|
||||
{ asBytes: () => fromHex(argsHash) },
|
||||
)
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => applauseDetails?.value.dec(value))
|
||||
map((value) => applausesForTransaction?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(applauseDetails) {
|
||||
next(null, applauseDetails)
|
||||
next(applausesForTransaction) {
|
||||
next(null, applausesForTransaction)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return applauseDetails
|
||||
return applausesForTransaction
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import useSWRSubscription from "swr/subscription"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
import { useMetadata } from "./MetadataProvider"
|
||||
|
||||
export const useGenesisSlot = () => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: genesisSlot } = useSWRSubscription(
|
||||
chainHead$ && chainId && metadata
|
||||
? ["genesisSlot", chainHead$, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const genesisSlot = builder.buildStorage("Babe", "GenesisSlot")
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
genesisSlot?.keys.enc()
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => genesisSlot?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(genesisSlot) {
|
||||
next(null, genesisSlot)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return genesisSlot
|
||||
}
|
||||
|
||||
export const useCurrentSlot = () => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: currentSlot } = useSWRSubscription(
|
||||
chainHead$ && chainId && metadata
|
||||
? ["currentSlot", chainHead$, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const currentSlot = builder.buildStorage("Babe", "CurrentSlot")
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
currentSlot?.keys.enc()
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => currentSlot?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(currentSlot) {
|
||||
next(null, currentSlot)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return currentSlot
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
import useSWRSubscription from "swr/subscription"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
import { useMetadata } from "./MetadataProvider"
|
||||
|
||||
export const useBlockCommitments = ({ evmChainId }) => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: blockCommitments } = useSWRSubscription(
|
||||
chainHead$ && chainId && metadata
|
||||
? ["blockCommitments", chainHead$, evmChainId, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, evmChainId, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const blockCommitments = builder.buildStorage("GhostSlowClaps", "BlockCommitments")
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
blockCommitments?.keys.enc(BigInt(evmChainId))
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => blockCommitments?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(blockCommitments) {
|
||||
next(null, blockCommitments)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return blockCommitments?.reduce((acc, [index, obj]) => {
|
||||
acc[index] = obj;
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import useSWRSubscription from "swr/subscription"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
import { useMetadata } from "./MetadataProvider"
|
||||
|
||||
export const useDisabledValidators = () => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: disabledIndexes, error } = useSWRSubscription(
|
||||
chainHead$ && chainId && metadata
|
||||
? ["disabledIndexes", chainHead$, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const disabledIndexes = builder.buildStorage("Session", "DisabledValidators")
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
disabledIndexes?.keys.enc()
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => disabledIndexes?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(disabledIndexes) {
|
||||
next(null, disabledIndexes)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return disabledIndexes ? disabledIndexes : []
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import useSWRSubscription from "swr/subscription"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
import { useMetadata } from "./MetadataProvider"
|
||||
|
||||
export const useEraIndex = () => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: eraIndex } = useSWRSubscription(
|
||||
chainHead$ && chainId && metadata
|
||||
? ["eraIndex", chainHead$, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const eraIndex = builder.buildStorage("Staking", "ActiveEra")
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
eraIndex?.keys.enc()
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => eraIndex?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(eraIndex) {
|
||||
next(null, eraIndex)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return eraIndex;
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import useSWRSubscription from "swr/subscription"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
import { useMetadata } from "./MetadataProvider"
|
||||
|
||||
export const useErasTotalStake = ({ epochIndex }) => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: eraTotalStake } = useSWRSubscription(
|
||||
chainHead$ && chainId && metadata
|
||||
? ["eraTotalStake", chainHead$, epochIndex, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, epochIndex, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const eraTotalStake = builder.buildStorage("Staking", "ErasTotalStake")
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
eraTotalStake?.keys.enc(epochIndex)
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => eraTotalStake?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(eraTotalStake) {
|
||||
next(null, eraTotalStake)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return eraTotalStake;
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import useSWRSubscription from "swr/subscription"
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
|
||||
export const useLatestBlockNumber = () => {
|
||||
const { chainHead$ } = useUnstableProvider();
|
||||
const [blockNumber, setBlockNumber] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainHead$) return;
|
||||
const subscription = chainHead$.best$.subscribe((block) => {
|
||||
setBlockNumber(block.number);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [chainHead$]);
|
||||
|
||||
return blockNumber;
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import useSWRSubscription from "swr/subscription"
|
||||
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||
|
||||
import { useUnstableProvider } from "./UnstableProvider"
|
||||
import { useMetadata } from "./MetadataProvider"
|
||||
|
||||
export const useValidators = ({ currentSession }) => {
|
||||
const { chainHead$, chainId } = useUnstableProvider()
|
||||
const metadata = useMetadata()
|
||||
const { data: slowClapValidators } = useSWRSubscription(
|
||||
chainHead$ && chainId && metadata
|
||||
? ["slowClapValidators", chainHead$, currentSession, chainId, metadata]
|
||||
: null,
|
||||
([_, chainHead$, currentSession, chainId, metadata], { next }) => {
|
||||
const { finalized$, storage$ } = chainHead$
|
||||
const subscription = finalized$.pipe(
|
||||
filter(Boolean),
|
||||
mergeMap((blockInfo) => {
|
||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||
const slowClapValidators = builder.buildStorage("GhostSlowClaps", "Validators")
|
||||
return storage$(blockInfo?.hash, "value", () =>
|
||||
slowClapValidators?.keys.enc(currentSession)
|
||||
).pipe(
|
||||
filter(Boolean),
|
||||
distinct(),
|
||||
map((value) => slowClapValidators?.value.dec(value))
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next(slowClapValidators) {
|
||||
next(null, slowClapValidators)
|
||||
},
|
||||
error: next,
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
)
|
||||
return slowClapValidators
|
||||
}
|
||||
@ -1,654 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { useReadContract, useReadContracts } from "wagmi";
|
||||
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
|
||||
import toast from "react-hot-toast";
|
||||
import { keccak256, stringToBytes } from 'viem'
|
||||
|
||||
import { isNetworkLegacyType } from "../../constants";
|
||||
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 getVoteValue = (forVotes, totalVotes) => {
|
||||
if (totalVotes == 0n) return 0;
|
||||
const value = forVotes * 100n / totalVotes;
|
||||
return Math.floor(Number(value.toString()));
|
||||
}
|
||||
|
||||
export const getVoteTarget = (totalVotes, totalSupply) => {
|
||||
if (totalSupply == 0n) return 80;
|
||||
|
||||
const precision = 10n ** 3n;
|
||||
const valueRaw = (totalVotes * precision) / totalSupply;
|
||||
const value = Number(valueRaw) / Number(precision);
|
||||
|
||||
const result = (5 - Math.sqrt(1 + 80/9 * (value - 0.1) )) / 4;
|
||||
return Math.floor(result * 100);
|
||||
}
|
||||
|
||||
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 { proposalCount } = useProposalCount(chainId);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const lastIndex = proposalCount === 0n ? 0n : proposalCount - 1n;
|
||||
const { proposalId } = useProposalDetailsAt(chainId, lastIndex, {
|
||||
enabled: proposalCount !== 0n
|
||||
});
|
||||
const { state } = useProposalState(chainId, proposalId, {
|
||||
enabled: proposalCount !== 0n && !!proposalId
|
||||
});
|
||||
|
||||
let threshold = new DecimalBigNumber(data ?? 0n, decimals);
|
||||
|
||||
if (proposalCount !== 0n && state !== undefined && 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(proposalId => {
|
||||
return {
|
||||
abi: GovernorStorageAbi,
|
||||
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
|
||||
functionName: "proposalDetails",
|
||||
args: [proposalId],
|
||||
scopeKey: `proposalDetails-${chainId}-${proposalId}`,
|
||||
chainId: chainId,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const { data: proposalDeadlines } = useReadContracts({
|
||||
contracts: indexes?.map((_, index) => {
|
||||
const proposalId = searchedIndexes
|
||||
? searchedIndexes?.at(index)
|
||||
: 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(index)
|
||||
: 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(index)
|
||||
: 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(index)
|
||||
: 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: proposalSnapshots?.map((proposal, index) => {
|
||||
const timepoint = proposal?.result;
|
||||
return {
|
||||
abi: GovernorAbi,
|
||||
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
|
||||
functionName: "quorum",
|
||||
args: [timepoint],
|
||||
scopeKey: `quorum-${chainId}-${index}`,
|
||||
chainId: chainId,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const { data: pastTotalSupplies } = useReadContracts({
|
||||
contracts: proposalSnapshots?.map((proposal, index) => {
|
||||
const timepoint = proposal?.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(index)
|
||||
: 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 = useMemo(() => {
|
||||
return indexes?.map((_, index) => {
|
||||
let result = { short: index + 1, full: undefined };
|
||||
const proposalId = searchedIndexes
|
||||
? searchedIndexes?.at(index)
|
||||
: proposalsDetailsAt?.at(index)?.result?.at(0);
|
||||
|
||||
if (proposalId) {
|
||||
const hash = "0x" + proposalId.toString(16);
|
||||
result.short = hash.slice(-5);
|
||||
result.full = hash;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}, [indexes, searchedIndexes, proposalsDetailsAt]);
|
||||
|
||||
const voteValues = useMemo(() => {
|
||||
return indexes?.map((_, idx) => {
|
||||
const index = indexes.length - idx - 1;
|
||||
const againstVotes = proposalVotes?.at(index)?.result?.at(0) ?? 0n
|
||||
const forVotes = proposalVotes?.at(index)?.result?.at(1) ?? 0n;
|
||||
|
||||
const totalVotes = againstVotes + forVotes;
|
||||
return getVoteValue(forVotes, totalVotes);
|
||||
}) ?? [];
|
||||
}, [indexes, proposalVotes]);
|
||||
|
||||
const voteTargets = useMemo(() => {
|
||||
return indexes?.map((_, idx) => {
|
||||
const index = indexes.length - idx - 1;
|
||||
const againstVotes = proposalVotes?.at(index)?.result?.at(0) ?? 0n
|
||||
const forVotes = proposalVotes?.at(index)?.result?.at(1) ?? 0n;
|
||||
const totalSupply = pastTotalSupplies?.at(index)?.result ?? 0n;
|
||||
|
||||
const totalVotes = againstVotes + forVotes;
|
||||
return getVoteTarget(totalVotes, totalSupply);
|
||||
}) ?? [];
|
||||
}, [indexes, proposalVotes, pastTotalSupplies]);
|
||||
|
||||
const proposalDetailsPrepared = useMemo(() => {
|
||||
if (!searchedIndexes) return proposalsDetailsAt;
|
||||
return proposalDetails?.map((obj, index) => {
|
||||
if (!obj?.result) return obj;
|
||||
const proposalId = searchedIndexes.at(index);
|
||||
return {
|
||||
...obj,
|
||||
result: [proposalId, ...obj.result],
|
||||
};
|
||||
}) ?? [];
|
||||
}, [searchedIndexes, proposalsDetailsAt, proposalDetails]);
|
||||
|
||||
const proposals = indexes?.map((_, index) => ({
|
||||
hashes: hashes?.at(index) ?? { short: "", full: "" },
|
||||
proposer: proposalProposer?.at(index)?.result,
|
||||
details: proposalDetailsPrepared?.at(index)?.result,
|
||||
deadline: proposalDeadlines?.at(index)?.result ?? 0n,
|
||||
snapshot: proposalSnapshots?.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),
|
||||
votes: proposalVotes?.at(index)?.result?.map(vote => new DecimalBigNumber(vote ?? 0n, decimals)),
|
||||
voteValue: voteValues?.at(index),
|
||||
voteTarget: voteTargets?.at(index),
|
||||
}));
|
||||
|
||||
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,
|
||||
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
|
||||
});
|
||||
|
||||
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,
|
||||
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
|
||||
});
|
||||
|
||||
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,
|
||||
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
|
||||
});
|
||||
|
||||
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,
|
||||
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,16 @@
|
||||
import {
|
||||
RESERVE_ADDRESSES,
|
||||
DAI_ADDRESSES,
|
||||
FTSO_ADDRESSES,
|
||||
STNK_ADDRESSES,
|
||||
GHST_ADDRESSES,
|
||||
FTSO_DAI_LP_ADDRESSES,
|
||||
WETH_ADDRESSES,
|
||||
} from "../constants/addresses";
|
||||
|
||||
import { tokenNameConverter } from "../helpers/tokenConverter";
|
||||
|
||||
import { abi as DaiAbi } from "../abi/Reserve.json";
|
||||
import { abi as FatsoAbi } from "../abi/Fatso.json";
|
||||
import { abi as StinkyAbi } from "../abi/Stinky.json";
|
||||
import { abi as GhostAbi } from "../abi/Ghost.json";
|
||||
import { abi as Erc20Abi } from "../abi/ERC20.json";
|
||||
import { abi as WethAbi } from "../abi/WETH9.json";
|
||||
|
||||
// TBD: should be extended on new tokens
|
||||
export const getTokenAbi = (name) => {
|
||||
@ -26,9 +22,6 @@ export const getTokenAbi = (name) => {
|
||||
case "GDAI":
|
||||
abi = DaiAbi;
|
||||
break;
|
||||
case "RESERVE":
|
||||
abi = DaiAbi;
|
||||
break;
|
||||
case "FTSO":
|
||||
abi = FatsoAbi;
|
||||
break;
|
||||
@ -47,12 +40,6 @@ export const getTokenAbi = (name) => {
|
||||
case "CSPR":
|
||||
abi = GhostAbi;
|
||||
break;
|
||||
case "WETH":
|
||||
abi = WethAbi;
|
||||
break;
|
||||
case "WMWETH":
|
||||
abi = WethAbi;
|
||||
break;
|
||||
}
|
||||
return abi;
|
||||
}
|
||||
@ -67,9 +54,6 @@ export const getTokenDecimals = (name) => {
|
||||
case "GDAI":
|
||||
decimals = 18;
|
||||
break;
|
||||
case "RESERVE":
|
||||
decimals = 18;
|
||||
break;
|
||||
case "FTSO":
|
||||
decimals = 9;
|
||||
break;
|
||||
@ -88,12 +72,6 @@ export const getTokenDecimals = (name) => {
|
||||
case "CSPR":
|
||||
decimals = 18;
|
||||
break;
|
||||
case "WETH":
|
||||
decimals = 18;
|
||||
break;
|
||||
case "WMWETH":
|
||||
decimals = 18;
|
||||
break;
|
||||
}
|
||||
return decimals;
|
||||
}
|
||||
@ -103,13 +81,10 @@ export const getTokenAddress = (chainId, name) => {
|
||||
let address = name;
|
||||
switch (name?.toUpperCase()) {
|
||||
case "DAI":
|
||||
address = RESERVE_ADDRESSES[chainId];
|
||||
address = DAI_ADDRESSES[chainId];
|
||||
break;
|
||||
case "GDAI":
|
||||
address = RESERVE_ADDRESSES[chainId];
|
||||
break;
|
||||
case "RESERVE":
|
||||
address = RESERVE_ADDRESSES[chainId];
|
||||
address = DAI_ADDRESSES[chainId];
|
||||
break;
|
||||
case "FTSO":
|
||||
address = FTSO_ADDRESSES[chainId];
|
||||
@ -132,33 +107,16 @@ export const getTokenAddress = (chainId, name) => {
|
||||
case "GDAI_FTSO":
|
||||
address = FTSO_DAI_LP_ADDRESSES[chainId];
|
||||
break;
|
||||
case "RESERVE_FTSO":
|
||||
address = FTSO_DAI_LP_ADDRESSES[chainId];
|
||||
break;
|
||||
case "WETH":
|
||||
address = WETH_ADDRESSES[chainId];
|
||||
break;
|
||||
case "WETC":
|
||||
address = WETH_ADDRESSES[chainId];
|
||||
break;
|
||||
case "WMETC":
|
||||
address = WETH_ADDRESSES[chainId];
|
||||
break;
|
||||
case "ETH":
|
||||
address = undefined;
|
||||
break;
|
||||
case "METC":
|
||||
address = undefined;
|
||||
break;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
// TBD: should be extended on new tokens
|
||||
export const getTokenIcons = (chainId, address) => {
|
||||
let icons = [""];
|
||||
switch (address) {
|
||||
case RESERVE_ADDRESSES[chainId]:
|
||||
icons = [tokenNameConverter(chainId, "WETH")];
|
||||
case DAI_ADDRESSES[chainId]:
|
||||
icons = ["GDAI"];
|
||||
break;
|
||||
case FTSO_ADDRESSES[chainId]:
|
||||
icons = ["FTSO"];
|
||||
@ -170,33 +128,27 @@ export const getTokenIcons = (chainId, address) => {
|
||||
icons = ["GHST"];
|
||||
break;
|
||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
||||
icons = ["FTSO", "WETH"];
|
||||
icons = ["FTSO", "GDAI"];
|
||||
break;
|
||||
default:
|
||||
icons = [""]
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
export const getBondNameDisplayName = (chainId, tokenAddress, baseTokenSymbol) => {
|
||||
let stringValue = tokenNameConverter(chainId, "WETH")
|
||||
export const getBondNameDisplayName = (chainId, stringValue, tokenAddress) => {
|
||||
if (tokenAddress.toUpperCase() === FTSO_DAI_LP_ADDRESSES[chainId].toUpperCase()) {
|
||||
stringValue = `${baseTokenSymbol}-${stringValue} LP`;
|
||||
stringValue = `LP ${stringValue}`;
|
||||
}
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => {
|
||||
let purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/dex/uniswap`;
|
||||
export const getTokenPurchaseLink = (chainId, tokenAddress) => {
|
||||
let purchaseUrl = "https://app.dao.ghostchain.io/#/dex/uniswap";
|
||||
switch (tokenAddress) {
|
||||
case RESERVE_ADDRESSES[chainId]:
|
||||
purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/faucet`;
|
||||
if (chainId == 63) {
|
||||
purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/wrapper`;
|
||||
}
|
||||
case DAI_ADDRESSES[chainId]:
|
||||
purchaseUrl = "https://app.dao.ghostchain.io/#/faucet";
|
||||
break;
|
||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
||||
purchaseUrl += `?pool=true&from=${RESERVE_ADDRESSES[chainId]}&to=${FTSO_ADDRESSES[chainId]}`;
|
||||
purchaseUrl += `?pool=true&from=${DAI_ADDRESSES[chainId]}&to=${FTSO_ADDRESSES[chainId]}`;
|
||||
break;
|
||||
}
|
||||
return purchaseUrl;
|
||||
|
||||