├── .editorconfig
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── todo.md
├── .gitignore
├── LICENSE.md
├── README.md
├── components
├── about
│ └── index.js
├── common
│ ├── Identicon.js
│ ├── ProfileBadge.js
│ ├── footer.js
│ ├── header.js
│ ├── hooks
│ │ └── useHover.js
│ ├── layouts
│ │ ├── base.js
│ │ ├── dashboard.js
│ │ └── documentation.js
│ ├── page.js
│ ├── side-menu-footer.js
│ ├── sidemenu.js
│ ├── utilities
│ │ └── popovers
│ │ │ └── network-popover.js
│ └── withSlideIn.js
├── council-member-profile
│ ├── CouncilMemberInfoHeader.js
│ ├── CouncilMemberKeyStats.js
│ ├── EditCouncilMemberProfileModal.js
│ ├── council-viz
│ │ ├── Circleandline.js
│ │ ├── CouncilViz.js
│ │ ├── Network.js
│ │ └── WhiteCircles.js
│ └── index.js
├── council-members
│ ├── MembersTable.js
│ └── index.js
├── home
│ ├── FAQs.js
│ ├── SocialProofStats.js
│ ├── SupportedNetworks.js
│ ├── index.js
│ ├── landing-page-calculator
│ │ ├── earnings-output.js
│ │ └── index.js
│ └── testimonials
│ │ └── index.js
├── new-payment
│ ├── ConfirmAmountChange.js
│ ├── ConfirmSelection.js
│ ├── RiskTag.js
│ ├── ValidatorsList.js
│ └── index.js
├── nominators
│ ├── NominatorsTable.js
│ ├── Top3Section.js
│ └── index.js
├── overview
│ ├── AllNominations.js
│ ├── AmountConfirmation.js
│ ├── AmountInput.js
│ ├── ChainErrorPage.js
│ ├── ChillAlert.js
│ ├── EarningsOutput.js
│ ├── EditControllerModal.js
│ ├── EditValidators.js
│ ├── ExpectedReturns.js
│ ├── FundsUpdate.js
│ ├── NominationsTable.js
│ ├── OverviewCards.js
│ ├── PastEarningsTimeRange.js
│ ├── RewardDestinationModal.js
│ ├── SuccessfullyBonded.js
│ └── index.js
├── payment
│ ├── Confirmation.js
│ ├── RewardDestination.js
│ ├── TermsOfService.js
│ ├── Transaction.js
│ └── index.js
├── policies
│ ├── disclaimer-component.js
│ ├── privacy-component.js
│ └── terms-component.js
├── reward-calculator
│ ├── AmountInput.js
│ ├── CompoundRewardSlider.js
│ ├── ExpectedReturnsCard.js
│ ├── RiskSelect.js
│ ├── RiskTag.js
│ ├── TimePeriodInput.js
│ ├── ValidatorsList.js
│ └── index.js
├── settings
│ └── index.js
├── validator-profile
│ ├── EditValidatorProfileModal.js
│ ├── LimitedTextArea.js
│ ├── LinkedValidatorsGroup.js
│ ├── ProfileTabs.js
│ ├── TeamMembers.js
│ ├── TransparencyScoreModal.js
│ ├── ValidatorInfoHeader.js
│ ├── ValidatorKeyStats.js
│ ├── ValidatorReturnsCalculator.js
│ ├── index.js
│ └── validator-viz
│ │ ├── Circleandline.js
│ │ ├── Network.js
│ │ ├── ValidatorViz.js
│ │ └── WhiteCircles.js
├── validators
│ ├── EditAmountModal.js
│ ├── FilterPanel.js
│ ├── ValidatorsResult.js
│ ├── ValidatorsTable.js
│ └── index.js
└── wallet-connect
│ ├── CreateWallet.js
│ ├── ImportAccount.js
│ ├── RecoverAuthInfo.js
│ ├── RejectedPage.js
│ ├── SelectAccount.js
│ ├── WalletDisclaimer.js
│ └── index.js
├── jsconfig.json
├── lib
├── analytics.js
├── axios.js
├── calculate-reward.js
├── convert-arr-to-object.js
├── convert-currency.js
├── format-currency.js
├── getClaimableRewards.js
├── getErasHistoric.js
├── getRewards.js
├── getTransactionFee.js
├── getUpdateFundsTransactionFee.js
├── polkadot-api.js
├── polkadot-extension.js
├── polkadot
│ ├── chill.js
│ ├── edit-controller.js
│ ├── nominate.js
│ ├── update-funds.js
│ └── update-payee.js
├── routes.js
├── stake.js
└── store.js
├── next.config.js
├── package.json
├── pages
├── 404.js
├── _app.js
├── _document.js
├── _error.js
├── about.js
├── council-member-profile
│ └── [id].js
├── council-members.js
├── disclaimer.js
├── index.js
├── nominators.js
├── overview.js
├── payment.js
├── privacy.js
├── reward-calculator.js
├── settings.js
├── terms.js
├── validator-profile
│ └── [id].js
└── validators.js
├── postcss.config.js
├── public
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── images
│ ├── 404.svg
│ ├── badges
│ │ ├── 1.svg
│ │ ├── 2.svg
│ │ ├── 3.svg
│ │ ├── 4.svg
│ │ └── gray-badge.svg
│ ├── baroque.png
│ ├── chris-hutchinson.png
│ ├── dave-ramico.jpg
│ ├── discord-logo.svg
│ ├── dollar-sign.jpg
│ ├── dollar-sign.svg
│ ├── empty-state.svg
│ ├── enea-arllai.jpg
│ ├── kusama-logo-light.png
│ ├── kusama-logo.png
│ ├── landing-bg.svg
│ ├── polkadot-logo.png
│ ├── polkadot-successfully-bonded.png
│ ├── polkadot-wallet-connect-info.png
│ ├── polkadot-wallet-connect-success.png
│ ├── polkadot-wallet-connect.png
│ ├── polkadot_alert.png
│ ├── riot-logo.svg
│ ├── ruben-russel.png
│ ├── team
│ │ ├── prastut-kumar.png
│ │ ├── sahil-nanda.png
│ │ └── saumya-karan.png
│ ├── telegram-logo.svg
│ ├── unicorn-sweat
│ │ ├── unicorn-sweat.png
│ │ ├── unicorn-sweat.svg
│ │ ├── unicorn-sweat@0.1x.png
│ │ ├── unicorn-sweat@0.25x.png
│ │ ├── unicorn-sweat@0.5x.png
│ │ └── unicorn-sweat@0.75x.png
│ ├── web3foundation_grants_badge_black.png
│ └── yieldscan-logo.svg
└── zeit.svg
├── styles
└── index.scss
├── tailwind.config.js
├── yarn.lock
└── yieldscan.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | trim_trailing_whitespace = true
2 | indent_size = 2
3 | indent_style = tab
4 | insert_final_newline = true
5 | quote_type = single
6 |
--------------------------------------------------------------------------------
/.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: ''
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 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE REQUEST]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/todo.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: TODO
3 | about: Add a TODO
4 | title: "[TODO] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | # TODO
11 |
12 | ## 🎯 What is the problem that needs to be solved?
13 |
14 | Try to be as specific as possible. Think about falsifiable outcomes.
15 |
16 | **Refer:** [SMART Goal Examples](https://www.thebalancesmb.com/smart-goal-examples-2951827)
17 |
18 | ### 🗒 Summary
19 | A brief overview of the problem to be solved. Include all relevant data points.
20 |
21 | ### ✅ Outcomes
22 | **Examples:**
23 |
24 | - [ ] Create the a functional UI for returns calculator which takes `stake_amount` as input and returns `annual_percentage_return` as output
25 | - [ ] Add a functional risk level input to the calculator
26 | - [ ] Add a switch input to compound returns
27 |
28 | ## ⏳ ETA
29 | **Example:** By `26th June, 2020 - 6 PM IST`
30 |
31 | If you don't specify a timezone, by default IST shall be assumed.
32 |
33 | Refer: [ETA culture document](https://docs.google.com/document/d/1ZGa6diesOmx-jSBjm0uj8j4oN75gYqux85qA7DJOm68/edit)
34 |
--------------------------------------------------------------------------------
/.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 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | .env*
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/components/common/Identicon.js:
--------------------------------------------------------------------------------
1 | import { noop } from "lodash";
2 | import { useToast } from "@chakra-ui/core";
3 | import BaseIdenticon from "@polkadot/react-identicon";
4 |
5 | const Identicon = ({
6 | address,
7 | size = "40",
8 | onClick = noop,
9 | showToast = true,
10 | }) => {
11 | const toast = useToast();
12 | const onCopy = () => {
13 | if (showToast) {
14 | toast({
15 | duration: 2000,
16 | status: "success",
17 | position: "top-right",
18 | description: "Address copied",
19 | });
20 | }
21 |
22 | onClick(address);
23 | };
24 |
25 | return (
26 |
ev.stopPropagation()} className="-mb-1">
27 |
33 |
34 | );
35 | };
36 |
37 | export default Identicon;
38 |
--------------------------------------------------------------------------------
/components/common/ProfileBadge.js:
--------------------------------------------------------------------------------
1 | import {
2 | Popover,
3 | PopoverTrigger,
4 | PopoverContent,
5 | PopoverHeader,
6 | PopoverBody,
7 | PopoverArrow,
8 | PopoverCloseButton,
9 | } from "@chakra-ui/core";
10 |
11 | const BADGE_STATIC_URL = "/images/badges/";
12 |
13 | const BadgeInstance = (image, text) => ({ image, text });
14 |
15 | const getBadge = (score) => {
16 | if (score > 0 && score < 200) {
17 | return BadgeInstance("1.svg", "BASIC TRANSPARENCY");
18 | } else if (score >= 200 && score < 330) {
19 | return BadgeInstance("2.svg", "EXCELLENT TRANSPARENCY");
20 | } else if (score >= 330) {
21 | return BadgeInstance("3.svg", "SUPREME TRANSPARENCY");
22 | } else {
23 | return null;
24 | }
25 | };
26 |
27 | const ProfileBadge = ({ score }) => {
28 | const badge = getBadge(score);
29 |
30 | return badge ? (
31 |
32 |
33 |
34 |
39 |
40 |
50 |
51 |
52 | {badge.text}
53 |
54 |
55 |
56 |
57 | ) : (
58 | ""
59 | );
60 | };
61 |
62 | export default ProfileBadge;
63 |
--------------------------------------------------------------------------------
/components/common/footer.js:
--------------------------------------------------------------------------------
1 | import { useAccounts, useHeaderLoading } from "@lib/store";
2 | import Link from "next/link";
3 | import React from "react";
4 | import { FaDiscord, FaTelegram } from "react-icons/fa";
5 | import { IoIosMail } from "react-icons/io";
6 |
7 |
8 | const Footer = () => {
9 | const { accountInfoLoading } = useAccounts();
10 | const { headerLoading } = useHeaderLoading();
11 | return (
12 | !accountInfoLoading &&
13 | !headerLoading && (
14 |
68 | )
69 | );
70 | };
71 |
72 | export default Footer;
73 |
--------------------------------------------------------------------------------
/components/common/hooks/useHover.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 |
3 | const useHover = () => {
4 | const [hovered, setHovered] = useState(false);
5 |
6 | const ref = useRef(null);
7 |
8 | const handleMouseOver = () => setHovered(true);
9 | const handleMouseOut = () => setHovered(false);
10 |
11 | useEffect(
12 | () => {
13 | const node = ref.current;
14 | if (node) {
15 | node.addEventListener('mouseover', handleMouseOver);
16 | node.addEventListener('mouseout', handleMouseOut);
17 |
18 | return () => {
19 | node.removeEventListener('mouseover', handleMouseOver);
20 | node.removeEventListener('mouseout', handleMouseOut);
21 | };
22 | }
23 | },
24 | [ref.current] // recall only if ref changes
25 | );
26 |
27 | return [ref, hovered];
28 | }
29 |
30 | export default useHover;
--------------------------------------------------------------------------------
/components/common/layouts/documentation.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import Footer from "../footer";
3 |
4 | const Header = dynamic(
5 | () => import("@components/common/header").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const withDocumentationLayout = (children) => {
10 | return () => (
11 |
18 | );
19 | };
20 |
21 | export default withDocumentationLayout;
22 |
--------------------------------------------------------------------------------
/components/common/page.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import React from "react";
3 | import { isMobile, isTablet } from "react-device-detect";
4 | import { trackEvent, Events } from "@lib/analytics";
5 | import { useEffect } from "react";
6 |
7 | window.setImmediate = (cb) => cb();
8 |
9 | const Page = ({ title, children, layoutProvider }) => {
10 | const layoutedChild = layoutProvider ? layoutProvider(children) : children;
11 |
12 | if (isMobile || isTablet) {
13 | return (
14 |
15 |
16 | {title} - YieldScan
17 |
18 |
22 |
23 | YieldScan
24 |
25 |
26 | We don't support this device yet. To use YieldScan please visit us
27 | on your desktop / laptop.
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | useEffect(() => {
35 | trackEvent(Events.PAGE_VIEW, { path: window.location.pathname });
36 | }, []);
37 |
38 | return (
39 |
40 |
41 | {title} - YieldScan
42 |
43 | {layoutedChild()}
44 |
45 | );
46 | };
47 |
48 | export default Page;
49 |
--------------------------------------------------------------------------------
/components/common/side-menu-footer.js:
--------------------------------------------------------------------------------
1 | import { FaDiscord, FaTelegram } from "react-icons/fa";
2 | import { IoIosMail } from "react-icons/io";
3 | import { MenuOption } from "@components/common/sidemenu";
4 | import { useRouter } from "next/router";
5 | import Routes from "@lib/routes";
6 |
7 | const SideMenuFooter = () => {
8 | const router = useRouter();
9 |
10 | return (
11 |
12 | {/*
*/}
17 |
22 |
45 |
46 | );
47 | };
48 |
49 | export default SideMenuFooter;
50 |
--------------------------------------------------------------------------------
/components/common/sidemenu.js:
--------------------------------------------------------------------------------
1 | import {
2 | Home,
3 | UserCheck,
4 | Users,
5 | Sliders,
6 | Globe,
7 | ChevronRight,
8 | } from "react-feather";
9 | import { useRouter } from "next/router";
10 | import Link from "next/link";
11 | import Routes from "@lib/routes";
12 | import { Badge, Collapse } from "@chakra-ui/core";
13 | import { useState } from "react";
14 |
15 | const MenuOption = ({
16 | label,
17 | Icon,
18 | selected = false,
19 | href,
20 | isExternal = false,
21 | }) => {
22 | return (
23 |
24 |
40 | {Icon && }
41 | {label}
42 |
43 |
44 | );
45 | };
46 |
47 | const SideMenu = () => {
48 | const router = useRouter();
49 | const [showPortfolioTabs, setShowPortfolioTabs] = useState(true);
50 | const [showExploreTabs, setShowExploreTabs] = useState(true);
51 |
52 | return (
53 |
54 | setShowPortfolioTabs(!showPortfolioTabs)}
57 | >
58 |
64 | Portfolio
65 |
66 |
67 |
73 |
79 |
80 | setShowExploreTabs(!showExploreTabs)}
83 | >
84 |
90 | Explore
91 | {/*
101 | unaudited
102 | */}
103 |
104 |
105 |
111 |
117 |
123 |
124 |
125 | );
126 | };
127 |
128 | export default SideMenu;
129 |
130 | export { MenuOption };
131 |
--------------------------------------------------------------------------------
/components/common/withSlideIn.js:
--------------------------------------------------------------------------------
1 | import { SlideIn } from '@chakra-ui/core';
2 |
3 | const withSlideIn = (Component) => (props) => (
4 |
5 | {(styles) => }
6 |
7 | );
8 |
9 | export default withSlideIn;
10 |
--------------------------------------------------------------------------------
/components/council-member-profile/CouncilMemberKeyStats.js:
--------------------------------------------------------------------------------
1 | import convertCurrency from "@lib/convert-currency";
2 | import { useEffect, useState } from "react";
3 | import formatCurrency from "@lib/format-currency";
4 |
5 | const CouncilMemberKeyStats = ({
6 | voters = 0,
7 | backingAmount = 0,
8 | totalBalance = 0,
9 | networkInfo,
10 | }) => {
11 | const [backingSubCurrency, setBackingSubCurrency] = useState();
12 | const [totalBalanceSubCurrency, setTotalBalanceSubCurrency] = useState();
13 | useEffect(() => {
14 | if (backingAmount) {
15 | convertCurrency(backingAmount, networkInfo.denom).then((value) =>
16 | setBackingSubCurrency(value)
17 | );
18 | }
19 | }, [backingAmount]);
20 | useEffect(() => {
21 | if (totalBalance) {
22 | convertCurrency(totalBalance, networkInfo.denom).then((value) =>
23 | setTotalBalanceSubCurrency(value)
24 | );
25 | }
26 | }, [totalBalance]);
27 | return (
28 |
29 |
30 | KEY STATISTICS
31 |
32 |
33 | No. of voters
34 |
{voters}
35 |
36 |
37 | Amount of Backing
38 |
39 | {formatCurrency.methods.formatAmount(
40 | Math.trunc((backingAmount || 0) * 10 ** networkInfo.decimalPlaces),
41 | networkInfo
42 | )}
43 |
44 | {backingSubCurrency && (
45 |
46 | $
47 | {formatCurrency.methods.formatNumber(backingSubCurrency.toFixed(2))}
48 |
49 | )}
50 |
51 |
52 | Total Account Balance
53 |
54 | {formatCurrency.methods.formatAmount(
55 | Math.trunc((totalBalance || 0) * 10 ** networkInfo.decimalPlaces),
56 | networkInfo
57 | )}
58 |
59 | {totalBalanceSubCurrency && (
60 |
61 | $
62 | {formatCurrency.methods.formatNumber(
63 | totalBalanceSubCurrency.toFixed(2)
64 | )}
65 |
66 | )}
67 |
68 |
69 | );
70 | };
71 |
72 | export default CouncilMemberKeyStats;
73 |
--------------------------------------------------------------------------------
/components/council-member-profile/council-viz/Circleandline.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Circle, Line, Text, Rect } from "react-konva";
3 |
4 | class Circleandline extends React.Component {
5 | constructor() {
6 | super();
7 | this.state = {
8 | showNominatorAddress: false,
9 | };
10 | }
11 | handleOnMouseOver = (e) => {
12 | e.target.setAttrs({
13 | scaleX: 1.3,
14 | scaleY: 1.3,
15 | });
16 | document.body.style.cursor = "pointer";
17 | this.setState({ showNominatorAddress: true });
18 | };
19 | handleOnMouseOut = (e) => {
20 | e.target.setAttrs({
21 | scaleX: 1,
22 | scaleY: 1,
23 | });
24 | document.body.style.cursor = "default";
25 | this.setState({ showNominatorAddress: false });
26 | };
27 |
28 | render() {
29 | const cardWidth = 159;
30 | const cardHeight = 69;
31 |
32 | let backer = this.props.name !== null ? this.props.name : this.props.backer;
33 |
34 | if (backer.length > 11) {
35 | backer = backer.slice(0, 5) + "..." + backer.slice(-5);
36 | }
37 | let stake =
38 | "Stake: " +
39 | this.props.stake.toString().slice(0, 7) +
40 | ` ${this.props.networkInfo.denom}`;
41 |
42 | let x1 = this.props.x + 15;
43 | let y1 = this.props.y - 8;
44 |
45 | if (x1 > this.props.width - cardWidth) {
46 | y1 = y1 + 20;
47 | x1 = x1 - cardWidth / 2;
48 | }
49 |
50 | return (
51 |
52 |
57 |
66 |
67 | {this.state.showNominatorAddress && (
68 |
80 | )}
81 |
82 | {this.state.showNominatorAddress && (
83 |
91 | )}
92 | {this.state.showNominatorAddress && (
93 |
94 | )}
95 |
96 | );
97 | }
98 | }
99 | export default Circleandline;
100 |
--------------------------------------------------------------------------------
/components/council-member-profile/council-viz/CouncilViz.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Stage, Layer, Circle, Line, Rect, Text } from "react-konva";
3 | import WhiteCircles from "./WhiteCircles";
4 | import Network from "./Network";
5 |
6 | class CouncilViz extends React.Component {
7 | constructor() {
8 | super();
9 | this.container = React.createRef();
10 | this.state = {
11 | stageWidth: undefined,
12 | stageHeight: undefined,
13 | };
14 | }
15 |
16 | componentDidMount() {
17 | this.setState({
18 | stageWidth: this.container.offsetWidth,
19 | stageHeight: this.container.offsetHeight,
20 | });
21 | }
22 |
23 | render() {
24 | const width =
25 | this.state.stageWidth !== undefined
26 | ? this.state.stageWidth
27 | : window.innerWidth;
28 | const height =
29 | this.state.stageHeight !== undefined
30 | ? this.state.stageHeight
31 | : window.innerHeight;
32 | let valText = "";
33 | if (this.props.memberInfo !== undefined) {
34 | valText =
35 | this.props.memberInfo.socialInfo.name !== null
36 | ? this.props.memberInfo.socialInfo.name
37 | : this.props.memberInfo.keyStats.stashId;
38 | if (valText.length > 11) {
39 | valText = valText.slice(0, 5) + "..." + valText.slice(-5);
40 | }
41 | }
42 | const NetworkName = this.props.networkName;
43 | let radius = 400;
44 |
45 | const validatorRectangleWidth = 110;
46 | const validatorRectangleHeight = 30;
47 |
48 | let opacity = 0.3;
49 |
50 | return this.props.memberInfo === undefined ? (
51 |
52 |
53 |
54 | ) : (
55 | <>
56 | {
59 | this.container = node;
60 | }}
61 | >
62 | {/*
68 | ←
69 |
*/}
70 |
71 |
72 |
73 |
81 | {/* Here n is number of white circles to draw
82 | r is radius of the imaginary circle on which we have to draw white circles
83 | x,y is center of imaginary circle
84 | */}
85 |
86 |
95 | {/* Adding 6 to stating and ending y point and 24 to length of line
96 | because the upper left corner of rectangle is at width/2,height/2
97 | so mid point of rectangle becomes width/2+12,height/2+6
98 | */}
99 |
106 | {/* Arc used to create the semicircle on the right,
107 | Rotation is used to rotate the arc drawn by 90 degrees in clockwise direction
108 | */}
109 |
119 |
120 |
130 |
141 |
147 |
148 |
149 |
150 | >
151 | );
152 | }
153 | }
154 |
155 | export default CouncilViz;
156 |
--------------------------------------------------------------------------------
/components/council-member-profile/council-viz/Network.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text } from "react-konva";
3 |
4 | class Network extends React.Component {
5 | render() {
6 | let angle = 180 * 0.0174533 - (1 / 5) * Math.PI;
7 | const maxAngle = (1 / 5) * 2 * Math.PI;
8 | const arr = [];
9 | const reversedName = this.props.networkInfo.name.split("").reverse();
10 |
11 | const radius = 220;
12 |
13 | reversedName.forEach((element, index) => {
14 | angle += maxAngle / (Number(reversedName.length) + 1);
15 | arr.push(
16 |
26 | );
27 | });
28 |
29 | return arr;
30 | }
31 | }
32 |
33 | export default Network;
34 |
--------------------------------------------------------------------------------
/components/council-member-profile/council-viz/WhiteCircles.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Circleandline from "./Circleandline";
3 |
4 | class WhiteCircles extends React.Component {
5 | render() {
6 | let angle = (2 / 3) * Math.PI;
7 | let maxAngle = (2 / 3) * Math.PI;
8 | const arr = [];
9 |
10 | // Uncomment in case we want to extend angle for nominators display
11 | // if (this.props.memberInfo.backersInfo.length > 5) {
12 | // angle = Math.PI / 4;
13 | // maxAngle = (3 / 4) * 2 * Math.PI;
14 | // }
15 |
16 | this.props.memberInfo.backersInfo.forEach((element, index) => {
17 | angle +=
18 | maxAngle / (Number(this.props.memberInfo.backersInfo.length) + 1);
19 | const radius = Math.floor(
20 | Math.random() * (this.props.maxRadius - 150) + 150
21 | );
22 | arr.push(
23 |
37 | );
38 | });
39 |
40 | return arr;
41 | }
42 | }
43 |
44 | export default WhiteCircles;
45 |
--------------------------------------------------------------------------------
/components/council-members/MembersTable.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import Routes from "@lib/routes";
3 | import Identicon from "@components/common/Identicon";
4 | import { ExternalLink } from "react-feather";
5 | import formatCurrency from "@lib/format-currency";
6 |
7 | const MemberCard = ({
8 | name,
9 | accountId,
10 | backing,
11 | totalBalance,
12 | numberOfBackers,
13 | onProfile,
14 | networkInfo,
15 | }) => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {name || accountId.slice(0, 18) + "..." || "-"}
25 |
26 |
27 | View Profile
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | No. of voters
36 |
37 |
{numberOfBackers}
38 |
39 |
40 | Backing
41 |
42 | {formatCurrency.methods.formatAmount(
43 | Math.trunc((backing || 0) * 10 ** networkInfo.decimalPlaces),
44 | networkInfo
45 | )}
46 |
47 |
48 |
49 | Balance
50 |
51 | {formatCurrency.methods.formatAmount(
52 | Math.trunc((totalBalance || 0) * 10 ** networkInfo.decimalPlaces),
53 | networkInfo
54 | )}
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | const MembersTable = ({ members, networkInfo }) => {
63 | const router = useRouter();
64 |
65 | return (
66 |
67 |
68 | {members.map((member, index) => (
69 |
77 | window.open(
78 | `${Routes.COUNCIL_MEMBER_PROFILE}/${member.accountId}`,
79 | "_blank"
80 | )
81 | }
82 | networkInfo={networkInfo}
83 | />
84 | ))}
85 |
86 |
91 |
92 | );
93 | };
94 |
95 | export default MembersTable;
96 |
--------------------------------------------------------------------------------
/components/council-members/index.js:
--------------------------------------------------------------------------------
1 | import axios from "@lib/axios";
2 | import { Spinner } from "@chakra-ui/core";
3 | import { useState, useEffect } from "react";
4 | import { AlertTriangle } from "react-feather";
5 | import { useCouncil, useSelectedNetwork } from "@lib/store";
6 | import MembersTable from "./MembersTable";
7 | import { getNetworkInfo } from "yieldscan.config";
8 |
9 | const Tabs = {
10 | COUNCIL_MEMBERS: "council-members",
11 | RUNNER_UPS: "runner-ups",
12 | };
13 |
14 | const CouncilMembers = () => {
15 | const [error, setError] = useState(false);
16 | const { selectedNetwork } = useSelectedNetwork();
17 | const networkInfo = getNetworkInfo(selectedNetwork);
18 | const {
19 | councilMembers,
20 | setCouncilMembers,
21 | councilLoading,
22 | setCouncilLoading,
23 | } = useCouncil();
24 | const [selectedTab, setSelectedTab] = useState(Tabs.COUNCIL_MEMBERS);
25 |
26 | // useEffect(() => {
27 | // if (!councilMembers) {
28 | // setError(false);
29 | // axios
30 | // .get(`/${networkInfo.coinGeckoDenom}/council/members`)
31 | // .then(({ data }) => {
32 | // setCouncilMembers(data);
33 | // })
34 | // .catch(() => {
35 | // setError(true);
36 | // })
37 | // .finally(() => {
38 | // setLoading(false);
39 | // });
40 | // } else {
41 | // setLoading(true);
42 | // }
43 | // }, []);
44 |
45 | useEffect(() => {
46 | if (!councilMembers) {
47 | // setCouncilLoading(true);
48 | setError(false);
49 | axios
50 | .get(`/${networkInfo.coinGeckoDenom}/council/members`)
51 | .then(({ data }) => {
52 | setCouncilMembers(data);
53 | })
54 | .catch(() => {
55 | setError(true);
56 | })
57 | .finally(() => {
58 | setCouncilLoading(false);
59 | });
60 | }
61 | }, [councilMembers, networkInfo]);
62 |
63 | if (error) {
64 | return (
65 |
66 |
67 |
68 |
69 | Sorry, no data for your account! :(
70 |
71 |
72 | Try changing the stash account.
73 |
74 |
75 |
76 | );
77 | }
78 |
79 | if (councilLoading) {
80 | return (
81 |
82 |
83 |
84 |
85 | Fetching council members...
86 |
87 |
88 |
89 | );
90 | }
91 |
92 | return (
93 |
94 |
95 |
96 | {selectedTab === Tabs.COUNCIL_MEMBERS
97 | ? "Council Members"
98 | : "Runner ups"}
99 |
100 |
101 | setSelectedTab(Tabs.COUNCIL_MEMBERS)}
108 | >
109 | Council Members
110 |
111 | setSelectedTab(Tabs.RUNNER_UPS)}
118 | >
119 | Runner Ups
120 |
121 |
122 |
123 |
131 |
132 | );
133 | };
134 |
135 | export default CouncilMembers;
136 |
--------------------------------------------------------------------------------
/components/home/FAQs.js:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionItem,
4 | AccordionHeader,
5 | Heading,
6 | AccordionIcon,
7 | AccordionPanel,
8 | Link,
9 | Box,
10 | } from "@chakra-ui/core";
11 |
12 | const FAQs = () => {
13 | return (
14 |
15 |
16 | Frequently Asked Questions
17 |
18 |
19 | Can’t find what you were looking for?{" "}
20 |
26 | Contact us
27 |
28 |
29 |
30 | {data.map((data, index) => (
31 |
37 |
41 |
42 |
49 | {data.question}
50 |
51 |
52 |
53 |
54 |
55 | {data.answer}{" "}
56 | {data.link !== undefined ? (
57 |
62 | {data.link.content}
63 |
64 | ) : (
65 | ""
66 | )}
67 |
68 |
69 | ))}
70 |
71 |
72 | );
73 | };
74 |
75 | export default FAQs;
76 |
77 | const data = [
78 | {
79 | id: 1,
80 | question: "What is YieldScan?",
81 | answer:
82 | "YieldScan is a portfolio management platform for NPoS (nominated proof-of-stake) networks like Kusama and Polkadot. We aim to simplify portfolio management to make yield optimization easier and more accessible, for technical and non-technical users alike.",
83 | },
84 | {
85 | id: 2,
86 | question: "What is staking?",
87 | answer: `Staking is the act of bonding tokens by putting them up as "collateral" for a chance to produce a valid block (and thus obtain a block reward). Validators and nominators stake their tokens in order to secure the network and are rewarded for the same.`,
88 | },
89 | // {
90 | // id: 3,
91 | // question: "What are the risks involved with staking?",
92 | // answer:
93 | // "Polkadot uses NPoS (Nominated Proof-of-Stake) as its mechanism for selecting the validator set. It is designed with the roles of validators and nominators, to maximize chain security.",
94 | // link: {
95 | // url:
96 | // "https://wiki.polkadot.network/docs/en/learn-staking#how-does-staking-work-in-polkadot",
97 | // content: "Learn more about staking",
98 | // isExternal: true,
99 | // },
100 | // },
101 | // {
102 | // id: 4,
103 | // question: "How can I obtain tokens to stake?",
104 | // answer:
105 | // "Nominators secure the relay chain by selecting good validators and staking DOTs.",
106 | // link: {
107 | // url: "https://wiki.polkadot.network/docs/en/maintain-nominator",
108 | // content: "Learn more about nominators",
109 | // isExternal: true,
110 | // },
111 | // },
112 | {
113 | id: 5,
114 | question: "What are your fees?",
115 | answer:
116 | "As of right now, YieldScan is 100% free to use. If this changes in the future, then the fees would be displayed on the confirmation page before sending your transaction.",
117 | },
118 | {
119 | id: 6,
120 | question: "Who keeps the custody of my funds?",
121 | answer:
122 | "You. YieldScan is 100% non-custodial. We do not hold any of your assets, you are in control of your funds at all times.",
123 | },
124 | ];
125 |
--------------------------------------------------------------------------------
/components/home/SocialProofStats.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Spinner } from "@chakra-ui/core";
3 | import axios from "@lib/axios";
4 | import convertCurrency from "@lib/convert-currency";
5 | import formatCurrency from "@lib/format-currency";
6 | import millify from "millify";
7 | import Link from "next/link";
8 |
9 | const StatCard = ({ stat, description, subText }) => {
10 | return (
11 |
12 |
{stat}
13 |
{description}
14 | {subText}
15 |
16 | );
17 | };
18 |
19 | const SocialProofStats = ({ networkName, networkDenom, networkUrl }) => {
20 | const [error, setError] = React.useState(false);
21 | const [loading, setLoading] = React.useState(true);
22 | const [nominatorsData, setNominatorsData] = React.useState([]);
23 | const [
24 | totalAmountStakedSubCurrency,
25 | setTotalAmountStakedSubCurrency,
26 | ] = React.useState();
27 | const [
28 | totalRewardsSubCurrency,
29 | setTotalRewardsSubCurrency,
30 | ] = React.useState();
31 |
32 | React.useEffect(() => {
33 | setLoading(true);
34 | setError(false);
35 | axios
36 | .get(`/${networkUrl}/actors/nominator/stats`)
37 | .then(({ data }) => {
38 | setNominatorsData(data);
39 | })
40 | .catch(() => {
41 | setError(true);
42 | })
43 | .finally(() => {
44 | setLoading(false);
45 | });
46 | }, [networkUrl]);
47 |
48 | React.useEffect(() => {
49 | if (nominatorsData.stats) {
50 | convertCurrency(
51 | nominatorsData.stats.totalAmountStaked,
52 | networkDenom
53 | ).then((value) => setTotalAmountStakedSubCurrency(value));
54 | convertCurrency(
55 | nominatorsData.stats.totalRewards,
56 | networkDenom
57 | ).then((value) => setTotalRewardsSubCurrency(value));
58 | }
59 | }, [nominatorsData, networkDenom]);
60 |
61 | if (error) {
62 | return (
63 |
64 |
🧐
65 |
66 | Sorry, something went wrong while fetching! We'll surely look into
67 | this.
68 |
69 |
70 | );
71 | }
72 | return (
73 |
74 |
78 | ) : (
79 | `$ ${millify(totalAmountStakedSubCurrency)}+`
80 | )
81 | }
82 | description={`Invested in staking on ${networkName}`}
83 | subText={
84 |
90 | }
91 | />
92 |
96 | ) : (
97 | formatCurrency.methods.formatNumber(
98 | Math.floor(nominatorsData.stats.nominatorsCount / 100) * 100
99 | ) + "+"
100 | )
101 | }
102 | description="Active nominators"
103 | subText={
104 |
105 |
View nominators
106 |
107 | }
108 | />
109 |
113 | ) : (
114 | `$ ${millify(totalRewardsSubCurrency)}+`
115 | )
116 | }
117 | description="Earned as staking rewards"
118 | subText={
In the past 24 hrs
}
119 | />
120 |
121 | );
122 | };
123 |
124 | export default SocialProofStats;
125 |
--------------------------------------------------------------------------------
/components/home/SupportedNetworks.js:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 |
3 | const supportedNetworks = ["kusama", "polkadot"];
4 |
5 | const SupportedNetworks = () => {
6 | return (
7 |
8 |
9 | Networks you can start investing in now
10 |
11 |
12 | {supportedNetworks.map((network) => (
13 |
14 |
19 |
{network}
20 |
21 | ))}
22 |
23 |
24 | );
25 | };
26 |
27 | export default SupportedNetworks;
28 |
--------------------------------------------------------------------------------
/components/home/testimonials/index.js:
--------------------------------------------------------------------------------
1 | import { Avatar, Box } from "@chakra-ui/core";
2 | import axios from "@lib/axios";
3 | import { times } from "lodash";
4 | import { useEffect, useState } from "react";
5 | import Marquee, { Motion, randomIntFromInterval } from "react-marquee-slider";
6 |
7 | const testimonialData = [
8 | {
9 | name: "Chris Hutchinson",
10 | avatar: "/images/chris-hutchinson.png",
11 | designation: "Head of Global Community, Web3 Foundation",
12 | comment:
13 | "Yieldscan has a very intuitive approach making it easy to select which validators to nominate with. Making staking simple for new users is a big deal, this tool does just that.",
14 | },
15 | {
16 | name: "Ruben Russel",
17 | avatar: "/images/ruben-russel.png",
18 | designation:
19 | "Co-founder, Caribbean Blockchain Network & former PolkaDAO community manager",
20 | comment:
21 | "Yieldscan is a powerful Polkadot & Kusama analytical tool, assisting its user to make valuable calculated decisions when staking your KSM or DOT tokens. It is easy, useful and fun!",
22 | },
23 | {
24 | name: "Baroque",
25 | avatar: "/images/baroque.png",
26 | designation: "Polkadot & Kusama community member",
27 | comment:
28 | "The yield calculator to optimise risk to reward on staking is superb. Ultimately yieldscan has value and provides great utility for stakers and nominators.",
29 | },
30 | {
31 | name: "Enea Arllai",
32 | avatar: "/images/enea-arllai.jpg",
33 | designation: "Polkadot & Kusama community member",
34 | comment:
35 | "As a novice in the Polkadot and Kusama ecosystems, it can be an extremely daunting introduction to the new world of parachains and relay chains, epoch periods, choosing validators, working the substrate portal, learning new terminology etc. Yieldscan brings state-of-the-art analytics to take the pressure off some of the Staking confusion and makes the general user’s experience one to remember!",
36 | },
37 | {
38 | name: "Dave Ramico",
39 | avatar: "/images/dave-ramico.jpg",
40 | designation: "PolkaDAO member",
41 | comment:
42 | "Yieldscan allows me to keep on top of my validator selection quickly and efficiently, leaving me more time to make Kusama pottery!",
43 | },
44 | ];
45 |
46 | const Testimonials = () => {
47 | const [users, setUsers] = useState([]);
48 | const [isPaused, setIsPaused] = useState(false);
49 | // useEffect(() => {
50 | // axios
51 | // .get("https://dummyapi.io/data/api/user?limit=10", {
52 | // headers: { "app-id": "5f97d648839b3f7fcc501aea" },
53 | // })
54 | // .then(({ data }) => setUsers(data));
55 | // });
56 |
57 | return (
58 |
59 |
60 | See what our users are saying
61 |
62 |
63 | {testimonialData.map(({ name, avatar, designation, comment }) => (
64 | setIsPaused(true)}
75 | onPointerLeave={() => setIsPaused(false)}
76 | >
77 |
78 | {name}
79 |
80 | {designation}
81 | {/*
82 | {designation ? designation : "YieldScan User"}
83 | */}
84 |
85 | {comment}
86 |
87 | ))}
88 |
89 |
90 | );
91 | };
92 |
93 | export default Testimonials;
94 |
--------------------------------------------------------------------------------
/components/new-payment/ConfirmAmountChange.js:
--------------------------------------------------------------------------------
1 | import ValidatorsList from "./ValidatorsList";
2 | import { ArrowRight } from "react-feather";
3 | import formatCurrency from "@lib/format-currency";
4 | import { Spinner, Alert, AlertIcon } from "@chakra-ui/core";
5 | const ConfirmAmountChange = ({
6 | stakingAmount,
7 | validators,
8 | selectedValidators,
9 | setSelectedValidators,
10 | onAdvancedSelection,
11 | bondedAmount,
12 | stakingLoading,
13 | stakingEvent,
14 | onConfirm,
15 | transactionHash,
16 | handlePopoverClose,
17 | networkInfo,
18 | }) => (
19 |
20 | {!stakingLoading && (
21 | <>
22 |
23 |
24 | Your nominations have been successfully updated.
25 |
30 | Track this transaction on Subscan
31 |
32 |
33 |
Change Bonding Amount
34 |
35 |
36 | Currently Bonded
37 |
{bondedAmount.currency}
38 |
39 | ${bondedAmount.subCurrency}
40 |
41 |
42 |
43 |
44 | Final Bonded Amount
45 |
{stakingAmount.currency}
46 |
47 | ${stakingAmount.subCurrency}
48 |
49 |
50 |
51 |
52 | You used a staking budget different than your currently bonded amount
53 | on the calculator. Would you like to update the amount to match?
54 |
55 |
59 | Back to Dashboard
60 |
61 |
65 | Confirm and Stake
66 |
67 | >
68 | )}
69 | {stakingLoading && (
70 |
71 | {/*
Status: */}
72 |
73 | {stakingEvent}
74 |
75 |
76 |
77 | )}
78 |
79 | );
80 |
81 | export default ConfirmAmountChange;
82 |
--------------------------------------------------------------------------------
/components/new-payment/ConfirmSelection.js:
--------------------------------------------------------------------------------
1 | import { get } from "lodash";
2 | import ValidatorsList from "./ValidatorsList";
3 | import formatCurrency from "@lib/format-currency";
4 | import { result } from "lodash";
5 | import { Link, Spinner } from "@chakra-ui/core";
6 | const ConfirmSelection = ({
7 | stakingAmount,
8 | validators,
9 | selectedValidators,
10 | setSelectedValidators,
11 | onAdvancedSelection,
12 | bondedAmount,
13 | stakingLoading,
14 | stakingEvent,
15 | handleSelectionConfirmation,
16 | result,
17 | networkInfo,
18 | }) => (
19 |
20 | {!stakingLoading && (
21 | <>
22 |
Confirm Selection
23 |
24 | You are about to stake your {networkInfo.denom} on the following
25 | validators. Please make sure you understand the risks before
26 | proceeding. Read the{" "}
27 |
28 | Terms of Service
29 |
30 | .
31 |
32 |
33 |
Estimated Returns
34 |
35 | {formatCurrency.methods.formatAmount(
36 | Math.trunc(
37 | result.returns.currency * 10 ** networkInfo.decimalPlaces
38 | ),
39 | networkInfo
40 | )}
41 |
42 |
43 |
52 |
56 | Confirm and Stake
57 |
58 | >
59 | )}
60 | {stakingLoading && (
61 |
62 | {/*
Status: */}
63 |
64 | {stakingEvent}
65 |
66 |
67 |
68 | )}
69 |
70 | );
71 |
72 | export default ConfirmSelection;
73 |
--------------------------------------------------------------------------------
/components/new-payment/RiskTag.js:
--------------------------------------------------------------------------------
1 | const getRiskCategory = riskAmount => {
2 | if (Number.isNaN(riskAmount)) throw new Error('Risk amount should be a number');
3 | if (riskAmount < 0.33) return 'Low';
4 | if (riskAmount >= 0.33 && riskAmount <= 0.66) return 'Medium';
5 | if (riskAmount > 0.66) return 'High';
6 | };
7 |
8 | const getStyleByRisk = (risk) => {
9 | switch (risk) {
10 | case 'Low': return 'text-teal-500 bg-teal-100';
11 | case 'Medium': return 'text-orange-500 bg-orange-100';
12 | case 'High': return 'text-red-500 bg-red-100';
13 | default: return '';
14 | }
15 | };
16 |
17 | const RiskTag = ({ risk, classes }) => {
18 | const riskCategory = getRiskCategory(risk);
19 | return (
20 |
25 | {risk}
26 |
27 | );
28 | };
29 |
30 | export default RiskTag;
--------------------------------------------------------------------------------
/components/nominators/NominatorsTable.js:
--------------------------------------------------------------------------------
1 | import Identicon from "@components/common/Identicon";
2 | import { ExternalLink } from "react-feather";
3 | import formatCurrency from "@lib/format-currency";
4 |
5 | const MemberCard = ({
6 | name,
7 | stashId,
8 | nominations,
9 | dailyEarnings,
10 | totalAmountStaked,
11 | onProfile,
12 | networkInfo,
13 | }) => {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {stashId.slice(0, 6) + "..." + stashId.slice(-6)}
23 |
24 |
25 | View on Polkascan
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Daily Earnings
34 |
35 |
36 | {formatCurrency.methods.formatAmount(
37 | Math.trunc(
38 | (dailyEarnings || 0) * 10 ** networkInfo.decimalPlaces
39 | ),
40 | networkInfo
41 | )}
42 |
43 |
44 |
45 |
46 | Total Amount Staked
47 |
48 |
49 | {formatCurrency.methods.formatAmount(
50 | Math.trunc(
51 | (totalAmountStaked || 0) * 10 ** networkInfo.decimalPlaces
52 | ),
53 | networkInfo
54 | )}
55 |
56 |
57 |
58 |
59 | Nominations
60 |
61 |
{nominations}
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | const NominatorsTable = ({ nominators = [], networkInfo }) => {
69 | return (
70 |
71 |
72 | {nominators.map((member, index) => (
73 |
81 | window.open(
82 | `https://polkascan.io/${networkInfo.coinGeckoDenom}/account/${member.nomId}`,
83 | "_blank"
84 | )
85 | }
86 | networkInfo={networkInfo}
87 | />
88 | ))}
89 |
90 |
91 | );
92 | };
93 |
94 | export default NominatorsTable;
95 |
--------------------------------------------------------------------------------
/components/nominators/Top3Section.js:
--------------------------------------------------------------------------------
1 | import Identicon from "@components/common/Identicon";
2 | import { ExternalLink } from "react-feather";
3 | import formatCurrency from "@lib/format-currency";
4 |
5 | const TopNominatorCard = ({
6 | name,
7 | stashId,
8 | nominations,
9 | totalStake,
10 | dailyEarnings,
11 | onProfile,
12 | networkInfo,
13 | }) => (
14 |
15 |
16 |
20 |
21 | {name || stashId.slice(0, 6) + "..." + stashId.slice(-6) || "-"}
22 |
23 |
27 | View on Polkascan
28 |
29 |
30 |
31 |
32 | Daily Earnings
33 |
34 |
35 | {formatCurrency.methods.formatAmount(
36 | Math.trunc((dailyEarnings || 0) * 10 ** networkInfo.decimalPlaces),
37 | networkInfo
38 | )}
39 |
40 |
41 | Total Amount Staked
42 |
43 |
44 | {formatCurrency.methods.formatAmount(
45 | Math.trunc((totalStake || 0) * 10 ** networkInfo.decimalPlaces),
46 | networkInfo
47 | )}
48 |
49 |
50 | Nominations
51 |
52 |
{nominations}
53 |
54 | );
55 |
56 | const Top3Section = ({ nominators = [], networkInfo }) => {
57 | return (
58 |
59 | {nominators.map((nominator) => (
60 |
61 |
68 | window.open(
69 | `https://polkascan.io/kusama/account/${nominator.nomId}`,
70 | "_blank"
71 | )
72 | }
73 | networkInfo={networkInfo}
74 | />
75 |
76 | ))}
77 |
78 | );
79 | };
80 |
81 | export default Top3Section;
82 |
--------------------------------------------------------------------------------
/components/overview/AllNominations.js:
--------------------------------------------------------------------------------
1 | import { noop } from "lodash";
2 | import { ExternalLink } from "react-feather";
3 | import Identicon from "@components/common/Identicon";
4 | import Routes from "@lib/routes";
5 | import formatCurrency from "@lib/format-currency";
6 | import RiskTag from "@components/reward-calculator/RiskTag";
7 |
8 | const ValidatorCard = ({
9 | name,
10 | stashId,
11 | riskScore,
12 | commission,
13 | totalStake,
14 | networkInfo,
15 | nominators,
16 | onProfile = noop,
17 | }) => {
18 | const displayName = name
19 | ? name.length > 13
20 | ? name.slice(0, 5) + "..." + name.slice(-5)
21 | : name
22 | : stashId.slice(0, 5) + "..." + stashId.slice(-5);
23 | return (
24 |
25 |
26 |
27 |
28 |
{displayName}
29 |
30 | View Profile
31 |
32 |
33 |
34 |
35 | {/*
*/}
36 |
37 | {/*
38 |
39 | Nominators
40 |
41 |
{nominators}
42 | */}
43 |
44 |
45 | Risk Score
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Commission
54 |
55 |
{commission}%
56 |
57 |
58 |
59 | Total Stake
60 |
61 |
62 | {formatCurrency.methods.formatAmount(
63 | Math.trunc(totalStake * Math.pow(10, networkInfo.decimalPlaces)),
64 | networkInfo
65 | )}
66 |
67 |
68 |
69 | {false && (
70 |
71 |
77 |
78 | Claim Rewards
79 | 3 days left
80 |
81 |
82 | )}
83 |
84 | );
85 | };
86 |
87 | const AllNominations = ({ nominations = [], networkInfo }) => {
88 | const onProfile = (validator) =>
89 | window.open(`${Routes.VALIDATOR_PROFILE}/${validator.stashId}`, "_blank");
90 |
91 | return (
92 |
93 | {nominations.map((nomination) => (
94 |
onProfile(nomination)}
104 | />
105 | ))}
106 | {!nominations.length && (
107 |
108 |
109 | No Nominations!
110 |
111 |
112 | )}
113 |
114 | );
115 | };
116 |
117 | export default AllNominations;
118 |
--------------------------------------------------------------------------------
/components/overview/AmountInput.js:
--------------------------------------------------------------------------------
1 | import { InputGroup, Input, InputRightElement } from "@chakra-ui/core";
2 | import formatCurrency from "@lib/format-currency";
3 | import { useAccounts } from "@lib/store";
4 | import { get } from "lodash";
5 | import { useState, useEffect } from "react";
6 |
7 | const AmountInputDefault = ({ bonded, type, value, onChange, networkInfo }) => {
8 | const { freeAmount, stashAccount } = useAccounts();
9 | const [inputValue, setInputValue] = useState(value.currency);
10 | const maxAmount =
11 | type === "bond"
12 | ? get(freeAmount, "currency") - networkInfo.minAmount < 0
13 | ? 0
14 | : get(freeAmount, "currency") - networkInfo.minAmount
15 | : bonded;
16 |
17 | // useEffect(() => {
18 | // if (bonded) {
19 | // onChange(bonded);
20 | // setInputValue(Number(Math.max(bonded, 0)));
21 | // }
22 | // }, [bonded]);
23 |
24 | const handleChange = (value) => {
25 | onChange(Number(value));
26 | setInputValue(value);
27 | };
28 |
29 | return (
30 |
31 | {
43 | const { value } = e.target;
44 | handleChange(value);
45 | }}
46 | border="none"
47 | fontSize="lg"
48 | color="gray.600"
49 | />
50 |
54 | ${formatCurrency.methods.formatNumber(value.subCurrency.toFixed(2))}
55 |
56 |
60 | {inputValue !== maxAmount && (
61 | {
64 | handleChange(maxAmount);
65 | }}
66 | >
67 | max
68 |
69 | )}
70 |
71 | {networkInfo.denom}
72 |
73 |
74 | }
75 | h="full"
76 | rounded="full"
77 | fontSize="xl"
78 | w="fit-content"
79 | px={4}
80 | />
81 |
82 | );
83 | };
84 |
85 | // TODO: handle `subCurrency` properly
86 | const AmountInputAlreadyBonded = ({ value, bonded, total, onChange }) => (
87 |
88 |
89 |
90 | Currently Bonded
91 |
{bonded.currency} KSM
92 |
93 | ${bonded.subCurrency}
94 |
95 |
96 |
97 |
Bond Additional Funds
98 |
99 | {
105 | const { value } = e.target;
106 | onChange(value === "" ? 0 : Number(value));
107 | }}
108 | />
109 |
110 | KSM
111 |
112 |
113 |
114 | ${value.subCurrency}
115 |
116 |
117 |
118 |
119 | Total Staking Amount
120 |
{total.currency} KSM
121 |
122 | ${total.subCurrency}
123 |
124 |
125 |
133 |
134 | );
135 |
136 | const AmountInput = ({ value, bonded, type, networkInfo, onChange }) => {
137 | return (
138 |
145 | );
146 | };
147 |
148 | export default AmountInput;
149 |
--------------------------------------------------------------------------------
/components/overview/ChainErrorPage.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from "@sentry/node";
2 | const ChainErrorPage = ({ onConfirm, errMessage }) => {
3 | return (
4 |
5 |
6 |
7 | Oops. There was an error processing this staking request
8 |
9 |
10 | If you think this is an error on our part, please share this with the
11 | help center and we will do our best to help. We typically respond within
12 | 2-3 days.
13 |
14 | {/*
19 | Track this transaction on PolkaScan
20 | */}
21 |
25 | Retry
26 |
27 |
30 | Sentry.showReportDialog({
31 | eventId: Sentry.captureException(errMessage),
32 | })
33 | }
34 | >
35 | Share this with the help center
36 |
37 |
38 | );
39 | };
40 |
41 | export default ChainErrorPage;
42 |
--------------------------------------------------------------------------------
/components/overview/ChillAlert.js:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import withSlideIn from "@components/common/withSlideIn";
3 | import {
4 | AlertDialog,
5 | AlertDialogBody,
6 | AlertDialogFooter,
7 | AlertDialogHeader,
8 | AlertDialogOverlay,
9 | AlertDialogContent,
10 | AlertDialogCloseButton,
11 | Button,
12 | useToast,
13 | } from "@chakra-ui/core";
14 | import chill from "@lib/polkadot/chill";
15 | import { useAccounts, usePolkadotApi } from "@lib/store";
16 |
17 | const ChillAlert = withSlideIn(({ styles, close }) => {
18 | const toast = useToast();
19 | const cancelRef = useRef();
20 | const { stashAccount } = useAccounts();
21 | const { apiInstance } = usePolkadotApi();
22 | const [chilling, setChilling] = useState(false);
23 | const [closeOnOverlayClick, setCloseOnOverlayClick] = useState(true);
24 |
25 | const onConfirm = () => {
26 | setChilling(true);
27 | setCloseOnOverlayClick(false);
28 | chill(stashAccount.address, apiInstance, {
29 | onEvent: ({ message }) => {
30 | toast({
31 | title: "Info",
32 | description: message,
33 | status: "info",
34 | duration: 3000,
35 | position: "top-right",
36 | isClosable: true,
37 | });
38 | },
39 | onFinish: (failed, message) => {
40 | toast({
41 | title: failed ? "Failure" : "Success",
42 | description: message,
43 | status: failed ? "error" : "success",
44 | duration: 3000,
45 | position: "top-right",
46 | isClosable: true,
47 | });
48 | setChilling(false);
49 | setCloseOnOverlayClick(true);
50 | close();
51 | },
52 | }).catch((error) => {
53 | setChilling(false);
54 | setCloseOnOverlayClick(true);
55 | toast({
56 | title: "Error",
57 | description: error.message,
58 | status: "error",
59 | duration: 3000,
60 | position: "top-right",
61 | isClosable: true,
62 | });
63 | });
64 | };
65 |
66 | return (
67 |
74 |
75 |
76 | Stop All Nominations?
77 | {closeOnOverlayClick && }
78 |
79 | Are you sure you want to stop all of your current nominations? This
80 | will keep your amount bonded in your account.
81 |
82 |
83 |
84 | No, Cancel
85 |
86 |
92 | Yes, Confirm
93 |
94 |
95 |
96 |
97 | );
98 | });
99 |
100 | export default ChillAlert;
101 |
--------------------------------------------------------------------------------
/components/overview/NominationsTable.js:
--------------------------------------------------------------------------------
1 | import { upperCase, noop } from "lodash";
2 | import { Star, ExternalLink } from "react-feather";
3 | import RiskTag from "@components/reward-calculator/RiskTag";
4 | import Identicon from "@components/common/Identicon";
5 | import Routes from "@lib/routes";
6 | import formatCurrency from "@lib/format-currency";
7 |
8 | const StatusTag = ({ status }) => {
9 | const getColor = () => {
10 | if (status === "active") return "teal-500";
11 | if (status === "inactive") return "red-500";
12 | if (status === "waiting") return "gray-500";
13 | };
14 |
15 | const color = getColor();
16 |
17 | return (
18 |
21 | {upperCase(status)}
22 |
23 | );
24 | };
25 |
26 | const ValidatorCard = ({
27 | name,
28 | stashId,
29 | riskScore,
30 | stakedAmount,
31 | commission,
32 | estimatedReward,
33 | onProfile = noop,
34 | networkInfo,
35 | }) => {
36 | const displayName = name
37 | ? name.length > 13
38 | ? name.slice(0, 5) + "..." + name.slice(-5)
39 | : name
40 | : stashId.slice(0, 5) + "..." + stashId.slice(-5);
41 | return (
42 |
43 |
44 |
45 |
46 |
{displayName}
47 |
48 | View Profile
49 |
50 |
51 |
52 |
53 | {/*
*/}
54 |
55 |
56 |
57 | Risk Score
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | Commission
66 |
67 |
{commission} %
68 |
69 |
70 | Stake
71 |
72 | {formatCurrency.methods.formatAmount(
73 | Math.trunc(stakedAmount * 10 ** networkInfo.decimalPlaces),
74 | networkInfo
75 | )}
76 |
77 |
78 |
79 |
80 | Estimated Reward *
81 |
82 |
83 | {formatCurrency.methods.formatAmount(
84 | Math.trunc(estimatedReward * 10 ** networkInfo.decimalPlaces),
85 | networkInfo
86 | )}
87 |
88 |
89 |
90 | {false && (
91 |
92 |
98 |
99 | Claim Rewards
100 | 3 days left
101 |
102 |
103 | )}
104 |
105 | );
106 | };
107 |
108 | const NominationsTable = ({ validators, networkInfo }) => {
109 | return (
110 |
111 |
112 | {validators
113 | .filter((validator) => validator.isElected)
114 | .map((validator) => (
115 |
124 | window.open(
125 | `${Routes.VALIDATOR_PROFILE}/${validator.stashId}`,
126 | "_blank"
127 | )
128 | }
129 | networkInfo={networkInfo}
130 | />
131 | ))}
132 |
133 | * Estimated Returns are calculated per era
134 |
135 |
136 |
141 |
142 | );
143 | };
144 |
145 | export default NominationsTable;
146 |
--------------------------------------------------------------------------------
/components/overview/OverviewCards.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { noop, get, isNil } from "lodash";
3 | import formatCurrency from "@lib/format-currency";
4 | import convertCurrency from "@lib/convert-currency";
5 | import { useAccounts, usePolkadotApi, useNetworkElection } from "@lib/store";
6 | import calculateReward from "@lib/calculate-reward";
7 | import { HelpPopover } from "@components/reward-calculator";
8 |
9 | const OverviewCards = ({
10 | stats,
11 | bondedAmount,
12 | activeStake,
13 | address,
14 | validators,
15 | unlockingBalances = [],
16 | openRewardDestinationModal = noop,
17 | bondFunds = noop,
18 | unbondFunds = noop,
19 | networkInfo,
20 | }) => {
21 | const totalUnlockingBalance = formatCurrency.methods.formatAmount(
22 | Math.trunc(
23 | unlockingBalances.reduce(
24 | (total, balanceInfo) => total + balanceInfo.value,
25 | 0
26 | )
27 | ),
28 | networkInfo
29 | );
30 |
31 | const { apiInstance } = usePolkadotApi();
32 | const { stashAccount } = useAccounts();
33 | const { isInElection } = useNetworkElection();
34 | const [compounding, setCompounding] = React.useState(false);
35 |
36 | const isActivelyStaking = isNil(validators)
37 | ? false
38 | : validators.filter((validator) => validator.isElected).length !== 0
39 | ? true
40 | : false;
41 |
42 | const [totalAmountStakedFiat, setTotalAmountStakedFiat] = React.useState();
43 | const [earningsFiat, setEarningsFiat] = React.useState();
44 | const [estimatedRewardsFiat, setEstimatedRewardsFiat] = React.useState();
45 | const [expectedAPR, setExpectedAPR] = React.useState(0);
46 |
47 | React.useEffect(() => {
48 | if (!isNil(apiInstance)) {
49 | apiInstance.query.staking.payee(stashAccount.address).then((payee) => {
50 | if (payee.isStaked) setCompounding(true);
51 | else {
52 | setCompounding(false);
53 | }
54 | });
55 | }
56 | }, [stashAccount, apiInstance]);
57 | React.useEffect(() => {
58 | if (stats) {
59 | convertCurrency(
60 | stats.totalAmountStaked,
61 | networkInfo.denom
62 | ).then((value) => setTotalAmountStakedFiat(value));
63 | }
64 |
65 | if (stats) {
66 | convertCurrency(stats.estimatedRewards, networkInfo.denom).then((value) =>
67 | setEstimatedRewardsFiat(value)
68 | );
69 | }
70 |
71 | if (validators) {
72 | calculateReward(
73 | validators.filter((validator) => validator.isElected),
74 | stats.totalAmountStaked,
75 | 12,
76 | "months",
77 | compounding,
78 | networkInfo
79 | ).then(({ yieldPercentage }) => setExpectedAPR(yieldPercentage));
80 | }
81 |
82 | if (stats) {
83 | convertCurrency(stats.earnings, networkInfo.denom).then((value) =>
84 | setEarningsFiat(value)
85 | );
86 | }
87 | }, [stats, compounding]);
88 |
89 | return (
90 |
91 |
92 |
93 |
Your investment
94 |
95 |
96 |
101 | {formatCurrency.methods.formatAmount(
102 | Math.trunc(
103 | Number(
104 | get(bondedAmount, "currency", 0) *
105 | 10 ** networkInfo.decimalPlaces
106 | )
107 | ),
108 | networkInfo
109 | )}
110 |
111 | {!isActivelyStaking && (
112 |
115 | Currently you are not earning any rewards on your
116 | investment.
117 |
118 | }
119 | />
120 | )}
121 |
122 | {bondedAmount && (
123 |
124 | $
125 | {formatCurrency.methods.formatNumber(
126 | get(bondedAmount, "subCurrency").toFixed(2)
127 | )}
128 |
129 | )}
130 |
131 |
132 |
139 | Invest more
140 |
141 |
142 |
149 | Withdraw
150 |
151 |
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default OverviewCards;
159 |
--------------------------------------------------------------------------------
/components/overview/PastEarningsTimeRange.js:
--------------------------------------------------------------------------------
1 | import { Select } from "@chakra-ui/core";
2 |
3 | const PastEarningsTimeRange = ({ unit, onUnitChange }) => {
4 | return (
5 | {
19 | onUnitChange(ev.target.value);
20 | }}
21 | >
22 | past 24h
23 | past week
24 | all time
25 |
26 | );
27 | };
28 |
29 | export default PastEarningsTimeRange;
30 |
--------------------------------------------------------------------------------
/components/overview/SuccessfullyBonded.js:
--------------------------------------------------------------------------------
1 | import { ExternalLink } from "react-feather";
2 | const SuccessfullyBonded = ({ transactionHash, onConfirm }) => {
3 | return (
4 |
5 |
6 |
7 | Your staking request is successfully sent to the network
8 |
9 |
10 | Your transaction is successfully sent to the network. You can safely
11 | close this page now. You can view the status of this transaction using
12 | the link below:
13 |
14 |
19 | Track this transaction on Subscan
20 |
21 |
25 | Continue
26 |
27 |
28 | );
29 | };
30 |
31 | export default SuccessfullyBonded;
32 |
--------------------------------------------------------------------------------
/components/payment/RewardDestination.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { get } from "lodash";
3 | import { CheckCircle, Circle } from "react-feather";
4 | import Identicon from "@components/common/Identicon";
5 | import formatCurrency from "@lib/format-currency";
6 | import { Icon } from "@chakra-ui/core";
7 | import { HelpPopover } from "@components/reward-calculator";
8 | import { Events, setUserProperties, trackEvent } from "@lib/analytics";
9 |
10 | const RewardDestination = ({
11 | stashAccount,
12 | transactionState,
13 | setTransactionState,
14 | onConfirm,
15 | networkInfo,
16 | }) => {
17 | const [compounding] = useState(get(transactionState, "compounding", true));
18 | const stakingAmount = get(transactionState, "stakingAmount", 0);
19 | const [destination, setDestination] = useState("Stash");
20 |
21 | const accounts = ["Stash", "Controller"];
22 | // if (!compounding) accounts.push("Controller");
23 |
24 | useEffect(() => {
25 | if (!compounding) {
26 | setTransactionState({
27 | rewardDestination: destination === "Stash" ? 1 : 2,
28 | });
29 | setUserProperties({
30 | rewardDestination: `${destination}, not compounding`,
31 | });
32 | } else {
33 | setTransactionState({ rewardDestination: 0 });
34 | setUserProperties({
35 | rewardDestination: `${destination}, compounding`,
36 | });
37 | }
38 | }, [destination]);
39 |
40 | return (
41 |
42 |
43 |
Reward Destination
44 |
47 | If you don't want to lock your reward for compounding and are
48 | using distinct stash and controller accounts for staking, then you
49 | can use this option to select the account where you would like the
50 | rewards to be credited.
51 |
52 | }
53 | />
54 |
55 |
56 | You chose to lock your rewards for compounding. For compounding, the
57 | funds can only be locked in your stash account
58 |
59 |
60 | {accounts.map((accountType) => (
61 |
{
72 | if (!compounding) {
73 | setDestination(accountType);
74 | trackEvent(Events.ADV_PREFS_EDIT, { rewardDestination: accountType });
75 | }
76 | }}
77 | >
78 | {destination === accountType ? (
79 |
80 | ) : (
81 |
82 | )}
83 |
84 | {accountType} Account
85 |
86 |
87 | ))}
88 |
89 |
90 | );
91 | };
92 |
93 | export default RewardDestination;
94 |
--------------------------------------------------------------------------------
/components/payment/TermsOfService.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | Modal,
4 | ModalBody,
5 | ModalOverlay,
6 | ModalContent,
7 | ModalCloseButton,
8 | ModalHeader,
9 | ModalFooter,
10 | Text,
11 | Popover,
12 | PopoverTrigger,
13 | PopoverContent,
14 | PopoverArrow,
15 | PopoverBody,
16 | } from "@chakra-ui/core";
17 | import TermsComponent from "@components/policies/terms-component";
18 |
19 | const TermsAndServicePopover = ({
20 | tcPopoverOpen,
21 | setTCPopoverOpen,
22 | setHasAgreed,
23 | onConfirm,
24 | }) => {
25 | const [clickDisabled, setClickDisabled] = useState(true);
26 | const [showPopover, setShowPopover] = useState(false);
27 |
28 | const handlePopoverClose = () => {
29 | setTCPopoverOpen(false);
30 | };
31 |
32 | const handleScroll = (e) => {
33 | const bottom =
34 | e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
35 | if (bottom) {
36 | setClickDisabled(false);
37 | }
38 | };
39 |
40 | return (
41 |
48 |
49 |
50 |
51 | Terms of Service
52 |
53 | Please ensure you understand the risks before proceeding
54 |
55 |
56 |
65 | handleScroll(e)}>
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | setShowPopover(false)}
75 | onPointerEnter={() => setShowPopover(true)}
76 | >
77 | {
84 | setHasAgreed(true);
85 | onConfirm();
86 | }}
87 | >
88 | Agree
89 |
90 |
91 |
92 |
101 |
102 |
103 | Please read the terms of service before proceeding
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default TermsAndServicePopover;
114 |
--------------------------------------------------------------------------------
/components/reward-calculator/CompoundRewardSlider.js:
--------------------------------------------------------------------------------
1 | import { Switch } from "@chakra-ui/core";
2 |
3 | const CompoundRewardSlider = ({
4 | checked,
5 | setChecked,
6 | trackRewardCalculatedEvent,
7 | }) => {
8 | return (
9 |
10 | {
15 | setChecked(e.target.checked);
16 | trackRewardCalculatedEvent({ compounding: e.target.checked });
17 | }}
18 | />
19 |
25 | {checked ? "Yes" : "No"}
26 |
27 |
28 | );
29 | };
30 |
31 | export default CompoundRewardSlider;
32 |
--------------------------------------------------------------------------------
/components/reward-calculator/RiskSelect.js:
--------------------------------------------------------------------------------
1 | const RiskSelect = ({ selected, setSelected, trackRewardCalculatedEvent }) => {
2 | const options = ["Low", "Medium", "High"];
3 | return (
4 |
5 | {options.map((option) => (
6 | {
25 | setSelected(option);
26 | trackRewardCalculatedEvent({ riskPreference: option });
27 | }}
28 | >
29 | {option}
30 |
31 | ))}
32 |
33 | );
34 | };
35 |
36 | export default RiskSelect;
37 |
--------------------------------------------------------------------------------
/components/reward-calculator/RiskTag.js:
--------------------------------------------------------------------------------
1 | const getRiskCategory = riskAmount => {
2 | if (Number.isNaN(riskAmount)) throw new Error('Risk amount should be a number');
3 | if (riskAmount < 0.33) return 'Low';
4 | if (riskAmount >= 0.33 && riskAmount <= 0.66) return 'Medium';
5 | if (riskAmount > 0.66) return 'High';
6 | };
7 |
8 | const getStyleByRisk = (risk) => {
9 | switch (risk) {
10 | case 'Low': return 'text-teal-500 bg-teal-100';
11 | case 'Medium': return 'text-orange-500 bg-orange-100';
12 | case 'High': return 'text-red-500 bg-red-100';
13 | default: return '';
14 | }
15 | };
16 |
17 | const RiskTag = ({ risk, classes }) => {
18 | const riskCategory = getRiskCategory(risk);
19 | return (
20 |
25 | {risk}
26 |
27 | );
28 | };
29 |
30 | export default RiskTag;
--------------------------------------------------------------------------------
/components/reward-calculator/TimePeriodInput.js:
--------------------------------------------------------------------------------
1 | import { Select, Input, InputRightElement, InputGroup } from "@chakra-ui/core";
2 |
3 | const TimePeriodInput = ({
4 | value,
5 | unit,
6 | onChange,
7 | onUnitChange,
8 | trackRewardCalculatedEvent,
9 | }) => {
10 | return (
11 |
12 |
13 | {
29 | onUnitChange(ev.target.value);
30 | trackRewardCalculatedEvent({
31 | timePeriod: `${value === 0 ? null : value} ${
32 | ev.target.value
33 | }`,
34 | });
35 | }}
36 | >
37 | eras
38 | days
39 | months
40 |
41 | }
42 | width="fit-content"
43 | rounded="full"
44 | py={4}
45 | px={2}
46 | fontSize="sm"
47 | />
48 | {
57 | ev.target.value < 1
58 | ? onChange("")
59 | : onChange(Number(ev.target.value));
60 | trackRewardCalculatedEvent({
61 | timePeriod: `${Number(ev.target.value)} ${unit}`,
62 | });
63 | }}
64 | border="none"
65 | fontSize="lg"
66 | />
67 |
68 | {/*
69 | onChange(Number(ev.target.value))}
74 | className="w-24 text-2xl p-0 outline-none"
75 | />
76 |
77 |
78 | onUnitChange(ev.target.value)}
89 | >
90 | eras
91 | days
92 | months
93 |
94 |
*/}
95 |
96 | );
97 | };
98 |
99 | export default TimePeriodInput;
100 |
--------------------------------------------------------------------------------
/components/settings/index.js:
--------------------------------------------------------------------------------
1 | const { Divider } = require("@chakra-ui/core");
2 | import Link from "next/link";
3 | import { ChevronRight } from "react-feather";
4 | import { FaDiscord, FaTelegram } from "react-icons/fa";
5 |
6 | const Settings = () => {
7 | return (
8 |
9 |
Settings
10 |
11 | Preferences
12 |
13 |
14 | window.Metomic.raise()}
16 | className="flex justify-between px-2 text-sm py-2 my-1 rounded-lg hover:text-teal-500 hover:bg-gray-400 hover:bg-opacity-22"
17 | >
18 | Manage cookies
19 |
20 |
21 |
22 |
42 |
43 | Legal
44 |
45 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default Settings;
77 |
--------------------------------------------------------------------------------
/components/validator-profile/LimitedTextArea.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | export default function LimitedTextarea({
3 | rows,
4 | cols,
5 | value,
6 | limit,
7 | className,
8 | placeholder,
9 | setVision
10 | }) {
11 | const [content, setContent] = React.useState(value);
12 |
13 | const setFormattedContent = (text) => {
14 | text.length > limit ? setContent(text.slice(0, limit)) : setContent(text);
15 | };
16 |
17 | React.useEffect(() => setVision(content), [content]);
18 |
19 | React.useEffect(() => {
20 | setFormattedContent(content);
21 | }, []);
22 |
23 | return (
24 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/components/validator-profile/LinkedValidatorsGroup.js:
--------------------------------------------------------------------------------
1 | import { useDisclosure } from "@chakra-ui/core";
2 | import {
3 | Modal,
4 | ModalOverlay,
5 | ModalContent,
6 | ModalHeader,
7 | ModalBody,
8 | ModalCloseButton,
9 | } from "@chakra-ui/core";
10 | import Identicon from "@components/common/Identicon";
11 | import withSlideIn from "@components/common/withSlideIn";
12 | import Routes from "@lib/routes";
13 |
14 | const ValidatorCard = ({ name, stashId, onProfile = noop }) => {
15 | return (
16 |
20 |
21 |
22 |
23 |
24 |
25 | {name || stashId.slice(0, 6) + "..." + stashId.slice(-6) || "-"}
26 |
27 |
28 |
{stashId || ""}
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | const LinkedValidatorsModal = withSlideIn(
36 | ({ validators, styles, onClose, onProfile }) => (
37 |
38 |
39 |
40 |
41 |
42 | Linked Validators
43 |
44 | {validators.length}
45 |
46 |
47 |
48 |
57 |
58 |
59 | {validators.map((validator) => (
60 | onProfile(validator.stashId)}
65 | />
66 | ))}
67 |
68 |
69 |
70 |
71 | )
72 | );
73 |
74 | const LinkedValidatorsGroup = ({ validators = [] }) => {
75 | const { onToggle, isOpen, onClose } = useDisclosure();
76 |
77 | const onProfile = (validatorId) =>
78 | window.open(`${Routes.VALIDATOR_PROFILE}/${validatorId}`, "_blank");
79 |
80 | const limitedValidators =
81 | validators.length > 5 ? validators.slice(0, 5) : validators;
82 |
83 | return validators.length ? (
84 |
85 |
86 | LINKED VALIDATORS
87 |
88 |
95 |
96 | {limitedValidators.map((validator) => (
97 |
98 |
104 |
105 | ))}
106 | {limitedValidators.length !== validators.length && (
107 |
111 | +{validators.length - limitedValidators.length}
112 |
113 | )}
114 |
115 |
116 | ) : (
117 | ""
118 | );
119 | };
120 |
121 | export default LinkedValidatorsGroup;
122 |
--------------------------------------------------------------------------------
/components/validator-profile/ProfileTabs.js:
--------------------------------------------------------------------------------
1 | const ProfileTabs = ({ tabs, selectedTab, setSelectedTab }) => {
2 | return (
3 |
4 | {Object.entries(tabs).map(([key, value]) => (
5 | setSelectedTab(value)}
12 | >
13 | {value}
14 |
15 | ))}
16 |
17 | );
18 | };
19 |
20 | export default ProfileTabs;
21 |
--------------------------------------------------------------------------------
/components/validator-profile/TeamMembers.js:
--------------------------------------------------------------------------------
1 | import { Avatar } from "@chakra-ui/core";
2 | import { Twitter } from "react-feather";
3 |
4 | const TeamMembers = ({ members = [] }) => {
5 | return (
6 |
7 | {members.length ? (
8 |
9 | {members.map((member, index) => (
10 |
window.open(`https://twitter.com/${member.twitter.slice(1)}`, '_blank')}
13 | className="m-2 w-48 h-56 p-5 cursor-pointer rounded-lg border border-gray-300 flex-center flex-col"
14 | >
15 |
16 |
{member.member}
17 |
22 |
23 | ))}
24 |
25 | ) : (
26 |
27 |
No Members.
28 |
29 | )}
30 |
31 | );
32 | };
33 |
34 | export default TeamMembers;
35 |
--------------------------------------------------------------------------------
/components/validator-profile/ValidatorKeyStats.js:
--------------------------------------------------------------------------------
1 | import formatCurrency from "@lib/format-currency";
2 | import { useEffect, useState } from "react";
3 | import convertCurrency from "@lib/convert-currency";
4 |
5 | const ValidatorKeyStats = ({ stats, networkInfo }) => {
6 | const [ownStakeSubCurrency, setOwnStakeSubCurrency] = useState();
7 | const [otherStakeSubCurrency, setOtherStakeSubCurrency] = useState();
8 | useEffect(() => {
9 | if (stats.ownStake) {
10 | convertCurrency(stats.ownStake, networkInfo.denom).then((value) =>
11 | setOwnStakeSubCurrency(value)
12 | );
13 | }
14 | if (stats.othersStake) {
15 | convertCurrency(stats.othersStake, networkInfo.denom).then((value) =>
16 | setOtherStakeSubCurrency(value)
17 | );
18 | }
19 | }, [stats]);
20 | return (
21 |
22 |
23 | KEY STATISTICS
24 |
25 |
26 | No. of nominators
27 |
{stats.numOfNominators}
28 |
29 |
30 | Own Stake
31 |
32 | {formatCurrency.methods.formatAmount(
33 | Math.trunc((stats.ownStake || 0) * 10 ** networkInfo.decimalPlaces),
34 | networkInfo
35 | )}
36 |
37 | {ownStakeSubCurrency && (
38 |
39 | $
40 | {formatCurrency.methods.formatNumber(
41 | ownStakeSubCurrency.toFixed(2)
42 | )}
43 |
44 | )}
45 |
46 |
47 | Other Stake
48 |
49 | {formatCurrency.methods.formatAmount(
50 | Math.trunc(
51 | (stats.othersStake || 0) * 10 ** networkInfo.decimalPlaces
52 | ),
53 | networkInfo
54 | )}
55 |
56 | {otherStakeSubCurrency && (
57 |
58 | $
59 | {formatCurrency.methods.formatNumber(
60 | otherStakeSubCurrency.toFixed(2)
61 | )}
62 |
63 | )}
64 |
65 |
66 | Commission
67 |
{stats.commission} %
68 |
69 |
70 | Risk Score
71 |
72 | {(stats.riskScore || 0).toFixed(2)}
73 |
74 |
75 | {false && (
76 |
77 | Total Account Balance
78 |
42000 {networkInfo.denom}
79 | $24000
80 |
81 | )}
82 |
83 | );
84 | };
85 |
86 | export default ValidatorKeyStats;
87 |
--------------------------------------------------------------------------------
/components/validator-profile/ValidatorReturnsCalculator.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { get } from "lodash";
3 | import calculateReward from "@lib/calculate-reward";
4 | import formatCurrency from "@lib/format-currency";
5 |
6 | const ValidatorReturnsCalculator = ({ validatorInfo, networkInfo }) => {
7 | const [amount, _setAmount] = useState(1000);
8 | const [returns, setReturns] = useState();
9 |
10 | useEffect(() => {
11 | if (amount > 0) {
12 | const validator = {
13 | ...validatorInfo,
14 | totalStake: validatorInfo.ownStake + validatorInfo.othersStake,
15 | };
16 |
17 | calculateReward(
18 | [validator],
19 | amount,
20 | 12,
21 | "months",
22 | true,
23 | networkInfo
24 | ).then((result) => {
25 | setReturns(result.returns);
26 | });
27 | }
28 | }, [amount]);
29 |
30 | const setAmount = (value) => {
31 | if (value === "") _setAmount(value);
32 | else _setAmount(Number(value));
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 | Staking Amount
40 |
41 |
42 | setAmount(ev.target.value)}
46 | className="w-24 text-gray-900 outline-none rounded-lg p-2"
47 | />
48 | {networkInfo.denom}
49 |
50 |
51 |
52 | ANNUAL EXPECTED RETURNS
53 |
54 | {get(returns, "currency") && (
55 |
56 |
57 | {formatCurrency.methods.formatAmount(
58 | Math.trunc(
59 | get(returns, "currency") * 10 ** networkInfo.decimalPlaces
60 | ),
61 | networkInfo
62 | )}
63 |
64 |
65 | $
66 | {formatCurrency.methods.formatNumber(
67 | get(returns, "subCurrency").toFixed(2)
68 | )}
69 |
70 |
71 | )}
72 |
73 | );
74 | };
75 |
76 | export default ValidatorReturnsCalculator;
77 |
--------------------------------------------------------------------------------
/components/validator-profile/validator-viz/Circleandline.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Circle, Line, Text, Rect } from "react-konva";
3 |
4 | class Circleandline extends React.Component {
5 | constructor() {
6 | super();
7 | this.state = {
8 | showNominatorAddress: false,
9 | };
10 | }
11 | handleOnMouseOver = (e) => {
12 | e.target.setAttrs({
13 | scaleX: 1.3,
14 | scaleY: 1.3,
15 | });
16 | document.body.style.cursor = "pointer";
17 | this.setState({ showNominatorAddress: true });
18 | };
19 | handleOnMouseOut = (e) => {
20 | e.target.setAttrs({
21 | scaleX: 1,
22 | scaleY: 1,
23 | });
24 | document.body.style.cursor = "default";
25 | this.setState({ showNominatorAddress: false });
26 | };
27 |
28 | render() {
29 | const cardWidth = 159;
30 | const cardHeight = 69;
31 |
32 | let nomaddress =
33 | this.props.nomId.toString().slice(0, 5) +
34 | "......" +
35 | this.props.nomId.toString().slice(-5);
36 |
37 | const nombonded =
38 | this.props.stake !== (undefined || null)
39 | ? "Bonded: " +
40 | this.props.stake.toString().slice(0, 7) +
41 | ` ${this.props.networkInfo.denom}`
42 | : undefined;
43 |
44 | let x1 = this.props.x + 15;
45 | let y1 = this.props.y - 8;
46 |
47 | if (x1 > this.props.width - cardWidth) {
48 | y1 = y1 + 20;
49 | x1 = x1 - cardWidth / 2;
50 | }
51 |
52 | return (
53 |
54 |
59 |
68 |
69 | {this.state.showNominatorAddress && (
70 |
82 | )}
83 |
84 | {this.state.showNominatorAddress && (
85 |
93 | )}
94 | {this.state.showNominatorAddress && this.props.isElected && (
95 |
96 | )}
97 |
98 | );
99 | }
100 | }
101 | export default Circleandline;
102 |
--------------------------------------------------------------------------------
/components/validator-profile/validator-viz/Network.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text } from "react-konva";
3 |
4 | class Network extends React.Component {
5 | render() {
6 | let angle = 180 * 0.0174533 - (1 / 5) * Math.PI;
7 | const maxAngle = (1 / 5) * 2 * Math.PI;
8 | const arr = [];
9 | const reversedName = `${this.props.networkInfo.name} Network`
10 | .split("")
11 | .reverse();
12 |
13 | const radius = 220;
14 |
15 | reversedName.forEach((element, index) => {
16 | angle += maxAngle / (Number(reversedName.length) + 1);
17 | arr.push(
18 |
28 | );
29 | });
30 |
31 | return arr;
32 | }
33 | }
34 |
35 | export default Network;
36 |
--------------------------------------------------------------------------------
/components/validator-profile/validator-viz/ValidatorViz.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Stage, Layer, Circle, Line, Rect, Text } from "react-konva";
3 | import WhiteCircles from "./WhiteCircles";
4 | import Network from "./Network";
5 |
6 | class ValidatorViz extends React.Component {
7 | constructor() {
8 | super();
9 | this.container = React.createRef();
10 | this.state = {
11 | stageWidth: undefined,
12 | stageHeight: undefined,
13 | };
14 | }
15 |
16 | componentDidMount() {
17 | this.setState({
18 | stageWidth: this.container.offsetWidth,
19 | stageHeight: this.container.offsetHeight,
20 | });
21 | }
22 |
23 | render() {
24 | const width =
25 | this.state.stageWidth !== undefined
26 | ? this.state.stageWidth
27 | : window.innerWidth;
28 | const height =
29 | this.state.stageHeight !== undefined
30 | ? this.state.stageHeight
31 | : window.innerHeight;
32 | let valText = "";
33 | if (this.props.validatorData !== undefined) {
34 | valText =
35 | this.props.validatorData.socialInfo.name !== null
36 | ? this.props.validatorData.socialInfo.name
37 | : this.props.validatorData.keyStats.stashId;
38 | if (valText.length > 11) {
39 | valText = valText.slice(0, 5) + "..." + valText.slice(-5);
40 | }
41 | }
42 | let radius = 400;
43 |
44 | const validatorRectangleWidth = 110;
45 | const validatorRectangleHeight = 30;
46 |
47 | let opacity = 0.3;
48 |
49 | return this.props.validatorData === undefined ? (
50 |
51 |
52 |
53 | ) : (
54 | <>
55 | {
58 | this.container = node;
59 | }}
60 | >
61 | {/*
67 | ←
68 |
*/}
69 |
70 |
71 |
72 |
80 | {/* Here n is number of white circles to draw
81 | r is radius of the imaginary circle on which we have to draw white circles
82 | x,y is center of imaginary circle
83 | */}
84 |
85 |
94 | {/* Adding 6 to stating and ending y point and 24 to length of line
95 | because the upper left corner of rectangle is at width/2,height/2
96 | so mid point of rectangle becomes width/2+12,height/2+6
97 | */}
98 |
107 | {/* Arc used to create the semicircle on the right,
108 | Rotation is used to rotate the arc drawn by 90 degrees in clockwise direction
109 | */}
110 |
120 |
121 |
131 |
142 |
147 |
148 |
149 |
150 | >
151 | );
152 | }
153 | }
154 |
155 | export default ValidatorViz;
156 |
--------------------------------------------------------------------------------
/components/validator-profile/validator-viz/WhiteCircles.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Circleandline from "./Circleandline";
3 |
4 | class WhiteCircles extends React.Component {
5 | render() {
6 | let angle = (2 / 3) * Math.PI;
7 | let maxAngle = (2 / 3) * Math.PI;
8 | const arr = [];
9 |
10 | // Uncomment in case we want to extend angle for nominators display
11 | // if (this.props.valinfo.stakingInfo.nominatorsInfo.length > 5) {
12 | // angle = Math.PI / 4;
13 | // maxAngle = (3 / 4) * 2 * Math.PI;
14 | // }
15 |
16 | this.props.valinfo.stakingInfo.nominatorsInfo.forEach((element, index) => {
17 | angle +=
18 | maxAngle /
19 | (Number(this.props.valinfo.stakingInfo.nominatorsInfo.length) + 1);
20 | const radius = Math.floor(
21 | Math.random() * (this.props.maxRadius - 150) + 150
22 | );
23 | arr.push(
24 |
38 | );
39 | });
40 |
41 | return arr;
42 | }
43 | }
44 |
45 | export default WhiteCircles;
46 |
--------------------------------------------------------------------------------
/components/validators/EditAmountModal.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import {
3 | Modal,
4 | ModalOverlay,
5 | ModalContent,
6 | ModalHeader,
7 | ModalCloseButton,
8 | ModalBody,
9 | Alert,
10 | AlertTitle,
11 | AlertDescription,
12 | Popover,
13 | PopoverTrigger,
14 | PopoverContent,
15 | PopoverArrow,
16 | PopoverBody,
17 | } from "@chakra-ui/core";
18 | import AmountInput from "@components/reward-calculator/AmountInput";
19 | import withSlideIn from "@components/common/withSlideIn";
20 | import { isNil, get } from "lodash";
21 | import convertCurrency from "@lib/convert-currency";
22 | import { useAccounts } from "@lib/store";
23 | import formatCurrency from "@lib/format-currency";
24 |
25 | const EditAmountModal = withSlideIn(
26 | ({
27 | styles,
28 | onClose,
29 | freeAmount,
30 | stashAccount,
31 | bondedAmount,
32 | amount = "",
33 | setAmount,
34 | networkInfo,
35 | trackRewardCalculatedEvent,
36 | }) => {
37 | const [stakingAmount, setStakingAmount] = useState(amount);
38 | const [subCurrency, setSubCurrency] = useState(0);
39 |
40 | const onConfirm = () => {
41 | if (stakingAmount) setAmount(stakingAmount);
42 | onClose();
43 | };
44 |
45 | useEffect(() => {
46 | convertCurrency(stakingAmount || 0, networkInfo.denom).then(
47 | (convertedAmount) => {
48 | setSubCurrency(convertedAmount);
49 | }
50 | );
51 | }, [stakingAmount]);
52 |
53 | const totalBalance = bondedAmount + get(freeAmount, "currency", 0);
54 | const calculationDisabled =
55 | (!totalBalance ||
56 | stakingAmount == 0 ||
57 | stakingAmount > totalBalance - networkInfo.minAmount) &&
58 | stashAccount;
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 | Edit Staking Amount
67 |
68 |
69 |
70 |
71 |
72 | {stashAccount &&
73 | stakingAmount > totalBalance - networkInfo.minAmount && (
74 |
82 |
83 | Insufficient Balance
84 |
85 |
86 | We cannot stake this amount since we recommend maintaining
87 | a minimum balance of {networkInfo.minAmount}{" "}
88 | {networkInfo.denom} in your account at all times.{" "}
89 |
90 |
91 | Why?
92 |
93 |
99 |
100 |
101 |
102 | This is to ensure that you have a decent amout of
103 | funds in your account to pay transaction fees for
104 | claiming rewards, unbonding funds, changing
105 | on-chain staking preferences, etc.
106 |
107 |
108 |
109 |
110 |
111 |
112 | )}
113 |
117 | Transferrable Balance:{" "}
118 | {formatCurrency.methods.formatAmount(
119 | Math.trunc(
120 | get(freeAmount, "currency", 0) *
121 | 10 ** networkInfo.decimalPlaces
122 | ),
123 | networkInfo
124 | )}
125 |
126 |
135 |
136 |
148 | Confirm
149 |
150 |
151 |
152 |
153 |
154 |
155 | );
156 | }
157 | );
158 |
159 | export default EditAmountModal;
160 |
--------------------------------------------------------------------------------
/components/wallet-connect/CreateWallet.js:
--------------------------------------------------------------------------------
1 | const CreateWallet = ({ onPrevious, onNext }) => (
2 |
3 |
Create a wallet
4 |
5 |
6 |
Step 1 Title
7 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
8 |
9 |
10 | Previous
11 | Next
12 |
13 |
14 | );
15 |
16 | export default CreateWallet;
17 |
--------------------------------------------------------------------------------
/components/wallet-connect/ImportAccount.js:
--------------------------------------------------------------------------------
1 | const ImportAccount = ({ onPrevious, onNext }) => (
2 |
3 |
Import an account
4 |
5 |
6 |
Step 1 Title
7 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
8 |
9 |
10 | Previous
11 | Next
12 |
13 |
14 | );
15 |
16 | export default ImportAccount;
17 |
--------------------------------------------------------------------------------
/components/wallet-connect/RecoverAuthInfo.js:
--------------------------------------------------------------------------------
1 | const RecoverAuthInfo = () => (
2 |
3 |
4 |
5 | Instructions for PolkadotJS permission recovery
6 |
7 |
8 | Currently the PolkadotJS extension doesn't have support for permission
9 | management. As a workaround, please restart your browser {" "}
10 | after quitting it and revisit this page to proceed with the authorization.
11 |
12 |
13 | );
14 |
15 | export default RecoverAuthInfo;
16 |
--------------------------------------------------------------------------------
/components/wallet-connect/RejectedPage.js:
--------------------------------------------------------------------------------
1 | const RejectedPage = ({ handleRecoveryAuth }) => (
2 |
3 |
4 |
5 | Connect to the PolkadotJS browser extension
6 |
7 |
8 | PolkadotJS extension is a simple browser extension for managing your
9 | accounts. It allows you to securely sign transactions using these accounts
10 | while maintaining complete control over your funds.
11 |
12 |
17 | Install extension
18 |
19 |
23 | Accidently rejected the permission request?
24 |
25 |
26 | Using a different wallet?{" "}
27 |
32 | Request feature
33 |
34 |
35 |
36 | );
37 |
38 | export default RejectedPage;
39 |
--------------------------------------------------------------------------------
/components/wallet-connect/SelectAccount.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Box, Skeleton, Spinner } from "@chakra-ui/core";
3 | import Identicon from "@components/common/Identicon";
4 | import { get } from "lodash";
5 | import formatCurrency from "@lib/format-currency";
6 |
7 | const SelectAccount = ({ accounts, onStashSelected, networkInfo }) => {
8 | return (
9 |
10 |
11 |
12 | {accounts.map((account) => {
13 | return (
14 |
onStashSelected(account)}
21 | >
22 |
23 |
24 |
25 |
26 | {account.meta.name}
27 |
28 | {account.balances ? (
29 |
30 | {formatCurrency.methods.formatAmount(
31 | Math.trunc(
32 | parseInt(account.balances.freeBalance) +
33 | parseInt(account.balances.reservedBalance)
34 | ),
35 | networkInfo
36 | )}
37 | {formatCurrency.methods.formatAmount(
38 | Math.trunc(
39 | parseInt(account.balances.freeBalance) +
40 | parseInt(account.balances.reservedBalance)
41 | ),
42 | networkInfo
43 | ) === "0" && ` ${networkInfo.denom}`}
44 |
45 | ) : (
46 |
47 |
48 | Loading...
49 |
50 |
51 | )}
52 |
53 |
54 | {account.address.slice(0, 6) +
55 | "...." +
56 | account.address.slice(-6)}
57 |
58 |
59 |
60 | );
61 | })}
62 |
63 |
64 |
69 |
70 | );
71 | };
72 |
73 | export default SelectAccount;
74 |
--------------------------------------------------------------------------------
/components/wallet-connect/WalletDisclaimer.js:
--------------------------------------------------------------------------------
1 | const WalletDisclaimer = ({ onCreate }) => (
2 |
3 |
4 |
Ensure that you have an account with KSM tokens that can be connect to the PolkaJS Wallet.
5 |
Yes, Proceed
6 |
No, how can I obtain KSM tokens?
7 |
8 | );
9 |
10 | export default WalletDisclaimer;
11 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@lib/*": ["lib/*"],
6 | "@layouts/*": ["layouts/*"],
7 | "@components/*": ["components/*"],
8 | },
9 | },
10 | "exclude": ["node_modules", "out", ".next"]
11 | }
--------------------------------------------------------------------------------
/lib/analytics.js:
--------------------------------------------------------------------------------
1 | if (!process.env.NEXT_PUBLIC_AMPLITUDE_API_TOKEN) {
2 | throw new Error("No Amplitude API key passed!");
3 | }
4 |
5 | let amplitude;
6 |
7 | const trackEvent = async (eventName = "", eventProperties = {}) => {
8 | if (!window) return;
9 | const obj = localStorage.getItem(
10 | "metomic-consented-pol:" + process.env.NEXT_PUBLIC_METOMIC_CONSENT_ID
11 | );
12 | if (obj && JSON.parse(obj).enabled) {
13 | if (!amplitude) {
14 | const Amplitude = await import("amplitude-js");
15 | amplitude = Amplitude.getInstance();
16 | amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_TOKEN, null, {
17 | includeUtm: true,
18 | includeReferrer: true,
19 | includeGclid: true,
20 | });
21 | }
22 |
23 | amplitude.logEvent(eventName, eventProperties);
24 | }
25 | };
26 |
27 | const setUserProperties = async (userProperties = {}) => {
28 | if (!window) return;
29 | const obj = localStorage.getItem(
30 | "metomic-consented-pol:" + process.env.NEXT_PUBLIC_METOMIC_CONSENT_ID
31 | );
32 | if (obj && JSON.parse(obj).enabled) {
33 | if (!amplitude) {
34 | const Amplitude = await import("amplitude-js");
35 | amplitude = Amplitude.getInstance();
36 | amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_TOKEN, null, {
37 | includeUtm: true,
38 | includeReferrer: true,
39 | includeGclid: true,
40 | });
41 | amplitude.clearUserProperties();
42 | }
43 | amplitude.setUserProperties(userProperties);
44 | }
45 | };
46 |
47 | const Events = {
48 | // General
49 | PAGE_VIEW: "Visited Page",
50 |
51 | LANDING_CTA_CLICK: "User clicked CTA to redirect to reward calculator",
52 |
53 | // Wallet connection popover
54 | WALLET_CONNECTED: "Wallet successfully authorized",
55 | EXTENSION_AUTH_REJECTED: "Extension auth rejected or extension unavailable",
56 | INTENT_CONNECT_WALLET: "Intention to connect wallet",
57 | INTENT_ACCOUNT_SELECTION: "Intention to select account",
58 | ACCOUNT_SELECTED: "Account Selected",
59 | AUTH_REJECTED: "extension auth rejected or no extension available",
60 | AUTH_ALLOWED: "Wallet successfully connected, auth approved",
61 |
62 | USER_ACCOUNT_SELECTION: "USER_ACCOUNT_SELECTION",
63 |
64 | // Reward Calculator
65 | REWARD_CALCULATED:
66 | "User changed at least one of the inputs on the calculator (boolean)",
67 | INTENT_ADVANCED_SELECTION: "INTENT_ADVANCED_SELECTION",
68 | CALCULATOR_CTA_CLICK: "User clicked CTA to redirect to confirmation",
69 | INTENT_STAKING: "INTENT_STAKING",
70 |
71 | // Payment steps (confirmation, reward destination and final transaction)
72 |
73 | TOGGLE_VALIDATORS: "User toggled show/hide suggested validators",
74 | TOGGLE_ADV_PREFS: "User toggled show/hide advanced preferences",
75 | ADV_PREFS_EDIT: "User edited an advanced preference",
76 |
77 | PAYMENT_STEP_UPDATED: "PAYMENT_STEP_UPDATED",
78 | INTENT_TRANSACTION: "INTENT_TRANSACTION",
79 | TRANSACTION_SENT: "Transaction successfully sent to chain",
80 | TRANSACTION_SUCCESS: "TRANSACTION_SUCCESS",
81 | TRANSACTION_FAILED: "TRANSACTION_FAILED",
82 | };
83 |
84 | export { setUserProperties, trackEvent, Events };
85 |
--------------------------------------------------------------------------------
/lib/axios.js:
--------------------------------------------------------------------------------
1 | import _axios from "axios";
2 |
3 | if (!process.env.NEXT_PUBLIC_API_BASE_URL) {
4 | throw new Error("No `NEXT_PUBLIC_API_BASE_URL` found in environment!");
5 | }
6 |
7 | const axios = _axios.create({
8 | baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
9 | });
10 |
11 | export default axios;
12 |
--------------------------------------------------------------------------------
/lib/calculate-reward.js:
--------------------------------------------------------------------------------
1 | import { get } from "lodash";
2 | import convertCurrency from "./convert-currency";
3 |
4 | const calculateReward = async (
5 | validators,
6 | amount,
7 | timePeriodValue,
8 | timePeriodUnit,
9 | compounding,
10 | networkInfo
11 | ) => {
12 | if (amount < 0) throw new Error("Amount cannot be negative.");
13 | if (amount > 10 ** 7)
14 | throw new Error("Toooo high... you sure about the amount?");
15 |
16 | const totalAmount = parseFloat(amount);
17 |
18 | if (totalAmount === 0) {
19 | return {
20 | returns: {
21 | currency: 0,
22 | subCurrency: 0,
23 | },
24 | portfolioValue: {
25 | currency: 0,
26 | subCurrency: 0,
27 | },
28 | yieldPercentage: 0,
29 | };
30 | }
31 |
32 | const amountPerValidator = Number(totalAmount) / validators.length;
33 |
34 | let timePeriodInEras = Number(timePeriodValue);
35 | if (timePeriodUnit === "months") {
36 | // TODO: don't consider each month as 30 days
37 | timePeriodInEras = timePeriodValue * 30 * networkInfo.erasPerDay; // 4 eras / day, 30 days / months
38 | } else if (timePeriodUnit === "days") {
39 | timePeriodInEras = timePeriodValue * networkInfo.erasPerDay;
40 | }
41 |
42 | // 21900 = number of eras in 15 years (365 * 4 * 15)
43 | if (timePeriodInEras > 365 * networkInfo.erasPerDay * 15)
44 | throw new Error(
45 | "15 years are enough to make you rich, please adjust the time period."
46 | );
47 |
48 | const totalReward = validators.reduce((tr, v) => {
49 | const stakeFraction =
50 | amountPerValidator / (amountPerValidator + v.totalStake);
51 | const reward =
52 | Math.max(
53 | v.estimatedPoolReward - (v.commission / 100) * v.estimatedPoolReward,
54 | 0
55 | ) * stakeFraction;
56 | return tr + reward;
57 | }, 0);
58 |
59 | const baseYieldFraction = totalReward / totalAmount;
60 | const compoundedReward =
61 | totalAmount * (1 + baseYieldFraction) ** timePeriodInEras - totalAmount;
62 | const returns = compounding
63 | ? Number(compoundedReward)
64 | : Number(totalReward * timePeriodInEras);
65 | const portfolioValue = Number(returns + totalAmount);
66 |
67 | // TODO: yield-percentage to be calculated (for annual basis)
68 | const yieldPercentage = Number(
69 | (((totalAmount + returns) / totalAmount - 1) * 100).toFixed(2)
70 | );
71 |
72 | return {
73 | returns: {
74 | currency: returns,
75 | subCurrency: await convertCurrency(returns, networkInfo.denom),
76 | },
77 | portfolioValue: {
78 | currency: portfolioValue,
79 | subCurrency: await convertCurrency(portfolioValue, networkInfo.denom),
80 | },
81 | yieldPercentage,
82 | };
83 | };
84 |
85 | export default calculateReward;
86 |
--------------------------------------------------------------------------------
/lib/convert-arr-to-object.js:
--------------------------------------------------------------------------------
1 | const convertArrayToObject = (array, key) => {
2 | const initialValue = {};
3 | return array.reduce((obj, item) => {
4 | return {
5 | ...obj,
6 | [item[key]]: item,
7 | };
8 | }, initialValue);
9 | };
10 |
11 | export default convertArrayToObject;
12 |
--------------------------------------------------------------------------------
/lib/convert-currency.js:
--------------------------------------------------------------------------------
1 | import _axios from "axios";
2 |
3 | const convertCurrency = async (
4 | value,
5 | currency = "KSM",
6 | subCurrency = "USD"
7 | ) => {
8 | let networkName = "kusama";
9 | //TODO: Make dynamic to fetch prices for other networks
10 | if (currency === "DOT") {
11 | networkName = "polkadot";
12 | }
13 | const res = await _axios(
14 | `https://api.coingecko.com/api/v3/simple/price?ids=${networkName}&vs_currencies=usd`
15 | );
16 | const data = res.data;
17 | if (currency === "DOT") {
18 | return value * data.polkadot.usd;
19 | } else {
20 | return value * data.kusama.usd;
21 | }
22 | };
23 |
24 | export default convertCurrency;
25 |
--------------------------------------------------------------------------------
/lib/format-currency.js:
--------------------------------------------------------------------------------
1 | import { formatBalance, isHex } from "@polkadot/util";
2 |
3 | import BN from "bn.js";
4 |
5 | export default {
6 | methods: {
7 | formatNumber(number) {
8 | if (isHex(number)) {
9 | return parseInt(number, 16)
10 | .toString()
11 | .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
12 | } else {
13 | return number.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
14 | }
15 | },
16 | formatAmount(amount, network) {
17 | formatBalance.setDefaults({
18 | decimals: network.decimalPlaces,
19 | unit: network.denom,
20 | });
21 | let bn;
22 | if (isHex(amount)) {
23 | bn = new BN(amount.substring(2, amount.length), 16);
24 | } else {
25 | bn = new BN(amount.toString());
26 | }
27 | return formatBalance(bn.toString(10));
28 | },
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/lib/getClaimableRewards.js:
--------------------------------------------------------------------------------
1 | const getClaimableRewards = async (
2 | address,
3 | networkInfo,
4 | api,
5 | erasHistoric,
6 | setClaimableRewards
7 | ) => {
8 | const reward = await api.derive.staking.stakerRewardsMultiEras(
9 | ["GvvafsWU5DxcPpPQ2DNmWSy6KYqUkMPovJxtVvvEtaHq5tW"],
10 | erasHistoric.slice(-2)
11 | );
12 | setClaimableRewards(reward);
13 | };
14 |
15 | export default getClaimableRewards;
16 |
--------------------------------------------------------------------------------
/lib/getErasHistoric.js:
--------------------------------------------------------------------------------
1 | const getErasHistoric = async (api, setErasHistoric) => {
2 | api.derive.staking.erasHistoric().then((data) => {
3 | setErasHistoric(data);
4 | });
5 | };
6 |
7 | export default getErasHistoric;
8 |
--------------------------------------------------------------------------------
/lib/getRewards.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const getRewards = async (address, networkInfo) => {
4 | let datafetched = false;
5 | const dataArr = [];
6 | // const currentTimeStamp = Date.now() / 1000;
7 | let pageNum = 0;
8 | const fetchRewards = async (pageNum, address) => {
9 | const rewards = axios
10 | .post(
11 | `https://${networkInfo.coinGeckoDenom}.subscan.io/api/scan/account/reward_slash`,
12 | {
13 | row: 100,
14 | page: pageNum,
15 | address: address,
16 | }
17 | )
18 | .then((rewards) => {
19 | return rewards.data.data.list;
20 | })
21 | .catch((err) => {
22 | throw new Error(`Couldn't fetch data from subscan. Error: ${err}`);
23 | });
24 |
25 | return rewards;
26 | };
27 |
28 | while (!datafetched) {
29 | const response = await fetchRewards(pageNum, address);
30 | // const rewards = response.filter(
31 | // (x) => x.block_timestamp > currentTimeStamp - 604800
32 | // );
33 |
34 | if (response.length == 0) {
35 | datafetched = true;
36 | } else {
37 | dataArr.push(...response);
38 | pageNum += 1;
39 | }
40 | }
41 |
42 | return dataArr;
43 | };
44 |
45 | export default getRewards;
46 |
--------------------------------------------------------------------------------
/lib/getTransactionFee.js:
--------------------------------------------------------------------------------
1 | import { web3FromAddress } from "@polkadot/extension-dapp";
2 | import { decodeAddress, encodeAddress } from "@polkadot/util-crypto";
3 | import { WsProvider, ApiPromise } from "@polkadot/api";
4 |
5 | // Stash-Controller 1:1 relationship
6 |
7 | // Polkadot API uses it in browser, we let it remain mad :]
8 | window.setImmediate = (cb) => cb();
9 |
10 | // TODO: error handling -> balance check, basics.
11 | const getTransactionFee = async (
12 | stashId,
13 | controllerId,
14 | stakeAmount,
15 | bondedAmount,
16 | payee,
17 | nominatedValidators = [],
18 | api,
19 | networkInfo
20 | ) => {
21 | if (
22 | !stashId ||
23 | !controllerId ||
24 | !stakeAmount ||
25 | !nominatedValidators ||
26 | payee === (undefined || null) ||
27 | !api
28 | ) {
29 | throw new Error({ message: "Incomplete argument list to stake!" });
30 | }
31 |
32 | const substrateStashId = encodeAddress(decodeAddress(stashId), 42);
33 | const substrateControllerId = encodeAddress(decodeAddress(controllerId), 42);
34 |
35 | const ledger = await api.query.staking.ledger(substrateControllerId);
36 |
37 | const transactions = [];
38 | if (ledger.isSome) {
39 | if (stakeAmount > bondedAmount) {
40 | const amount =
41 | (stakeAmount - bondedAmount) * 10 ** networkInfo.decimalPlaces; // 12 decimal places
42 | transactions.push(api.tx.staking.bondExtra(amount));
43 | } else if (stakeAmount < bondedAmount) {
44 | const amount =
45 | (bondedAmount - stakeAmount) * 10 ** networkInfo.decimalPlaces; // 12 decimal places
46 | transactions.push(api.tx.staking.unbond(amount));
47 | }
48 | transactions.push(api.tx.staking.nominate(nominatedValidators));
49 | } else {
50 | // Take the origin account (stash account) as a stash and lock up value of its balance.
51 | // controller will be the account that controls it.
52 | const amount = stakeAmount * 10 ** networkInfo.decimalPlaces; // 12 decimal places
53 | transactions.push(
54 | api.tx.staking.bond(substrateControllerId, amount, payee)
55 | );
56 | transactions.push(api.tx.staking.nominate(nominatedValidators));
57 | }
58 |
59 | const info = await api.tx.utility.batch(transactions).paymentInfo(stashId);
60 |
61 | return info.partialFee.toNumber();
62 | };
63 |
64 | export default getTransactionFee;
65 |
--------------------------------------------------------------------------------
/lib/getUpdateFundsTransactionFee.js:
--------------------------------------------------------------------------------
1 | import { web3FromAddress } from "@polkadot/extension-dapp";
2 | import { decodeAddress, encodeAddress } from "@polkadot/util-crypto";
3 | import { WsProvider, ApiPromise } from "@polkadot/api";
4 |
5 | // Stash-Controller 1:1 relationship
6 |
7 | // Polkadot API uses it in browser, we let it remain mad :]
8 | window.setImmediate = (cb) => cb();
9 |
10 | // TODO: error handling -> balance check, basics.
11 | const getUpdateFundsTransactionFee = async (
12 | stashId,
13 | amount,
14 | type,
15 | bondedAmount,
16 | api,
17 | networkInfo
18 | ) => {
19 | if (!stashId || !amount || !api) {
20 | throw new Error({ message: "Incomplete argument list to stake!" });
21 | }
22 |
23 | const substrateStashId = encodeAddress(decodeAddress(stashId), 42);
24 | // const substrateControllerId = encodeAddress(decodeAddress(controllerId), 42);
25 |
26 | // const ledger = await api.query.staking.ledger(substrateControllerId);
27 |
28 | // const transactions = [];
29 |
30 | const rawAmount = Math.trunc(
31 | amount * Math.pow(10, networkInfo.decimalPlaces)
32 | );
33 |
34 | if (type === "bond") {
35 | if (bondedAmount == 0) {
36 | const bondFee = await api.tx.staking
37 | .bond(substrateStashId, rawAmount, 0)
38 | .paymentInfo(substrateStashId);
39 | return bondFee.partialFee.toNumber();
40 | } else {
41 | const bondExtraFee = await api.tx.staking
42 | .bondExtra(rawAmount)
43 | .paymentInfo(substrateStashId);
44 | return bondExtraFee.partialFee.toNumber();
45 | }
46 | } else {
47 | const unbondFee = api.tx.staking
48 | .unbond(rawAmount)
49 | .paymentInfo(substrateStashId);
50 | return unbondFee;
51 | }
52 | };
53 |
54 | export default getUpdateFundsTransactionFee;
55 |
--------------------------------------------------------------------------------
/lib/polkadot-api.js:
--------------------------------------------------------------------------------
1 | import { WsProvider, ApiPromise } from "@polkadot/api";
2 |
3 | const createPolkadotAPIInstance = async (networkName, apiInstance) => {
4 | if (apiInstance) {
5 | console.info("Polkadot api instance aleady exists.");
6 | return apiInstance;
7 | }
8 | let wsURL = "wss://kusama-rpc.polkadot.io/";
9 | if (networkName !== "Kusama") {
10 | wsURL = "wss://rpc.polkadot.io/";
11 | }
12 | const wsProvider = new WsProvider(wsURL);
13 | const api = await ApiPromise.create({ provider: wsProvider });
14 | return api;
15 | };
16 |
17 | export default createPolkadotAPIInstance;
18 |
--------------------------------------------------------------------------------
/lib/polkadot-extension.js:
--------------------------------------------------------------------------------
1 | import { web3Enable, web3Accounts } from "@polkadot/extension-dapp";
2 |
3 | const getPolkadotExtensionInfo = async ({ onEvent }) => {
4 | const createEventInstance = (message, ...params) => ({ message, ...params });
5 | onEvent(
6 | createEventInstance(
7 | "Waiting for you to allow access to polkadot-js extension..."
8 | )
9 | );
10 | const injectedExtensions = await web3Enable("YieldScan");
11 | const isExtensionAvailable = injectedExtensions.length > 0;
12 | if (isExtensionAvailable) {
13 | onEvent(createEventInstance("Fetching your accounts..."));
14 | const accounts = await web3Accounts();
15 | return { isExtensionAvailable, accounts };
16 | }
17 | return { isExtensionAvailable };
18 | };
19 |
20 | export default getPolkadotExtensionInfo;
21 |
--------------------------------------------------------------------------------
/lib/polkadot/chill.js:
--------------------------------------------------------------------------------
1 | import { web3FromAddress } from "@polkadot/extension-dapp";
2 | import { encodeAddress, decodeAddress } from "@polkadot/util-crypto";
3 |
4 | const createEventInstance = (message, ...params) => ({ message, ...params });
5 |
6 | // Polkadot API uses it in browser, we let it remain mad :]
7 | window.setImmediate = (cb) => cb();
8 |
9 | const chill = async (
10 | stashId,
11 | apiInstance,
12 | { onEvent, onFinish },
13 | ) => {
14 | let controllerAccountId = stashId;
15 |
16 | const controllerAccount = await apiInstance.query.staking.bonded(stashId);
17 | if (controllerAccount.isSome) {
18 | controllerAccountId = encodeAddress(decodeAddress(controllerAccount.toString()), 42);
19 | }
20 |
21 | const injector = await web3FromAddress(controllerAccountId);
22 | apiInstance.setSigner(injector.signer);
23 |
24 | return apiInstance.tx.staking
25 | .chill()
26 | .signAndSend(
27 | controllerAccountId,
28 | ({ events = [], status }) => {
29 | console.log(`status: ${JSON.stringify(status, null, 4)}`);
30 | if (status.isInBlock) {
31 | console.log(`batched included in ${status.asInBlock}`);
32 | onEvent(createEventInstance(`Included in block : ${status.asInBlock}...`));
33 | }
34 |
35 | if (status.isFinalized) {
36 | console.log(`finalized: ${status.asFinalized}`);
37 |
38 | let failed = false;
39 | events.forEach((d) => {
40 | const { phase, event: { data, method, section } } = d;
41 | console.log(`${phase}: ${section}.${method}:: ${data}`);
42 | if (method === 'BatchInterrupted') {
43 | failed = true;
44 | }
45 | });
46 |
47 | onFinish(
48 | failed ? 1 : 0,
49 | failed ?
50 | 'Reason Unknown.' :
51 | 'All nominations stopped.'
52 | );
53 | }
54 | }
55 | );
56 | };
57 |
58 | export default chill;
59 |
--------------------------------------------------------------------------------
/lib/polkadot/edit-controller.js:
--------------------------------------------------------------------------------
1 | import { web3FromAddress } from "@polkadot/extension-dapp";
2 |
3 | const createEventInstance = (message, ...params) => ({ message, ...params });
4 |
5 | // Polkadot API uses it in browser, we let it remain mad :]
6 | window.setImmediate = (cb) => cb();
7 |
8 | const editController = async (
9 | newControllerId,
10 | stashId,
11 | apiInstance,
12 | { onEvent, onFinish },
13 | ) => {
14 | const injector = await web3FromAddress(stashId);
15 | apiInstance.setSigner(injector.signer);
16 | return apiInstance.tx.staking
17 | .setController(newControllerId)
18 | .signAndSend(
19 | stashId,
20 | ({ events = [], status }) => {
21 | console.log(`status: ${JSON.stringify(status, null, 4)}`);
22 | if (status.isInBlock) {
23 | console.log(`batched included in ${status.asInBlock}`);
24 | onEvent(createEventInstance(`Included in block : ${status.asInBlock}...`));
25 | }
26 |
27 | if (status.isFinalized) {
28 | console.log(`finalized: ${status.asFinalized}`);
29 |
30 | let failed = false;
31 | events.forEach((d) => {
32 | const { phase, event: { data, method, section } } = d;
33 | console.log(`${phase}: ${section}.${method}:: ${data}`);
34 | if (method === 'BatchInterrupted') {
35 | failed = true;
36 | }
37 | });
38 |
39 | onFinish(
40 | failed ? 1 : 0,
41 | failed ?
42 | 'Reason Unknown.' :
43 | 'Controller edited successfully.'
44 | );
45 | }
46 | }
47 | );
48 | };
49 |
50 | export default editController;
51 |
--------------------------------------------------------------------------------
/lib/polkadot/nominate.js:
--------------------------------------------------------------------------------
1 | import { web3FromAddress } from "@polkadot/extension-dapp";
2 | import { encodeAddress, decodeAddress } from "@polkadot/util-crypto";
3 |
4 | const createEventInstance = (message, ...params) => ({ message, ...params });
5 |
6 | // Polkadot API uses it in browser, we let it remain mad :]
7 | window.setImmediate = (cb) => cb();
8 |
9 | const nominate = async (
10 | stashId,
11 | validators,
12 | isNominateOnly,
13 | apiInstance,
14 | { onEvent, onFinish, onSuccessfullSigning }
15 | ) => {
16 | let controllerAccountId = stashId;
17 |
18 | const controllerAccount = await apiInstance.query.staking.bonded(stashId);
19 | if (controllerAccount.isSome) {
20 | controllerAccountId = encodeAddress(
21 | decodeAddress(controllerAccount.toString()),
22 | 42
23 | );
24 | }
25 |
26 | const injector = await web3FromAddress(controllerAccountId);
27 | apiInstance.setSigner(injector.signer);
28 |
29 | onEvent(
30 | createEventInstance(
31 | "Waiting for you to sign the transaction to nominate the selected validators..."
32 | )
33 | );
34 | return apiInstance.tx.staking
35 | .nominate(validators)
36 | .signAndSend(controllerAccountId, ({ events = [], status }) => {
37 | console.log(`status: ${JSON.stringify(status, null, 4)}`);
38 | onEvent(createEventInstance("Sending your request to the chain..."));
39 | if (status.isInBlock) {
40 | console.log(`batched included in ${status.asInBlock}`);
41 | onEvent(
42 | createEventInstance(`Included in block : ${status.asInBlock}...`)
43 | );
44 | onSuccessfullSigning(createEventInstance(`${status.asInBlock}`));
45 | }
46 |
47 | let failed = true;
48 | events.forEach((d) => {
49 | const {
50 | phase,
51 | event: { data, method, section },
52 | } = d;
53 | console.log(`${phase}: ${section}.${method}:: ${data}`);
54 | if (method === "ExtrinsicSuccess") {
55 | failed = false;
56 | }
57 | });
58 |
59 | if (status.isFinalized) {
60 | console.log(`finalized: ${status.asFinalized}`);
61 | onEvent(createEventInstance(`Finalised: ${status.asFinalized}...`));
62 | onFinish(
63 | failed ? 1 : 0,
64 | failed ? "Reason Unknown." : "Transaction sent to chain successful!",
65 | isNominateOnly
66 | );
67 | }
68 | });
69 | };
70 |
71 | export default nominate;
72 |
--------------------------------------------------------------------------------
/lib/polkadot/update-funds.js:
--------------------------------------------------------------------------------
1 | import { web3FromAddress } from "@polkadot/extension-dapp";
2 | import { get } from "lodash";
3 | import { encodeAddress, decodeAddress } from "@polkadot/util-crypto";
4 | import { isNil } from "lodash";
5 |
6 | const createEventInstance = (message, ...params) => ({ message, ...params });
7 |
8 | // Polkadot API uses it in browser, we let it remain mad :]
9 | window.setImmediate = (cb) => cb();
10 |
11 | const updateFunds = async (
12 | type,
13 | stashId,
14 | amount,
15 | apiInstance,
16 | { onEvent, onFinish, onSuccessfullSigning },
17 | networkInfo
18 | ) => {
19 | const substrateStashId = encodeAddress(decodeAddress(stashId.toString()), 42);
20 | // if (type !== "bond") {
21 | // const controllerAccount = await apiInstance.query.staking.bonded(stashId);
22 | // if (controllerAccount.isSome) {
23 | // controllerAccountId = encodeAddress(
24 | // decodeAddress(controllerAccount.toString()),
25 | // 42
26 | // );
27 | // }
28 | // }
29 |
30 | const rawAmount = amount * 10 ** networkInfo.decimalPlaces;
31 |
32 | onEvent(createEventInstance("Fetching substrate address..."));
33 | const injector = await web3FromAddress(substrateStashId);
34 | apiInstance.setSigner(injector.signer);
35 |
36 | const operation = get(
37 | apiInstance.tx.staking,
38 | type === "bond" ? "bondExtra" : "unbond"
39 | );
40 |
41 | onEvent(createEventInstance("Waiting for you to sign the transaction..."));
42 | return operation(rawAmount).signAndSend(
43 | substrateStashId,
44 | ({ events = [], status }) => {
45 | console.log(`status: ${JSON.stringify(status, null, 4)}`);
46 | onEvent(createEventInstance("Sending your request to the chain..."));
47 | if (status.isInBlock) {
48 | console.log(`batched included in ${status.asInBlock}`);
49 | onEvent(
50 | createEventInstance(`Included in block : ${status.asInBlock}...`)
51 | );
52 | onSuccessfullSigning(createEventInstance(`${status.asInBlock}`));
53 | }
54 |
55 | if (status.isFinalized) {
56 | const tranHash = status.asFinalized.toString();
57 | console.info("transaction hash: " + tranHash);
58 |
59 | let failed = false;
60 | events.forEach((d) => {
61 | const {
62 | phase,
63 | event: { data, method, section },
64 | } = d;
65 | console.log(`${phase}: ${section}.${method}:: ${data}`);
66 | if (method === "BatchInterrupted") {
67 | failed = true;
68 | }
69 | });
70 |
71 | const eventLogs = events.map((d) => {
72 | const {
73 | phase,
74 | event: { data, method, section },
75 | } = d;
76 | return `${phase}: ${section}.${method}:: ${data}`;
77 | });
78 |
79 | onFinish(
80 | failed ? 1 : 0,
81 | failed ? "Reason Unknown." : "Funds Updated successfully!",
82 | eventLogs,
83 | isNil(tranHash) ? "N/A" : tranHash
84 | );
85 | }
86 | }
87 | );
88 | };
89 |
90 | export default updateFunds;
91 |
--------------------------------------------------------------------------------
/lib/polkadot/update-payee.js:
--------------------------------------------------------------------------------
1 | import { encodeAddress, decodeAddress } from "@polkadot/util-crypto";
2 | import { web3FromAddress } from "@polkadot/extension-dapp";
3 |
4 | const createEventInstance = (message, ...params) => ({ message, ...params });
5 |
6 | // Polkadot API uses it in browser, we let it remain mad :]
7 | window.setImmediate = (cb) => cb();
8 |
9 | const updatePayee = async (
10 | stashId,
11 | payee,
12 | apiInstance,
13 | { onEvent, onFinish, onSuccessfullSigning }
14 | ) => {
15 | let controllerAccountId = stashId;
16 |
17 | const controllerAccount = await apiInstance.query.staking.bonded(stashId);
18 | if (controllerAccount.isSome) {
19 | controllerAccountId = encodeAddress(
20 | decodeAddress(controllerAccount.toString()),
21 | 42
22 | );
23 | }
24 |
25 | onEvent(createEventInstance("Fetching substrate address..."));
26 | const injector = await web3FromAddress(controllerAccountId);
27 | apiInstance.setSigner(injector.signer);
28 |
29 | onEvent(createEventInstance("Waiting for you to sign the transaction..."));
30 | return apiInstance._extrinsics.staking
31 | .setPayee(payee)
32 | .signAndSend(controllerAccountId, ({ events = [], status }) => {
33 | console.log(`status: ${JSON.stringify(status, null, 4)}`);
34 | onEvent(createEventInstance("Sending your request to the chain..."));
35 | if (status.isInBlock) {
36 | console.log(`batched included in ${status.asInBlock}`);
37 | onEvent(
38 | createEventInstance(`Included in block : ${status.asInBlock}...`)
39 | );
40 | onSuccessfullSigning(createEventInstance(`${status.asInBlock}`));
41 | }
42 |
43 | if (status.isFinalized) {
44 | console.log(`finalized: ${status.asFinalized}`);
45 |
46 | let failed = false;
47 | events.forEach((d) => {
48 | const {
49 | phase,
50 | event: { data, method, section },
51 | } = d;
52 | console.log(`${phase}: ${section}.${method}:: ${data}`);
53 | if (method === "BatchInterrupted") {
54 | failed = true;
55 | }
56 | });
57 |
58 | const eventLogs = events.map((d) => {
59 | const {
60 | phase,
61 | event: { data, method, section },
62 | } = d;
63 | return `${phase}: ${section}.${method}:: ${data}`;
64 | });
65 |
66 | onFinish(
67 | failed ? 1 : 0,
68 | failed ? "Reason Unknown." : "Payee Updated!",
69 | eventLogs
70 | );
71 | }
72 | });
73 | };
74 |
75 | export default updatePayee;
76 |
--------------------------------------------------------------------------------
/lib/routes.js:
--------------------------------------------------------------------------------
1 | const Routes = {
2 | OVERVIEW: "/overview",
3 | CALCULATOR: "/reward-calculator",
4 | VALIDATORS: "/validators",
5 | NOMINATORS: "/nominators",
6 | GOVERNANCE: "/council-members",
7 | VALIDATOR_PROFILE: "/validator-profile",
8 | COUNCIL_MEMBER_PROFILE: "/council-member-profile",
9 | SETTINGS: "/settings",
10 | };
11 |
12 | export default Routes;
13 |
--------------------------------------------------------------------------------
/lib/stake.js:
--------------------------------------------------------------------------------
1 | import { web3FromAddress } from "@polkadot/extension-dapp";
2 | import { decodeAddress, encodeAddress } from "@polkadot/util-crypto";
3 | import { WsProvider, ApiPromise } from "@polkadot/api";
4 | import { isNil } from "lodash";
5 |
6 | // Stash-Controller 1:1 relationship
7 |
8 | // Polkadot API uses it in browser, we let it remain mad :]
9 | window.setImmediate = (cb) => cb();
10 |
11 | const createEventInstance = (message, ...params) => ({ message, ...params });
12 |
13 | // TODO: error handling -> balance check, basics.
14 | const stake = async (
15 | stashId,
16 | controllerId,
17 | stakeAmount,
18 | bondedAmount,
19 | payee,
20 | nominatedValidators = [],
21 | api,
22 | { onEvent, onFinish, onSuccessfullSigning },
23 | networkInfo
24 | ) => {
25 | if (
26 | !stashId ||
27 | !controllerId ||
28 | !stakeAmount ||
29 | !nominatedValidators ||
30 | payee === (undefined || null) ||
31 | !api
32 | ) {
33 | throw new Error({ message: "Incomplete argument list to stake!" });
34 | }
35 |
36 | const substrateStashId = encodeAddress(decodeAddress(stashId), 42);
37 | const substrateControllerId = encodeAddress(decodeAddress(controllerId), 42);
38 |
39 | // get substrate address
40 | onEvent(createEventInstance("Fetching substrate address..."));
41 | const injector = await web3FromAddress(substrateStashId);
42 | api.setSigner(injector.signer);
43 |
44 | onEvent(createEventInstance("Fetching existing ledger..."));
45 | const ledger = await api.query.staking.ledger(substrateControllerId);
46 |
47 | const transactions = [];
48 | if (ledger.isSome) {
49 | onEvent(createEventInstance("BondExtra staking amount..."));
50 | if (stakeAmount > bondedAmount) {
51 | const amount =
52 | (stakeAmount - bondedAmount) * 10 ** networkInfo.decimalPlaces; // 12 decimal places
53 | transactions.push(api.tx.staking.bondExtra(amount));
54 | } else if (stakeAmount < bondedAmount) {
55 | const amount =
56 | (bondedAmount - stakeAmount) * 10 ** networkInfo.decimalPlaces; // 12 decimal places
57 | transactions.push(api.tx.staking.unbond(amount));
58 | }
59 | transactions.push(api.tx.staking.nominate(nominatedValidators));
60 | } else {
61 | // Take the origin account (stash account) as a stash and lock up value of its balance.
62 | // controller will be the account that controls it.
63 | const amount = stakeAmount * 10 ** networkInfo.decimalPlaces; // 12 decimal places
64 | onEvent(createEventInstance("Bond staking amount..."));
65 | transactions.push(
66 | api.tx.staking.bond(substrateControllerId, amount, payee)
67 | );
68 | onEvent(createEventInstance("Sending nomination request..."));
69 | transactions.push(api.tx.staking.nominate(nominatedValidators));
70 | }
71 |
72 | if (stashId !== controllerId) {
73 | onEvent(createEventInstance("Set controller on chain..."));
74 | transactions.push(api.tx.staking.setController(substrateControllerId));
75 | }
76 |
77 | onEvent(createEventInstance("Waiting for you to sign the transaction..."));
78 | return api.tx.utility
79 | .batch(transactions)
80 | .signAndSend(substrateStashId, ({ events = [], status }) => {
81 | onEvent(createEventInstance("Sending your request to the chain..."));
82 | if (status.isInBlock) {
83 | onEvent(
84 | createEventInstance(`Included in block : ${status.asInBlock}...`)
85 | );
86 | onSuccessfullSigning(createEventInstance(`${status.asInBlock}`));
87 | }
88 |
89 | if (status.isFinalized) {
90 | const tranHash = status.asFinalized.toString();
91 | console.info("transaction hash: " + tranHash);
92 | let failed = false;
93 | events.forEach((d) => {
94 | const {
95 | phase,
96 | event: { data, method, section },
97 | } = d;
98 | if (method === "BatchInterrupted") {
99 | failed = true;
100 | }
101 | });
102 |
103 | const eventLogs = events.map((d) => {
104 | const {
105 | phase,
106 | event: { data, method, section },
107 | } = d;
108 | return `${phase}: ${section}.${method}:: ${data}`;
109 | });
110 |
111 | onFinish(
112 | failed ? 1 : 0,
113 | failed
114 | ? "Reason Unknown. If your amount is bonded, it's safe in your account, you can retry with different set of validators."
115 | : "Bonded and Nominated.",
116 | eventLogs,
117 | isNil(tranHash) ? "N/A" : tranHash
118 | );
119 | }
120 | });
121 | };
122 |
123 | export default stake;
124 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | // Use the hidden-source-map option when you don't want the source maps to be
2 | // publicly available on the servers, only to the error reporting
3 | const withSourceMaps = require("@zeit/next-source-maps")();
4 |
5 | // Use the SentryWebpack plugin to upload the source maps during build step
6 | const SentryWebpackPlugin = require("@sentry/webpack-plugin");
7 | const {
8 | NEXT_PUBLIC_SENTRY_DSN: SENTRY_DSN,
9 | SENTRY_ORG,
10 | SENTRY_PROJECT,
11 | SENTRY_AUTH_TOKEN,
12 | NODE_ENV,
13 | VERCEL_GITHUB_COMMIT_SHA,
14 | VERCEL_GITLAB_COMMIT_SHA,
15 | VERCEL_BITBUCKET_COMMIT_SHA,
16 | } = process.env;
17 |
18 | const COMMIT_SHA =
19 | VERCEL_GITHUB_COMMIT_SHA ||
20 | VERCEL_GITLAB_COMMIT_SHA ||
21 | VERCEL_BITBUCKET_COMMIT_SHA;
22 |
23 | process.env.SENTRY_DSN = SENTRY_DSN;
24 |
25 | module.exports = withSourceMaps({
26 | serverRuntimeConfig: {
27 | rootDir: __dirname,
28 | },
29 | webpack: (config, options) => {
30 | config.module.rules.push({
31 | type: "javascript/auto",
32 | test: /\.mjs$/,
33 | use: [],
34 | });
35 | // In `pages/_app.js`, Sentry is imported from @sentry/browser. While
36 | // @sentry/node will run in a Node.js environment. @sentry/node will use
37 | // Node.js-only APIs to catch even more unhandled exceptions.
38 | //
39 | // This works well when Next.js is SSRing your page on a server with
40 | // Node.js, but it is not what we want when your client-side bundle is being
41 | // executed by a browser.
42 | //
43 | // Luckily, Next.js will call this webpack function twice, once for the
44 | // server and once for the client. Read more:
45 | // https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config
46 | //
47 | // So ask Webpack to replace @sentry/node imports with @sentry/browser when
48 | // building the browser's bundle
49 | if (!options.isServer) {
50 | config.resolve.alias["@sentry/node"] = "@sentry/browser";
51 | }
52 |
53 | // When all the Sentry configuration env variables are available/configured
54 | // The Sentry webpack plugin gets pushed to the webpack plugins to build
55 | // and upload the source maps to sentry.
56 | // This is an alternative to manually uploading the source maps
57 | // Note: This is disabled in development mode.
58 | if (
59 | SENTRY_DSN &&
60 | SENTRY_ORG &&
61 | SENTRY_PROJECT &&
62 | SENTRY_AUTH_TOKEN &&
63 | COMMIT_SHA &&
64 | NODE_ENV === "production"
65 | ) {
66 | config.plugins.push(
67 | new SentryWebpackPlugin({
68 | include: ".next",
69 | ignore: ["node_modules"],
70 | stripPrefix: ["webpack://_N_E/"],
71 | urlPrefix: "~/_next",
72 | release: COMMIT_SHA,
73 | })
74 | );
75 | }
76 |
77 | return config;
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yieldscan-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "export": "next export",
9 | "start": "next start",
10 | "git:commit": "git cz"
11 | },
12 | "dependencies": {
13 | "@chakra-ui/core": "^0.8.0",
14 | "@emotion/core": "^10.0.28",
15 | "@emotion/styled": "^10.0.27",
16 | "@metomic/react": "^0.1.2",
17 | "@polkadot/api": "^3.0.1",
18 | "@polkadot/extension-dapp": "^0.35.1",
19 | "@polkadot/react-identicon": "^0.58.1",
20 | "@polkadot/ui-keyring": "^0.63.1",
21 | "@polkadot/util-crypto": "^5.0.1",
22 | "@sentry/browser": "^5.24.1",
23 | "@sentry/node": "^5.24.1",
24 | "@sentry/webpack-plugin": "^1.11.1",
25 | "@zeit/next-source-maps": "0.0.4-canary.1",
26 | "amplitude-js": "^7.1.1",
27 | "axios": "^0.20.0",
28 | "bn.js": "^5.1.3",
29 | "bn.js-typings": "^1.0.1",
30 | "confetti-js": "^0.0.18",
31 | "date-fns": "^2.16.1",
32 | "emotion-theming": "^10.0.27",
33 | "konva": "^7.1.3",
34 | "lodash": "^4.17.19",
35 | "millify": "^3.3.0",
36 | "next": "^10.0.3",
37 | "nookies": "^2.4.0",
38 | "react": "^17.0.1",
39 | "react-countup": "^4.3.3",
40 | "react-device-detect": "^1.13.1",
41 | "react-dom": "^17.0.1",
42 | "react-feather": "^2.0.8",
43 | "react-icons": "^4.1.0",
44 | "react-konva": "^17.0.0-0",
45 | "react-marquee-slider": "^1.1.2",
46 | "react-progressive-image": "^0.6.0",
47 | "react-scroll": "^1.8.1",
48 | "rifm": "^0.12.0",
49 | "sass": "^1.26.11",
50 | "styled-components": "^5.2.0",
51 | "tawkto-react": "^1.0.11",
52 | "zustand": "^3.1.2"
53 | },
54 | "devDependencies": {
55 | "git-cz": "^4.7.1",
56 | "postcss-preset-env": "^6.7.0",
57 | "tailwindcss": "^1.8.10"
58 | },
59 | "resolutions": {
60 | "next/**/node-fetch": "^2.6.1",
61 | "@metomic/react/**/node-fetch": "^2.6.1"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/pages/404.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
7 |
404
8 |
You seem to be lost...
9 |
10 | Either the internet has broken or we couldn't find the file that you
11 | were looking for.
12 |
13 |
14 |
15 | Take me back
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { ConsentGate, MetomicProvider } from "@metomic/react";
2 | import * as Sentry from "@sentry/node";
3 | import tawkTo from "tawkto-react";
4 |
5 | import { ThemeProvider, theme } from "@chakra-ui/core";
6 | import "../styles/index.scss";
7 | import { useEffect } from "react";
8 |
9 | const customIcons = {
10 | secureLogo: {
11 | path: (
12 |
19 |
20 |
29 |
30 |
36 |
37 |
38 |
47 |
48 |
56 |
63 |
71 |
72 |
73 |
82 |
83 |
89 |
93 |
94 |
95 |
101 |
102 |
103 |
104 | ),
105 | },
106 | };
107 |
108 | const customTheme = {
109 | ...theme,
110 | icons: {
111 | ...theme.icons,
112 | ...customIcons,
113 | },
114 | colors: {
115 | ...theme.colors,
116 | teal: {
117 | ...theme.colors.teal,
118 | 300: "#45E2E2",
119 | 500: "#2BCACA",
120 | 700: "#20B1B1",
121 | },
122 | pink: {
123 | ...theme.colors.pink,
124 | 300: "#FF9DC0",
125 | 500: "#FF7CAB",
126 | 700: "#EF6093",
127 | },
128 | orange: {
129 | ...theme.colors.orange,
130 | 500: "#F5B100",
131 | },
132 | },
133 | opacity: {
134 | ...theme.opacity,
135 | 10: ".1",
136 | 20: ".2",
137 | 30: ".3",
138 | 40: ".4",
139 | 50: ".5",
140 | 60: ".6",
141 | 70: ".7",
142 | 80: ".8",
143 | 90: ".9",
144 | },
145 | };
146 |
147 | if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
148 | Sentry.init({
149 | environment: process.env.NODE_ENV,
150 | enabled: process.env.NODE_ENV === "production",
151 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
152 | });
153 | }
154 |
155 | export default function YieldScanApp({ Component, pageProps, err }) {
156 | useEffect(() => {
157 | tawkTo(process.env.NEXT_PUBLIC_TAWK_PROP_ID);
158 | }, []);
159 | return (
160 |
161 |
162 |
163 |
164 |
165 | );
166 | }
167 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | class MyDocument extends Document {
4 | static async getInitialProps(ctx) {
5 | const initialProps = await Document.getInitialProps(ctx);
6 | return { ...initialProps };
7 | }
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
19 |
24 |
29 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
51 | export default MyDocument;
--------------------------------------------------------------------------------
/pages/_error.js:
--------------------------------------------------------------------------------
1 | import NextErrorComponent from "next/error";
2 | import * as Sentry from "@sentry/node";
3 | import Link from "next/link";
4 | import { get } from "lodash";
5 |
6 | const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
7 | if (!hasGetInitialPropsRun && err) {
8 | // getInitialProps is not called in case of
9 | // https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
10 | // err via _app.js so it can be captured
11 | Sentry.captureException(err);
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 | {statusCode ? statusCode : "Oops!"}
19 |
20 |
Woah! This is embarrasing...
21 |
22 | Either the internet has broken or you found a bug.
23 |
24 |
25 |
26 | Take me back
27 |
28 |
29 | {/* {!hasGetInitialPropsRun && err ? ( */}
30 |
33 | Sentry.showReportDialog({
34 | eventId: Sentry.captureException(err),
35 | })
36 | }
37 | >
38 | Report feedback
39 |
40 | {/* ) : (
41 | ""
42 | )} */}
43 |
44 |
49 |
50 | );
51 | };
52 |
53 | MyError.getInitialProps = async ({ res, err, asPath }) => {
54 | const errorInitialProps = await NextErrorComponent.getInitialProps({
55 | res,
56 | err,
57 | });
58 |
59 | // Workaround for https://github.com/vercel/next.js/issues/8592, mark when
60 | // getInitialProps has run
61 | errorInitialProps.hasGetInitialPropsRun = true;
62 |
63 | // Running on the server, the response object (`res`) is available.
64 | //
65 | // Next.js will pass an err on the server if a page's data fetching methods
66 | // threw or returned a Promise that rejected
67 | //
68 | // Running on the client (browser), Next.js will provide an err if:
69 | //
70 | // - a page's `getInitialProps` threw or returned a Promise that rejected
71 | // - an exception was thrown somewhere in the React lifecycle (render,
72 | // componentDidMount, etc) that was caught by Next.js's React Error
73 | // Boundary. Read more about what types of exceptions are caught by Error
74 | // Boundaries: https://reactjs.org/docs/error-boundaries.html
75 |
76 | if (get(res, "statusCode") === 404) {
77 | // Opinionated: do not record an exception in Sentry for 404
78 | return { statusCode: 404 };
79 | }
80 | if (err) {
81 | Sentry.captureException(err);
82 | await Sentry.flush(2000);
83 | return errorInitialProps;
84 | }
85 |
86 | // If this point is reached, getInitialProps was called without any
87 | // information about what the error might be. This is unexpected and may
88 | // indicate a bug introduced in Next.js, so record it in Sentry
89 | Sentry.captureException(
90 | new Error(`_error.js getInitialProps missing data at path: ${asPath}`)
91 | );
92 | await Sentry.flush(2000);
93 |
94 | return errorInitialProps;
95 | };
96 |
97 | export default MyError;
98 |
--------------------------------------------------------------------------------
/pages/about.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withBaseLayout from "@components/common/layouts/base";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const AboutComponent = dynamic(
10 | () => import("@components/about").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const About = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default About;
21 |
--------------------------------------------------------------------------------
/pages/council-member-profile/[id].js:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 | import withDashboardLayout from '@components/common/layouts/dashboard';
3 |
4 | const Page = dynamic(
5 | () => import('@components/common/page').then(mod => mod.default),
6 | { ssr: false },
7 | );
8 |
9 | const CouncilMemberProfileComponent = dynamic(
10 | () => import('@components/council-member-profile').then(mod => mod.default),
11 | { ssr: false },
12 | );
13 |
14 | const CouncilMemberProfile = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default CouncilMemberProfile;
21 |
--------------------------------------------------------------------------------
/pages/council-members.js:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 | import withDashboardLayout from '@components/common/layouts/dashboard';
3 |
4 | const Page = dynamic(
5 | () => import('@components/common/page').then(mod => mod.default),
6 | { ssr: false },
7 | );
8 |
9 | const CouncilMembersPage = dynamic(
10 | () => import('@components/council-members').then(mod => mod.default),
11 | { ssr: false },
12 | );
13 |
14 | const CouncilMembers = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default CouncilMembers;
21 |
--------------------------------------------------------------------------------
/pages/disclaimer.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withDocumentationLayout from "@components/common/layouts/documentation";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const DisclaimerComponent = dynamic(
10 | () =>
11 | import("@components/policies/disclaimer-component").then((mod) => mod.default),
12 | { ssr: false }
13 | );
14 |
15 | const Privacy = () => (
16 |
17 | {() => }
18 |
19 | );
20 |
21 | export default Privacy;
22 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withBaseLayout from "@components/common/layouts/base";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const HomeComponent = dynamic(
10 | () => import("@components/home").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const HomePage = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default HomePage;
21 |
--------------------------------------------------------------------------------
/pages/nominators.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withDashboardLayout from "@components/common/layouts/dashboard";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const NominatorsComponent = dynamic(
10 | () => import("@components/nominators").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const Nominators = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default Nominators;
21 |
--------------------------------------------------------------------------------
/pages/overview.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withDashboardLayout from "@components/common/layouts/dashboard";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const OverviewComponent = dynamic(
10 | () => import("@components/overview").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const Payment = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default Payment;
21 |
--------------------------------------------------------------------------------
/pages/payment.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withBaseLayout from "@components/common/layouts/base";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const PaymentComponent = dynamic(
10 | () => import("@components/payment").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const Payment = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default Payment;
21 |
--------------------------------------------------------------------------------
/pages/privacy.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withDocumentationLayout from "@components/common/layouts/documentation";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const PrivacyComponent = dynamic(
10 | () =>
11 | import("@components/policies/privacy-component").then((mod) => mod.default),
12 | { ssr: false }
13 | );
14 |
15 | const Privacy = () => (
16 |
17 | {() => }
18 |
19 | );
20 |
21 | export default Privacy;
22 |
--------------------------------------------------------------------------------
/pages/reward-calculator.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withDashboardLayout from "@components/common/layouts/dashboard";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const RewardCalculatorComponent = dynamic(
10 | () => import("@components/reward-calculator").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const RewardCalculator = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default RewardCalculator;
21 |
--------------------------------------------------------------------------------
/pages/settings.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withDashboardLayout from "@components/common/layouts/dashboard";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const SettingsComponent = dynamic(
10 | () => import("@components/settings").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const Settings = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default Settings;
21 |
--------------------------------------------------------------------------------
/pages/terms.js:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 | import withDocumentationLayout from '@components/common/layouts/documentation';
3 |
4 | const Page = dynamic(
5 | () => import('@components/common/page').then(mod => mod.default),
6 | { ssr: false },
7 | );
8 |
9 | const TermsComponent = dynamic(
10 | () => import('@components/policies/terms-component').then(mod => mod.default),
11 | { ssr: false },
12 | );
13 |
14 | const Terms = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default Terms;
--------------------------------------------------------------------------------
/pages/validator-profile/[id].js:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 | import withDashboardLayout from '@components/common/layouts/dashboard';
3 |
4 | const Page = dynamic(
5 | () => import('@components/common/page').then(mod => mod.default),
6 | { ssr: false },
7 | );
8 |
9 | const ValidatorProfileComponent = dynamic(
10 | () => import('@components/validator-profile').then(mod => mod.default),
11 | { ssr: false },
12 | );
13 |
14 | const Payment = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default Payment;
21 |
--------------------------------------------------------------------------------
/pages/validators.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import withDashboardLayout from "@components/common/layouts/dashboard";
3 |
4 | const Page = dynamic(
5 | () => import("@components/common/page").then((mod) => mod.default),
6 | { ssr: false }
7 | );
8 |
9 | const ValidatorsComponent = dynamic(
10 | () => import("@components/validators").then((mod) => mod.default),
11 | { ssr: false }
12 | );
13 |
14 | const Payment = () => (
15 |
16 | {() => }
17 |
18 | );
19 |
20 | export default Payment;
21 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | // ...
4 | 'tailwindcss',
5 | 'autoprefixer',
6 | // ...
7 | ]
8 | }
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/badges/1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/public/images/badges/4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/public/images/badges/gray-badge.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/images/baroque.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/baroque.png
--------------------------------------------------------------------------------
/public/images/chris-hutchinson.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/chris-hutchinson.png
--------------------------------------------------------------------------------
/public/images/dave-ramico.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/dave-ramico.jpg
--------------------------------------------------------------------------------
/public/images/discord-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/images/dollar-sign.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/dollar-sign.jpg
--------------------------------------------------------------------------------
/public/images/dollar-sign.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/images/enea-arllai.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/enea-arllai.jpg
--------------------------------------------------------------------------------
/public/images/kusama-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/kusama-logo-light.png
--------------------------------------------------------------------------------
/public/images/kusama-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/kusama-logo.png
--------------------------------------------------------------------------------
/public/images/landing-bg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/polkadot-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/polkadot-logo.png
--------------------------------------------------------------------------------
/public/images/polkadot-successfully-bonded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/polkadot-successfully-bonded.png
--------------------------------------------------------------------------------
/public/images/polkadot-wallet-connect-info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/polkadot-wallet-connect-info.png
--------------------------------------------------------------------------------
/public/images/polkadot-wallet-connect-success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/polkadot-wallet-connect-success.png
--------------------------------------------------------------------------------
/public/images/polkadot-wallet-connect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/polkadot-wallet-connect.png
--------------------------------------------------------------------------------
/public/images/polkadot_alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/polkadot_alert.png
--------------------------------------------------------------------------------
/public/images/riot-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ruben-russel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/ruben-russel.png
--------------------------------------------------------------------------------
/public/images/team/prastut-kumar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/team/prastut-kumar.png
--------------------------------------------------------------------------------
/public/images/team/sahil-nanda.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/team/sahil-nanda.png
--------------------------------------------------------------------------------
/public/images/team/saumya-karan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/team/saumya-karan.png
--------------------------------------------------------------------------------
/public/images/telegram-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/images/unicorn-sweat/unicorn-sweat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/unicorn-sweat/unicorn-sweat.png
--------------------------------------------------------------------------------
/public/images/unicorn-sweat/unicorn-sweat@0.1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/unicorn-sweat/unicorn-sweat@0.1x.png
--------------------------------------------------------------------------------
/public/images/unicorn-sweat/unicorn-sweat@0.25x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/unicorn-sweat/unicorn-sweat@0.25x.png
--------------------------------------------------------------------------------
/public/images/unicorn-sweat/unicorn-sweat@0.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/unicorn-sweat/unicorn-sweat@0.5x.png
--------------------------------------------------------------------------------
/public/images/unicorn-sweat/unicorn-sweat@0.75x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/unicorn-sweat/unicorn-sweat@0.75x.png
--------------------------------------------------------------------------------
/public/images/web3foundation_grants_badge_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buidl-labs/yieldscan-frontend/4c069eb4c5fe9f897dc7525714e433d229f49263/public/images/web3foundation_grants_badge_black.png
--------------------------------------------------------------------------------
/public/images/yieldscan-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/zeit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // tailwind.config.js
2 | const {
3 | colors,
4 | borderRadius,
5 | maxWidth,
6 | scale,
7 | } = require("tailwindcss/defaultTheme");
8 |
9 | module.exports = {
10 | theme: {
11 | extend: {
12 | colors: {
13 | teal: {
14 | ...colors.teal,
15 | "300": "#45E2E2",
16 | "500": "#2BCACA",
17 | "700": "#20B1B1",
18 | },
19 | pink: {
20 | ...colors.pink,
21 | "300": "#FF9DC0",
22 | "500": "#FF7CAB",
23 | "700": "#EF6093",
24 | },
25 | orange: {
26 | ...colors.orange,
27 | "500": "#F5B100",
28 | },
29 | bgGray: {
30 | "100": "#F1F8FF",
31 | "200": "#E2ECF9",
32 | "300": "#D1DDEA",
33 | "400": "#C7D3E0",
34 | "500": "#BEC7D2",
35 | "900": "#B3BECC",
36 | },
37 | textGray: {
38 | "100": "#94A4B7",
39 | "200": "#798594",
40 | "300": "#626D7B",
41 | "400": "#48607C",
42 | "500": "#35475C",
43 | "700": "#212D3B",
44 | },
45 | },
46 | borderRadius: {
47 | ...borderRadius,
48 | xl: "1rem",
49 | },
50 | maxWidth: {
51 | ...maxWidth,
52 | xs: "16rem",
53 | xxs: "12rem",
54 | },
55 | cursor: {
56 | help: "help",
57 | },
58 | opacity: {
59 | "10": "0.1",
60 | "22": "0.22",
61 | },
62 | scale: {
63 | ...scale,
64 | "102": "1.02",
65 | },
66 | },
67 | },
68 | variants: {},
69 | plugins: [],
70 | };
71 |
--------------------------------------------------------------------------------
/yieldscan.config.js:
--------------------------------------------------------------------------------
1 | // Selected network
2 | // const selectedNetwork = `Polkadot`;
3 | const selectedNetwork = `Kusama`;
4 | // const selectedNetwork = `Westend`;
5 |
6 | // Substrate networks
7 | export const networks = [
8 | {
9 | id: "polkadot-cc1",
10 | name: "Polkadot",
11 | denom: "DOT",
12 | coinGeckoDenom: "polkadot",
13 | decimalPlaces: 10,
14 | twitterUrl: "@Polkadot",
15 | addressPrefix: 0,
16 | nodeWs: "wss://rpc.polkadot.io",
17 | erasPerDay: 1,
18 | lockUpPeriod: 28,
19 | minAmount: 1,
20 | about: "Polkadot is a heterogeneous multi‑chain technology.",
21 | },
22 | {
23 | id: "kusama-cc3",
24 | name: "Kusama",
25 | denom: "KSM",
26 | twitterUrl: "@kusamanetwork",
27 | coinGeckoDenom: "kusama",
28 | decimalPlaces: 12,
29 | addressPrefix: 2,
30 | nodeWs: "wss://kusama-rpc.polkadot.io",
31 | erasPerDay: 4,
32 | lockUpPeriod: 7,
33 | minAmount: 0.1,
34 | about: "Kusama is an early, unaudited, and unrefined release of Polkadot.",
35 | },
36 | {
37 | id: "westend",
38 | name: "Westend",
39 | denom: "WND",
40 | coinGeckoDenom: undefined,
41 | decimalPlaces: 12,
42 | addressPrefix: 42,
43 | nodeWs: "wss://westend-rpc.polkadot.io",
44 | backendWs: "wss://westend.polkastats.io/api/v3",
45 | backendHttp: "http://westend.polkastats.io/api/v3",
46 | erasPerDay: 4,
47 | lockUpPeriod: 7,
48 | validator: undefined,
49 | },
50 | ];
51 |
52 | export const getNetworkInfo = (networkName) => {
53 | return networks.find(({ name }) => name === networkName);
54 | };
55 |
56 | export const network = networks.find(({ name }) => name === selectedNetwork);
57 |
--------------------------------------------------------------------------------