├── src ├── components │ ├── marketplace │ │ ├── MarketplaceOverview.js │ │ ├── MarketplaceOffers.js │ │ └── MarketplaceOfferCard.js │ ├── buttons │ │ ├── FooterButton.js │ │ ├── SocialIconButton.js │ │ ├── PrimaryButton.js │ │ ├── CancelButton.js │ │ ├── DetailsButton.js │ │ ├── DeclineButton.js │ │ ├── ApplyButton.js │ │ ├── ApproveButton.js │ │ └── StakingPotIcon.js │ ├── error │ │ └── ErrorHandler.js │ ├── icons │ │ ├── MoonIcon.js │ │ ├── WalletIcon.js │ │ ├── SunIcon.js │ │ ├── Pattern.js │ │ ├── BookIcon.js │ │ └── CalculatorIcon.js │ ├── menu │ │ └── DropdownMenu.js │ ├── common │ │ └── svg │ │ │ ├── CheckmarkSvg.js │ │ │ └── NotCheckmarkSvg.js │ ├── Page.js │ ├── Loadable.js │ ├── staking │ │ ├── StakingStatusTag.js │ │ ├── StakingSteps.js │ │ ├── tabs │ │ │ └── AccountTab.js │ │ ├── StakingOverview.js │ │ ├── StakingOptionCard.js │ │ └── StakingCalculatorCard.js │ ├── cards │ │ └── CardStatistics.js │ ├── HeaderBreadcrumbs.js │ ├── Logo.js │ ├── wallet │ │ ├── WalletCard.js │ │ └── WalletRewardCard.js │ └── contract │ │ └── StakingPotContracts.js ├── utils │ ├── StakingRequestType.js │ ├── StakingPotStatus.js │ ├── Math.js │ ├── web3Utils.js │ ├── uuid.js │ ├── numberFormatter.js │ └── StakingPotCalculator.js ├── connectors │ └── connectors.js ├── setupTests.js ├── hooks │ ├── useTheme.js │ ├── useCollapseDrawer.js │ ├── useCopyToClipboard.js │ └── useLocalStorage.js ├── reportWebVitals.js ├── pages │ ├── NotFoundPage.js │ └── app │ │ ├── AccountPage.js │ │ ├── DashboardPage.js │ │ ├── TransactionsPage.js │ │ ├── MarketplacePage.js │ │ ├── NotFound.js │ │ ├── StakingPage.js │ │ └── StakingPotDetailsPage.js ├── index.css ├── redux │ ├── rootReducer.js │ ├── store.js │ └── slices │ │ └── staking.js ├── routes │ ├── ProtectedRoute.js │ ├── paths.js │ └── routes.js ├── App.js ├── operations │ ├── signer.js │ ├── etnyContract.js │ └── etnyStakingContract.js ├── contexts │ ├── CollapseDrawerContext.js │ └── ThemeContext.js ├── layouts │ ├── app │ │ ├── MobileSidebarMenu.js │ │ ├── SidebarMenu.js │ │ └── Navbar.js │ └── ApplicationLayout.js ├── index.js └── contract │ └── etnyContract.js ├── .env ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── static │ ├── 10.png │ ├── hero.png │ ├── hero-logo.png │ ├── pattern-1.png │ ├── pattern-2.png │ ├── person │ │ ├── 1.jpg │ │ ├── 3.jpg │ │ └── 4.jpg │ ├── team │ │ ├── mark.jpg │ │ ├── vlad.jpg │ │ ├── daniel.jpg │ │ ├── florin.jpg │ │ ├── ioan.jpeg │ │ ├── iosif.jpg │ │ ├── andrei.jpeg │ │ ├── silviu.jpeg │ │ └── ghost.svg │ ├── world_map.png │ ├── card_etny_logo.png │ ├── dotted_pattern.png │ ├── icons │ │ ├── remove.png │ │ ├── bitcoin-wallet.png │ │ ├── book.svg │ │ └── wallet.svg │ ├── square_pattern.png │ ├── hero_logo_mobile.png │ ├── world_map_light.png │ ├── logo │ │ ├── logo_200x200.png │ │ ├── logo-white-blue.png │ │ ├── logo_etny.svg │ │ ├── logo_etny_blue.svg │ │ ├── logo_ethc_blue.svg │ │ └── logo_ethc.svg │ ├── background-blur-right.png │ ├── square_pattern_light.png │ ├── empty.svg │ ├── img_icn_contract.svg │ ├── img_icn_network.svg │ ├── img_icn_chain.svg │ └── img_icn_programing.svg ├── manifest.json └── index.html ├── postcss.config.js ├── .prettierrc ├── jsconfig.json ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── README.md ├── .eslintrc ├── package.json └── tailwind.config.js /src/components/marketplace/MarketplaceOverview.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | USE_BLOXBERG=false 2 | INFURA_API_KEY=your_infura_api_key -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/static/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/10.png -------------------------------------------------------------------------------- /public/static/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/hero.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /public/static/hero-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/hero-logo.png -------------------------------------------------------------------------------- /public/static/pattern-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/pattern-1.png -------------------------------------------------------------------------------- /public/static/pattern-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/pattern-2.png -------------------------------------------------------------------------------- /public/static/person/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/person/1.jpg -------------------------------------------------------------------------------- /public/static/person/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/person/3.jpg -------------------------------------------------------------------------------- /public/static/person/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/person/4.jpg -------------------------------------------------------------------------------- /public/static/team/mark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/mark.jpg -------------------------------------------------------------------------------- /public/static/team/vlad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/vlad.jpg -------------------------------------------------------------------------------- /public/static/world_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/world_map.png -------------------------------------------------------------------------------- /public/static/team/daniel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/daniel.jpg -------------------------------------------------------------------------------- /public/static/team/florin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/florin.jpg -------------------------------------------------------------------------------- /public/static/team/ioan.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/ioan.jpeg -------------------------------------------------------------------------------- /public/static/team/iosif.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/iosif.jpg -------------------------------------------------------------------------------- /public/static/card_etny_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/card_etny_logo.png -------------------------------------------------------------------------------- /public/static/dotted_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/dotted_pattern.png -------------------------------------------------------------------------------- /public/static/icons/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/icons/remove.png -------------------------------------------------------------------------------- /public/static/square_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/square_pattern.png -------------------------------------------------------------------------------- /public/static/team/andrei.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/andrei.jpeg -------------------------------------------------------------------------------- /public/static/team/silviu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/team/silviu.jpeg -------------------------------------------------------------------------------- /public/static/hero_logo_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/hero_logo_mobile.png -------------------------------------------------------------------------------- /public/static/world_map_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/world_map_light.png -------------------------------------------------------------------------------- /src/utils/StakingRequestType.js: -------------------------------------------------------------------------------- 1 | export const StakingRequestType = { 2 | BASE: 'Base Staking', 3 | EXTENDED: 'Extended Staking' 4 | }; 5 | -------------------------------------------------------------------------------- /public/static/logo/logo_200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/logo/logo_200x200.png -------------------------------------------------------------------------------- /src/utils/StakingPotStatus.js: -------------------------------------------------------------------------------- 1 | export const StakingPotStatus = { 2 | PENDING: 0, 3 | APPROVED: 1, 4 | DECLINED: 2, 5 | CANCELED: 3 6 | }; 7 | -------------------------------------------------------------------------------- /public/static/background-blur-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/background-blur-right.png -------------------------------------------------------------------------------- /public/static/icons/bitcoin-wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/icons/bitcoin-wallet.png -------------------------------------------------------------------------------- /public/static/logo/logo-white-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/logo/logo-white-blue.png -------------------------------------------------------------------------------- /public/static/square_pattern_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethernity-cloud/ethernity-staking-dapp/HEAD/public/static/square_pattern_light.png -------------------------------------------------------------------------------- /src/utils/Math.js: -------------------------------------------------------------------------------- 1 | export function randomIntFromInterval(min, max) { 2 | // min and max included 3 | return Math.floor(Math.random() * (max - min + 1) + min); 4 | } 5 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "baseUrl": "." 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/web3Utils.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | export const isAddress = (address) => { 4 | try { 5 | ethers.utils.getAddress(address); 6 | } catch (e) { 7 | return false; 8 | } 9 | return true; 10 | }; 11 | -------------------------------------------------------------------------------- /src/connectors/connectors.js: -------------------------------------------------------------------------------- 1 | // declare supported chains 2 | import { InjectedConnector } from '@web3-react/injected-connector'; 3 | 4 | export const injectedConnector = new InjectedConnector({ 5 | supportedChainIds: [3, 8995] /// BloxBerg 6 | }); 7 | -------------------------------------------------------------------------------- /src/utils/uuid.js: -------------------------------------------------------------------------------- 1 | export const uuidv4 = () => 2 | ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => 3 | // eslint-disable-next-line no-bitwise 4 | (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) 5 | ); 6 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/components/buttons/FooterButton.js: -------------------------------------------------------------------------------- 1 | export const FooterButton = ({ children }) => ( 2 | 6 | {children} 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/buttons/SocialIconButton.js: -------------------------------------------------------------------------------- 1 | export const SocialIconButton = ({ children }) => ( 2 | 6 | {children} 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/hooks/useTheme.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ThemeContext } from '../contexts/ThemeContext'; 3 | 4 | // ---------------------------------------------------------------------- 5 | 6 | const useTheme = () => useContext(ThemeContext); 7 | 8 | export default useTheme; 9 | -------------------------------------------------------------------------------- /src/hooks/useCollapseDrawer.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { CollapseDrawerContext } from '../contexts/CollapseDrawerContext'; 3 | 4 | // ---------------------------------------------------------------------- 5 | 6 | const useCollapseDrawer = () => useContext(CollapseDrawerContext); 7 | 8 | export default useCollapseDrawer; 9 | -------------------------------------------------------------------------------- /src/components/error/ErrorHandler.js: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | 3 | const ErrorHandler = () => ( 4 | 9 | Go to home 10 | 11 | } 12 | /> 13 | ); 14 | 15 | export default ErrorHandler; 16 | -------------------------------------------------------------------------------- /src/utils/numberFormatter.js: -------------------------------------------------------------------------------- 1 | export const formatNumber = (n) => { 2 | if (n < 1e6) return n; 3 | // if (n >= 1e3 && n < 1e6) return `${+(n / 1e3).toFixed(3)}K`; 4 | if (n >= 1e6 && n < 1e9) return `${+(n / 1e6).toFixed(3)}M`; 5 | if (n >= 1e9 && n < 1e12) return `${+(n / 1e9).toFixed(3)}B`; 6 | if (n >= 1e12) return `${+(n / 1e12).toFixed(3)}T`; 7 | 8 | return n; 9 | }; 10 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/pages/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import Page from '../components/Page'; 3 | 4 | const NotFoundPage = () => ( 5 | 6 | Back Home} 11 | /> 12 | 13 | ); 14 | 15 | export default NotFoundPage; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | package-lock.json 26 | 27 | .idea 28 | .vs 29 | -------------------------------------------------------------------------------- /src/components/icons/MoonIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MoonIcon = ({ className }) => ( 4 | 5 | 9 | 10 | ); 11 | export default MoonIcon; 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | @import "tailwindcss/utilities"; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /src/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import storage from 'redux-persist/lib/storage'; 3 | 4 | // slices 5 | import stakingReducer from './slices/staking'; 6 | 7 | // ---------------------------------------------------------------------- 8 | 9 | const rootPersistConfig = { 10 | key: 'root', 11 | storage, 12 | keyPrefix: 'redux-', 13 | whitelist: [] 14 | }; 15 | 16 | const rootReducer = combineReducers({ 17 | staking: stakingReducer 18 | }); 19 | 20 | export { rootPersistConfig, rootReducer }; 21 | -------------------------------------------------------------------------------- /src/routes/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import { useWeb3React } from '@web3-react/core'; 2 | import { Navigate } from 'react-router-dom'; 3 | import useLocalStorage from '../hooks/useLocalStorage'; 4 | 5 | export const ProtectedRoute = ({ redirectPath = '/welcome', children }) => { 6 | const [isMetamaskLoggedIn] = useLocalStorage('etny-metamask-logged-in', null); 7 | const { active } = useWeb3React(); 8 | 9 | if (!isMetamaskLoggedIn || !active) { 10 | return ; 11 | } 12 | 13 | return children; 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/menu/DropdownMenu.js: -------------------------------------------------------------------------------- 1 | import { Button, Dropdown } from 'antd'; 2 | import { EllipsisOutlined } from '@ant-design/icons'; 3 | 4 | const DropdownMenu = ({ menu }) => ( 5 | 6 | 19 | 20 | ); 21 | 22 | export default DropdownMenu; 23 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import { useWeb3React } from '@web3-react/core'; 3 | import { useEffect } from 'react'; 4 | import ApplicationLayout from './layouts/ApplicationLayout'; 5 | import { injectedConnector } from './connectors/connectors'; 6 | 7 | export default function App() { 8 | const web3React = useWeb3React(); 9 | useEffect(() => { 10 | const activate = async () => { 11 | await web3React.activate(injectedConnector, undefined, true); 12 | console.log('activated'); 13 | }; 14 | activate(); 15 | }, []); 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/common/svg/CheckmarkSvg.js: -------------------------------------------------------------------------------- 1 | export const CheckmarkSvg = () => ( 2 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/components/Page.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Helmet } from 'react-helmet-async'; 3 | import { forwardRef } from 'react'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | const Page = forwardRef(({ children, title = '', className, ...other }, ref) => ( 8 |
9 | 10 | {title} 11 | 12 | {children} 13 |
14 | )); 15 | 16 | Page.propTypes = { 17 | children: PropTypes.node.isRequired, 18 | title: PropTypes.string 19 | }; 20 | 21 | export default Page; 22 | -------------------------------------------------------------------------------- /src/operations/signer.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | const signMessage = async (library, account, message) => { 4 | const signer = library.getSigner(account); 5 | // const hexMessage = utils.hexlify(utils.toUtf8Bytes(message)); 6 | // eslint-disable-next-line no-return-await 7 | return await signer.signMessage(message); 8 | }; 9 | 10 | export const isMessageSigned = async (library, account, message) => { 11 | try { 12 | const signature = await signMessage(library, account, message); 13 | const signerAddress = ethers.utils.verifyMessage(message, signature); 14 | return signerAddress === account; 15 | } catch (err) { 16 | return false; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Loadable.js: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from 'react'; 2 | import { LoadingOutlined } from '@ant-design/icons'; 3 | 4 | import { Spin } from 'antd'; 5 | 6 | const loadingIndicator = ; 7 | const loading = () => ( 8 |
9 | 10 |
11 | ); 12 | 13 | const Loadable = (Component) => (props) => 14 | ( 15 | // eslint-disable-next-line react-hooks/rules-of-hooks 16 | {loading()}}> 17 | 18 | 19 | ); 20 | 21 | export default Loadable; 22 | -------------------------------------------------------------------------------- /src/components/staking/StakingStatusTag.js: -------------------------------------------------------------------------------- 1 | import { Tag } from 'antd'; 2 | import PropTypes from 'prop-types'; 3 | import { StakingPotStatus } from '../../utils/StakingPotStatus'; 4 | 5 | const StakingStatusTag = ({ status }) => ( 6 | <> 7 | {status === StakingPotStatus.PENDING && PENDING} 8 | {status === StakingPotStatus.APPROVED && APPROVED} 9 | {status === StakingPotStatus.DECLINED && DECLINED} 10 | {status === StakingPotStatus.CANCELED && CANCELED} 11 | 12 | ); 13 | StakingStatusTag.propTypes = { 14 | status: PropTypes.number 15 | }; 16 | export default StakingStatusTag; 17 | -------------------------------------------------------------------------------- /src/routes/paths.js: -------------------------------------------------------------------------------- 1 | const path = (root, sublink) => `${root}${sublink}`; 2 | 3 | const ROOTS_AUTH = '/auth'; 4 | const ROOTS_APP = '/'; 5 | 6 | export const PATH_AUTH = { 7 | root: ROOTS_AUTH, 8 | login: path(ROOTS_AUTH, '/login'), 9 | loginUnprotected: path(ROOTS_AUTH, '/login-unprotected'), 10 | register: path(ROOTS_AUTH, '/register'), 11 | registerUnprotected: path(ROOTS_AUTH, '/register-unprotected'), 12 | resetPassword: path(ROOTS_AUTH, '/reset-password'), 13 | verify: path(ROOTS_AUTH, '/verify') 14 | }; 15 | 16 | export const PATH_APP = { 17 | root: ROOTS_APP, 18 | staking: { 19 | root: path(ROOTS_APP, '/staking'), 20 | create: path(ROOTS_APP, '/staking/create') 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/common/svg/NotCheckmarkSvg.js: -------------------------------------------------------------------------------- 1 | export const NotCheckmarkSvg = () => ( 2 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/components/buttons/PrimaryButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const PrimaryButton = ({ className, isSubmitButton, icon, label, onClick }) => ( 5 | 15 | ); 16 | 17 | PrimaryButton.propTypes = { 18 | className: PropTypes.string, 19 | isSubmitButton: PropTypes.bool, 20 | icon: PropTypes.element, 21 | label: PropTypes.string, 22 | onClick: PropTypes.func 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/buttons/CancelButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { CloseOutlined } from '@ant-design/icons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export const CancelButton = ({ className, onCancel, hasIcon }) => ( 6 | 15 | ); 16 | CancelButton.propTypes = { 17 | className: PropTypes.string, 18 | hasIcon: PropTypes.bool, 19 | onCancel: PropTypes.func 20 | }; 21 | CancelButton.defaultProps = { 22 | hasIcon: true 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/buttons/DetailsButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { RightOutlined } from '@ant-design/icons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export const DetailsButton = ({ className, onDetails, hasIcon }) => ( 6 | 15 | ); 16 | DetailsButton.propTypes = { 17 | className: PropTypes.string, 18 | hasIcon: PropTypes.bool, 19 | onDetails: PropTypes.func 20 | }; 21 | DetailsButton.defaultProps = { 22 | hasIcon: true 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/buttons/DeclineButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { StopOutlined } from '@ant-design/icons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export const DeclineButton = ({ className, onDecline, hasIcon }) => ( 6 | 15 | ); 16 | DeclineButton.propTypes = { 17 | className: PropTypes.string, 18 | hasIcon: PropTypes.bool, 19 | onDecline: PropTypes.func 20 | }; 21 | DeclineButton.defaultProps = { 22 | hasIcon: true 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/cards/CardStatistics.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const CardStatistics = ({ label, value, valuePrefix, valueSuffix, hasTextOnRight }) => ( 4 |
5 | {label} 6 |

7 | {valuePrefix} 8 | {value} 9 | {valueSuffix}{' '} 10 |

11 |
12 | ); 13 | 14 | CardStatistics.propTypes = { 15 | label: PropTypes.string, 16 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 17 | valuePrefix: PropTypes.string, 18 | valueSuffix: PropTypes.string, 19 | hasTextOnRight: PropTypes.bool 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/buttons/ApplyButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { CheckCircleOutlined } from '@ant-design/icons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export const ApplyButton = ({ className, onApply, hasIcon }) => ( 6 | 15 | ); 16 | 17 | ApplyButton.propTypes = { 18 | className: PropTypes.string, 19 | hasIcon: PropTypes.bool, 20 | onApprove: PropTypes.func 21 | }; 22 | ApplyButton.defaultProps = { 23 | hasIcon: true 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/buttons/ApproveButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { CheckCircleOutlined } from '@ant-design/icons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export const ApproveButton = ({ className, onApprove, hasIcon }) => ( 6 | 15 | ); 16 | 17 | ApproveButton.propTypes = { 18 | className: PropTypes.string, 19 | hasIcon: PropTypes.bool, 20 | onApprove: PropTypes.func 21 | }; 22 | ApproveButton.defaultProps = { 23 | hasIcon: true 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/HeaderBreadcrumbs.js: -------------------------------------------------------------------------------- 1 | import { Breadcrumb } from 'antd'; 2 | import { HomeOutlined } from '@ant-design/icons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | HeaderBreadcrumbs.propTypes = { 6 | links: PropTypes.array, 7 | action: PropTypes.node 8 | }; 9 | 10 | export default function HeaderBreadcrumbs({ links, action, ...other }) { 11 | return ( 12 |
13 | /}> 14 | 15 | 16 | 17 | {links.map((link, index) => ( 18 | 19 | {link.name} 20 | 21 | ))} 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: BUG 5 | labels: bug 6 | assignees: odyss3um, varvarasd 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | 34 | **Bug-bounty required information (optional)** 35 | Please provide your email address if you wish to join the bug-bounty 36 | -------------------------------------------------------------------------------- /src/contexts/CollapseDrawerContext.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { createContext, useState } from 'react'; 3 | 4 | // ---------------------------------------------------------------------- 5 | 6 | const initialState = { 7 | onToggleCollapse: () => {} 8 | }; 9 | 10 | const CollapseDrawerContext = createContext(initialState); 11 | 12 | CollapseDrawerProvider.propTypes = { 13 | children: PropTypes.node 14 | }; 15 | 16 | function CollapseDrawerProvider({ children }) { 17 | const [collapsed, setCollapsed] = useState(false); 18 | 19 | const handleToggleCollapse = () => { 20 | setCollapsed(!collapsed); 21 | }; 22 | 23 | return ( 24 | 30 | {children} 31 | 32 | ); 33 | } 34 | 35 | export { CollapseDrawerProvider, CollapseDrawerContext }; 36 | -------------------------------------------------------------------------------- /src/hooks/useCopyToClipboard.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function useCopyToClipboard() { 4 | const [copiedText, setCopiedText] = useState(null); 5 | 6 | const copy = async (text) => { 7 | if (!navigator?.clipboard) { 8 | console.warn('Clipboard not supported'); 9 | return false; 10 | } 11 | 12 | // Try to save to clipboard then save it in the state if worked 13 | try { 14 | const permissionsResult = await navigator.permissions.query({ name: 'clipboard-write' }); 15 | if (permissionsResult.state === 'granted' || permissionsResult.state === 'prompt') { 16 | /* write to the clipboard now */ 17 | await navigator.clipboard.writeText(text); 18 | setCopiedText(text); 19 | return true; 20 | } 21 | return false; 22 | } catch (error) { 23 | console.warn('Copy failed', error); 24 | setCopiedText(null); 25 | return false; 26 | } 27 | }; 28 | 29 | return [copiedText, copy]; 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethernity Staking dApp 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm install` 8 | 9 | Run this command in order to install all project dependencies 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.\ 14 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 15 | 16 | The page will reload when you make changes.\ 17 | You may also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.\ 22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.\ 25 | Your app is ready to be deployed! 26 | 27 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 28 | 29 | ### Disclamer 30 | The Staking Dapp is a work in progress and it's not meant to be installed by the community. 31 | -------------------------------------------------------------------------------- /public/static/icons/book.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/icons/wallet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/app/MobileSidebarMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { useLocation, Link } from 'react-router-dom'; 3 | import { Layout, Menu } from 'antd'; 4 | import Icon from '@ant-design/icons'; 5 | 6 | import { authRoutes } from '../../routes/routes'; 7 | 8 | const MobileSidebarMenu = ({ className, onMenuItemSelect }) => { 9 | const location = useLocation(); 10 | 11 | return ( 12 | 13 | 14 | {authRoutes.map((route, index) => 15 | route.visible ? ( 16 | 17 | 18 | 19 | {route.name} 20 | 21 | 22 | ) : ( 23 | 24 | ) 25 | )} 26 | 27 | 28 | ); 29 | }; 30 | export default MobileSidebarMenu; 31 | -------------------------------------------------------------------------------- /src/operations/etnyContract.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { contract } from '../contract/etnyContract'; 3 | 4 | class EtnyContract { 5 | library = null; 6 | 7 | etnyContract = null; 8 | 9 | ethereum = null; 10 | 11 | constructor(library) { 12 | this.library = library; 13 | this.etnyContract = new ethers.Contract(contract.address, contract.abi, library); 14 | this.ethereum = window.ethereum; 15 | } 16 | 17 | getProvider() { 18 | const provider = new ethers.providers.Web3Provider(this.ethereum, 'any'); 19 | const { provider: ethereum } = provider; 20 | return ethereum; 21 | } 22 | 23 | async getBalance(account) { 24 | try { 25 | const balance = await this.etnyContract.balanceOf(account); 26 | 27 | // convert a currency unit from wei to ether 28 | const balanceFormatted = ethers.utils.formatEther(balance); 29 | 30 | console.log(`balance: ${balanceFormatted} ETNY`); 31 | 32 | return balanceFormatted; 33 | } catch (ex) { 34 | return ex.message; 35 | } 36 | } 37 | } 38 | 39 | export default EtnyContract; 40 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { useDispatch as useReduxDispatch, useSelector as useReduxSelector } from 'react-redux'; 2 | import { applyMiddleware, createStore } from 'redux'; 3 | import { persistStore, persistReducer } from 'redux-persist'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | 6 | // 7 | import { rootPersistConfig, rootReducer } from './rootReducer'; 8 | 9 | // ---------------------------------------------------------------------- 10 | // 11 | // const store = configureStore({ 12 | // reducer: persistReducer(rootPersistConfig, rootReducer), 13 | // middleware: getDefaultMiddleware({ 14 | // serializableCheck: false, 15 | // immutableCheck: false 16 | // }) 17 | // }); 18 | 19 | const composedEnhancer = applyMiddleware(thunkMiddleware); 20 | const persistedReducer = persistReducer(rootPersistConfig, rootReducer); 21 | const store = createStore(persistedReducer, composedEnhancer); 22 | const persistor = persistStore(store); 23 | 24 | const useSelector = useReduxSelector; 25 | 26 | const useDispatch = () => useReduxDispatch(); 27 | 28 | export { store, persistor, useSelector, useDispatch }; 29 | -------------------------------------------------------------------------------- /src/pages/app/AccountPage.js: -------------------------------------------------------------------------------- 1 | import { Button, Menu, PageHeader, Space, Tag } from 'antd'; 2 | import { FaArrowDown, FaArrowUp, FaCoins } from 'react-icons/fa'; 3 | import Page from '../../components/Page'; 4 | import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs'; 5 | 6 | export default function AccountPage() { 7 | return ( 8 | 9 | 10 | Account} 12 | extra={[ 13 | , 19 | , 25 | 31 | ]} 32 | /> 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/icons/WalletIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const WalletIcon = () => ( 4 | 15 | 21 | 22 | ); 23 | export default WalletIcon; 24 | -------------------------------------------------------------------------------- /src/pages/app/DashboardPage.js: -------------------------------------------------------------------------------- 1 | import { Button, Menu, PageHeader, Space, Tag } from 'antd'; 2 | import { FaArrowDown, FaArrowUp, FaCoins } from 'react-icons/fa'; 3 | import Page from '../../components/Page'; 4 | import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs'; 5 | 6 | export default function DashboardPage() { 7 | return ( 8 | 9 | 10 | Dashboard} 12 | extra={[ 13 | , 19 | , 25 | 31 | ]} 32 | /> 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/app/TransactionsPage.js: -------------------------------------------------------------------------------- 1 | import { Button, Menu, PageHeader, Space, Tag } from 'antd'; 2 | import { FaArrowDown, FaArrowUp, FaCoins } from 'react-icons/fa'; 3 | import Page from '../../components/Page'; 4 | import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs'; 5 | 6 | export default function TransactionsPage() { 7 | return ( 8 | 9 | 10 | Transactions} 12 | extra={[ 13 | , 19 | , 25 | 31 | ]} 32 | /> 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /public/static/logo/logo_etny.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/static/logo/logo_etny_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/icons/SunIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SunIcon = ({ className }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 23 | 27 | 28 | ); 29 | export default SunIcon; 30 | -------------------------------------------------------------------------------- /src/components/marketplace/MarketplaceOffers.js: -------------------------------------------------------------------------------- 1 | import { Col, Row } from 'antd'; 2 | import MarketplaceOfferCard from './MarketplaceOfferCard'; 3 | import MarketplaceOfferCardV1 from './MarketplaceOfferCardV1'; 4 | 5 | const MarketplaceOffers = ({ status, className }) => ( 6 |
7 | 8 | {Array(30) 9 | .fill() 10 | .map((i, index) => ( 11 | 12 | 27 | {/* */} 28 | 29 | ))} 30 | 31 |
32 | ); 33 | export default MarketplaceOffers; 34 | -------------------------------------------------------------------------------- /src/components/icons/Pattern.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Pattern = () => ( 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 | 21 | 22 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | export default Pattern; 37 | -------------------------------------------------------------------------------- /src/contexts/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { createContext, useEffect } from 'react'; 3 | import useLocalStorage from '../hooks/useLocalStorage'; 4 | 5 | // ---------------------------------------------------------------------- 6 | 7 | const initialState = { 8 | onThemeChange: () => {} 9 | }; 10 | 11 | const ThemeContext = createContext(initialState); 12 | 13 | ThemeProvider.propTypes = { 14 | children: PropTypes.node 15 | }; 16 | 17 | function ThemeProvider({ children }) { 18 | const [theme, setTheme] = useLocalStorage('etny-theme', 'light'); 19 | const THEME_LIGHT = 'light'; 20 | const THEME_DARK = 'dark'; 21 | 22 | useEffect(() => { 23 | handleThemeChange(theme); 24 | }, []); 25 | 26 | const handleThemeChange = (theme) => { 27 | setTheme(theme); 28 | if (theme === 'dark') { 29 | document.documentElement.classList.add('dark'); 30 | } else { 31 | document.documentElement.classList.remove('dark'); 32 | } 33 | }; 34 | 35 | return ( 36 | 44 | {children} 45 | 46 | ); 47 | } 48 | 49 | export { ThemeProvider, ThemeContext }; 50 | -------------------------------------------------------------------------------- /src/components/icons/BookIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const BookIcon = () => ( 4 | 13 | 14 | 18 | 22 | 23 | 24 | ); 25 | export default BookIcon; 26 | -------------------------------------------------------------------------------- /public/static/logo/logo_ethc_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | // Hook 2 | import { useState } from 'react'; 3 | 4 | export default function useLocalStorage(key, initialValue) { 5 | // State to store our value 6 | // Pass initial state function to useState so logic is only executed once 7 | const [storedValue, setStoredValue] = useState(() => { 8 | if (typeof window === 'undefined') { 9 | return initialValue; 10 | } 11 | try { 12 | // Get from local storage by key 13 | const item = window.localStorage.getItem(key); 14 | // Parse stored json or if none return initialValue 15 | return item !== 'undefined' && item !== null ? JSON.parse(item) : initialValue; 16 | } catch (error) { 17 | // If error also return initialValue 18 | console.log(error); 19 | return initialValue; 20 | } 21 | }); 22 | // Return a wrapped version of useState's setter function that ... 23 | // ... persists the new value to localStorage. 24 | const setValue = (value) => { 25 | try { 26 | // Allow value to be a function so we have same API as useState 27 | const valueToStore = value instanceof Function ? value(storedValue) : value; 28 | // Save state 29 | setStoredValue(valueToStore); 30 | // Save to local storage 31 | if (typeof window !== 'undefined') { 32 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 33 | } 34 | } catch (error) { 35 | // A more advanced implementation would handle the error case 36 | console.log(error); 37 | } 38 | }; 39 | return [storedValue, setValue]; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/staking/StakingSteps.js: -------------------------------------------------------------------------------- 1 | import { Button, Steps, message } from 'antd'; 2 | import { useState } from 'react'; 3 | 4 | const { Step } = Steps; 5 | 6 | const steps = [ 7 | { 8 | title: 'First', 9 | content: 'First-content' 10 | }, 11 | { 12 | title: 'Second', 13 | content: 'Second-content' 14 | }, 15 | { 16 | title: 'Last', 17 | content: 'Last-content' 18 | } 19 | ]; 20 | const StakingSteps = ({ ...props }) => { 21 | const [current, setCurrent] = useState(0); 22 | 23 | const next = () => { 24 | setCurrent(current + 1); 25 | }; 26 | 27 | const prev = () => { 28 | setCurrent(current - 1); 29 | }; 30 | 31 | return ( 32 | <> 33 | 34 | {steps.map((item) => ( 35 | 36 | ))} 37 | 38 |
{steps[current].content}
39 |
40 | {current < steps.length - 1 && ( 41 | 44 | )} 45 | {current === steps.length - 1 && ( 46 | 49 | )} 50 | {current > 0 && ( 51 | 54 | )} 55 |
56 | 57 | ); 58 | }; 59 | 60 | export default StakingSteps; 61 | -------------------------------------------------------------------------------- /src/components/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useTheme from '../hooks/useTheme'; 3 | 4 | const Logo = () => { 5 | const { theme, THEME_LIGHT } = useTheme(); 6 | 7 | return ( 8 |
9 | 17 | 18 | 27 | 34 | 35 | 36 | 37 | Ethernity Cloud 38 |
39 | ); 40 | }; 41 | 42 | export default Logo; 43 | -------------------------------------------------------------------------------- /src/components/staking/tabs/AccountTab.js: -------------------------------------------------------------------------------- 1 | import { Col, Row, Tabs } from 'antd'; 2 | import { ArrowUpOutlined } from '@ant-design/icons'; 3 | import WalletCard from '../../wallet/WalletCard'; 4 | import StakingOffers from './StakingOffers'; 5 | import { StakingPotStatus } from '../../../utils/StakingPotStatus'; 6 | 7 | const { TabPane } = Tabs; 8 | 9 | const AccountTab = () => ( 10 |
11 | 12 | 13 | } 17 | suffix="ETNY" 18 | className="bg-[#BEECFF]" 19 | actionLabel="Refresh" 20 | /> 21 | 22 | 23 | } 27 | suffix="ETNY" 28 | className="bg-[#BEECFF]" 29 | actionLabel="Refresh" 30 | /> 31 | 32 | 33 | } 37 | suffix="ETNY" 38 | className="bg-[#FFC7BA]" 39 | actionLabel="Refresh" 40 | /> 41 | 42 | 43 | } 47 | value={2322.3} 48 | suffix="ETNY" 49 | className="bg-[#DEF0BF]" 50 | actionLabel="Refresh" 51 | /> 52 | 53 | 54 |
55 | ); 56 | 57 | AccountTab.propTypes = {}; 58 | 59 | AccountTab.defaultProps = {}; 60 | export default AccountTab; 61 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb", "prettier", "plugin:jsx-a11y/recommended", "plugin:react-hooks/recommended"], 8 | "plugins": ["prettier", "react", "react-hooks", "jsx"], 9 | "parser": "@babel/eslint-parser", 10 | "parserOptions": { 11 | "ecmaVersion": 8, 12 | "requireConfigFile": false, 13 | "ecmaFeatures": { 14 | "experimentalObjectRestSpread": true, 15 | "impliedStrict": true, 16 | "jsx": true 17 | } 18 | }, 19 | "rules": { 20 | "import": 0, 21 | "max-len": 0, 22 | "no-alert": 0, 23 | "no-shadow": 0, 24 | "no-console": 0, 25 | "comma-dangle": 0, 26 | "import/no-cycle": 0, 27 | "react/prop-types": 1, 28 | "no-return-assign": 0, 29 | "consistent-return": 1, 30 | "no-param-reassign": 0, 31 | "react/display-name": 0, 32 | "no-use-before-define": 0, 33 | "no-underscore-dangle": 0, 34 | "react/button-has-type": 1, 35 | "react/no-children-prop": 0, 36 | "react/forbid-prop-types": 0, 37 | "jsx-a11y/anchor-is-valid": 0, 38 | "react/react-in-jsx-scope": 0, 39 | "react/no-array-index-key": 0, 40 | "react/no-unused-prop-types": 1, 41 | "react/require-default-props": 0, 42 | "react/no-unescaped-entities": 0, 43 | "import/prefer-default-export": 0, 44 | "react/jsx-props-no-spreading": 0, 45 | "react/jsx-filename-extension": 0, 46 | "react/destructuring-assignment": 0, 47 | "import/no-extraneous-dependencies": 0, 48 | "react/jsx-key": 1, 49 | "react-hooks/rules-of-hooks": 2, 50 | "no-unused-vars": [ 51 | 1, 52 | { 53 | "ignoreRestSiblings": false 54 | } 55 | ], 56 | "prettier/prettier": [ 57 | 2, 58 | { 59 | "printWidth": 120, 60 | "singleQuote": true, 61 | "trailingComma": "none", 62 | // "tabWidth": 2, 63 | "endOfLine":"auto" 64 | } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { HelmetProvider } from 'react-helmet-async'; 6 | import { Web3ReactProvider } from '@web3-react/core'; 7 | import { Web3Provider } from '@ethersproject/providers'; 8 | import { Provider as ReduxProvider } from 'react-redux'; 9 | import { PersistGate } from 'redux-persist/integration/react'; 10 | import { ErrorBoundary } from 'react-error-boundary'; 11 | import reportWebVitals from './reportWebVitals'; 12 | import { CollapseDrawerProvider } from './contexts/CollapseDrawerContext'; 13 | import { ThemeProvider } from './contexts/ThemeContext'; 14 | 15 | import App from './App'; 16 | 17 | // redux 18 | import { persistor, store } from './redux/store'; 19 | import ErrorHandler from './components/error/ErrorHandler'; 20 | 21 | const getLibrary = (provider) => new Web3Provider(provider); 22 | 23 | ReactDOM.render( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | , 43 | document.getElementById('root') 44 | ); 45 | 46 | // If you want to start measuring performance in your app, pass a function 47 | // to log results (for example: reportWebVitals(console.log)) 48 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 49 | reportWebVitals(); 50 | -------------------------------------------------------------------------------- /src/layouts/app/SidebarMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useLocation, Link } from 'react-router-dom'; 4 | import { Layout, Menu } from 'antd'; 5 | import Icon from '@ant-design/icons'; 6 | 7 | import { useWeb3React } from '@web3-react/core'; 8 | import { authRoutes } from '../../routes/routes'; 9 | import useCollapseDrawer from '../../hooks/useCollapseDrawer'; 10 | import useTheme from '../../hooks/useTheme'; 11 | 12 | export default function SidebarMenu({ className }) { 13 | const location = useLocation(); 14 | const { isCollapsed } = useCollapseDrawer(); 15 | const { theme } = useTheme(); 16 | const { active } = useWeb3React(); 17 | 18 | if (!active) { 19 | return <>; 20 | } 21 | if (active) { 22 | return ( 23 | 28 | 34 | {authRoutes.map((route, index) => 35 | route.visible && !route.welcome ? ( 36 | 37 | 38 | 39 | {route.name} 40 | 41 | 42 | ) : ( 43 | 44 | ) 45 | )} 46 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | SidebarMenu.propTypes = { 53 | user: PropTypes.object, 54 | location: PropTypes.object 55 | }; 56 | -------------------------------------------------------------------------------- /src/pages/app/MarketplacePage.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Button, DatePicker, PageHeader, Space, Tabs } from 'antd'; 3 | import { ReloadOutlined, ShoppingCartOutlined } from '@ant-design/icons'; 4 | import Page from '../../components/Page'; 5 | import StakingOffers from '../../components/staking/tabs/StakingOffers'; 6 | import { StakingPotStatus } from '../../utils/StakingPotStatus'; 7 | 8 | const { RangePicker } = DatePicker; 9 | 10 | const MarketplacePage = () => { 11 | const [stakingDrawerVisible, setStakingDrawerVisible] = useState(false); 12 | 13 | const onCreateStake = () => { 14 | setStakingDrawerVisible(true); 15 | }; 16 | 17 | const onDrawerClosed = () => { 18 | setStakingDrawerVisible(false); 19 | }; 20 | 21 | return ( 22 | 23 | {/* */} 24 | 27 | 28 | Marketplace Overview 29 | 30 | } 31 | extra={[ 32 | , 33 | 47 | ]} 48 | footer={ 49 | 50 | } 51 | /> 52 | 53 | ); 54 | }; 55 | export default MarketplacePage; 56 | -------------------------------------------------------------------------------- /src/layouts/ApplicationLayout.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Suspense } from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import { Drawer, Layout, Spin } from 'antd'; 4 | import { LoadingOutlined } from '@ant-design/icons'; 5 | import { authRoutes } from '../routes/routes'; 6 | import Navbar from './app/Navbar'; 7 | import MobileSidebarMenu from './app/MobileSidebarMenu'; 8 | 9 | const { Content } = Layout; 10 | 11 | const loadingIndicator = ; 12 | const loading = () => ( 13 |
14 | 15 |
16 | ); 17 | 18 | const ApplicationLayout = () => { 19 | const [mobileMenuVisible, setMobileMenuVisible] = useState(false); 20 | 21 | const onDrawerClosed = () => { 22 | setMobileMenuVisible(false); 23 | }; 24 | 25 | return ( 26 | 27 | setMobileMenuVisible(!mobileMenuVisible)} /> 28 | 29 | {/* */} 30 | 40 | 41 | 42 | 43 | 44 | 45 | {authRoutes.map((route, index) => ( 46 | 47 | ))} 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | export default ApplicationLayout; 56 | -------------------------------------------------------------------------------- /src/routes/routes.js: -------------------------------------------------------------------------------- 1 | import { AreaChartOutlined, BankOutlined, ShoppingCartOutlined } from '@ant-design/icons'; 2 | import { Navigate } from 'react-router-dom'; 3 | import NotFoundPage from '../pages/NotFoundPage'; 4 | import StakingPage from '../pages/app/StakingPage'; 5 | import MarketplacePage from '../pages/app/MarketplacePage'; 6 | import WelcomePage from '../pages/WelcomePage'; 7 | import { ProtectedRoute } from './ProtectedRoute'; 8 | import StakingPotDetailsPage from '../pages/app/StakingPotDetailsPage'; 9 | 10 | const authRoutes = [ 11 | { 12 | exact: true, 13 | path: '/welcome', 14 | name: 'Welcome', 15 | icon: AreaChartOutlined, 16 | element: , 17 | visible: false 18 | }, 19 | { 20 | path: '/staking', 21 | name: 'Staking', 22 | icon: BankOutlined, 23 | element: ( 24 | 25 | 26 | 27 | ), 28 | visible: true 29 | }, 30 | { 31 | path: '/staking/:id', 32 | name: 'Staking pot details', 33 | icon: BankOutlined, 34 | element: ( 35 | 36 | 37 | 38 | ), 39 | visible: false 40 | }, 41 | { 42 | path: '/marketplace', 43 | name: 'Marketplace', 44 | icon: ShoppingCartOutlined, 45 | element: ( 46 | 47 | 48 | 49 | ), 50 | visible: true 51 | }, 52 | { path: '404', name: '404', element: }, 53 | { path: '*', element: } 54 | ]; 55 | 56 | const welcomeRoutes = [ 57 | { 58 | exact: true, 59 | path: '/welcome', 60 | name: 'Welcome', 61 | icon: AreaChartOutlined, 62 | element: , 63 | visible: false 64 | }, 65 | { path: '404', name: '404', element: }, 66 | { path: '*', element: } 67 | ]; 68 | // const StakingPage = Loadable(lazy(() => import('../pages/app/StakingPage'))); 69 | 70 | export { authRoutes, welcomeRoutes }; 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 32 | Ethernity Staking 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/staking/StakingOverview.js: -------------------------------------------------------------------------------- 1 | import { Button, Card, Col, Row, Statistic } from 'antd'; 2 | import { ArrowUpOutlined } from '@ant-design/icons'; 3 | import WalletCard from '../wallet/WalletCard'; 4 | import MarketplaceOfferCardV1 from '../marketplace/MarketplaceOfferCardV1'; 5 | import { randomIntFromInterval } from '../../utils/Math'; 6 | import { useSelector } from '../../redux/store'; 7 | 8 | const statuses = ['PENDING', 'APPROVED', 'DECLINED']; 9 | const pools = ['GOLD', 'PLATINUM', 'DIAMOND']; 10 | const StakingOverview = ({ ...props }) => { 11 | const { error, isLoading, stakes } = useSelector((state) => state.staking); 12 | 13 | return ( 14 | <> 15 | 16 | 17 |
18 | 19 | {stakes.map((stake, index) => ( 20 | 21 | 45 | 46 | ))} 47 | 48 |
49 | 50 |
51 | 52 | ); 53 | }; 54 | 55 | export default StakingOverview; 56 | -------------------------------------------------------------------------------- /src/redux/slices/staking.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | isLoading: false, 5 | error: false, 6 | stakes: [] 7 | }; 8 | 9 | export const slice = createSlice({ 10 | name: 'staking', 11 | initialState, 12 | reducers: { 13 | // START LOADING 14 | startLoading: (state) => { 15 | state.isLoading = true; 16 | state.error = false; 17 | }, 18 | // HAS ERROR 19 | hasError: (state, action) => { 20 | state.isLoading = false; 21 | state.error = action.payload !== null; 22 | }, 23 | // GET STAKES 24 | getStakesSuccess(state, action) { 25 | state.isLoading = false; 26 | state.error = false; 27 | state.stakes = action.payload || []; 28 | }, 29 | // GET STAKES 30 | createStakeSuccess(state, action) { 31 | state.isLoading = false; 32 | state.error = false; 33 | console.log(action.payload); 34 | state.stakes.push(action.payload); 35 | }, 36 | updateStakingPot(state, action) { 37 | state.isLoading = false; 38 | state.error = false; 39 | const { id, status } = action.payload; 40 | const stake = state.stakes.find((s) => s.id === id); 41 | if (stake) stake.status = status; 42 | } 43 | } 44 | }); 45 | 46 | // Action creators are generated for each case reducer function 47 | export const { updateStakingPot } = slice.actions; 48 | 49 | // Reducer 50 | export default slice.reducer; 51 | 52 | // ---------------------------------------------------------------------- 53 | export function getStakes() { 54 | return async (dispatch) => { 55 | dispatch(slice.actions.startLoading()); 56 | try { 57 | dispatch(slice.actions.getStakesSuccess([])); 58 | } catch (error) { 59 | dispatch(slice.actions.hasError(error)); 60 | } 61 | }; 62 | } 63 | 64 | export function createStake(stake) { 65 | return async (dispatch) => { 66 | dispatch(slice.actions.startLoading()); 67 | try { 68 | dispatch(slice.actions.createStakeSuccess(stake)); 69 | } catch (error) { 70 | dispatch(slice.actions.hasError(error)); 71 | } 72 | }; 73 | } 74 | 75 | export function updateStakeStatus(status, id) { 76 | return async (dispatch) => { 77 | dispatch(slice.actions.startLoading()); 78 | try { 79 | dispatch(slice.actions.updateStakingPot(status, id)); 80 | } catch (error) { 81 | dispatch(slice.actions.hasError(error)); 82 | } 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethernity-staking", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject", 10 | "lint": "eslint --ext .js,.jsx ./src", 11 | "lint:fix": "eslint --fix --ext .js,.jsx ./src" 12 | }, 13 | "eslintConfig": { 14 | "extends": [ 15 | "react-app", 16 | "react-app/jest" 17 | ] 18 | }, 19 | "babel": { 20 | "presets": [ 21 | "@babel/preset-react" 22 | ] 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | }, 36 | "dependencies": { 37 | "@ant-design/icons": "^4.7.0", 38 | "@ethersproject/providers": "^5.6.0", 39 | "@reduxjs/toolkit": "^1.8.0", 40 | "@testing-library/jest-dom": "^5.16.2", 41 | "@testing-library/react": "^12.1.3", 42 | "@testing-library/user-event": "^13.5.0", 43 | "@web3-react/core": "^6.1.9", 44 | "@web3-react/injected-connector": "^6.0.7", 45 | "antd": "^4.18.9", 46 | "ethers": "^5.5.4", 47 | "moment": "^2.29.1", 48 | "prop-types": "^15.8.1", 49 | "react": "^17.0.2", 50 | "react-device-detect": "^2.1.2", 51 | "react-dom": "^17.0.2", 52 | "react-error-boundary": "^3.1.4", 53 | "react-helmet": "^6.1.0", 54 | "react-helmet-async": "^1.2.3", 55 | "react-icons": "^4.3.1", 56 | "react-qrcode-logo": "^2.7.0", 57 | "react-redux": "^7.2.6", 58 | "react-router-dom": "^6.2.2", 59 | "react-scripts": "5.0.0", 60 | "redux": "^4.1.2", 61 | "redux-persist": "^6.0.0", 62 | "redux-thunk": "^2.4.1", 63 | "web-vitals": "^2.1.4" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.15.5", 67 | "@babel/eslint-parser": "^7.15.4", 68 | "autoprefixer": "^10.4.2", 69 | "babylon": "^6.18.0", 70 | "eslint": "^7.32.0", 71 | "eslint-config-airbnb": "^18.2.1", 72 | "eslint-config-prettier": "^8.3.0", 73 | "eslint-config-react-app": "^6.0.0", 74 | "eslint-plugin-flowtype": "^6.0.1", 75 | "eslint-plugin-import": "^2.24.2", 76 | "eslint-plugin-jsx": "^0.1.0", 77 | "eslint-plugin-jsx-a11y": "^6.4.1", 78 | "eslint-plugin-prettier": "^4.0.0", 79 | "eslint-plugin-react": "^7.25.2", 80 | "eslint-plugin-react-hooks": "^4.2.0", 81 | "postcss": "^8.4.7", 82 | "prettier": "^2.4.1", 83 | "tailwindcss": "^3.0.23" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /public/static/empty.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/buttons/StakingPotIcon.js: -------------------------------------------------------------------------------- 1 | export const StakingPotIcon = ({ label }) => ( 2 |
3 | 4 | 8 | 12 | 16 | 20 | 21 | {label} 22 |
23 | ); 24 | -------------------------------------------------------------------------------- /public/static/img_icn_contract.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/pages/app/NotFound.js: -------------------------------------------------------------------------------- 1 | import { PageHeader, Menu, Dropdown, Button, Tag, Typography, Row } from 'antd'; 2 | import { EllipsisOutlined } from '@ant-design/icons'; 3 | import Page from '../../components/Page'; 4 | import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs'; 5 | import DropdownMenu from '../../components/menu/DropdownMenu'; 6 | 7 | const { Paragraph } = Typography; 8 | 9 | const menu = ( 10 | 11 | 12 | 13 | 1st menu item 14 | 15 | 16 | 17 | 18 | 2nd menu item 19 | 20 | 21 | 22 | 23 | 3rd menu item 24 | 25 | 26 | 27 | ); 28 | 29 | const routes = [ 30 | { 31 | path: 'index', 32 | breadcrumbName: 'First-level Menu' 33 | }, 34 | { 35 | path: 'first', 36 | breadcrumbName: 'Second-level Menu' 37 | }, 38 | { 39 | path: 'second', 40 | breadcrumbName: 'Third-level Menu' 41 | } 42 | ]; 43 | 44 | const IconLink = ({ src, text }) => ( 45 | 46 | {text} 47 | {text} 48 | 49 | ); 50 | 51 | const content = ( 52 | <> 53 | 54 | Ant Design interprets the color system into two levels: a system-level color system and a product-level color 55 | system. 56 | 57 | 58 | Ant Design's design team preferred to design with the HSB color model, which makes it easier for designers to 59 | have a clear psychological expectation of color when adjusting colors, as well as facilitate communication in 60 | teams. 61 | 62 | 63 | ); 64 | 65 | const Content = ({ children, extraContent }) => ( 66 | 67 |
{children}
68 |
{extraContent}
69 |
70 | ); 71 | 72 | export default function NotFound() { 73 | return ( 74 | 75 | 81 | Running} 86 | extra={[ 87 | , 88 | , 89 | , 92 | 93 | ]} 94 | > 95 | {content} 96 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /public/static/img_icn_network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/components/staking/StakingOptionCard.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Button, Card } from 'antd'; 3 | import { NotCheckmarkSvg } from '../common/svg/NotCheckmarkSvg'; 4 | import { CheckmarkSvg } from '../common/svg/CheckmarkSvg'; 5 | import useTheme from '../../hooks/useTheme'; 6 | import { CardStatistics } from '../cards/CardStatistics'; 7 | 8 | const StakingOptionCard = ({ 9 | title, 10 | subtitle, 11 | description, 12 | pro, 13 | cons, 14 | poolSize, 15 | percent, 16 | apr, 17 | maturityPeriod, 18 | rewardSplit 19 | }) => { 20 | const { theme, onThemeChange, THEME_LIGHT, THEME_DARK } = useTheme(); 21 | 22 | const background = theme === THEME_LIGHT ? 'white' : '#000046'; 23 | 24 | return ( 25 | 32 |

{title}

33 |

{subtitle}

34 | 35 |

{description}

36 | 37 |
38 |
39 | 40 | 41 |
42 | 43 | {pro.length > 0 && ( 44 |
    45 | {pro.map((item, index) => ( 46 |
  • 47 | 48 | {item} 49 |
  • 50 | ))} 51 |
52 | )} 53 | 54 | {cons.length > 0 && ( 55 |
    56 | {cons.map((item, index) => ( 57 |
  • 58 | 59 | 60 | {item} 61 |
  • 62 | ))} 63 |
64 | )} 65 | 66 |
67 | 68 | 69 |
70 |
71 | 72 | 78 |
79 | ); 80 | }; 81 | 82 | StakingOptionCard.propTypes = { 83 | type: PropTypes.oneOf(['base', 'extended']), 84 | title: PropTypes.string.isRequired, 85 | description: PropTypes.string, 86 | pro: PropTypes.array, 87 | cons: PropTypes.array 88 | }; 89 | 90 | StakingOptionCard.defaultProps = { 91 | pro: [], 92 | cons: [] 93 | }; 94 | export default StakingOptionCard; 95 | -------------------------------------------------------------------------------- /public/static/img_icn_chain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | module.exports = { 4 | import: true, 5 | // Active dark mode on class basis 6 | darkMode: 'class', 7 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'blur-right': "url('../public/static/background-blur-right.png')", 12 | hero: "url('../public/static/hero.png')", 13 | 'pattern-1': "url('../public/static/pattern-1.png')", 14 | 'pattern-2': "url('../public/static/pattern-2.png')", 15 | 'logo-pattern': "url('../public/static/hero-logo.png')", 16 | 'logo-mobile-pattern': "url('../public/static/hero_logo_mobile.png')", 17 | 'square-pattern': "url('../public/static/square_pattern.png')", 18 | 'map-pattern': "url('../public/static/world_map.png')", 19 | 'map-pattern-light': "url('../public/static/world_map_light.png')", 20 | 'dotted-pattern': "url('../public/static/dotted_pattern.png')", 21 | 'card-etny-logo-pattern': "url('../public/static/card_etny_logo.png')" 22 | }, 23 | fontFamily: { 24 | sans: ['Inter'], 25 | grotesk: ['Space Grotesk'] 26 | } 27 | }, 28 | colors: { 29 | transparent: 'transparent', 30 | current: 'currentColor', 31 | success: '#61C454', 32 | primary: '#F89430', 33 | 'etny-navbar': '#070E1D', 34 | 'etny-background': '#070E1D', 35 | 'etny-button': { 36 | primary: '#F89430', 37 | hover: '#FFB259', 38 | focus: '#D1711D' 39 | }, 40 | 'etny-primary-button': { 41 | primary: '#27278B', 42 | hover: '#3B46B3', 43 | focus: '#0F0B40' 44 | }, 45 | 'etny-secondary-button': { 46 | primary: '#FFFFFF', 47 | hover: '#FFECD8', 48 | focus: '#FFCFA0' 49 | }, 50 | 'etny-cancel': { 51 | primary: '#FFFFFF', 52 | hover: '#FFE6E0', 53 | focus: '#FA998C', 54 | text: '#EE6A5F' 55 | }, 56 | 'etny-approve': { 57 | primary: '#FFFFFF', 58 | hover: '#DAEBD5', 59 | focus: '#88D17B', 60 | text: '#61C454' 61 | }, 62 | 'etny-blue-gray': { 63 | 100: '#677D9E', 64 | 150: '#6E8EBE', 65 | 450: '#161639', 66 | 500: '#161D2D', 67 | 600: '#203345' 68 | }, 69 | 'etny-orange': { 70 | 100: '#FFECD8', 71 | 200: '#FDE4CB', 72 | 300: '#FFCFA0', 73 | 350: '#FFDE69', 74 | 400: '#FFB259', 75 | 450: '#FFC121', 76 | 500: '#F89430', 77 | 600: '#D1711D' 78 | }, 79 | 'etny-dark': { 80 | 100: '#27278B', 81 | 200: '#161D2D', 82 | 300: '#101D39', 83 | 400: '#161D2D', 84 | 500: '#161D2D', 85 | 600: '#161D2D', 86 | 700: '#09142D', 87 | 800: '#161D2D', 88 | 900: '#09142D' 89 | }, 90 | etny: { 91 | 100: '#C4E2FF', 92 | 200: '#48A4FF', 93 | 500: '#0C86FF', 94 | 700: '#0F1153', 95 | 800: '#030363', 96 | 900: '#131345' 97 | }, 98 | neutral: { 99 | 40: '#F4F4F4', 100 | 50: '#FCFDFD', 101 | 100: '#F7F8F9', 102 | 200: '#F2F3F3', 103 | 300: '#D9D9D9', 104 | 400: '#BFBFBE', 105 | 450: '#999DA3', 106 | 500: '#8D8D8D', 107 | 600: '#595959', 108 | 700: '#434444', 109 | 800: '#1E1F1F', 110 | 900: '#131313', 111 | 950: '#1E1F1F' 112 | }, 113 | black: colors.black, 114 | white: colors.white, 115 | gray: colors.gray, 116 | emerald: colors.emerald, 117 | indigo: colors.indigo, 118 | yellow: colors.yellow, 119 | red: colors.red, 120 | green: colors.green, 121 | blue: colors.blue 122 | } 123 | }, 124 | plugins: [] 125 | }; 126 | -------------------------------------------------------------------------------- /public/static/logo/logo_ethc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/utils/StakingPotCalculator.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { StakingRequestType } from './StakingRequestType'; 3 | 4 | export const ratesPerYear = { 5 | 2022: 10, 6 | 2023: 9, 7 | 2024: 8, 8 | 2025: 7, 9 | 2026: 6, 10 | 2027: 5, 11 | 2028: 4, 12 | 2029: 3, 13 | 2030: 2, 14 | 2031: 1, 15 | 2032: 0 16 | }; 17 | 18 | export const getDaysForYear = (year) => { 19 | const firstDayInYear = moment(`${year}-01-01`); 20 | const lastDayInYear = moment(`${year}-12-31`); 21 | return moment.duration(lastDayInYear.diff(firstDayInYear)).asDays(); 22 | }; 23 | 24 | export const getRewardPerDay = (amount, year) => { 25 | const days = getDaysForYear(year); 26 | const percentage = ratesPerYear[year]; 27 | 28 | return (((amount / days) * percentage) / 100).toFixed(4); 29 | }; 30 | 31 | export const getDaysLeftUntilEndOfYear = (year, lastPeriod) => { 32 | const endOfYearDate = lastPeriod || moment(`${year}-12-31`); 33 | let currentDate = moment(); 34 | const isCurrentYear = currentDate.year() === year; 35 | if (!isCurrentYear) currentDate = moment(`${year}-01-01`); 36 | return endOfYearDate.diff(currentDate, 'days'); 37 | }; 38 | 39 | export const getDaysForPeriods = (periods) => { 40 | console.log('_____________________________'); 41 | console.log(periods); 42 | const daysPerYear = {}; 43 | const lastPeriod = moment().add(periods, 'M'); 44 | const currentDate = moment().startOf('day'); 45 | const currentYear = moment().year(); 46 | // const maxYear = getMaxYearFromPeriods(periods); 47 | // const years = moment().diff(lastPeriod, 'years'); 48 | const y = moment.duration(lastPeriod.diff(currentDate)).asYears(); 49 | const years = parseInt(y.toFixed(), 10); 50 | const lastYear = lastPeriod.year() < currentYear + years ? lastPeriod.year() : currentYear + years; 51 | 52 | // eslint-disable-next-line no-plusplus 53 | for (let year = currentYear; year <= lastYear; year++) { 54 | console.log(getDaysForYear(year)); 55 | // first year 56 | let daysUntilEndOfYear; 57 | if (year === currentYear || year !== lastYear) { 58 | daysUntilEndOfYear = getDaysLeftUntilEndOfYear(year); 59 | } else { 60 | // last year 61 | // if (year === lastYear) 62 | daysUntilEndOfYear = getDaysLeftUntilEndOfYear(year, lastPeriod); 63 | } 64 | // console.log(`${year} = ${daysUntilEndOfYear} remaining`); 65 | daysPerYear[year] = daysUntilEndOfYear; 66 | } 67 | return daysPerYear; 68 | }; 69 | 70 | export const calculate = (type, amount, periods, split) => { 71 | const daysPerYears = getDaysForPeriods(periods); 72 | const rewardPerYear = []; 73 | Object.keys(daysPerYears).forEach((year) => { 74 | const reward = getRewardPerDay(amount, year); 75 | const totalReward = 76 | type === StakingRequestType.BASE ? reward * daysPerYears[year] : (reward * daysPerYears[year] * split) / 100; 77 | rewardPerYear.push({ 78 | year, 79 | apy: ratesPerYear[year], 80 | reward: totalReward.toFixed(4), 81 | rewardPerDay: reward, 82 | days: daysPerYears[year] 83 | }); 84 | console.log( 85 | `Reward for year ${year} is: rewardPerDay = ${reward}; days = ${daysPerYears[year]}; totalReward = ${totalReward} ETNY` 86 | ); 87 | }); 88 | 89 | return rewardPerYear; 90 | }; 91 | 92 | export const getPercentOfDaysUntil = (createdOn, months) => { 93 | const currentDate = moment(); 94 | const lastDate = moment(createdOn).add(months, 'M'); 95 | 96 | const daysUntil = lastDate.diff(currentDate, 'days'); 97 | 98 | return 365 / daysUntil; 99 | }; 100 | 101 | export const getDaysUntil = (createdOn, months) => { 102 | const currentDate = moment(); 103 | const lastDate = moment(createdOn).add(months, 'M'); 104 | 105 | return lastDate.diff(currentDate, 'days'); 106 | }; 107 | 108 | export const getRatePerYear = (createdOn) => ratesPerYear[moment(createdOn).year()]; 109 | 110 | export const formatDate = (createdOn) => moment(createdOn).format('YYYY-MM-DD | HH:mm'); 111 | -------------------------------------------------------------------------------- /src/components/wallet/WalletCard.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Button, Card, notification, Row, Statistic } from 'antd'; 3 | import { useEffect, useState } from 'react'; 4 | import { useWeb3React } from '@web3-react/core'; 5 | import { ethers } from 'ethers'; 6 | import useTheme from '../../hooks/useTheme'; 7 | import EtnyContract from '../../operations/etnyContract'; 8 | 9 | // this card should handle all kind of operation like displaying data about account balance / available tokens, staked tokens, reward claimed 10 | const WalletCard = ({ type, title, prefix, value, suffix, actionLabel, onAction, className }) => { 11 | const { account, library } = useWeb3React(); 12 | const etnyContract = new EtnyContract(library); 13 | const [loading, setLoading] = useState(false); 14 | const [balance, setBalance] = useState('0.0'); 15 | const { theme, THEME_LIGHT } = useTheme(); 16 | 17 | const getProvider = () => { 18 | const provider = new ethers.providers.Web3Provider(window.ethereum, 'any'); 19 | const { provider: ethereum } = provider; 20 | return ethereum; 21 | }; 22 | 23 | useEffect(() => { 24 | // The "any" network will allow spontaneous network changes 25 | const provider = getProvider(); 26 | provider.on('accountsChanged', async (accounts) => { 27 | if (accounts.length === 1) await getAccountBalance(accounts[0]); 28 | }); 29 | }, []); 30 | 31 | useEffect(() => { 32 | getAccountBalance(); 33 | }, []); 34 | 35 | const getAccountBalance = async (accountFromEvent) => { 36 | try { 37 | setLoading(true); 38 | 39 | console.log('here'); 40 | const balance = await etnyContract.getBalance(accountFromEvent || account); 41 | 42 | setTimeout(() => { 43 | if (!balance) { 44 | notification.error({ 45 | placement: 'bottomRight', 46 | className: 'bg-white dark:bg-black text-black dark:text-white', 47 | message: Ethernity, 48 | description: 'Not able to retrieve wallet balance' 49 | }); 50 | } else { 51 | setBalance(balance); 52 | } 53 | setLoading(false); 54 | }, 1000); 55 | } catch (error) { 56 | setLoading(false); 57 | } 58 | }; 59 | 60 | const onRefresh = async () => { 61 | await getAccountBalance(); 62 | }; 63 | 64 | return ( 65 | 71 |
72 |
73 | {title}} 75 | value={value || balance} 76 | precision={2} 77 | valueStyle={{ 78 | fontFamily: 'Space Grotesk', 79 | fontWeight: 'bold', 80 | fontFeatureSettings: `'zero' on, 'cv01' on, 'cv02' on, 'cv03' on, 'cv04' on`, 81 | fontVariantNumeric: 'slashed-zero', 82 | color: theme === THEME_LIGHT ? '#000000' : '#FFFFFF' 83 | }} 84 | style={{ fontWeight: 500 }} 85 | prefix={prefix} 86 | suffix={suffix} 87 | /> 88 | 89 | 98 | 99 |
100 |
101 |
102 | ); 103 | }; 104 | 105 | WalletCard.propTypes = { 106 | type: PropTypes.oneOf(['available', 'total', 'reward']), 107 | title: PropTypes.string, 108 | prefix: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.element]), 109 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 110 | suffix: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.element]), 111 | onAction: PropTypes.func, 112 | className: PropTypes.string 113 | }; 114 | 115 | export default WalletCard; 116 | -------------------------------------------------------------------------------- /src/components/wallet/WalletRewardCard.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Button, Card, Col, notification, Row, Statistic } from 'antd'; 3 | import { useEffect, useState } from 'react'; 4 | import { useWeb3React } from '@web3-react/core'; 5 | import { ethers } from 'ethers'; 6 | import useTheme from '../../hooks/useTheme'; 7 | import { calculate } from '../../utils/StakingPotCalculator'; 8 | import EtnyContract from '../../operations/etnyContract'; 9 | 10 | const WalletRewardCard = ({ requestType, amount, period, split, value, actionLabel, className }) => { 11 | const { account, library } = useWeb3React(); 12 | const etnyContract = new EtnyContract(library); 13 | const [loading, setLoading] = useState(false); 14 | const [balance, setBalance] = useState('0.0'); 15 | const [estimatedReward, setEstimatedReward] = useState(0.0); 16 | const { theme, THEME_LIGHT } = useTheme(); 17 | 18 | const getProvider = () => { 19 | const provider = new ethers.providers.Web3Provider(window.ethereum, 'any'); 20 | const { provider: ethereum } = provider; 21 | return ethereum; 22 | }; 23 | 24 | useEffect(() => { 25 | // The "any" network will allow spontaneous network changes 26 | const provider = getProvider(); 27 | provider.on('accountsChanged', async (accounts) => { 28 | if (accounts.length === 1) await getAccountBalance(accounts[0]); 29 | }); 30 | }, []); 31 | 32 | useEffect(() => { 33 | if (!value) { 34 | getAccountBalance(); 35 | } 36 | }, []); 37 | 38 | useEffect(() => { 39 | const rewardsPerYearObject = calculate(requestType, amount, period, split); 40 | const rewardsPerYear = rewardsPerYearObject.map((rewardObject) => parseFloat(rewardObject.reward)); 41 | const estimatedRewardSum = rewardsPerYear.reduce((prev, current) => prev + current); 42 | setEstimatedReward(estimatedRewardSum); 43 | }, [requestType, amount, period, split]); 44 | 45 | const getAccountBalance = async (accountFromEvent) => { 46 | try { 47 | setLoading(true); 48 | 49 | const balance = await etnyContract.getBalance(accountFromEvent || account); 50 | 51 | setTimeout(() => { 52 | if (!balance) { 53 | notification.error({ 54 | placement: 'bottomRight', 55 | className: 'bg-white dark:bg-black text-black dark:text-white', 56 | message: Ethernity, 57 | description: 'Not able to retrieve wallet balance' 58 | }); 59 | } else { 60 | setBalance(balance); 61 | } 62 | setLoading(false); 63 | }, 1000); 64 | } catch (error) { 65 | setLoading(false); 66 | } 67 | }; 68 | 69 | return ( 70 | 76 |
77 |
78 | 79 | 80 |
81 |

Estimated Reward

82 |

83 | {estimatedReward} 84 | ETNY 85 |

86 |
87 | 88 | 89 |
90 |

91 | Account Balance 92 |

93 |

94 | {value || balance} 95 | ETNY 96 |

97 |
98 | 99 |
100 |
101 |
102 |
103 | ); 104 | }; 105 | 106 | WalletRewardCard.propTypes = { 107 | requestType: PropTypes.oneOf(['Base Staking', 'Extended Staking']), 108 | amount: PropTypes.number, 109 | period: PropTypes.number, 110 | split: PropTypes.number, 111 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 112 | actionLabel: PropTypes.string, 113 | className: PropTypes.string 114 | }; 115 | 116 | export default WalletRewardCard; 117 | -------------------------------------------------------------------------------- /src/contract/etnyContract.js: -------------------------------------------------------------------------------- 1 | export const contract = { 2 | // address: '0x549A6E06BB2084100148D50F51CF77a3436C3Ae7', 3 | address: '0xfb1b8c7bef3fc44496d2ee483e6e11db7ee9ef2b', // ropsten 4 | abi: [ 5 | { 6 | constant: true, 7 | inputs: [], 8 | name: 'name', 9 | outputs: [ 10 | { 11 | name: '', 12 | type: 'string' 13 | } 14 | ], 15 | payable: false, 16 | stateMutability: 'view', 17 | type: 'function' 18 | }, 19 | { 20 | constant: false, 21 | inputs: [ 22 | { 23 | name: '_spender', 24 | type: 'address' 25 | }, 26 | { 27 | name: '_value', 28 | type: 'uint256' 29 | } 30 | ], 31 | name: 'approve', 32 | outputs: [ 33 | { 34 | name: '', 35 | type: 'bool' 36 | } 37 | ], 38 | payable: false, 39 | stateMutability: 'nonpayable', 40 | type: 'function' 41 | }, 42 | { 43 | constant: true, 44 | inputs: [], 45 | name: 'totalSupply', 46 | outputs: [ 47 | { 48 | name: '', 49 | type: 'uint256' 50 | } 51 | ], 52 | payable: false, 53 | stateMutability: 'view', 54 | type: 'function' 55 | }, 56 | { 57 | constant: false, 58 | inputs: [ 59 | { 60 | name: '_from', 61 | type: 'address' 62 | }, 63 | { 64 | name: '_to', 65 | type: 'address' 66 | }, 67 | { 68 | name: '_value', 69 | type: 'uint256' 70 | } 71 | ], 72 | name: 'transferFrom', 73 | outputs: [ 74 | { 75 | name: '', 76 | type: 'bool' 77 | } 78 | ], 79 | payable: false, 80 | stateMutability: 'nonpayable', 81 | type: 'function' 82 | }, 83 | { 84 | constant: true, 85 | inputs: [], 86 | name: 'decimals', 87 | outputs: [ 88 | { 89 | name: '', 90 | type: 'uint8' 91 | } 92 | ], 93 | payable: false, 94 | stateMutability: 'view', 95 | type: 'function' 96 | }, 97 | { 98 | constant: true, 99 | inputs: [ 100 | { 101 | name: '_owner', 102 | type: 'address' 103 | } 104 | ], 105 | name: 'balanceOf', 106 | outputs: [ 107 | { 108 | name: 'balance', 109 | type: 'uint256' 110 | } 111 | ], 112 | payable: false, 113 | stateMutability: 'view', 114 | type: 'function' 115 | }, 116 | { 117 | constant: true, 118 | inputs: [], 119 | name: 'symbol', 120 | outputs: [ 121 | { 122 | name: '', 123 | type: 'string' 124 | } 125 | ], 126 | payable: false, 127 | stateMutability: 'view', 128 | type: 'function' 129 | }, 130 | { 131 | constant: false, 132 | inputs: [ 133 | { 134 | name: '_to', 135 | type: 'address' 136 | }, 137 | { 138 | name: '_value', 139 | type: 'uint256' 140 | } 141 | ], 142 | name: 'transfer', 143 | outputs: [ 144 | { 145 | name: '', 146 | type: 'bool' 147 | } 148 | ], 149 | payable: false, 150 | stateMutability: 'nonpayable', 151 | type: 'function' 152 | }, 153 | { 154 | constant: true, 155 | inputs: [ 156 | { 157 | name: '_owner', 158 | type: 'address' 159 | }, 160 | { 161 | name: '_spender', 162 | type: 'address' 163 | } 164 | ], 165 | name: 'allowance', 166 | outputs: [ 167 | { 168 | name: '', 169 | type: 'uint256' 170 | } 171 | ], 172 | payable: false, 173 | stateMutability: 'view', 174 | type: 'function' 175 | }, 176 | { 177 | payable: true, 178 | stateMutability: 'payable', 179 | type: 'fallback' 180 | }, 181 | { 182 | anonymous: false, 183 | inputs: [ 184 | { 185 | indexed: true, 186 | name: 'owner', 187 | type: 'address' 188 | }, 189 | { 190 | indexed: true, 191 | name: 'spender', 192 | type: 'address' 193 | }, 194 | { 195 | indexed: false, 196 | name: 'value', 197 | type: 'uint256' 198 | } 199 | ], 200 | name: 'Approval', 201 | type: 'event' 202 | }, 203 | { 204 | anonymous: false, 205 | inputs: [ 206 | { 207 | indexed: true, 208 | name: 'from', 209 | type: 'address' 210 | }, 211 | { 212 | indexed: true, 213 | name: 'to', 214 | type: 'address' 215 | }, 216 | { 217 | indexed: false, 218 | name: 'value', 219 | type: 'uint256' 220 | } 221 | ], 222 | name: 'Transfer', 223 | type: 'event' 224 | } 225 | ] 226 | }; 227 | -------------------------------------------------------------------------------- /src/pages/app/StakingPage.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Button, Drawer, PageHeader, Space, Tabs } from 'antd'; 3 | import { FaCoins, FaArrowUp, FaArrowDown } from 'react-icons/fa'; 4 | import { HomeOutlined } from '@ant-design/icons'; 5 | import Page from '../../components/Page'; 6 | import StakingForm from '../../components/staking/StakingForm'; 7 | import useTheme from '../../hooks/useTheme'; 8 | import AccountTab from '../../components/staking/tabs/AccountTab'; 9 | import StakingOffers from '../../components/staking/tabs/StakingOffers'; 10 | import { StakingPotStatus } from '../../utils/StakingPotStatus'; 11 | 12 | const { TabPane } = Tabs; 13 | 14 | const StakingPage = () => { 15 | const [stakingDrawerVisible, setStakingDrawerVisible] = useState(false); 16 | const { theme, THEME_LIGHT } = useTheme(); 17 | const [updatingPending, setUpdatingPending] = useState(false); 18 | const [updatingApproved, setUpdatingApproved] = useState(false); 19 | const [updatingCanceled, setUpdatingCanceled] = useState(false); 20 | const [updatingDeclined, setUpdatingDeclined] = useState(false); 21 | 22 | const onCreateStake = () => { 23 | setStakingDrawerVisible(true); 24 | }; 25 | 26 | const onDrawerClosed = () => { 27 | setStakingDrawerVisible(false); 28 | }; 29 | 30 | const onStakingTabChanged = (activeKey) => { 31 | // eslint-disable-next-line default-case 32 | switch (parseInt(activeKey, 10)) { 33 | case 2: 34 | setUpdatingPending(true); 35 | break; 36 | case 3: 37 | setUpdatingApproved(true); 38 | break; 39 | case 4: 40 | setUpdatingCanceled(true); 41 | break; 42 | case 5: 43 | setUpdatingDeclined(true); 44 | break; 45 | } 46 | }; 47 | 48 | return ( 49 | 50 | {/* */} 51 | 54 | 55 | Staking Overview 56 | 57 | } 58 | extra={[ 59 | , 71 | , 83 | 97 | ]} 98 | footer={ 99 | 104 | Account} key="1"> 105 | 106 | 107 | Pending} key="2"> 108 | setUpdatingPending(false)} 112 | updating={updatingPending} 113 | /> 114 | 115 | Approved} key="3"> 116 | setUpdatingApproved(false)} 120 | updating={updatingApproved} 121 | /> 122 | 123 | Canceled} key="4"> 124 | setUpdatingCanceled(false)} 128 | updating={updatingCanceled} 129 | /> 130 | 131 | Declined} key="5"> 132 | setUpdatingDeclined(false)} 136 | updating={updatingDeclined} 137 | /> 138 | 139 | 140 | } 141 | /> 142 | 143 | 166 | 167 | 168 | 169 | ); 170 | }; 171 | export default StakingPage; 172 | -------------------------------------------------------------------------------- /src/components/marketplace/MarketplaceOfferCard.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { Button, Card, Col, Drawer, Modal, notification, Row, Space, Statistic, Tag, Tooltip, Typography } from 'antd'; 3 | import { ExclamationCircleOutlined, FileDoneOutlined, FileExclamationOutlined, InfoOutlined } from '@ant-design/icons'; 4 | import React, { useState } from 'react'; 5 | import StakingForm from '../staking/StakingForm'; 6 | 7 | const { Title } = Typography; 8 | const { Meta } = Card; 9 | 10 | const MarketplaceOfferCard = ({ index, status, type }) => { 11 | const [stakingDrawerVisible, setStakingDrawerVisible] = useState(false); 12 | 13 | const onDrawerClosed = () => { 14 | setStakingDrawerVisible(false); 15 | }; 16 | 17 | const getStatusColor = () => { 18 | switch (status) { 19 | case 'PENDING': 20 | return 'gold'; 21 | case 'APPROVED': 22 | return 'success'; 23 | case 'DECLINED': 24 | return 'error'; 25 | default: 26 | return 'success'; 27 | } 28 | }; 29 | const onApprove = () => { 30 | Modal.confirm({ 31 | title: 'Warning', 32 | icon: , 33 | wrapClassName: 'shadow-md dark:shadow-gray-500 etny-modal dark:etny-modal', 34 | content: 'Are you sure you want to decline staking pot #001?', 35 | okText: 'Confirm', 36 | cancelText: 'Cancel', 37 | onOk: () => { 38 | notification.success({ 39 | className: 'bg-white dark:bg-black text-black dark:text-white', 40 | message: Staking pot #001, 41 | description: 'Offer for the staking pot #001 has been approved' 42 | }); 43 | } 44 | }); 45 | }; 46 | 47 | const onDecline = () => { 48 | Modal.confirm({ 49 | title: 'Warning', 50 | icon: , 51 | wrapClassName: 'shadow-md dark:shadow-gray-500 etny-modal dark:etny-modal', 52 | content: 'Are you sure you want to approve staking pot #001?', 53 | okText: 'Confirm', 54 | cancelText: 'Cancel', 55 | onOk: () => { 56 | notification.error({ 57 | className: 'bg-white dark:bg-black text-black dark:text-white', 58 | message: Staking pot #001, 59 | description: 'Offer for staking pot #001 has been rejected' 60 | }); 61 | } 62 | }); 63 | }; 64 | 65 | const onInfoDetails = () => { 66 | setStakingDrawerVisible(true); 67 | }; 68 | const createCardActions = () => [ 69 | 70 | {/* */} 75 | 76 | {/* */} 77 | , 78 | 79 | 80 | , 81 | 82 | 83 | 84 | ]; 85 | 86 | return ( 87 | <> 88 | {type}, {status}]} 93 | className="text-black dark:text-white bg-white dark:bg-gray-300 dark:border-0" 94 | > 95 | 100 | {/* } */} 102 | {/* title="Card title" */} 103 | {/* description="This is the description" */} 104 | {/* /> */} 105 | 106 | 107 | 113 | 114 | 115 | 121 | 122 | 123 | 130 | 131 | 132 | 139 | 140 | 141 | 142 | {/*
*/} 143 | {/* */} 144 | {/* */} 145 | {/*

Cycle ends in:

*/} 146 | {/* */} 147 | {/* */} 148 | {/*

23:00:01

*/} 149 | {/* */} 150 | {/*
*/} 151 | {/*
*/} 152 | 153 |
154 | 163 | 164 | 167 | 168 | } 169 | // extra={ 170 | // 171 | // 172 | // 175 | // 176 | // } 177 | > 178 | 179 | 180 | 181 | ); 182 | }; 183 | export default MarketplaceOfferCard; 184 | -------------------------------------------------------------------------------- /public/static/img_icn_programing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 135 | 136 | 137 | 138 | 139 | 140 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/pages/app/StakingPotDetailsPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Col, PageHeader, Row, Tabs } from 'antd'; 3 | import { FaArrowLeft } from 'react-icons/fa'; 4 | import { useParams } from 'react-router-dom'; 5 | import { useWeb3React } from '@web3-react/core'; 6 | import Page from '../../components/Page'; 7 | import { StakingPotStatus } from '../../utils/StakingPotStatus'; 8 | import MarketplaceOfferCardV1 from '../../components/marketplace/MarketplaceOfferCardV1'; 9 | import { getDaysUntil, getPercentOfDaysUntil, getRatePerYear } from '../../utils/StakingPotCalculator'; 10 | import { formatNumber } from '../../utils/numberFormatter'; 11 | import { StakingRequestType } from '../../utils/StakingRequestType'; 12 | import EtnyStakingContract from '../../operations/etnyStakingContract'; 13 | import StakingPotContracts from '../../components/contract/StakingPotContracts'; 14 | 15 | const { TabPane } = Tabs; 16 | 17 | const StakingPotDetailsPage = () => { 18 | const { id } = useParams(); 19 | const { account, library } = useWeb3React(); 20 | const etnyStakingContract = new EtnyStakingContract(library); 21 | const [stake, setStake] = useState({ 22 | type: StakingRequestType.BASE, 23 | amount: 1900, 24 | period: 12, 25 | split: 100, 26 | stakingAddress: account, 27 | nodeAddress: '', 28 | rewardAddress: account, 29 | canBeSplitted: true, 30 | isPreApproved: false, 31 | pendingContracts: 0, 32 | approvedContracts: 0, 33 | declinedContracts: 0 34 | }); 35 | const [contracts, setContracts] = useState([]); 36 | const [isLoading, setIsLoading] = useState(false); 37 | 38 | const [updatingPending, setUpdatingPending] = useState(false); 39 | const [updatingApproved, setUpdatingApproved] = useState(false); 40 | const [updatingCanceled, setUpdatingCanceled] = useState(false); 41 | const [updatingDeclined, setUpdatingDeclined] = useState(false); 42 | 43 | useEffect(() => { 44 | getStakingPotDetails(id); 45 | }, [id]); 46 | 47 | const getStakingPotDetails = async (id) => { 48 | setIsLoading(true); 49 | const item = await etnyStakingContract.getExtendedStake(id - 1); 50 | const stats = await etnyStakingContract.getExtendedStakeRequestContractStats(id - 1); 51 | item.total = stats.total; 52 | item.approvedContracts = stats.approvedContracts; 53 | item.canceledContracts = stats.canceledContracts; 54 | item.declinedContracts = stats.declinedContracts; 55 | item.pendingContracts = stats.pendingContracts; 56 | item.terminatedContracts = stats.terminatedContracts; 57 | 58 | setStake(item); 59 | 60 | const contractsPromises = []; 61 | for (let i = 0; i < item.total; i += 1) { 62 | // eslint-disable-next-line no-await-in-loop 63 | contractsPromises.push(await etnyStakingContract.getStakeContractForStake(id - 1, i)); 64 | } 65 | const contractsResult = await Promise.all(contractsPromises); 66 | console.log(contractsResult); 67 | 68 | setContracts(contractsResult); 69 | 70 | setIsLoading(false); 71 | }; 72 | const onStakingTabChanged = (activeKey) => { 73 | // eslint-disable-next-line default-case 74 | switch (parseInt(activeKey, 10)) { 75 | case 2: 76 | setUpdatingPending(true); 77 | break; 78 | case 3: 79 | setUpdatingApproved(true); 80 | break; 81 | case 4: 82 | setUpdatingCanceled(true); 83 | break; 84 | case 5: 85 | setUpdatingDeclined(true); 86 | break; 87 | } 88 | }; 89 | 90 | return ( 91 | 92 | window.history.back()} 94 | backIcon={} 95 | footer={ 96 | 101 | Pending ({stake.pendingContracts})} key="2"> 102 | contract.status === StakingPotStatus.PENDING)} 105 | onUpdateFinished={() => setUpdatingPending(false)} 106 | updating={updatingPending} 107 | /> 108 | 109 | Approved ({stake.approvedContracts})} key="3"> 110 | contract.status === StakingPotStatus.APPROVED)} 113 | onUpdateFinished={() => setUpdatingApproved(false)} 114 | updating={updatingApproved} 115 | /> 116 | 117 | Declined ({stake.declinedContracts})} key="4"> 118 | contract.status === StakingPotStatus.CANCELED)} 121 | onUpdateFinished={() => setUpdatingCanceled(false)} 122 | updating={updatingCanceled} 123 | /> 124 | 125 | 126 | } 127 | title={Back to Marketplace} 128 | > 129 | 130 | 131 | 165 | 166 | 167 | 168 | 169 | ); 170 | }; 171 | export default StakingPotDetailsPage; 172 | -------------------------------------------------------------------------------- /src/layouts/app/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import Icon, { MenuUnfoldOutlined, EditOutlined, ClearOutlined, BellFilled } from '@ant-design/icons'; 3 | import { Avatar, Badge, Button, Card, Col, Layout, List, Menu, Popover, Row, Tag } from 'antd'; 4 | import { useWeb3React } from '@web3-react/core'; 5 | import { isMobile } from 'react-device-detect'; 6 | import { Link } from 'react-router-dom'; 7 | import { FaMoon, FaSun } from 'react-icons/fa'; 8 | import useCollapseDrawer from '../../hooks/useCollapseDrawer'; 9 | import MetaMaskButton from '../../components/MetaMaskButton'; 10 | import useTheme from '../../hooks/useTheme'; 11 | import { randomIntFromInterval } from '../../utils/Math'; 12 | import { authRoutes } from '../../routes/routes'; 13 | import Logo from '../../components/Logo'; 14 | import useLocalStorage from '../../hooks/useLocalStorage'; 15 | 16 | const { Header } = Layout; 17 | 18 | const data = [ 19 | { 20 | title: 'Staking pot 0001', 21 | avatar: 'https://joeschmoe.io/api/v1/random', 22 | description: 'APPROVED', 23 | datetime: '10/02/2022' 24 | }, 25 | { 26 | title: 'Staking pot 0001', 27 | avatar: 'https://joeschmoe.io/api/v1/random', 28 | description: 'DECLINED', 29 | datetime: '10/02/2022' 30 | }, 31 | { 32 | title: 'Staking pot 0003', 33 | avatar: 'https://joeschmoe.io/api/v1/random', 34 | description: 'NEW STAKING', 35 | datetime: '10/02/2022' 36 | }, 37 | { 38 | title: 'Staking pot 0003', 39 | avatar: 'https://joeschmoe.io/api/v1/random', 40 | description: 'APPROVED', 41 | datetime: '10/02/2022' 42 | }, 43 | { 44 | avatar: 'https://joeschmoe.io/api/v1/random', 45 | description: 'APPROVED', 46 | datetime: '10/02/2022' 47 | }, 48 | { 49 | avatar: 'https://joeschmoe.io/api/v1/random', 50 | description: 'APPROVED', 51 | datetime: '10/02/2022' 52 | }, 53 | { 54 | avatar: 'https://joeschmoe.io/api/v1/random', 55 | description: 'Desc', 56 | datetime: '10/02/2022' 57 | } 58 | ]; 59 | const tabList = [ 60 | { 61 | key: 'tab1', 62 | tab: 'Notifications' 63 | }, 64 | { 65 | key: 'tab2', 66 | tab: 'News' 67 | } 68 | ]; 69 | const menu = ( 70 | , ]} 74 | bodyStyle={{ padding: 0 }} 75 | > 76 | ( 80 | 81 | 85 | } 86 | title={item.title || 'New staking offer'} 87 | description={ 88 | 89 | 90 | {item.description} 91 | 92 | 93 | 1day ago 94 | 95 | 96 | } 97 | /> 98 | 99 | )} 100 | /> 101 | 102 | ); 103 | 104 | const Navbar = ({ onMenuClick }) => { 105 | const { isCollapsed, onToggleCollapse } = useCollapseDrawer(); 106 | const [isMetamaskLoggedIn] = useLocalStorage('etny-metamask-logged-in', null); 107 | const { active } = useWeb3React(); 108 | const { theme, onThemeChange, THEME_LIGHT, THEME_DARK } = useTheme(); 109 | 110 | const logoWidth = () => { 111 | if (isMobile) return 0; 112 | if (!active) return 60; 113 | return isCollapsed ? 80 : 200; 114 | }; 115 | const onNavMenuClick = () => { 116 | if (!isMobile) { 117 | onToggleCollapse(); 118 | } 119 | onMenuClick(); 120 | }; 121 | 122 | const onThemeChanged = () => { 123 | // On page load or when changing themes, best to add inline in `head` to avoid FOUC 124 | if (theme === THEME_LIGHT) { 125 | onThemeChange('dark'); 126 | } else { 127 | onThemeChange('light'); 128 | } 129 | }; 130 | 131 | const backgroundColor = isMetamaskLoggedIn ? `bg-etny-500 dark:bg-etny-800` : `bg-white dark:bg-etny-background`; 132 | return ( 133 |
134 | 135 | {active && isMetamaskLoggedIn && isMobile && ( 136 | 140 | )} 141 | 142 | 143 | 144 | {active && isMetamaskLoggedIn && ( 145 | 151 | {authRoutes.map((route, index) => 152 | // eslint-disable-next-line no-nested-ternary 153 | route.visible && !route.welcome ? ( 154 | index % 2 === 0 ? ( 155 | 159 | 160 | 161 | {route.name} 162 | 163 | 164 | ) : ( 165 | 166 | 167 | 168 | {route.name} 169 | 170 | 171 | ) 172 | ) : ( 173 | 174 | ) 175 | )} 176 | 177 | )} 178 | 179 |
180 | {theme === THEME_LIGHT && ( 181 |
215 |
216 |
217 | ); 218 | }; 219 | 220 | export default Navbar; 221 | -------------------------------------------------------------------------------- /src/operations/etnyStakingContract.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { contract } from '../contract/etnyStakingContract'; 3 | import { StakingRequestType } from '../utils/StakingRequestType'; 4 | 5 | class EtnyStakingContract { 6 | library = null; 7 | 8 | stakingContract = null; 9 | 10 | stakingContractWithSigner = null; 11 | 12 | ethereum = null; 13 | 14 | constructor(library) { 15 | this.library = library; 16 | this.stakingContract = new ethers.Contract(contract.address, contract.abi, library); 17 | this.stakingContractWithSigner = new ethers.Contract(contract.address, contract.abi, library.getSigner()); 18 | this.ethereum = window.ethereum; 19 | } 20 | 21 | getProvider() { 22 | const provider = new ethers.providers.Web3Provider(this.ethereum, 'any'); 23 | const { provider: ethereum } = provider; 24 | return ethereum; 25 | } 26 | 27 | async getMinBaseStakeAmount() { 28 | try { 29 | const amount = await this.stakingContract.getMinBaseStakeAmount(); 30 | return amount.toNumber(); 31 | } catch (ex) { 32 | return ex.message; 33 | } 34 | } 35 | 36 | async getMaxBaseStakeAmount() { 37 | try { 38 | const amount = await this.stakingContract.getMaxBaseStakeAmount(); 39 | return amount.toNumber(); 40 | } catch (ex) { 41 | return ex.message; 42 | } 43 | } 44 | 45 | async getCurrentApyPercentage() { 46 | try { 47 | const apyPercentage = await this.stakingContract.getCurrentApyPercentage(); 48 | return apyPercentage.toNumber(); 49 | } catch (ex) { 50 | return ex.message; 51 | } 52 | } 53 | 54 | async getMinPeriodForStake() { 55 | try { 56 | const minPeriod = await this.stakingContract.getMinPeriodForStake(); 57 | return minPeriod.toNumber(); 58 | } catch (ex) { 59 | return ex.message; 60 | } 61 | } 62 | 63 | async getStakeTotalSupply() { 64 | try { 65 | const totalSupply = await this.stakingContract.getMaxBaseStakeAmount(); 66 | return totalSupply.toNumber(); 67 | } catch (ex) { 68 | return ex.message; 69 | } 70 | } 71 | 72 | // BASE STAKING REQUEST 73 | async getBaseStake(baseStakeId) { 74 | try { 75 | const item = await this.stakingContract.getBaseStake(baseStakeId); 76 | return { 77 | nodeAddress: item.nodeAddress, 78 | stakeHolderAddress: item.stakeHolderAddress, 79 | amount: item.amount.toNumber(), 80 | period: item.period.toNumber(), 81 | status: item.status, 82 | split: item.operatorReward || 100, 83 | isPreApproved: item.autoConfirm, 84 | canBeSplitted: item.allowMultipleOp, 85 | // timestamp received is in seconds, so we have to convert it to milliseconds 86 | timestamp: item.timestamp.toNumber() * 1000, 87 | type: item.stakeType === 0 ? StakingRequestType.BASE : StakingRequestType.EXTENDED, 88 | id: item._baseStakeId.toNumber() 89 | }; 90 | } catch (ex) { 91 | return ex.message; 92 | } 93 | } 94 | 95 | async getBaseStakeRequestTotal() { 96 | try { 97 | const total = await this.stakingContract.getBaseStakeRequestTotal(); 98 | return total.toNumber(); 99 | } catch (ex) { 100 | return ex.message; 101 | } 102 | } 103 | 104 | async addBaseStakeRequest(nodeAddress, amount, period) { 105 | try { 106 | // const receipt = await transaction.wait(); 107 | return await this.stakingContractWithSigner.addBaseStakeRequest(nodeAddress, amount, period); 108 | } catch (ex) { 109 | return ex.message; 110 | } 111 | } 112 | 113 | async approveBaseStakeRequest(baseStakeId, rewardAddress) { 114 | try { 115 | return await this.stakingContractWithSigner.approveBaseStakeRequest(baseStakeId, rewardAddress); 116 | } catch (ex) { 117 | return ex.message; 118 | } 119 | } 120 | 121 | async cancelBaseStakeRequest(baseStakeId) { 122 | try { 123 | return await this.stakingContractWithSigner.cancelBaseStakeRequest(baseStakeId); 124 | } catch (ex) { 125 | return ex.message; 126 | } 127 | } 128 | 129 | async declineBaseStakeRequest(baseStakeId) { 130 | try { 131 | return await this.stakingContractWithSigner.declineBaseStakeRequest(baseStakeId); 132 | } catch (ex) { 133 | return ex.message; 134 | } 135 | } 136 | 137 | // EXTENDED STAKING REQUEST 138 | async getExtendedStake(extendedStakeId) { 139 | try { 140 | const item = await this.stakingContract.getExtendedStake(extendedStakeId); 141 | console.log(item); 142 | return { 143 | canBeSplitted: item.allowMultipleOp, 144 | amount: item.amount.toNumber(), 145 | amountBooked: item.amountBooked.toNumber(), 146 | isPreApproved: item.autoConfirm, 147 | nodeAddress: item.nodeAddress, 148 | split: item.operatorReward || 100, 149 | period: item.period.toNumber(), 150 | rewardAddress: item.rewardAddress, 151 | stakeHolderAddress: item.stakeHolderAddress, 152 | stakingContracts: item.stakingContracts.toNumber(), 153 | status: item.status, 154 | // timestamp received is in seconds, so we have to convert it to milliseconds 155 | timestamp: item.timestamp.toNumber() * 1000, 156 | type: item.stakeType === 0 ? StakingRequestType.BASE : StakingRequestType.EXTENDED, 157 | id: item._baseStakeId.toNumber() 158 | }; 159 | } catch (ex) { 160 | return ex.message; 161 | } 162 | } 163 | 164 | async getExtendedStakeRequestContractStats(extendedStakeId) { 165 | try { 166 | const item = await this.stakingContract.getExtendedStakeRequestContractStats(extendedStakeId); 167 | return { 168 | total: item.allContracts.toNumber(), 169 | approvedContracts: item.approvedContracts.toNumber(), 170 | canceledContracts: item.canceledContracts.toNumber(), 171 | declinedContracts: item.declinedContracts.toNumber(), 172 | pendingContracts: item.pendingContracts.toNumber(), 173 | terminatedContracts: item.terminatedContracts.toNumber(), 174 | id: item._extendedStakeId.toNumber() 175 | }; 176 | } catch (ex) { 177 | return ex.message; 178 | } 179 | } 180 | 181 | async getStakeContractForStake(extendedStakeId, stakeContract) { 182 | try { 183 | const item = await this.stakingContract.getStakeContractForStake(extendedStakeId, stakeContract); 184 | return { 185 | amount: item.amount.toNumber(), 186 | nodeAddress: item.nodeAddress, 187 | nodeRewardAddress: item.nodeRewardAddress, 188 | period: item.period.toNumber(), 189 | stakeContractId: item.stakeContractId.toNumber(), 190 | stakeHolderAddress: item.stakeHolderAddress, 191 | stakeHolderRewardAddress: item.stakeHolderRewardAddress, 192 | status: item.status, 193 | // split: item.operatorReward || 100, 194 | // isPreApproved: item.autoConfirm, 195 | // canBeSplitted: item.allowMultipleOp, 196 | // timestamp received is in seconds, so we have to convert it to milliseconds 197 | timestamp: item.timestamp.toNumber() * 1000, 198 | // type: item.stakeType === 0 ? StakingRequestType.BASE : StakingRequestType.EXTENDED, 199 | id: item._stakeId.toNumber() 200 | }; 201 | } catch (ex) { 202 | return ex.message; 203 | } 204 | } 205 | 206 | async getExtendedStakeRequestTotal() { 207 | try { 208 | const total = await this.stakingContract.getExtendedStakeRequestTotal(); 209 | return total.toNumber(); 210 | } catch (ex) { 211 | return ex.message; 212 | } 213 | } 214 | 215 | async addExtendedStakeRequest(nodeAddress, amount, rewardAddress, period, opReward, allowPotSplitting, autoConfirm) { 216 | try { 217 | // const receipt = await transaction.wait(); 218 | return await this.stakingContractWithSigner.addExtendedStakeRequest( 219 | nodeAddress, 220 | amount, 221 | rewardAddress, 222 | period, 223 | opReward, 224 | allowPotSplitting, 225 | autoConfirm 226 | ); 227 | } catch (ex) { 228 | return ex.message; 229 | } 230 | } 231 | 232 | async applyExtendedStakeRequest(extendedStakeId, amount, rewardAddress) { 233 | try { 234 | return await this.stakingContractWithSigner.applyExtendedStakeRequest(extendedStakeId, amount, rewardAddress); 235 | } catch (ex) { 236 | return ex.message; 237 | } 238 | } 239 | 240 | async approveExtendedStakeRequest(extendedStakeId, rewardAddress) { 241 | try { 242 | return await this.stakingContractWithSigner.approveExtendedStakeRequest(extendedStakeId, rewardAddress); 243 | } catch (ex) { 244 | return ex.message; 245 | } 246 | } 247 | 248 | async cancelExtendedStakeRequest(extendStakeId) { 249 | try { 250 | return await this.stakingContractWithSigner.cancelExtendedStakeRequest(extendStakeId); 251 | } catch (ex) { 252 | return ex.message; 253 | } 254 | } 255 | 256 | async declineExtendedStakeRequest(extendStakeId) { 257 | try { 258 | return await this.stakingContractWithSigner.declineExtendedStakeRequest(extendStakeId); 259 | } catch (ex) { 260 | return ex.message; 261 | } 262 | } 263 | } 264 | 265 | export default EtnyStakingContract; 266 | -------------------------------------------------------------------------------- /public/static/team/ghost.svg: -------------------------------------------------------------------------------- 1 | 2 | 59 | 60 | 61 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 87 | 88 | 89 | 90 | 98 | 99 | 100 | 101 | 108 | 109 | 110 | 111 | 119 | 120 | 121 | 122 | 129 | 130 | 131 | 132 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /src/components/contract/StakingPotContracts.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Col, Empty, Form, Input, Modal, notification, Row, Spin, Tooltip } from 'antd'; 4 | import { CopyOutlined, ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons'; 5 | import { useWeb3React } from '@web3-react/core'; 6 | import MarketplaceOfferCardV1 from '../marketplace/MarketplaceOfferCardV1'; 7 | import { formatNumber } from '../../utils/numberFormatter'; 8 | import StakingStatusTag from '../staking/StakingStatusTag'; 9 | import { formatDate, getDaysUntil, getPercentOfDaysUntil, getRatePerYear } from '../../utils/StakingPotCalculator'; 10 | import EtnyStakingContract from '../../operations/etnyStakingContract'; 11 | 12 | const StakingPotContracts = ({ status, contracts }) => { 13 | const { account, library } = useWeb3React(); 14 | const etnyStakingContract = new EtnyStakingContract(library); 15 | const [isLoading, setIsLoading] = useState(false); 16 | const [form] = Form.useForm(); 17 | const [currentSelected, setCurrentSelected] = useState(null); 18 | 19 | useEffect(() => { 20 | // The "any" network will allow spontaneous network changes 21 | const provider = etnyStakingContract.getProvider(); 22 | provider.on('accountsChanged', onAccountChanged); 23 | 24 | return () => { 25 | provider.removeAllListeners('accountsChanged'); 26 | }; 27 | }, []); 28 | 29 | const onAccountChanged = async (accounts) => { 30 | console.log('accountChanged'); 31 | // if (accounts.length === 1) await initialize(); 32 | }; 33 | 34 | const onApproveFormSubmited = async (values) => { 35 | await etnyStakingContract.approveBaseStakeRequest(currentSelected, values.rewardAddress); 36 | notification.success({ 37 | placement: 'bottomRight', 38 | className: 'bg-white dark:bg-black text-black dark:text-white', 39 | message: Ethernity, 40 | description: `Offer for staking pot ${currentSelected + 1} has been approved.` 41 | }); 42 | }; 43 | 44 | const onApprove = (stakingPot) => { 45 | Modal.confirm({ 46 | title: 'Warning', 47 | icon: , 48 | wrapClassName: 'shadow-md dark:shadow-gray-500 etny-modal dark:etny-modal', 49 | content: ( 50 | <> 51 |

{`Are you sure you want to approve the offer for the staking pot ${stakingPot.id + 1}?`}

52 |
61 | Reward wallet address} 64 | rules={[{ required: true, message: 'Please enter the reward wallet address' }]} 65 | > 66 | 70 | 72 | notification.success({ 73 | placement: 'bottomRight', 74 | message: `Ethernity`, 75 | description: `Wallet address has been copied.` 76 | }) 77 | } 78 | /> 79 | 80 | } 81 | placeholder="Reward account address" 82 | /> 83 | 84 |
85 | 86 | ), 87 | okText: 'Confirm', 88 | cancelText: 'Cancel', 89 | onOk: () => { 90 | setCurrentSelected(stakingPot.id); 91 | form.submit(); 92 | } 93 | }); 94 | }; 95 | 96 | const onDecline = (stakingPot) => { 97 | Modal.confirm({ 98 | title: 'Warning', 99 | icon: , 100 | wrapClassName: 'shadow-md dark:shadow-gray-500 etny-modal dark:etny-modal', 101 | content: `Are you sure you want to decline the offer for the staking pot ${stakingPot.id + 1}?`, 102 | okText: 'Confirm', 103 | cancelText: 'Cancel', 104 | onOk: async () => { 105 | // if (stakingPot.type === StakingRequestType.BASE) { 106 | // await etnyStakingContract.declineBaseStakeRequest(stakingPot.id); 107 | // } else { 108 | // await etnyStakingContract.declineExtendedStakeRequest(stakingPot.id); 109 | // } 110 | notification.success({ 111 | placement: 'bottomRight', 112 | message: `Ethernity`, 113 | description: `Offer for staking pot ${stakingPot.id + 1} has been declined.` 114 | }); 115 | } 116 | }); 117 | }; 118 | 119 | const onCancel = (stakingPot) => { 120 | Modal.confirm({ 121 | title: 'Warning', 122 | icon: , 123 | wrapClassName: 'shadow-md dark:shadow-gray-500 etny-modal dark:etny-modal', 124 | content: `Are you sure you want to cancel the offer for the staking pot ${stakingPot.id + 1}?`, 125 | okText: 'Confirm', 126 | cancelText: 'Cancel', 127 | onOk: async () => { 128 | // if (stakingPot.type === StakingRequestType.BASE) { 129 | // await etnyStakingContract.cancelBaseStakeRequest(stakingPot.id); 130 | // } else { 131 | // await etnyStakingContract.cancelExtendedStakeRequest(stakingPot.id); 132 | // } 133 | notification.success({ 134 | placement: 'bottomRight', 135 | message: `Ethernity`, 136 | description: `Offer for staking pot ${stakingPot.id + 1} has been canceled.` 137 | }); 138 | } 139 | }); 140 | }; 141 | 142 | const filteredByStatusStakes = contracts.filter((stake) => stake.status === status); 143 | if (filteredByStatusStakes.length === 0) { 144 | return ( 145 | 153 | There are no available contracts with status 154 | 155 | } 156 | /> 157 | ); 158 | } 159 | 160 | const antIcon = ; 161 | if (isLoading) { 162 | return ( 163 | 164 | 165 | 166 | ); 167 | } 168 | 169 | return ( 170 | 171 | 172 |
173 | 174 | {filteredByStatusStakes.map((contract, index) => ( 175 | 176 | onApprove(contract)} 215 | onDecline={() => onDecline(contract)} 216 | onCancel={() => onCancel(contract)} 217 | /> 218 | 219 | ))} 220 | 221 |
222 | 223 |
224 | ); 225 | }; 226 | 227 | StakingPotContracts.propTypes = { 228 | status: PropTypes.oneOfType([PropTypes.number, PropTypes.array]), 229 | contracts: PropTypes.array 230 | }; 231 | 232 | export default StakingPotContracts; 233 | -------------------------------------------------------------------------------- /src/components/staking/StakingCalculatorCard.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { isMobile } from 'react-device-detect'; 4 | import { Button, Col, Form, InputNumber, Row, Select, Table } from 'antd'; 5 | import { LeftOutlined } from '@ant-design/icons'; 6 | import useTheme from '../../hooks/useTheme'; 7 | import { calculate } from '../../utils/StakingPotCalculator'; 8 | import { StakingRequestType } from '../../utils/StakingRequestType'; 9 | 10 | const { Option } = Select; 11 | 12 | const STAKING_MODE_TEXT = 0; 13 | const STAKING_MODE_TABLE = 1; 14 | 15 | const StakingCalculatorCard = ({ description, pro, cons }) => { 16 | const [mode, setMode] = useState(0); // 0 means details, 1 means details per years 17 | const [requestType, setRequestType] = useState(StakingRequestType.BASE); 18 | const [amount, setAmount] = useState(1900); 19 | const [dataTable, setDataTable] = useState([]); 20 | const { theme, THEME_LIGHT } = useTheme(); 21 | 22 | const columns = [ 23 | { 24 | title: 'Year', 25 | dataIndex: 'year', 26 | key: 'year' 27 | }, 28 | { 29 | title: 'APY (%)', 30 | dataIndex: 'apy', 31 | key: 'apy' 32 | }, 33 | { 34 | title: 'Days', 35 | dataIndex: 'days', 36 | key: 'days' 37 | }, 38 | { 39 | title: 'Reward/Day (ETNY)', 40 | dataIndex: 'rewardPerDay', 41 | key: 'rewardPerDay' 42 | }, 43 | { 44 | title: 'Total Reward (ETNY)', 45 | dataIndex: 'reward', 46 | key: 'reward', 47 | render: (text, record) => {record.reward} 48 | } 49 | ]; 50 | 51 | const onRequestTypeChanged = (value) => { 52 | setRequestType(value); 53 | }; 54 | 55 | const calculateReward = (type, amount, periods, split) => { 56 | const rewardsPerYear = calculate(type, amount, periods, split); 57 | 58 | const rewardsPerYearWithKeys = rewardsPerYear.map((item, index) => ({ key: index, ...item })); 59 | setDataTable(rewardsPerYearWithKeys); 60 | }; 61 | 62 | const onFormSubmit = (values) => { 63 | calculateReward(requestType, values.amount, values.period, values.split); 64 | 65 | setMode(STAKING_MODE_TABLE); 66 | }; 67 | 68 | const border = theme === THEME_LIGHT ? '15px solid #C4E2FF' : '15px solid #01014F'; 69 | 70 | return ( 71 | 78 | {mode === STAKING_MODE_TEXT && ( 79 | 80 |

{requestType} Request

81 |

{description}

82 |
83 |
84 |

85 | What's included 86 |

87 |
88 |
89 | 90 | {pro.map((item, index) => ( 91 | 92 |
93 | {item} 94 | 95 | ))} 96 |
97 | 98 |
99 |
100 |

101 | What's not included 102 |

103 |
104 | {cons.map((item, index) => ( 105 | 106 |
107 | {item} 108 | 109 | ))} 110 |
111 | 112 | )} 113 | 114 | {mode === STAKING_MODE_TABLE && ( 115 | 120 |

121 | setMode(STAKING_MODE_TEXT)} className="text-etny-500" /> 122 | {requestType} Request 123 |

124 | 125 |
126 | 133 | 134 | 135 | )} 136 | 137 | 142 |

