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 |
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 |
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 |
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 |
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 |
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 |
19 |
--------------------------------------------------------------------------------
/public/static/logo/logo_etny_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/src/components/icons/SunIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SunIcon = ({ className }) => (
4 |
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 |
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 |
24 | );
25 | export default BookIcon;
26 |
--------------------------------------------------------------------------------
/public/static/logo/logo_ethc_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
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 |
21 |
{label}
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/public/static/img_icn_contract.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
177 | )}
178 |
179 |
180 | {theme === THEME_LIGHT && (
181 |
}
187 | />
188 | )}
189 |
190 | {theme === THEME_DARK && (
191 |
}
197 | />
198 | )}
199 |
200 | {active && isMetamaskLoggedIn && (
201 |
202 |
203 | }
208 | />
209 |
210 |
211 | )}
212 |
213 |
214 |
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 |
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 | 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 |
134 |
135 | )}
136 |
137 |
142 | Staking Calculator
143 | 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 |
296 | );
297 | export default CalculatorIcon;
298 |
--------------------------------------------------------------------------------