Staking Calculator

143 | 150 | Staking request type} 154 | rules={[{ required: true, message: 'Please enter amount for staking' }]} 155 | > 156 | 160 | 161 | 162 | Staking amount (ETNY)} 166 | rules={[{ required: true, message: 'Please enter amount for staking' }]} 167 | > 168 | setAmount(value)} 174 | /> 175 | 176 | 177 | Staking period (months)} 181 | rules={[{ required: true, message: 'Please select staking period' }]} 182 | > 183 | 195 | 196 | 197 | Operator reward split (%)} 201 | rules={[{ required: true, message: 'Please choose the reward split' }]} 202 | > 203 | 219 | 220 | 221 |
222 | 234 |
235 | 236 | 237 | 238 | ); 239 | }; 240 | 241 | StakingCalculatorCard.propTypes = { 242 | description: PropTypes.string, 243 | pro: PropTypes.array, 244 | cons: PropTypes.array 245 | }; 246 | 247 | export default StakingCalculatorCard; 248 | -------------------------------------------------------------------------------- /src/components/icons/CalculatorIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CalculatorIcon = () => ( 4 | 5 | 6 | 15 | 16 | 17 | 18 | 29 | 38 | 47 | 56 | 65 | 74 | 83 | 92 | 101 | 110 | 119 | 128 | 137 | 146 | 155 | 164 | 173 | 182 | 186 | 195 | 204 | 213 | 222 | 231 | 240 | 249 | 258 | 267 | 276 | 285 | 294 | 295 | 296 | ); 297 | export default CalculatorIcon; 298 | --------------------------------------------------------------------------------