diff --git a/.github/ISSUE_TEMPLATE/suggest_staking_product.md b/.github/ISSUE_TEMPLATE/suggest_staking_product.md new file mode 100644 index 00000000000..6538479a9fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggest_staking_product.md @@ -0,0 +1,126 @@ +--- +name: Suggest a staking product or service +about: Suggest a staking product or service to list on ethereum.org +title: "" +labels: "feature :sparkles:, content :fountain_pen:" +assignees: "" +--- + +Before suggesting a staking product or service, make sure you've read [our listing policy](/contributing/adding-staking-product/). + +Only continue with the issue if the staking product meets the criteria listed there. + +If it does, complete the following information which we need to accurately list the product/service. + +**Product type** + + + +- [ ] Node/client tooling +- [ ] Key management +- [ ] Staking as a service +- [ ] Staking pool +- [ ] Other: (describe) + + + +**Name** + + + +**Logo** + + + +**Description** + + + +**Website** + + + +**If software is involved, is everything open source?** + + + +- [ ] Yes, everything is open source: _provide link(s) to project repo(s)_ +- [ ] Portions of code are open source: _provide link(s) to project repo(s)_ +- [ ] No, code is closed source +- [ ] N/A, no custom software is involved + +**Is the project a fork? If yes, which project was forked?** + + + +**Is the product out of _beta_ development?** + + + +**What wallets support the product or service?** + + + +**If the product or service enables staking with <32 ETH, what is the minimum ETH required to stake?** + + + +**If a service, what are the fees associated with using the service?** + + + +**If the product or service involved a liquidity token, what are the tokens involved?** + + + +**What date did the project or service go live?** + + + +**Has the project undergone an external security audit?** + + + +**Has the project undergone any security bug bounties?** + + + +**Is the project being actively maintained?** + + + +**Is the product or service free of trusted/human intermediaries?** + + + +**If a pooled staking service, can users participate as a node operator without permission?** + + + +**If listing a staking-as-a-service, are users required to sign-up for an account?** + + + +**If listing a staking-as-a-service, who holds the signing keys, and withdrawal keys?** + + + +**If a pooled staking service, what percent of node operators are running a supermajority CL client?** + + + +**If listing node or client tooling, which CL clients (Lighthouse, Teku, Nimbus or Prysm) are supported?** + + + +**What platforms are supported?** + + + +**What user interfaces are supported?** + + + +**Social media links** + + diff --git a/src/assets/staking/abyss-glyph.svg b/src/assets/staking/abyss-glyph.svg new file mode 100644 index 00000000000..3f9a4ea6040 --- /dev/null +++ b/src/assets/staking/abyss-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/allnodes-glyph.svg b/src/assets/staking/allnodes-glyph.svg new file mode 100644 index 00000000000..dacd6c4b317 --- /dev/null +++ b/src/assets/staking/allnodes-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/ankr-glyph.svg b/src/assets/staking/ankr-glyph.svg new file mode 100644 index 00000000000..2de04e55f0e --- /dev/null +++ b/src/assets/staking/ankr-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/audited.svg b/src/assets/staking/audited.svg new file mode 100644 index 00000000000..f2ca0fd88af --- /dev/null +++ b/src/assets/staking/audited.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/staking/battle-tested.svg b/src/assets/staking/battle-tested.svg new file mode 100644 index 00000000000..03243afd13a --- /dev/null +++ b/src/assets/staking/battle-tested.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/staking/bloxstaking-glyph.svg b/src/assets/staking/bloxstaking-glyph.svg new file mode 100644 index 00000000000..7a31cb48541 --- /dev/null +++ b/src/assets/staking/bloxstaking-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/bug-bounty.svg b/src/assets/staking/bug-bounty.svg new file mode 100644 index 00000000000..1ac804ac0ed --- /dev/null +++ b/src/assets/staking/bug-bounty.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/staking/caution-product-glyph.svg b/src/assets/staking/caution-product-glyph.svg new file mode 100644 index 00000000000..cdaea421411 --- /dev/null +++ b/src/assets/staking/caution-product-glyph.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/staking/dappnode-glyph.svg b/src/assets/staking/dappnode-glyph.svg new file mode 100644 index 00000000000..88335331d14 --- /dev/null +++ b/src/assets/staking/dappnode-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/default-open-source-glyph.svg b/src/assets/staking/default-open-source-glyph.svg new file mode 100644 index 00000000000..8734f3ac766 --- /dev/null +++ b/src/assets/staking/default-open-source-glyph.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/staking/docker-icon.svg b/src/assets/staking/docker-icon.svg new file mode 100644 index 00000000000..4870b8aec72 --- /dev/null +++ b/src/assets/staking/docker-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/economical.svg b/src/assets/staking/economical.svg new file mode 100644 index 00000000000..a80137319ed --- /dev/null +++ b/src/assets/staking/economical.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/green-check-product-glyph.svg b/src/assets/staking/green-check-product-glyph.svg new file mode 100644 index 00000000000..e26e9d317b4 --- /dev/null +++ b/src/assets/staking/green-check-product-glyph.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/staking/leslie-pool.png b/src/assets/staking/leslie-pool.png new file mode 100644 index 00000000000..3423c46fd47 Binary files /dev/null and b/src/assets/staking/leslie-pool.png differ diff --git a/src/assets/staking/leslie-saas.png b/src/assets/staking/leslie-saas.png new file mode 100644 index 00000000000..596e37c68f9 Binary files /dev/null and b/src/assets/staking/leslie-saas.png differ diff --git a/src/assets/staking/leslie-solo.png b/src/assets/staking/leslie-solo.png new file mode 100644 index 00000000000..07fede0bd41 Binary files /dev/null and b/src/assets/staking/leslie-solo.png differ diff --git a/src/assets/staking/lido-glyph.svg b/src/assets/staking/lido-glyph.svg new file mode 100644 index 00000000000..c09320d9341 --- /dev/null +++ b/src/assets/staking/lido-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/liquidity-token.svg b/src/assets/staking/liquidity-token.svg new file mode 100644 index 00000000000..9efca1037f8 --- /dev/null +++ b/src/assets/staking/liquidity-token.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/multi-client.svg b/src/assets/staking/multi-client.svg new file mode 100644 index 00000000000..56eca75a434 --- /dev/null +++ b/src/assets/staking/multi-client.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/staking/open-source.svg b/src/assets/staking/open-source.svg new file mode 100644 index 00000000000..13a81907e04 --- /dev/null +++ b/src/assets/staking/open-source.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/permissionless.svg b/src/assets/staking/permissionless.svg new file mode 100644 index 00000000000..ebc37000b58 --- /dev/null +++ b/src/assets/staking/permissionless.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/staking/rocket-pool-glyph.svg b/src/assets/staking/rocket-pool-glyph.svg new file mode 100644 index 00000000000..5e5a8f54a71 --- /dev/null +++ b/src/assets/staking/rocket-pool-glyph.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/staking/self-custody.svg b/src/assets/staking/self-custody.svg new file mode 100644 index 00000000000..507636d463c --- /dev/null +++ b/src/assets/staking/self-custody.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/staking/stafi-glyph.svg b/src/assets/staking/stafi-glyph.svg new file mode 100644 index 00000000000..5babc932520 --- /dev/null +++ b/src/assets/staking/stafi-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/stakefish-glyph.svg b/src/assets/staking/stakefish-glyph.svg new file mode 100644 index 00000000000..1eb1ed14eda --- /dev/null +++ b/src/assets/staking/stakefish-glyph.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/staking/stakewise-glyph.svg b/src/assets/staking/stakewise-glyph.svg new file mode 100644 index 00000000000..87f76eb2920 --- /dev/null +++ b/src/assets/staking/stakewise-glyph.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/staking/staking-glyph-centralized.svg b/src/assets/staking/staking-glyph-centralized.svg new file mode 100644 index 00000000000..95620a7ad82 --- /dev/null +++ b/src/assets/staking/staking-glyph-centralized.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/staking-glyph-cloud.svg b/src/assets/staking/staking-glyph-cloud.svg new file mode 100644 index 00000000000..eb9322d922c --- /dev/null +++ b/src/assets/staking/staking-glyph-cloud.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/staking-glyph-cpu.svg b/src/assets/staking/staking-glyph-cpu.svg new file mode 100644 index 00000000000..e348f18d496 --- /dev/null +++ b/src/assets/staking/staking-glyph-cpu.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/staking-glyph-ether-circle.svg b/src/assets/staking/staking-glyph-ether-circle.svg new file mode 100644 index 00000000000..3a7fddca972 --- /dev/null +++ b/src/assets/staking/staking-glyph-ether-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/staking/staking-glyph-token-wallet.svg b/src/assets/staking/staking-glyph-token-wallet.svg new file mode 100644 index 00000000000..a78f8fff8b2 --- /dev/null +++ b/src/assets/staking/staking-glyph-token-wallet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/staking/stereum-glyph.svg b/src/assets/staking/stereum-glyph.svg new file mode 100644 index 00000000000..67b478bb711 --- /dev/null +++ b/src/assets/staking/stereum-glyph.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/stereum-logo.png b/src/assets/staking/stereum-logo.png new file mode 100644 index 00000000000..626bfc6a4ae Binary files /dev/null and b/src/assets/staking/stereum-logo.png differ diff --git a/src/assets/staking/trustless.svg b/src/assets/staking/trustless.svg new file mode 100644 index 00000000000..05c1469f8f0 --- /dev/null +++ b/src/assets/staking/trustless.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/staking/unknown-product-glyph.svg b/src/assets/staking/unknown-product-glyph.svg new file mode 100644 index 00000000000..fd0159a6b80 --- /dev/null +++ b/src/assets/staking/unknown-product-glyph.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/staking/wagyu-glyph.svg b/src/assets/staking/wagyu-glyph.svg new file mode 100644 index 00000000000..046ff83c362 --- /dev/null +++ b/src/assets/staking/wagyu-glyph.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/staking/warning-product-glyph.svg b/src/assets/staking/warning-product-glyph.svg new file mode 100644 index 00000000000..477b9b9db60 --- /dev/null +++ b/src/assets/staking/warning-product-glyph.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/ButtonDropdown.js b/src/components/ButtonDropdown.js index 88d5dfe8f57..9ce5da90d47 100644 --- a/src/components/ButtonDropdown.js +++ b/src/components/ButtonDropdown.js @@ -1,14 +1,19 @@ +// Libraries import React, { useState, createRef } from "react" import styled from "styled-components" import { useIntl } from "gatsby-plugin-intl" import { motion } from "framer-motion" +// Components import Icon from "./Icon" import Link from "./Link" import Translation from "./Translation" import { ButtonSecondary } from "./SharedStyledComponents" + +// Utils import { useOnClickOutside } from "../hooks/useOnClickOutside" import { translateMessageId } from "../utils/translations" +import { trackCustomEvent } from "../utils/matomo" const Container = styled.div` position: relative; @@ -97,6 +102,17 @@ const NavLink = styled(Link)` } ` +const NakedNavLink = styled.div` + text-decoration: none; + display: block; + padding: 0.5rem; + color: ${(props) => props.theme.colors.text}; + cursor: pointer; + &:hover { + color: ${(props) => props.theme.colors.primary}; + } +` + const ButtonDropdown = ({ list, className }) => { const [isOpen, setIsOpen] = useState(false) const intl = useIntl() @@ -130,11 +146,28 @@ const ButtonDropdown = ({ list, className }) => { variants={listVariants} initial="closed" > - {list.items.map((item, idx) => ( + {list.items.map(({ text, to, matomo, callback }, idx) => ( setIsOpen(false)}> - - - + {!!to && !!matomo && ( + { + trackCustomEvent(matomo) + }} + to={to} + tabIndex="-1" + > + + + )} + {!!to && !matomo && ( + + + + )} + {!!callback && ( + callback(idx)}>{text} + )} ))} diff --git a/src/components/CalloutBanner.js b/src/components/CalloutBanner.js index 605a43631cc..0e305480be2 100644 --- a/src/components/CalloutBanner.js +++ b/src/components/CalloutBanner.js @@ -7,12 +7,7 @@ import Translation from "./Translation" const StyledCard = styled.div` display: flex; flex-direction: row-reverse; - background: linear-gradient( - 49.21deg, - rgba(127, 127, 213, 0.2) 19.87%, - rgba(134, 168, 231, 0.2) 58.46%, - rgba(145, 234, 228, 0.2) 97.05% - ); + background: ${({ theme }) => theme.colors.layer2Gradient}; padding: 3rem; margin: 1rem; margin-top: 6rem; @@ -30,7 +25,7 @@ const StyledCard = styled.div` ` const Content = styled.div` - padding-left: 5rem; + padding-left: 2rem; flex: 1 0 50%; display: flex; flex-direction: column; @@ -78,8 +73,9 @@ const CalloutBanner = ({ alt, children, className, + id, }) => ( - + {alt}

diff --git a/src/components/Card.js b/src/components/Card.js index 5d9ce270513..b43698cc8c0 100644 --- a/src/components/Card.js +++ b/src/components/Card.js @@ -5,7 +5,6 @@ import Emoji from "./Emoji" const StyledCard = styled.div` display: flex; flex-direction: column; - justify-content: space-between; background: ${(props) => props.theme.colors.ednBackground}; border-radius: 2px; border: 1px solid ${(props) => props.theme.colors.lightBorder}; @@ -21,7 +20,7 @@ const TopContent = styled.div`` const Card = ({ emoji, title, description, children, className }) => ( - {emoji && } + {emoji && }

{title}

{description}
diff --git a/src/components/CardList.js b/src/components/CardList.js index 9f00ebecb7e..a50f187c943 100644 --- a/src/components/CardList.js +++ b/src/components/CardList.js @@ -6,9 +6,7 @@ import Link from "./Link" const Table = styled.div` background-color: ${(props) => props.theme.colors.background}; - box-shadow: ${(props) => props.theme.colors.tableBoxShadow}; width: 100%; - margin-bottom: 2rem; ` const Item = styled.div` @@ -17,11 +15,11 @@ const Item = styled.div` display: flex; justify-content: space-between; color: ${(props) => props.theme.colors.text} !important; - box-shadow: 0 1px 1px ${(props) => props.theme.colors.tableItemBoxShadow}; - margin-bottom: 1px; + border: 1px solid ${(props) => props.theme.colors.border}; padding: 1rem; width: 100%; color: #000000; + margin-bottom: 1rem; &:hover { border-radius: 4px; box-shadow: 0 0 1px ${(props) => props.theme.colors.primary}; @@ -34,11 +32,11 @@ const ItemLink = styled(Link)` display: flex; justify-content: space-between; color: ${(props) => props.theme.colors.text} !important; - box-shadow: 0 1px 1px ${(props) => props.theme.colors.tableItemBoxShadow}; - margin-bottom: 1px; + border: 1px solid ${(props) => props.theme.colors.border}; padding: 1rem; width: 100%; color: #000000; + margin-bottom: 1rem; &:hover { border-radius: 4px; box-shadow: 0 0 1px ${(props) => props.theme.colors.primary}; diff --git a/src/components/EthExchanges.js b/src/components/EthExchanges.js index b3baaa94798..6565172b598 100644 --- a/src/components/EthExchanges.js +++ b/src/components/EthExchanges.js @@ -2,7 +2,6 @@ import React, { useState } from "react" import { useStaticQuery, graphql } from "gatsby" import { getImage } from "gatsby-plugin-image" import { useIntl } from "gatsby-plugin-intl" -import Select from "react-select" import styled from "styled-components" import CardList from "./CardList" @@ -11,6 +10,7 @@ import { getLocaleTimestamp } from "../utils/time" import { trackCustomEvent } from "../utils/matomo" import Emoji from "./Emoji" import Translation from "./Translation" +import { StyledSelect as Select } from "./SharedStyledComponents" import { translateMessageId } from "../utils/translations" const Container = styled.div` @@ -20,62 +20,6 @@ const Container = styled.div` align-items: center; ` -// https://react-select.com/styles#using-classnames -// Pass menuIsOpen={true} to component to debug -const StyledSelect = styled(Select)` - width: 100%; - max-width: 640px; - color: black; - /* Component */ - .react-select__control { - border: 1px solid ${(props) => props.theme.colors.searchBorder}; - background: ${(props) => props.theme.colors.searchBackground}; - /* Dropdown arrow */ - .react-select__indicator { - color: ${(props) => props.theme.colors.searchBorder}; - } - &.react-select__control--is-focused { - border-color: ${(props) => props.theme.colors.primary} !important; - box-shadow: 0 0 0 1px ${(props) => props.theme.colors.primary} !important; - .react-select__value-container { - border-color: ${(props) => props.theme.colors.primary} !important; - } - } - } - .react-select__placeholder { - color: ${(props) => props.theme.colors.text200}; - } - .react-select__single-value { - color: ${(props) => props.theme.colors.text}; - } - .react-select__menu { - background: ${(props) => props.theme.colors.searchBackground}; - color: ${(props) => props.theme.colors.text}; - } - .react-select__input { - color: ${(props) => props.theme.colors.text}; - } - .react-select__option { - &:hover { - background-color: ${(props) => props.theme.colors.selectHover}; - } - &:active { - background-color: ${(props) => props.theme.colors.selectActive}; - color: ${(props) => props.theme.colors.buttonColor} !important; - } - } - .react-select__option--is-focused { - background-color: ${(props) => props.theme.colors.selectHover}; - } - .react-select__option--is-selected { - background-color: ${(props) => props.theme.colors.primary}; - color: ${(props) => props.theme.colors.buttonColor}; - &:hover { - background-color: ${(props) => props.theme.colors.primary}; - } - } -` - const ListContainer = styled.div` margin-top: 4rem; flex: 1 1 50%; @@ -152,6 +96,10 @@ const Disclaimer = styled.p` margin-bottom: 0; ` +const StyledSelect = styled(Select)` + max-width: 640px; +` + const NoResults = ({ children }) => ( diff --git a/src/components/ExpandableCard.js b/src/components/ExpandableCard.js index 1b613e16f76..90eefef92ce 100644 --- a/src/components/ExpandableCard.js +++ b/src/components/ExpandableCard.js @@ -1,9 +1,14 @@ +// Libraries +import React, { useState } from "react" import styled from "styled-components" import { motion } from "framer-motion" + +// Components import { FakeLink } from "./SharedStyledComponents" import Translation from "../components/Translation" -import React, { useState } from "react" +// Utils +import { trackCustomEvent } from "../utils/matomo" const Card = styled.div` border: 1px solid ${(props) => props.theme.colors.border}; @@ -84,9 +89,16 @@ const ButtonLink = styled.button` color: ${(props) => props.theme.colors.primary}; ` -const ExpandableCard = ({ children, contentPreview, title, svg, alt }) => { +const ExpandableCard = ({ + children, + contentPreview, + title, + Svg, + alt, + eventCategory, + eventName, +}) => { const [isVisible, setIsVisible] = useState(false) - const Svg = svg const expandCollapse = { collapsed: { @@ -126,7 +138,11 @@ const ExpandableCard = ({ children, contentPreview, title, svg, alt }) => { }, }, } - + const matomo = { + eventAction: `Clicked`, + eventCategory: `ExpandableCard${eventCategory}`, + eventName, + } return ( @@ -137,7 +153,12 @@ const ExpandableCard = ({ children, contentPreview, title, svg, alt }) => { {contentPreview} - + { + trackCustomEvent(matomo) + setIsVisible(!isVisible) + }} + > {!isVisible && ( setIsVisible(true)}> diff --git a/src/components/FeedbackCard.js b/src/components/FeedbackCard.js index 75b69091131..655cdf6e48d 100644 --- a/src/components/FeedbackCard.js +++ b/src/components/FeedbackCard.js @@ -3,6 +3,7 @@ import styled from "styled-components" import { ButtonSecondary } from "./SharedStyledComponents" import { trackCustomEvent } from "../utils/matomo" import Translation from "./Translation" +import Link from "./Link" const Card = styled.div` border: 1px solid ${({ theme }) => theme.colors.border}; @@ -41,12 +42,35 @@ const ButtonContainer = styled.div` const FeedbackCard = ({ prompt, className }) => { const [feedbackSubmitted, setFeedbackSubmitted] = useState(false) const [isHelpful, setIsHelpful] = useState(false) - const feedbackPrompt = prompt || - const title = isHelpful ? ( - - ) : ( - - ) + const location = typeof window !== "undefined" ? window.location.href : "" + const isStaking = location.includes("staking") + + const getTitle = (feedbackSubmitted, isStaking, isHelpful) => { + if (!feedbackSubmitted) + return prompt || + if (isStaking) + return isHelpful ? ( + <> +

Thanks for the feedback! Want to add more input?

+ + Check out our current staking survey! + + + ) : ( + <> +

How can we do better?

+ + Check out our current staking survey! + + + ) + + return isHelpful ? ( + + ) : ( + + ) + } const handleClick = (isHelpful) => { trackCustomEvent({ @@ -60,7 +84,7 @@ const FeedbackCard = ({ prompt, className }) => { return ( - {feedbackSubmitted ? title : feedbackPrompt} + {getTitle(feedbackSubmitted, isStaking, isHelpful)} {!feedbackSubmitted && ( handleClick(true)}> diff --git a/src/components/Layer2/Layer2Onboard.js b/src/components/Layer2/Layer2Onboard.js index 37518ad1ac2..e7c28b209d2 100644 --- a/src/components/Layer2/Layer2Onboard.js +++ b/src/components/Layer2/Layer2Onboard.js @@ -1,12 +1,12 @@ // Libraries import { GatsbyImage } from "gatsby-plugin-image" import React, { useState } from "react" -import Select from "react-select" import styled from "styled-components" // Components import ButtonLink from "../../components/ButtonLink" import Link from "../../components/Link" +import { StyledSelect as Select } from "../SharedStyledComponents" // Data import cexSupport from "../../data/layer-2/cex-layer-2-support.json" @@ -53,56 +53,7 @@ const TwoColumnContent = styled.div` // https://react-select.com/styles#using-classnames // Pass menuIsOpen={true} to component to debug const StyledSelect = styled(Select)` - width: 100%; - color: black; - /* Component */ - .react-select__control { - border: 1px solid ${(props) => props.theme.colors.searchBorder}; - background: ${(props) => props.theme.colors.searchBackground}; - /* Dropdown arrow */ - .react-select__indicator { - color: ${(props) => props.theme.colors.searchBorder}; - } - &.react-select__control--is-focused { - border-color: ${(props) => props.theme.colors.primary} !important; - box-shadow: 0 0 0 1px ${(props) => props.theme.colors.primary} !important; - .react-select__value-container { - border-color: ${(props) => props.theme.colors.primary} !important; - } - } - } - .react-select__placeholder { - color: ${(props) => props.theme.colors.text200}; - } - .react-select__single-value { - color: ${(props) => props.theme.colors.text}; - } - .react-select__menu { - background: ${(props) => props.theme.colors.searchBackground}; - color: ${(props) => props.theme.colors.text}; - } - .react-select__input { - color: ${(props) => props.theme.colors.text}; - } - .react-select__option { - &:hover { - background-color: ${(props) => props.theme.colors.selectHover}; - } - &:active { - background-color: ${(props) => props.theme.colors.selectActive}; - color: ${(props) => props.theme.colors.buttonColor} !important; - } - } - .react-select__option--is-focused { - background-color: ${(props) => props.theme.colors.selectHover}; - } - .react-select__option--is-selected { - background-color: ${(props) => props.theme.colors.primary}; - color: ${(props) => props.theme.colors.buttonColor}; - &:hover { - background-color: ${(props) => props.theme.colors.primary}; - } - } + max-width: none; @media (max-width: ${(props) => props.theme.breakpoints.s}) { .react-select__control { diff --git a/src/components/Link.js b/src/components/Link.js index 8ec69618721..51e76207909 100644 --- a/src/components/Link.js +++ b/src/components/Link.js @@ -70,6 +70,8 @@ const Link = ({ className, isPartiallyActive = true, ariaLabel, + customEventOptions, + onClick = () => {}, }) => { // markdown pages pass `href`, not `to` to = to || href @@ -120,7 +122,11 @@ const Link = ({ href={to} target="_blank" rel="noopener noreferrer" - onClick={() => trackCustomEvent(eventOptions)} + onClick={() => + trackCustomEvent( + customEventOptions ? customEventOptions : eventOptions + ) + } aria-label={ariaLabel} > {children} @@ -131,7 +137,11 @@ const Link = ({ href={to} target="_blank" rel="noopener noreferrer" - onClick={() => trackCustomEvent(eventOptions)} + onClick={() => + trackCustomEvent( + customEventOptions ? customEventOptions : eventOptions + ) + } aria-label={ariaLabel} > {children} @@ -148,6 +158,7 @@ const Link = ({ to={to} activeClassName="active" partiallyActive={isPartiallyActive} + onClick={onClick} > {children} @@ -175,6 +186,7 @@ const Link = ({ to={to} activeClassName="active" partiallyActive={isPartiallyActive} + onClick={onClick} > {children} {isGlossary && ( diff --git a/src/components/PageHero.js b/src/components/PageHero.js index b0700f6874f..e927aede36c 100644 --- a/src/components/PageHero.js +++ b/src/components/PageHero.js @@ -51,7 +51,7 @@ const Header = styled.h2` margin-bottom: 0rem; color: ${(props) => props.theme.colors.text00}; @media (max-width: ${(props) => props.theme.breakpoints.l}) { - font-size: 2.5rem + font-size: 2.5rem; } ` diff --git a/src/components/ProductCard.js b/src/components/ProductCard.js index 4c93408e452..dd30f356775 100644 --- a/src/components/ProductCard.js +++ b/src/components/ProductCard.js @@ -80,13 +80,13 @@ const SubjectPill = styled.div` case "Vyper": return theme.colors.tagBlue case "web3": - return theme.colors.tagTurqouise + return theme.colors.tagTurquoise case "JavaScript": return theme.colors.tagRed case "TypeScript": return theme.colors.tagBlue case "Go": - return theme.colors.tagTurqouise + return theme.colors.tagTurquoise case "Python": return theme.colors.tagMint case "Rust": diff --git a/src/components/SharedStyledComponents.js b/src/components/SharedStyledComponents.js index d3c07a4a5d4..b9fe56aea07 100644 --- a/src/components/SharedStyledComponents.js +++ b/src/components/SharedStyledComponents.js @@ -1,5 +1,6 @@ import styled from "styled-components" import { margin } from "styled-system" +import Select from "react-select" import Card from "./Card" import Link from "./Link" @@ -164,6 +165,24 @@ export const CardGrid = styled.div` gap: 2rem; ` +export const InfoGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); + gap: 2rem; + & > div { + height: fit-content; + margin: 0; + &:hover { + transition: 0.1s; + transform: scale(1.01); + svg { + transition: 0.1s; + transform: scale(1.1); + } + } + } +` + export const StyledCard = styled(Card)` margin: 1rem; padding: 1.5rem; @@ -401,3 +420,58 @@ export const OptionText = styled.div` font-weight: 600; } ` + +// https://react-select.com/styles#using-classnames +// Pass menuIsOpen={true} to component to debug +export const StyledSelect = styled(Select)` + width: 100%; + color: black; + /* Component */ + .react-select__control { + border: 1px solid ${({ theme }) => theme.colors.searchBorder}; + background: ${({ theme }) => theme.colors.searchBackground}; + /* Dropdown arrow */ + .react-select__indicator { + color: ${({ theme }) => theme.colors.searchBorder}; + } + &.react-select__control--is-focused { + border-color: ${({ theme }) => theme.colors.primary} !important; + box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.primary} !important; + .react-select__value-container { + border-color: ${({ theme }) => theme.colors.primary} !important; + } + } + } + .react-select__placeholder { + color: ${({ theme }) => theme.colors.text200}; + } + .react-select__single-value { + color: ${({ theme }) => theme.colors.text}; + } + .react-select__menu { + background: ${({ theme }) => theme.colors.searchBackground}; + color: ${({ theme }) => theme.colors.text}; + } + .react-select__input { + color: ${({ theme }) => theme.colors.text}; + } + .react-select__option { + &:hover { + background-color: ${({ theme }) => theme.colors.selectHover}; + } + &:active { + background-color: ${({ theme }) => theme.colors.selectActive}; + color: ${({ theme }) => theme.colors.buttonColor} !important; + } + } + .react-select__option--is-focused { + background-color: ${({ theme }) => theme.colors.selectHover}; + } + .react-select__option--is-selected { + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.buttonColor}; + &:hover { + background-color: ${({ theme }) => theme.colors.primary}; + } + } +` diff --git a/src/components/Staking/StakingCommunityCallout.js b/src/components/Staking/StakingCommunityCallout.js new file mode 100644 index 00000000000..ae9d8cc4305 --- /dev/null +++ b/src/components/Staking/StakingCommunityCallout.js @@ -0,0 +1,98 @@ +import React from "react" +import { useIntl } from "gatsby-plugin-intl" +import styled from "styled-components" +import { graphql, useStaticQuery } from "gatsby" +import { getImage } from "gatsby-plugin-image" + +import ButtonLink from "../ButtonLink" +import CalloutBanner from "../CalloutBanner" + +import { translateMessageId } from "../../utils/translations" +import { trackCustomEvent } from "../../utils/matomo" + +const StyledCallout = styled(CalloutBanner)` + margin: 4rem 0; +` + +const ButtonContaier = styled.div` + display: flex; + gap: 1rem; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + flex-direction: column; + } +` + +const StyledButtonLink = styled(ButtonLink)` + @media (max-width: ${({ theme }) => theme.breakpoints.s}) { + width: 100%; + } +` + +const StakingCommunityCallout = (props) => { + const intl = useIntl() + const { image } = useStaticQuery(graphql` + { + image: file(relativePath: { eq: "enterprise-eth.png" }) { + childImageSharp { + gatsbyImageData( + width: 500 + layout: CONSTRAINED + placeholder: BLURRED + quality: 100 + ) + } + } + } + `) + + return ( + + + { + trackCustomEvent({ + eventCategory: `StakingCommunityCallout`, + eventAction: `Clicked`, + eventName: "clicked discord", + }) + }} + to="https://discord.io/ethstaker" + > + Discord + + { + trackCustomEvent({ + eventCategory: `StakingCommunityCallout`, + eventAction: `Clicked`, + eventName: "clicked reddit", + }) + }} + to="https://reddit.com/r/ethstaker" + > + Reddit + + { + trackCustomEvent({ + eventCategory: `StakingCommunityCallout`, + eventAction: `Clicked`, + eventName: "clicked website", + }) + }} + to="https://ethstaker.cc" + > + Website + + + + ) +} + +export default StakingCommunityCallout diff --git a/src/components/Staking/StakingComparison.js b/src/components/Staking/StakingComparison.js new file mode 100644 index 00000000000..e317098867f --- /dev/null +++ b/src/components/Staking/StakingComparison.js @@ -0,0 +1,176 @@ +import React, { useContext } from "react" +import styled, { ThemeContext } from "styled-components" +import { useIntl } from "gatsby-plugin-intl" + +import Link from "../Link" +import Translation from "../Translation" + +import { translateMessageId } from "../../utils/translations" +import { trackCustomEvent } from "../../utils/matomo" + +import SoloGlyph from "../../assets/staking/staking-glyph-cpu.svg" +import SaasGlyph from "../../assets/staking/staking-glyph-cloud.svg" +import PoolGlyph from "../../assets/staking/staking-glyph-token-wallet.svg" + +const GradientContainer = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; + background: linear-gradient( + 83.46deg, + rgba(127, 127, 213, 0.2) 7.03%, + rgba(138, 168, 231, 0.2) 52.42%, + rgba(145, 234, 228, 0.2) 98.77% + ); + padding: 2rem; + margin-top: 4rem; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + padding: 2rem 1.5rem; + } + + h3 { + margin: 0 0 0.5rem; + } +` + +const Flex = styled.div` + display: flex; + gap: 1.5rem; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + flex-direction: column; + } +` + +const Glyph = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + width: 3rem; + max-height: 3rem; +` + +const StyledSoloGlyph = styled(SoloGlyph)` + path { + fill: ${({ theme }) => theme.colors.stakingGold}; + } +` +const StyledSaasGlyph = styled(SaasGlyph)` + path { + fill: ${({ theme }) => theme.colors.stakingGreen}; + } +` +const StyledPoolGlyph = styled(PoolGlyph)` + path { + fill: ${({ theme }) => theme.colors.stakingBlue}; + } +` + +const StakingComparison = ({ page, className }) => { + const intl = useIntl() + const themeContext = useContext(ThemeContext) + const { stakingGold, stakingGreen, stakingBlue } = themeContext.colors + + const solo = { + title: "Solo staking", + linkText: "Learn more about solo staking", + to: "/staking/solo/", + matomo: { + eventCategory: `StakingComparison`, + eventAction: `Clicked`, + eventName: "clicked solo staking", + }, + color: stakingGold, + glyph: , + } + const saas = { + title: "Staking as a service (SaaS)", + linkText: "Learn more about staking as a service", + to: "/staking/saas/", + matomo: { + eventCategory: `StakingComparison`, + eventAction: `Clicked`, + eventName: "clicked staking as a service", + }, + color: stakingGreen, + glyph: , + } + const pools = { + title: "Pooled staking", + linkText: "Learn more about pooled staking", + to: "/staking/pools/", + matomo: { + eventCategory: `StakingComparison`, + eventAction: `Clicked`, + eventName: "clicked pooled staking", + }, + color: stakingBlue, + glyph: , + } + const data = { + solo: [ + { + ...saas, + content: + "With SaaS providers you're still required to deposit 32 ETH, but don't have to run hardware. You typically maintain access to your validator keys, but also need to share your signing keys so the operator can act on behalf of your validator. This introduces a layer of trust not present when running your own hardware, and unlike solo staking at home, SaaS does not help as much with geographic distribution of nodes. If you're uncomfortable operating hardware but still looking to stake 32 ETH, using a SaaS provider may be a good option for you.", + }, + { + ...pools, + content: + "Solo staking is significantly more involved than staking with a pooling service, but offer full access to ETH rewards, and full control over the setup and security of your validator. Pooled staking has a significantly lower barrier to entry. Users can stake small amounts of ETH, are not required to generate validator keys, and have no hardware requirements beyond a standard internet connection. Liquidity tokens enable the ability to exit from staking before this is enabled at the protocol level. If you're interested in these features, pooled staking may be a good fit.", + }, + ], + saas: [ + { + ...solo, + content: + "Similarities include having your own validator keys without having to pool funds, but with SaaS you must trust a third-party, who may potentially act maliciously or become a target of attack or regulation themselves. If these trust assumptions or centralization risks concern you, the gold standard of self-sovereign staking is solo staking.", + }, + { + ...pools, + content: + "These are similar in that you're generally relying on someone else to run the validator client, but unlike SaaS, pooled staking allows you to participate with smaller amounts of ETH. If you're looking to stake with less than 32 ETH, consider checking these out.", + }, + ], + pools: [ + { + ...solo, + content: + "Pooled staking has a significantly lower barrier to entry when compared to solo staking, but comes with additional risk by delegating all node operations to a third-party, and with a fee. Solo staking gives full sovereignty and control over the choices that go into choosing a staking setup. Stakers never have to hand over their keys, and they earn full rewards without any middlemen taking a cut.", + }, + { + ...saas, + content: + "These are similar in that stakers do not run the validator software themselves, but unlike pooling options, SaaS requires a full 32 ETH deposit to activate a validator. Rewards accumulate to the staker, and usually involve a monthly fee or other stake to use the service. If you'd prefer your own validator keys and are looking to stake at least 32 ETH, using a SaaS provider may be a good option for you.", + }, + ], + } + const selectedData = data[page] + + return ( + +

Comparison with other options

+ {selectedData.map( + ({ title, linkText, to, color, content, glyph, matomo }, idx) => ( + + {!!glyph && {glyph}} +
+

{title}

+

{content}

+ { + trackCustomEvent(matomo) + }} + to={to} + > + {linkText} + +
+
+ ) + )} +
+ ) +} + +export default StakingComparison diff --git a/src/components/Staking/StakingConsiderations.js b/src/components/Staking/StakingConsiderations.js new file mode 100644 index 00000000000..ed6eef3a1f0 --- /dev/null +++ b/src/components/Staking/StakingConsiderations.js @@ -0,0 +1,547 @@ +import React, { useState } from "react" +import styled from "styled-components" +import { useIntl } from "gatsby-plugin-intl" +// SVG imports +import GreenCheck from "../../assets/staking/green-check-product-glyph.svg" +import Caution from "../../assets/staking/caution-product-glyph.svg" +import Warning from "../../assets/staking/warning-product-glyph.svg" +import OpenSource from "../../assets/staking/open-source.svg" +import Audited from "../../assets/staking/audited.svg" +import BugBounty from "../../assets/staking/bug-bounty.svg" +import BattleTested from "../../assets/staking/battle-tested.svg" +import Trustless from "../../assets/staking/trustless.svg" +import Permissionless from "../../assets/staking/permissionless.svg" +import MultiClient from "../../assets/staking/multi-client.svg" +import SelfCustody from "../../assets/staking/self-custody.svg" +import Economical from "../../assets/staking/economical.svg" +import LiquidityToken from "../../assets/staking/liquidity-token.svg" +// Component imports +import ButtonDropdown from "../ButtonDropdown" +import { trackCustomEvent } from "../../utils/matomo" + +const Container = styled.div` + display: flex; + gap: 2rem; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + flex-direction: column; + } +` + +const List = styled.div` + flex: 1; + ul { + list-style-type: none; + padding: 0; + margin: 0; + } + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + display: none; + } +` + +// TODO: Make mobile responsive + +const ListItem = styled.li` + padding: 0.125rem 0.5rem; + cursor: pointer; + box-sizing: border-box; + position: relative; + height: 2rem; + ${({ theme, active }) => + active + ? ` + background: ${theme.colors.primary}; + color: ${theme.colors.background}; + &::after{ + content:""; + position:absolute; + height:0; + width:0; + left:100%; + top:0; + border: 1rem solid transparent; + border-left: 1rem solid ${theme.colors.primary}; + }` + : ` + color: ${theme.colors.primary}; + `}; +` + +const StyledButtonDropdown = styled(ButtonDropdown)` + display: none; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + display: inline-block; + } +` + +const Content = styled.div` + flex: 2; + display: flex; + flex-direction: column; + align-items: center; + min-height: 410px; + background: ${({ theme }) => theme.colors.offBackground}; + padding: 1.5rem; + h3 { + font-weight: 700; + font-size: 27px; + } +` + +const IndicatorRow = styled.div` + display: flex; + gap: 2rem; + justify-content: center; + margin-top: auto; +` + +const Indicator = styled.div` + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + gap: 0.5rem; + width: max-content; + @media (max-width: ${({ theme }) => theme.breakpoints.s}) { + width: fit-content; + } + p { + font-size: 0.75rem; + padding: 0; + text-align: center; + width: max-content; + @media (max-width: ${({ theme }) => theme.breakpoints.s}) { + width: fit-content; + } + } +` + +const data = { + solo: [ + { + title: "Open source", + description: + "Essential code is 100% open source and available to the public to fork and use", + valid: "Open source", + caution: "", + warning: "Closed source", + Svg: OpenSource, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo open source", + }, + }, + { + title: "Audited", + description: + "Essential code has undergone formal auditing with results published and available publicly", + valid: "Audited", + caution: "", + warning: "None", + Svg: Audited, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo audited", + }, + }, + { + title: "Bug bounty", + description: + "A public bug bounty has been performed on any essential code to rewards users for safely reporting and/or fixing vulnerabilities", + valid: "Currently active", + caution: "Completed", + warning: "None", + Svg: BugBounty, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo bug bounty", + }, + }, + { + title: "Battle tested", + description: + "Software has been available and used by the public for the indicated period of time", + valid: "Live > 1 year", + caution: "Live > 6 months", + warning: "Newly released", + Svg: BattleTested, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo battle tested", + }, + }, + { + title: "Trustless", + description: + "Validator keys are not entrusted to any other human at any time in the validator lifecycle. Any smart contracts involved are free of back doors, without reliance on privileged permissions for execution.", + valid: "Trustless", + caution: "", + warning: "Trusted", + Svg: Trustless, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo trustless", + }, + }, + { + title: "Permissionless", + description: + "Users do not require any special permission to operate a validator using the software or service", + valid: "No permission", + caution: "", + warning: "Permission required", + Svg: Permissionless, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo permissionless", + }, + }, + { + title: "Multi-client", + description: + "Software enables users to pick from and switch between at least two or more consensus layer clients", + valid: "Easy client switching", + caution: "", + warning: "Limited to majority client", + Svg: MultiClient, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo multi-client", + }, + }, + { + title: "Self custody", + description: + "User maintains custody of any validator credentials, including signing and withdrawal keys", + valid: "Self custody", + caution: "", + warning: "Third-party custodian", + Svg: SelfCustody, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo self custody", + }, + }, + { + title: "Economical", + description: + "Users can operate a validator by staking less than 32 ETH, utilizing pooled funds from others", + valid: "< 32 ETH", + caution: "", + warning: "32 ETH", + Svg: Economical, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked solo economical", + }, + }, + ], + saas: [ + { + title: "Open source", + description: + "Essential code is 100% open source and available to the public to fork and use", + valid: "Open source", + caution: "", + warning: "Closed source", + Svg: OpenSource, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked saas open source", + }, + }, + { + title: "Audited", + description: + "Essential code has undergone formal auditing with results published and available publicly", + valid: "Audited", + caution: "", + warning: "None", + Svg: Audited, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked saas audited", + }, + }, + { + title: "Bug bounty", + description: + "A public bug bounty has been performed on any essential code to rewards users for safely reporting and/or fixing vulnerabilities", + valid: "Currently active", + caution: "Completed", + warning: "None", + Svg: BugBounty, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked saas bug-bounty", + }, + }, + { + title: "Battle tested", + description: + "Service has been available and used by the public for the indicated period of time", + valid: "Live > 1 year", + caution: "Live > 6 months", + warning: "Newly released", + Svg: BattleTested, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked saas battle tested", + }, + }, + { + title: "Permissionless", + description: + "Users do not require any special permission, account sign up or KYC to participate with the service", + valid: "Anyone can join", + caution: "", + warning: "Permission required", + Svg: Permissionless, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked saas permissionless", + }, + }, + { + title: "Diverse clients", + description: + "Service should not run more than 50% of their aggregate validators with a majority validator client", + valid: "Less than 50%", + caution: "Currently unknown", + warning: "More than 50%", + Svg: MultiClient, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked saas diverse clients", + }, + }, + { + title: "Self custody", + description: + "User maintains custody of any validator credentials, including signing and withdrawal keys", + valid: "Self custody", + caution: "", + warning: "Third-party custodian", + Svg: SelfCustody, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked saas self custody", + }, + }, + ], + pools: [ + { + title: "Open source", + description: + "Essential code is 100% open source and available to the public to fork and use", + valid: "Open source", + caution: "", + warning: "Closed source", + Svg: OpenSource, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled open source", + }, + }, + { + title: "Audited", + description: + "Essential code has undergone formal auditing with results published and available publicly", + valid: "Audited", + caution: "", + warning: "None", + Svg: Audited, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled audited", + }, + }, + { + title: "Bug bounty", + description: + "A public bug bounty has been performed on any essential code to rewards users for safely reporting and/or fixing vulnerabilities", + valid: "Currently active", + caution: "Completed", + warning: "None", + Svg: BugBounty, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled bug bounty", + }, + }, + { + title: "Battle tested", + description: + "Service has been available and used by the public for the indicated period of time", + valid: "Live > 1 year", + caution: "Live > 6 months", + warning: "Newly released", + Svg: BattleTested, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled battle tested", + }, + }, + { + title: "Trustless", + description: + "Service does not require trusting any humans to custody your keys or distribute rewards", + valid: "Trustless", + caution: "", + warning: "Trusted", + Svg: Trustless, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled trustless", + }, + }, + { + title: "Permissionless nodes", + description: + "Service allows anyone to join as a node operator for the pool, without permission", + valid: "Anyone can join", + caution: "", + warning: "Permission required", + Svg: Permissionless, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled permissionless nodes", + }, + }, + { + title: "Diverse clients", + description: + "Service should not run more than 50% of their aggregate validators with a supermajority validator client", + valid: "Less than 50%", + caution: "Currently unknown", + warning: "More than 50%", + Svg: MultiClient, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled diverse clients", + }, + }, + { + title: "Liquidity token", + description: + "Offers tradable liquidity token representing your staked ETH, held in your own wallet", + valid: "Liquidity token(s)", + caution: "", + warning: "No liquidity token", + Svg: LiquidityToken, + matomo: { + eventCategory: `StakingConsiderations`, + eventAction: `Clicked`, + eventName: "clicked pooled liquidity token", + }, + }, + ], +} + +const StakingConsiderations = ({ page }) => { + const [activeIndex, setActiveIndex] = useState(0) + const intl = useIntl() + + const pageData = data[page] + const { title, description, valid, caution, warning, Svg } = + pageData[activeIndex] + + const dropdownLinks = { + text: "Staking Considerations", + ariaLabel: "Dropdown menu for staking considerations", + items: pageData.map(({ title }) => ({ + text: title, + callback: setActiveIndex, + })), + } + + const handleSelection = (idx) => { + setActiveIndex(idx) + } + + const selectionSvgStyle = { width: 72, height: "auto" } + const indicatorSvgStyle = { width: 20, height: "auto" } + const StyledSvg = !!Svg + ? styled(Svg)` + path { + fill: ${({ theme }) => theme.colors.text}; + } + ` + : styled.div` + display: none; + ` + + return ( + + + + {!!pageData && ( +
    + {pageData.map(({ title, matomo }, idx) => ( + { + handleSelection(idx) + trackCustomEvent(matomo) + }} + active={idx === activeIndex} + > + {title} + + ))} +
+ )} +
+ + +

{title}

+

{description}

+ + {!!valid && ( + + +

{valid}

+
+ )} + {!!caution && ( + + +

{caution}

+
+ )} + {!!warning && ( + + +

{warning}

+
+ )} +
+
+
+ ) +} + +export default StakingConsiderations diff --git a/src/components/Staking/StakingGuides.js b/src/components/Staking/StakingGuides.js new file mode 100644 index 00000000000..9b08b0678d8 --- /dev/null +++ b/src/components/Staking/StakingGuides.js @@ -0,0 +1,29 @@ +// Libraries +import React from "react" + +// Components +import CardList from "../CardList" + +const StakingGuides = () => { + const guides = [ + { + title: "CoinCashew's Ethereum 2.0 Guide", + link: "https://www.coincashew.com/coins/overview-eth/guide-or-how-to-setup-a-validator-on-eth2-mainnet", + description: "Linux (CLI)", + }, + { + title: "Somer Esat", + link: "https://github.com/SomerEsat/ethereum-staking-guide", + description: "Linux (CLI)", + }, + { + title: "Rocket Pool Node Operators", + link: "https://rocketpool.net/node-operators", + description: "Linux, macOS (CLI)", + }, + ] + + return +} + +export default StakingGuides diff --git a/src/components/Staking/StakingHierarchy.js b/src/components/Staking/StakingHierarchy.js new file mode 100644 index 00000000000..a392a0e9c99 --- /dev/null +++ b/src/components/Staking/StakingHierarchy.js @@ -0,0 +1,441 @@ +// Libraries +import React from "react" +import styled from "styled-components" + +// Components +import ButtonLink from "../ButtonLink" +import Link from "../Link" + +// Assets +import EtherSvg from "../../assets/staking/staking-glyph-ether-circle.svg" +import SoloGlyph from "../../assets/staking/staking-glyph-cpu.svg" +import SaasGlyph from "../../assets/staking/staking-glyph-cloud.svg" +import PoolGlyph from "../../assets/staking/staking-glyph-token-wallet.svg" +import CexGlyph from "../../assets/staking/staking-glyph-centralized.svg" + +// Utils +import { trackCustomEvent } from "../../utils/matomo" + +const Container = styled.div` + border-radius: 0.5rem; + background: linear-gradient( + 180deg, + rgba(237, 194, 84, 0.1) 13.39%, + rgba(75, 231, 156, 0.1) 44.21%, + rgba(231, 202, 200, 0.1) 82.88% + ); + padding: 2rem; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + display: flex; + flex-direction: column; + --gold: ${({ theme }) => theme.colors.stakingGold}; + --green: ${({ theme }) => theme.colors.stakingGreen}; + --blue: ${({ theme }) => theme.colors.stakingBlue}; + --red: ${({ theme }) => theme.colors.stakingRed}; + border-image: linear-gradient( + to bottom, + var(--gold) 5%, + var(--green) 30%, + var(--blue) 55%, + var(--red) 80% + ) + 1 100%; + border-left: solid 4px; + border-right: 0; + border-radius: 0; + gap: 4rem; + a { + width: 100%; + } + } +` + +const Section = styled.div` + --color: ${({ number, theme }) => { + switch (number) { + case "1": + return theme.colors.stakingGold + case "2": + return theme.colors.stakingGreen + case "3": + return theme.colors.stakingBlue + case "4": + return theme.colors.stakingRed + default: + return "#000000" + } + }}; + --next-color: ${({ number, theme }) => { + switch (number) { + case "1": + return theme.colors.stakingGreen + case "2": + return theme.colors.stakingBlue + case "3": + return theme.colors.stakingRed + case "4": + return "#00000000" + default: + return "#000000" + } + }}; + --fill-color: ${({ number, theme }) => { + switch (number) { + case "1": + return theme.colors.stakingGoldFill + case "2": + return theme.colors.stakingGreenFill + case "3": + return theme.colors.stakingBlueFill + case "4": + return theme.colors.stakingRedFill + default: + return "#000000" + } + }}; + display: grid; + position: relative; + gap: 0 2rem; + grid-template-columns: 5rem 1fr 5rem; + grid-template-areas: + "ether header glyph" + "decorator content content"; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + gap: 1rem; + grid-template-columns: 1fr; + grid-template-areas: + "ether" + "header" + "content"; + svg { + height: 5rem; + aspect-ratio: 1; + } + } + h2 { + color: var(--color); + } + + path { + fill: var(--color); + } + + #transparentBackground { + fill: var(--fill-color); + } + + .subtext { + p { + color: var(--color); + margin: 0; + position: relative; + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--color); + opacity: 0.125; + border-radius: 0.125rem; + } + } + } + + aside::after { + border-image: linear-gradient(to bottom, var(--color), var(--next-color)) 1 + 100%; + --scale: ${({ number }) => 1.05 + number / 70}; + --translate: ${({ number }) => number}px; + transform: scale(var(--scale)) translateY(var(--translate)); + } +` + +const Header = styled.div` + grid-area: header; + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.5rem; + h2 { + margin: 0; + } + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + align-items: center; + h2 { + text-align: center; + } + } +` + +const Pills = styled.div` + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + p { + padding: 0.125rem 0.375rem; + white-space: nowrap; + } +` + +const Content = styled.div` + grid-area: content; + margin: 1rem 0 3rem; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + margin: 0; + } +` + +const FlexCentered = styled.div` + display: flex; + justify-content: center; + align-items: center; +` + +const Glyph = styled(FlexCentered)` + grid-area: glyph; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + grid-area: content; + svg { + width: 50%; + height: 50%; + opacity: 0.1; + } + } +` + +const Ether = styled(FlexCentered)` + grid-area: ether; + z-index: 2; + max-width: 5rem; + margin: 0 auto; +` + +const StyledEtherSvg = styled(EtherSvg)` + --size: ${({ size }) => size}; + width: var(--size); + height: var(--size); +` + +const Line = styled.aside` + grid-column: 1; + grid-row: 1/3; + width: 100%; + height: 100%; + text-align: center; + font-size: 28px; + position: relative; + &::after { + content: ""; + height: calc(100% - 50px); + border-left: solid 4px orange; + position: absolute; + left: calc(50% - 2px); + top: 50px; + z-index: 1; + } + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + display: none; + } +` + +const Gold = styled.span` + color: ${({ theme }) => theme.colors.stakingGold}; + font-weight: 600; +` + +const StakingHierarchy = () => { + return ( + +
+ + + + +
+

Solo home staking

+ +

+ Most impactful +

+

Full control

+

Full rewards

+

Trustless

+
+
+ + + + +

+ Solo staking on Ethereum is the gold standard for + staking. It provides full participation rewards, improves the + decentralization of the network, and never requires trusting anyone + else with your funds. +

+

+ Those considering solo staking should have at least 32 ETH and a + dedicated computer connected to the internet ~24/7. Some technical + know-how is helpful, but easy-to-use tools now exist to help + simplify this process. +

+ { + trackCustomEvent({ + eventCategory: `StakingHierarchy`, + eventAction: `Clicked`, + eventName: "clicked solo staking", + }) + }} + > + More on solo staking + +
+
+
+ + + + +
+

Staking as a service

+ +

Your 32 ETH

+

Your validator keys

+

Entrusted node operation

+
+
+ + + + +

+ If you don't want or don't feel comfortable dealing with hardware + but still want to stake your 32 ETH, staking-as-a-service options + allow you to delegate the hard part while you earn native block + rewards. +

+

+ These options usually walk you through creating a set of validator + credentials, uploading your signing keys to them, and depositing + your 32 ETH. This allows the service to validate on your behalf. +

+

+ This method of staking requires a certain level of trust in the + provider. To limit counter-party risk, the keys to withdrawal your + ETH are usually kept in your possession. +

+ { + trackCustomEvent({ + eventCategory: `StakingHierarchy`, + eventAction: `Clicked`, + eventName: "clicked staking as a service", + }) + }} + to="/staking/saas/" + > + More on staking as a service + +
+
+
+ + + + +
+

Pooled staking

+ +

Stake any amount

+

Earn rewards

+

Keep it simple

+

+ Popular +

+
+
+ + + + +

+ Several pooling solutions now exist to assist users who do not have + or feel comfortable staking 32 ETH. +

+

+ Many of these options include what is known as "liquid staking" + which involves an ERC-20 liquidity token that represents your staked + ETH. +

+

+ Liquid staking enables easy and anytime exiting and makes staking as + simple as a token swap. This option also allows users to hold + custody of their assets in their own Ethereum wallet. +

+

+ Pooled staking is not native to the Ethereum network. Third parties + are building these solutions, and they carry their own risks. +

+ { + trackCustomEvent({ + eventCategory: `StakingHierarchy`, + eventAction: `Clicked`, + eventName: "clicked pooled staking", + }) + }} + to="/staking/pools/" + > + More on pooled staking + +
+
+
+ + + + +
+

Centralized exchanges

+ +

+ Least impactful +

+

Highest trust assumptions

+
+
+ + + + +

+ Many centralized exchanges provide staking services if you are not + yet comfortable holding ETH in your own wallet. They can be a + fallback to allow you to earn some yield on your ETH holdings with + minimal oversight or effort. +

+

+ The trade-off here is that centralized providers consolidate large + pools of ETH to run large numbers of validators. This can be + dangerous for the network and its users as it creates a large + centralized target and point of failure, making the network more + vulnerable to attack or bugs. +

+

+ If you don't feel comfortable holding your own keys, that's okay. + These options are here for you. In the meantime, consider checking + out our wallets page, where you can get + started learning how to take true ownership over your funds. When + you're ready, come back and level up your staking game by trying one + of the self-custody pooled staking services offered. +

+
+
+
+ ) +} + +export default StakingHierarchy diff --git a/src/components/Staking/StakingHomeTableOfContents.js b/src/components/Staking/StakingHomeTableOfContents.js new file mode 100644 index 00000000000..a25cb16750e --- /dev/null +++ b/src/components/Staking/StakingHomeTableOfContents.js @@ -0,0 +1,80 @@ +import React from "react" +import { motion } from "framer-motion" +import { Link } from "gatsby" +import styled from "styled-components" + +const StyledTableOfContentsLink = styled(Link)` + position: relative; + display: inline-block; + color: ${({ theme }) => theme.colors.text300}; + margin-bottom: 0.5rem !important; +` + +const TableOfContentsLink = ({ item: { id, title } }) => { + const url = `#${id}` + let isActive = false + if (typeof window !== `undefined`) { + isActive = window.location.hash === url + } + // const isNested = depth === 2 + let classes = "nested" + if (isActive) { + classes += " active" + } + return ( + + {title} + + ) +} + +const OuterList = styled(motion.ul)` + list-style-type: none; + list-style-image: none; + padding: 0; + margin: 0; + font-size: 1.25rem; + text-align: right; + line-height: 1.6; + font-weight: 400; + padding-right: 0.25rem; + padding-left: 1rem; + + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + display: none; + } +` + +const Aside = styled.aside` + padding: 0rem; + text-align: right; + margin-bottom: 2rem; + overflow-y: auto; +` + +const ListItem = styled.li` + margin: 0; +` + +const ItemsList = ({ items }) => + items.map((item, index) => ( + +
+ +
+
+ )) + +const StakingHomeTableOfContents = ({ items, className }) => { + if (!items) return null + + return ( + + ) +} + +export default StakingHomeTableOfContents diff --git a/src/components/Staking/StakingHowSoloWorks.js b/src/components/Staking/StakingHowSoloWorks.js new file mode 100644 index 00000000000..efade3172b5 --- /dev/null +++ b/src/components/Staking/StakingHowSoloWorks.js @@ -0,0 +1,57 @@ +import React from "react" +import { useIntl } from "gatsby-plugin-intl" +import styled from "styled-components" +import { graphql, useStaticQuery } from "gatsby" +import { GatsbyImage, getImage } from "gatsby-plugin-image" + +import Link from "../Link" +import OrderedList from "../OrderedList" + +const Flex = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + flex-direction: column; + } +` + +const Image = styled(GatsbyImage)`` + +const StakingHowSoloWorks = () => { + const intl = useIntl() + const { image } = useStaticQuery(graphql` + { + image: file(relativePath: { eq: "hackathon_transparent.png" }) { + childImageSharp { + gatsbyImageData( + width: 400 + layout: CONSTRAINED + placeholder: BLURRED + quality: 100 + ) + } + } + } + `) + + const items = [ +

+ Get some hardware: You need to run a node{" "} + to stake. +

, +

Sync an execution layer client

, +

Sync a consensus layer client

, +

Generate your keys and load them into your validator client

, +

Monitor and maintain your node

, + ] + + return ( + + + + + ) +} + +export default StakingHowSoloWorks diff --git a/src/components/Staking/StakingLaunchpadWidget.js b/src/components/Staking/StakingLaunchpadWidget.js new file mode 100644 index 00000000000..8b6693ff242 --- /dev/null +++ b/src/components/Staking/StakingLaunchpadWidget.js @@ -0,0 +1,122 @@ +import React, { useState } from "react" +import styled from "styled-components" + +import { StyledSelect as Select } from "../SharedStyledComponents" +import Link from "../Link" +import ButtonLink from "../ButtonLink" +import Emoji from "../Emoji" + +import { trackCustomEvent } from "../../utils/matomo" + +const Container = styled.div` + display: flex; + flex-direction: column; + background: ${({ theme }) => theme.colors.layer2Gradient}; + border-radius: 0.25rem; + padding: 2rem; + @media (max-width: ${(props) => props.theme.breakpoints.m}) { + padding: 1.5rem; + } + span { + color: ${({ theme }) => theme.colors.text200}; + } +` + +const SelectContainer = styled.div` + margin: 1rem 0; +` + +const StyledSelect = styled(Select)` + max-width: 50%; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + max-width: 100%; + } +` + +const ButtonContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 1rem; + @media (max-width: ${(props) => props.theme.breakpoints.m}) { + a { + width: 100%; + } + } +` + +const StakingLaunchpadWidget = () => { + const [selection, setSelection] = useState("testnet") + + const handleChange = (e) => { + trackCustomEvent({ + eventCategory: `Selected testnet vs mainnet for Launchpad link`, + eventAction: `Clicked`, + eventName: `${e.label} bridge selected`, + eventValue: `${e.value}`, + }) + setSelection(e.value) + } + + const data = { + testnet: { + label: "Goerli/Prater testnet", + url: "https://prater.launchpad.ethereum.org", + }, + mainnet: { + label: "Mainnet", + url: "https://launchpad.ethereum.org", + }, + } + + const selectOptions = Object.keys(data).map((key) => ({ + label: data[key].label, + value: key, + })) + + return ( + +
+ Choose network + + + +

+ Solo validators are expected to test their setup and + operational skills on the {data.testnet.label} before risking funds. + Remember it is important to choose a{" "} + + minority client + {" "} + as it improves the security of the network and limits your risk. +

+

+ If you're comfortable with it, you can set up everything needed from + the command line using the Staking Launchpad alone. +

+ + + Start staking on {data[selection].label} + + +

+ To make things easier, check out some of the tools and guides below + that can help you alongside the Staking Launchpad to get your clients + set up with ease. +

+ + + Software tools and guide + + +
+
+ ) +} + +export default StakingLaunchpadWidget diff --git a/src/components/Staking/StakingProductsCardGrid.js b/src/components/Staking/StakingProductsCardGrid.js new file mode 100644 index 00000000000..f3924c56840 --- /dev/null +++ b/src/components/Staking/StakingProductsCardGrid.js @@ -0,0 +1,502 @@ +import React, { useContext } from "react" +import styled, { ThemeContext } from "styled-components" +import { shuffle } from "lodash" +// Data imports +import stakingProducts from "../../data/staking-products.json" +// Component imports +import ButtonLink from "../ButtonLink" +// SVG imports +import GreenCheck from "../../assets/staking/green-check-product-glyph.svg" +import Caution from "../../assets/staking/caution-product-glyph.svg" +import Warning from "../../assets/staking/warning-product-glyph.svg" +import Unknown from "../../assets/staking/unknown-product-glyph.svg" +// Product SVGs +import Abyss from "../../assets/staking/abyss-glyph.svg" +import Allnodes from "../../assets/staking/allnodes-glyph.svg" +import Ankr from "../../assets/staking/ankr-glyph.svg" +import Bloxstaking from "../../assets/staking/bloxstaking-glyph.svg" +import Dappnode from "../../assets/staking/dappnode-glyph.svg" +import DefaultOpenSource from "../../assets/staking/default-open-source-glyph.svg" +import Docker from "../../assets/staking/docker-icon.svg" +import Lido from "../../assets/staking/lido-glyph.svg" +import RocketPool from "../../assets/staking/rocket-pool-glyph.svg" +import Stafi from "../../assets/staking/stafi-glyph.svg" +import Stakefish from "../../assets/staking/stakefish-glyph.svg" +import Stakewise from "../../assets/staking/stakewise-glyph.svg" +import Stereum from "../../assets/staking/stereum-glyph.svg" +import Wagyu from "../../assets/staking/wagyu-glyph.svg" +// When adding a product svg, be sure to add to mapping below as well. + +const CardGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); + gap: 2rem; + margin: 3rem 0; +` + +const Card = styled.div` + display: flex; + flex-direction: column; + background: ${({ theme }) => theme.colors.offBackground}; + border-radius: 0.25rem; + &:hover { + transition: 0.1s; + transform: scale(1.01); + } +` + +const PaddedDiv = styled.div` + padding: 1.5rem 2rem; +` + +const Spacer = styled.div` + flex: 1; +` + +const Banner = styled(PaddedDiv)` + display: flex; + align-items: center; + gap: 1.5rem; + background: ${({ color }) => color} + linear-gradient(0deg, rgba(0, 0, 0, 30%), rgba(0, 0, 0, 0)); + border-radius: 0.25rem; + max-height: 6rem; + h2 { + margin: 0; + color: white; + font-size: 1.5rem; + } + svg { + height: 2rem; + } +` + +const MinEthBar = styled.div` + display: flex; + justify-content: center; + align-items: center; + font-weight: 700; + font-size: 1rem; + color: ${({ theme }) => theme.colors.textTableOfContents}; + text-transform: uppercase; + padding-top: 1.5rem; +` + +const Pills = styled(PaddedDiv)` + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + /* padding-top: 1rem; */ +` + +const Pill = styled.div` + text-align: center; + padding: 0.25rem 0.75rem; + color: ${({ theme, type }) => + type ? "rgba(0, 0, 0, 0.6)" : theme.colors.text200}; + background: ${({ theme, type }) => { + if (!type) return "transparent" + switch (type.toLowerCase()) { + case "ui": + return theme.colors.stakingPillUI + case "platform": + return theme.colors.stakingPillPlatform + default: + return theme.colors.tagGray + } + }}; + font-size: ${({ theme }) => theme.fontSizes.xs}; + border: 1px solid ${({ theme }) => theme.colors.lightBorder}; + border-radius: 0.25rem; +` + +const Content = styled(PaddedDiv)` + padding-top: 0; + padding-bottom: 0; + + ul { + list-style: none; + margin-left: 0; + padding-left: 0; + } +` + +const Item = styled.li` + display: flex; + align-items: center; + text-indent: 1em; + text-transform: uppercase; + font-weight: 400; + font-size: 0.75rem; + line-height: 0.875rem; + letter-spacing: 0.04em; +` + +const Cta = styled(PaddedDiv)` + a { + width: 100%; + } +` + +const Status = ({ status }) => { + if (!status) return null + const styles = { width: "24", height: "auto" } + switch (status) { + case "green-check": + return + case "caution": + return + case "warning": + case "false": + return + default: + return + } +} + +const getSvgFromPath = (svgPath) => { + const mapping = { + "abyss-glyph.svg": Abyss, + "allnodes-glyph.svg": Allnodes, + "ankr-glyph.svg": Ankr, + "bloxstaking-glyph.svg": Bloxstaking, + "dappnode-glyph.svg": Dappnode, + "docker-icon.svg": Docker, + "default-open-source-glyph.svg": DefaultOpenSource, + "lido-glyph.svg": Lido, + "rocket-pool-glyph.svg": RocketPool, + "stafi-glyph.svg": Stafi, + "stakewise-glyph.svg": Stakewise, + "stereum-glyph.svg": Stereum, + "wagyu-glyph.svg": Wagyu, + "stakefish-glyph.svg": Stakefish, + } + return mapping[svgPath] +} + +const StakingProductCard = ({ + product: { + name, + svgPath, + color, + url, + socials, + platforms, + ui, + minEth, + openSource, + audited, + bugBounty, + battleTested, + trustless, + permissionless, + permissionlessNodes, + multiClient, + diverseClients, + economical, + matomo, + }, +}) => { + const Svg = getSvgFromPath(svgPath) + const data = [ + { + label: "Open source", + status: openSource, + }, + { + label: "Audited", + status: audited, + }, + { + label: "Bug bounty", + status: bugBounty, + }, + { + label: "Battle Tested", + status: battleTested, + }, + { + label: "Trustless", + status: trustless, + }, + { + label: "Permissionless", + status: permissionless, + }, + { + label: "Permissionless Nodes", + status: permissionlessNodes, + }, + { + label: "Multi-client", + status: multiClient, + }, + { + label: "Diverse Clients", + status: diverseClients, + }, + { + label: "Economical", + status: economical, + }, + ].filter(({ status }) => !!status) + + return ( + + + {!!Svg && } +

{name}

+
+ {typeof minEth !== "undefined" && ( + + {minEth > 0 ? `From ${minEth} ETH` : "Any amount"} + + )} + + {platforms && + platforms.map((platform, idx) => ( + + {platform} + + ))} + {ui && + ui.map((_ui, idx) => ( + + {_ui} + + ))} + + + +
    + {data && + data.map(({ label, status }, idx) => ( + + +

    {label}

    +
    + ))} +
+
+ + + Get started + + +
+ ) +} + +const StakingProductCardGrid = ({ category }) => { + const themeContext = useContext(ThemeContext) + const isDarkTheme = themeContext.isDark + const [VALID_FLAG, CAUTION_FLAG, WARNING_FLAG, FALSE_FLAG, UNKNOWN_FLAG] = [ + "green-check", + "caution", + "warning", + "false", + "unknown", + ] + const [SAT, LUM] = isDarkTheme ? ["50%", "35%"] : ["75%", "60%"] + + const scoreOpenSource = (product) => { + return product.openSource === VALID_FLAG ? 1 : 0 + } + + const scoreAudited = (product) => { + return product.audited === VALID_FLAG ? 1 : 0 + } + + const scoreBugBounty = (product) => { + return product.bugBounty === VALID_FLAG ? 1 : 0 + } + + const scoreBattleTested = (product) => { + return product.battleTested === VALID_FLAG + ? 2 + : product.battleTested === CAUTION_FLAG + ? 1 + : 0 + } + + const scoreTrustless = (product) => { + return product.trustless === VALID_FLAG ? 1 : 0 + } + + const scorePermissionless = (product) => { + return product.permissionless === VALID_FLAG ? 1 : 0 + } + + const scorePermissionlessNodes = (product) => { + return product.permissionlessNodes === VALID_FLAG ? 1 : 0 + } + + const scoreMultiClient = (product) => { + return product.multiClient === VALID_FLAG ? 1 : 0 + } + + const scoreDiverseClients = (product) => { + return product.diverseClients === VALID_FLAG + ? 2 + : product.diverseClients === WARNING_FLAG + ? 1 + : 0 + } + + const scoreEconomical = (product) => { + return product.economical === VALID_FLAG ? 1 : 0 + } + + const getRankingScore = (product) => { + let score = 0 + score += scoreOpenSource(product) + score += scoreAudited(product) + score += scoreBugBounty(product) + score += scoreBattleTested(product) + score += scoreTrustless(product) + score += scorePermissionless(product) + score += scorePermissionlessNodes(product) + score += scoreMultiClient(product) + score += scoreDiverseClients(product) + score += scoreEconomical(product) + return score + } + + const getBattleTestedFlag = (_launchDate) => { + let battleTested = WARNING_FLAG + const launchDate = new Date(_launchDate) + const now = new Date() + const halfYearAgo = new Date() + const oneYearAgo = new Date() + halfYearAgo.setDate(now.getDate() - 183) + oneYearAgo.setDate(now.getDate() - 365) + if (halfYearAgo > launchDate) { + battleTested = CAUTION_FLAG + } + if (oneYearAgo > launchDate) { + battleTested = VALID_FLAG + } + return battleTested + } + + const getDiversityOfClients = (_pctMajorityClient) => { + if (!_pctMajorityClient) return UNKNOWN_FLAG + if (_pctMajorityClient > 50) return WARNING_FLAG + return VALID_FLAG + } + + const getFlagFromBoolean = (bool) => (!!bool ? VALID_FLAG : FALSE_FLAG) + + const getBrandProperties = ({ + name, + svgPath, + hue, + url, + socials, + matomo, + }) => ({ + name, + svgPath, + color: `hsla(${hue}, ${SAT}, ${LUM}, 1)`, + url, + socials, + matomo, + }) + + const getTagProperties = ({ platforms, ui }) => ({ + platforms, + ui, + }) + + const getSharedSecurityProperties = ({ + isFoss, + audits, + hasBugBounty, + launchDate, + isTrustless, + }) => ({ + openSource: getFlagFromBoolean(isFoss), + audited: getFlagFromBoolean(audits?.length), + bugBounty: getFlagFromBoolean(hasBugBounty), + battleTested: getBattleTestedFlag(launchDate), + trustless: getFlagFromBoolean(isTrustless), + }) + + const categoryProducts = stakingProducts[category] + + const products = [] + + // Solo staking products + if (category === "nodeTools") { + products.push( + ...categoryProducts.map((listing) => ({ + ...getBrandProperties(listing), + ...getTagProperties(listing), + ...getSharedSecurityProperties(listing), + permissionless: getFlagFromBoolean(listing.isPermissionless), + multiClient: getFlagFromBoolean(listing.multiClient), + selfCustody: getFlagFromBoolean(true), + economical: getFlagFromBoolean(listing.minEth < 32), + minEth: listing.minEth, + })) + ) + } + // Staking as a service + if (category === "saas") { + products.push( + ...categoryProducts.map((listing) => ({ + ...getBrandProperties(listing), + ...getTagProperties(listing), + ...getSharedSecurityProperties(listing), + permissionless: getFlagFromBoolean(listing.isPermissionless), + diverseClients: getDiversityOfClients(listing.pctMajorityClient), + selfCustody: getFlagFromBoolean(listing.isSelfCustody), + minEth: listing.minEth, + })) + ) + } + // Pooled staking services + if (category === "pools") { + products.push( + ...categoryProducts.map((listing) => ({ + ...getBrandProperties(listing), + ...getTagProperties(listing), + ...getSharedSecurityProperties(listing), + permissionlessNodes: getFlagFromBoolean(listing.hasPermissionlessNodes), + diverseClients: getDiversityOfClients(listing.pctMajorityClient), + selfCustody: getFlagFromBoolean(listing.tokens?.length), + minEth: listing.minEth, + })) + ) + } + // Key generators + if (category === "keyGen") { + products.push( + ...categoryProducts.map((listing) => ({ + ...getBrandProperties(listing), + ...getTagProperties(listing), + ...getSharedSecurityProperties(listing), + permissionless: getFlagFromBoolean(listing.isPermissionless), + selfCustody: getFlagFromBoolean(listing.isSelfCustody), + })) + ) + } + + if (!products) return null + + const rankedProducts = shuffle(products) + .map((product) => ({ + ...product, + rankingScore: getRankingScore(product), + })) + .sort((a, b) => b.rankingScore - a.rankingScore) + + return ( + + {rankedProducts.map((product) => ( + + ))} + + ) +} +export default StakingProductCardGrid diff --git a/src/components/Staking/StakingStatsBox.js b/src/components/Staking/StakingStatsBox.js new file mode 100644 index 00000000000..814cccc5ad4 --- /dev/null +++ b/src/components/Staking/StakingStatsBox.js @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from "react" +import styled from "styled-components" +import { useIntl } from "gatsby-plugin-intl" + +import Translation from "../Translation" +import { getData } from "../../utils/cache" +import calculateStakingRewards from "../../utils/calculateStakingRewards" + +const Container = styled.div` + display: flex; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + flex-direction: column; + } +` + +const Cell = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 1rem 2rem; + border-left: 1px solid ${({ theme }) => theme.colors.preBorder}; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + border-left: none; + border-top: 1px solid #33333355; + } + &:first-child { + border-left: none; + border-top: none; + } +` + +const Value = styled.code` + font-weight: 700; + font-size: 2rem; + background: none; + display: flex; + align-items: center; + text-align: center; + text-transform: uppercase; + color: ${({ theme }) => theme.colors.primary}; +` + +const Label = styled.p` + text-transform: uppercase; + font-size: 0.875rem; + margin-top: 0.5rem; +` + +const IndicatorSpan = styled.span` + font-size: 2rem; +` + +const ErrorMessage = () => ( + + + +) + +const LoadingMessage = () => ( + + + +) + +const StatsBoxGrid = () => { + const intl = useIntl() + const [totalEth, setTotalEth] = useState(0) + const [totalValidators, setTotalValidators] = useState(0) + const [currentApr, setCurrentApr] = useState(0) + const [error, setError] = useState(false) + + useEffect(() => { + const formatInteger = (amount) => + new Intl.NumberFormat(intl.locale).format(amount) + + const formatPercentage = (amount) => + new Intl.NumberFormat(intl.locale, { + style: "percent", + minimumSignificantDigits: 2, + maximumSignificantDigits: 3, + }).format(amount) + + ;(async () => { + try { + const { + data: { totalvalidatorbalance, validatorscount }, + } = await getData("https://mainnet.beaconcha.in/api/v1/epoch/latest") + + const valueTotalEth = formatInteger( + (totalvalidatorbalance * 1e-9).toFixed(0) + ) + const valueTotalValidators = formatInteger(validatorscount) + const currentAprDecimal = calculateStakingRewards( + totalvalidatorbalance * 1e-9 + ) + const valueCurrentApr = formatPercentage(currentAprDecimal) + setTotalEth(valueTotalEth) + setTotalValidators(valueTotalValidators) + setCurrentApr(valueCurrentApr) + setError(false) + } catch (error) { + setTotalEth("n/a") + setTotalValidators("n/a") + setCurrentApr("n/a") + setError(true) + } + })() + }, []) + + // TODO: Improve error handling + if (error) return + + return ( + + + {totalEth} + + + + {totalValidators} + + + + {currentApr} + + + + ) +} + +export default StatsBoxGrid diff --git a/src/components/TableOfContents.js b/src/components/TableOfContents.js index 37cc97a4e60..21de2f9c778 100644 --- a/src/components/TableOfContents.js +++ b/src/components/TableOfContents.js @@ -178,8 +178,9 @@ const AsideMobile = styled.aside` /* TODO find better way - this accounts for huge header margin top */ /* but if doc DOESN'T start w/ header, it overlaps, e.g. /docs/accounts/ */ /* margin-bottom: -10rem; */ - @media (min-width: ${(props) => props.theme.breakpoints.l}) { - display: none; + display: none; + @media (max-width: ${({ theme }) => theme.breakpoints.l}) { + display: block; } ` diff --git a/src/components/TutorialTags.js b/src/components/TutorialTags.js index 4340b775ca2..b8dc6139fc9 100644 --- a/src/components/TutorialTags.js +++ b/src/components/TutorialTags.js @@ -20,7 +20,7 @@ const colors = [ "tagOrange", "tagGreen", "tagRed", - "tagTurqouise", + "tagTurquoise", "tagGray", "tagYellow", "tagMint", diff --git a/src/content/contributing/adding-staking-product/index.md b/src/content/contributing/adding-staking-product/index.md new file mode 100644 index 00000000000..811df01d12d --- /dev/null +++ b/src/content/contributing/adding-staking-product/index.md @@ -0,0 +1,172 @@ +--- +title: Adding staking products or services +description: The policy we use when adding a staking products or services to ethereum.org +lang: en +sidebar: true +--- + +# Adding staking products or services {#adding-staking-products-or-services} + +We want to make sure we list the best resources possible while keeping users safe and confident. + +Anyone is free to suggest adding a staking products or service on ethereum.org. If there's one that we have missed, **[please suggest it](https://github.com/ethereum/ethereum-org-website/issues/new?&template=suggest_staking_product.md)!** + +We currently list staking products and services on the following pages: + +- [Solo staking](/staking/solo/) +- [Staking as a service](/staking/saas/) +- [Staking pools](/staking/pools/) + +Proof-of-stake on the Beacon Chain has been live since December 1, 2020. While staking is still relatively new, we've tried to create a fair and transparent framework for consideration on ethereum.org but the listing criteria will change and evolve over time, and is ultimately at the discretion of the ethereum.org website team. + +## The decision framework {#the-decision-framework} + +The decision to list a product on ethereum.org is not dependent on any one factor. Multiple criteria are considered together when deciding to list a product or service. The more or these criteria are met, the more likely it is to be listed. + +**First, which category of product or service is it?** + +- Node or client tooling +- Key management +- Staking as a service (SaaS) +- Staking pool + +Currently we are only listing products or services in these categories. + +### Criteria for inclusion {#criteria-for-inclusion} + +Staking products or services submissions will be assessed by the following criteria: + +**When was the project or service launched?** + +- Is there evidence of when the product or service became available to the public? +- This is used to determine the products "battle tested" score. + +**Is the project being actively maintained?** + +- Is there an active team developing the project? Who is involved? +- Only actively maintained products will be considered. + +**Is the product or service free of trusted/human intermediaries?** + +- What steps in the users journey require trusting humans to either hold the keys to their funds, or to properly distribute rewards? +- This is used to determine the product or services "trustless" score. + +**What platforms are supported?** + +- i.e. Linux, macOS, Windows, iOS, Android + +#### Software and smart contracts {#software-and-smart-contracts} + +For any custom software or smart contracts involved: + +**Is everything open source?** + +- Open source projects should have a publicly available source code repository +- This is used to determine the products "open source" score. + +**Is the product out of _beta_ development?** + +- Where is the product at in its development cycle? +- Products in the beta stage are not considered for inclusion on ethereum.org + +**Has the software undergone an external security audit?** + +- If not, are there plans to conduct an external audit? +- This is used to determine the products "audited" score. + +**Does the project have a bug bounty program?** + +- If not, are there plans to create a security bug bounty? +- This is used to determine the products "bug bounty" score. + +#### Node or client tooling {#node-or-client-tooling} + +For software products related to node or client setup, management or migration: + +**Which consensus layer clients (ie. Lighthouse, Teku, Nimbus, Prysm) are supported?** + +- Which clients are supported? Can the user choose? +- This is used to determine the products "multi-client" score. + +#### Staking as a service {#staking-as-a-service} + +For [staking-as-a-service listings](/staking/saas/) (ie. delegated node operation): + +**What are the fees associated with using the service?** + +- What is the fee structure, e.g. is there a monthly fee for the service? +- Any additional staking requirements? + +**Are users required to sign-up for an account?** + +- Can someone use the service without permission or KYC? +- This is used to determine the products "permissionless" score. + +**Who holds the signing keys, and withdrawal keys?** + +- What keys does the user maintain access to? What keys does the service gain access to? +- This is used to determine the products "trustless" score. + +**What is the client diversity of the nodes being operated?** + +- What percent of validator keys are being run a majority consensus layer (CL) client? +- As of last edit, Prysm is the consensus layer client being run by a majority of node operators, which is dangerous for the network. If any CL client is currently being used by over 33% of the network, we request data related to its usage. +- This is used to determine the products "diverse clients" score. + +#### Staking pool {#staking-pool} + +For [pooled staking services](/staking/pools/): + +**What is the minimum ETH required to stake?** + +- e.g. 0.01 ETH + +**What are the fees or staking requirements involved?** + +- What percentage of rewards are removed as fees? +- Any additional staking requirements? + +**Is there a liquidity token?** + +- What are the tokens involved? How do they work? What are the contract addresses? +- This is used to determine the products "liquidity token" score. + +**Can users participate as a node operator?** + +- What is required to run validator clients using the pooled funds? +- Does this require permission from an individual, company or DAO? +- This is used to determine the products "permissionless nodes" score. + +**What is the client diversity of the pool node operators?** + +- What percent of node operators are running a majority consensus layer (CL) client? +- As of last edit, Prysm is the consensus layer client being run by a majority of node operators, which is dangerous for the network. If any CL client is currently being used by over 33% of the network, we request data related to its usage. +- This is used to determine the products "diverse clients" score. + +### Other criteria: the nice-to-haves {#other-criteria} + +**What user interfaces are supported?** + +- i.e. Browser app, desktop app, mobile app, CLI + +**For node tooling, does the software provide an easy way to switch between clients?** + +- Can the user easily and safely change clients using the tool? + +**For SaaS, how many validators are currently being operated by the service?** + +- This gives us an idea of the reach of your service so far. + +## How we display results {#product-ordering} + +The [criteria for inclusion](#criteria-for-inclusion) above are used to calculate a cumulative score for each product or service. This is used as a means of sorting and showcasing products that meet certain objective criteria. The more criteria that evidence is provided for, the higher a product will be sorted, with ties being randomized on load. + +The code logic and weights for these criteria is currently contained in [this javascript component](https://github.com/ethereum/ethereum-org-website/blob/dev/src/components/StakingProductsCardGrid.js) in our repo. + +## Add your product or service {#add-product} + +If you want to add a staking product or service to ethereum.org, create an issue on GitHub. + + + Create an issue + diff --git a/src/content/contributing/index.md b/src/content/contributing/index.md index cdb89daa729..47c528e78e1 100644 --- a/src/content/contributing/index.md +++ b/src/content/contributing/index.md @@ -36,7 +36,9 @@ The ethereum.org website, like Ethereum more broadly, is an open-source project. _– Help us continue to expand the Ethereum [glossary](/glossary/)_ - [Create/edit content](/contributing/#how-to-update-content) _– Suggest new pages or makes tweaks to what's here already_ -- [Add a layer 2](/contributing/adding-layer-2) _- Add a layer 2 to a relevant page_ +- [Add a layer 2](/contributing/adding-layer-2) + _- Add a layer 2 to a relevant page_ +- [Add a staking product or service](/contributing/adding-staking-product/) - _Add a project that helps facilitate solo staking, pooled staking, or staking as a service_ _Any questions?_ 🤔 Reach out on our [Discord server](https://discord.gg/CetY6Y4) diff --git a/src/content/developers/tutorials/how-to-mint-an-nft/index.md b/src/content/developers/tutorials/how-to-mint-an-nft/index.md index 77d2e8a0a5b..3405cf2f540 100644 --- a/src/content/developers/tutorials/how-to-mint-an-nft/index.md +++ b/src/content/developers/tutorials/how-to-mint-an-nft/index.md @@ -308,9 +308,7 @@ async function mintNFT(tokenURI) { }) } -mintNFT( - "ipfs://QmYueiuRNmL4MiA2GwtVMm6ZagknXnSpQnB3z2gWbz36hP" -) +mintNFT("ipfs://QmYueiuRNmL4MiA2GwtVMm6ZagknXnSpQnB3z2gWbz36hP") ``` Now, run `node scripts/mint-nft.js` to deploy your NFT. After a couple of seconds, you should see a response like this in your terminal: diff --git a/src/content/staking/pools/index.md b/src/content/staking/pools/index.md new file mode 100644 index 00000000000..2b4734ca0c9 --- /dev/null +++ b/src/content/staking/pools/index.md @@ -0,0 +1,86 @@ +--- +title: Pooled staking +description: An overview of how to get started with pooled ETH staking +lang: en +template: staking +emoji: ":money_with_wings:" +sidebar: true +image: ../../../assets/staking/leslie-pool.png +alt: Leslie the rhino swimming in the pool. +sidebarDepth: 2 +summaryPoints: + - Stake and earn rewards with any amount of ETH by joining forces with others + - Skip the hard part and entrust validator operation to a third-party + - Hold liquidity tokens in your own wallet +--- + +## What are staking pools? {#what-are-staking-pools} + +Staking pools are a collaborative approach to allow many with smaller amounts of ETH to obtain the 32 ETH required to activate a set of validator keys. Pooling functionality is not natively supported within the protocol, so solutions were built out separately to address this need. + +Some pools operate using smart contracts, where funds can be deposited to a contract, which trustlessly manages and tracks your stake, and issues you a token that represents this value. Other pools may not involve smart contracts and are instead mediated off-chain. + +## Why stake with a pool? {#why-stake-with-a-pool} + +In addition to the benefits we outlined in our [intro to staking](/staking/), staking with a pool comes with a number of distinct benefits. + + + + Not a whale? No problem. Most staking pools let you stake virtually any amount of ETH by joining forces with other stakers, unlike staking solo which requires 32 ETH. + + + Staking with a pool is as easy as a token swap. No need to worry about hardware setup and node maintenance. Pools allow you to deposit your ETH which enables node operators to run validators. Rewards are then distributed to contributors minus a fee for node operations. + + + Many staking pools provide a token that represents a claim on your staked ETH and the rewards it generates. This allows you to make use of your staked ETH, e.g. as collateral in DeFi applications. + + + + + +## What to consider {#what-to-consider} + +Pooled or delegated staking is not natively supported by the Ethereum protocol, but given the demand for users to stake less than 32 ETH a growing number of solutions have been built out to serve this demand. + +Each pool and the tools or smart contracts they use have been built out by different teams and each come with their own risks and benefits. + +Attribute indicators are used below to signal notable strengths or weaknesses a listed staking pool may have. Use this section as a reference for how we define these attributes while you're choosing a pool to join. + + + +## Explore staking pools {#explore-staking-pools} + +There are a variety of options available to help you with your setup. Use the above indicators to help guide you through the tools below. + + +Please note the importance of choosing a service that takes client diversity seriously, as it improves the security of the network, and limits your risk. Services that have evidence of limiting majority client use are marked as "diverse clients." + + + + +Have a suggestion for a staking tool we missed? Check out our [product listing policy](/contributing/adding-staking-product/) to see if it would be a good fit, and to submit it for review. + +## FAQ {#faq} + + +Typically ERC-20 liquidity tokens are issued to stakers that represents the value of their staked ETH plus rewards. Keep in mine that different pools will distribute staking rewards to their users via slightly different methods, but this is the common theme. + + + +Currently, withdrawing funds from a validator on the Beacon Chain is not possible, which currently limits the ability to actually redeem your liquidity token for the ETH rewards locked in the consensus layer. + +Alternatively, pools that utilize an ERC-20 liquidity token allow users to trade this token in the open market, effectively allowing you to "withdraw" without actually removing ETH from the Beacon Chain. + + + +There are many similarities between these pooled staking options and centralized exchanges, such as the ability to stake small amounts of ETH and have them bundled together to activate validators. + +Unlike centralized exchanges, many other pooled staking options utilize smart contracts and/or liquidity tokens, which are usually ERC-20 tokens that can be held in your own wallet, and bought or sold just like any other token. This offers a layer of sovereignty and security by giving you control over your tokens, but still does not give you direct control over the validator client attesting on your behalf in the background. + +Some pooling options are more decentralized than others when it comes to the nodes that back them. To promote the health and decentralization of the network, stakers are always encourage to select a pooling service that enables a permissionless decentralized set of node operators. + + +## Further reading {#further-reading} + +- [Staking with Rocket Pool - Staking Overview](https://docs.rocketpool.net/guides/staking/overview.html) - _RocketPool docs_ +- [Staking Ethereum With Lido](https://help.lido.fi/en/collections/2947324-staking-ethereum-with-lido) - _Lido help docs_ diff --git a/src/content/staking/saas/index.md b/src/content/staking/saas/index.md new file mode 100644 index 00000000000..f7f127c5cee --- /dev/null +++ b/src/content/staking/saas/index.md @@ -0,0 +1,96 @@ +--- +title: Staking as a service +description: An overview of how to get started with pooled ETH staking +lang: en +template: staking +emoji: ":money_with_wings:" +sidebar: true +image: ../../../assets/staking/leslie-saas.png +alt: Leslie the rhino floating in the clouds. +sidebarDepth: 2 +summaryPoints: + - Third-party node operators handle the operation of your validator client + - Great option for anyone with 32 ETH who doesn't feel comfortable dealing with the technical complexity of running a node + - Reduce trust, and maintain custody of your withdrawal keys +--- + +## What is staking as a service? {#what-is-staking-as-a-service} + +Staking as a service (“SaaS") represents a category of staking services where you deposit your own 32 ETH for a validator, but delegate node operations to a third-party operator. This process usually involves being guided through the initial setup, including key generation and deposit, then uploading your signing keys to the operator. This allows the service to operate your validator on your behalf, usually for a monthly fee. + +## Why stake with a service? {#why-stake-with-a-service} + +The Ethereum protocol does not natively support delegation of stake, so these services have been built out to fill this demand. If you have 32 ETH to stake, but don't feel comfortable dealing with hardware, SaaS services allow you to delegate the hard part while you earn native block rewards. + + + + Deposit your own 32 ETH to activate your own set of signing keys that will participate in Ethereum consensus. Monitor your progress with dashboards to watch those ETH rewards accumulate. + + + Forget about hardware specs, setup, node maintenance and upgrades. + SaaS providers let you to outsource the hard part by uploading your own signing credentials, allowing them to run a validator on your behalf, for a small cost. + + + In many cases users do not have to give up access to the keys that enable withdrawing or transferring staked funds. These are different than the signing keys, and can be stored separately to limit (but not eliminate) your risk as a staker. + + + + + +## What to consider {#what-to-consider} + +There are a growing number of staking-as-a-service providers to help you stake your ETH, but each come with different risks and benefits. + +Attribute indicators are used below to signal notable strengths or weaknesses a listed SaaS provider may have. Use this section as a reference for how we define these attributes while you're choosing a service to help with your staking journey. + + + +## Explore staking service providers {#saas-providers} + +Below are some available SaaS provider. Use the above indicators to help guide you through these services + + +Please note the importance of supporting client diversity as it improves the security of the network, and limits your risk. Services that have evidence of limiting majority client use are marked as "diverse clients." + + +#### SaaS providers + + + +#### Key Generators + + + +Have a suggestion for a staking-as-a-service provider we missed? Check out our [product listing policy](/contributing/adding-staking-product/) to see if it would be a good fit, and to submit it for review. + +## FAQ {#faq} + + + Arrangements will differ from provider-to-provider, but commonly you will be guided through setting up any signing keys you need (one per 32 ETH), and uploading these to your provider to allow them to validate on your behalf. The signing keys alone do not give any ability to withdraw, transfer or spend your funds. However, they do provide the ability to cast votes towards consensus, which if not done properly can result in offline penalties or slashing. + + + +Yes. Each account is comprised of both signing keys, and withdrawal keys. In order for a validator to attest to the state of the chain, participate in sync committees and propose blocks, the signing keys much be readily accessible by a validator client. These must be connected to the internet in some form, and are thus inherently considered to be "hot" keys. This is a requirement for your validator to be able to attest, and thus the keys used to transfer or withdraw funds are separated for security reasons. + +All of these keys can always be regenerated in a reproducible manner using your 24-word mnemonic seed phrase. Make certain you back this seed phrase up safely or you will be unable to generate your withdraw keys when the time comes. + + + + When you stake 32 ETH with a SaaS provider, that ETH is still deposited to the official staking deposit contract. As such, SaaS stakers are currently limited by the same withdrawal restrictions as solo stakers. This means that staking your ETH is currently a one-way deposit. This will be the case until the Shanghai upgrade planned to follow the Merge. + + + + After the Merge, SaaS stakers will begin to receive unburnt transaction fees/tips. Check with your provider to determine how to update your settings to include an Ethereum address you control where these funds will be sent when the time comes. + +The Merge will not enable the ability to withdraw your stake or protocol rewards; this feature is planned for the Shanghai upgrade, which will follow the Merge by an estimated six months to a year. + + + +By using an SaaS provider, you are entrusting the operation of your node to someone else. This comes with the risk of poor node performance, which is not in your control. In the event your validator is slashed, your validator balance will be penalized and forcibly removed from the validator pool. These funds will be locked until withdrawals are enabled at the protocol level. + +Contact individual SaaS provider for more details on any guarantees or insurance options. If you'd prefer to be in full control of your validator setup, learn more about how to solo stake your ETH. + + +## Further reading {#further-reading} + +- [Evaluating Staking Services](https://www.attestant.io/posts/evaluating-staking-services/) - _Jim McDonald 2020_ diff --git a/src/content/staking/solo/index.md b/src/content/staking/solo/index.md new file mode 100644 index 00000000000..232241e2a7a --- /dev/null +++ b/src/content/staking/solo/index.md @@ -0,0 +1,199 @@ +--- +title: Solo stake your ETH +description: An overview of how to get started solo staking your ETH +lang: en +template: staking +emoji: ":money_with_wings:" +sidebar: true +image: ../../../assets/staking/leslie-solo.png +alt: Leslie the rhino on her own computer chip. +sidebarDepth: 2 +summaryPoints: + - Receive maximum rewards directly from the protocol (including unburnt fees after The Merge) for keeping your validator properly functioning and online + - Run home hardware and personally add to the security and decentralization of the Ethereum network + - Remove trust, and never give up control of the keys to your funds +--- + +## What is solo staking? {#what-is-solo-staking} + +Solo staking is the act of [running an Ethereum node](/run-a-node/) connected to the internet and depositing 32 ETH to activate a [validator](#faq), giving you the ability to participate directly in network consensus. + +An Ethereum node consists of both an execution layer (EL) client, as well as a consensus layer (CL) client. These clients are software that work together, along with a valid set of signing keys, to verify transactions and blocks, attest to the correct head of the chain, aggregate attestations, and propose blocks. + +Solo stakers are responsible for operating the hardware needed to run these clients. It is highly recommended to use a dedicated machine for this that you operate from home–this is extremely beneficial to the health of the network. + +A solo staker receives rewards directly from the protocol for keeping their validator properly functioning and online. + +## Why stake solo? {#why-stake-solo} + +Solo staking comes with more responsibility, but provides you with maximum control over your funds and staking setup. + + + + Earn ETH-denominated rewards directly from the protocol when your validator is online, without any middlemen taking a cut. + + + Keep your own keys. Choose the combination of clients and hardware that allows you to minimize your risk and best contribute to the health and security of the network. Third-party staking services make these decisions for you, and they don't always make the safest choices. + + + Solo staking is the most impactful way to stake. By running a validator on your own hardware at home, you strengthen the robustness, decentralization, and security of the Ethereum protocol. + + + +## Considerations before staking solo {#considerations-before-staking-solo} + +As much as we wish that solo staking was accessible and risk free to everyone, this is not reality. There are some practical and serious considerations to keep in mind before choosing to solo stake your ETH. + + + + When operating your own node you should spend some time learning how to use the software you've chosen. This involves reading relevant documentation and being attune to communication channels of those dev teams. + The more you understand about the software you're running and how proof-of-stake works, the less risky it will be as a staker, and the easier it will be to fix any issues that may arise along the way as a node operator. + + + Node setup requires a reasonable comfort level when working with computers, although new tools are making this easier over time. Understanding of the command-line interface is helpful, but no longer strictly required. + It also requires very basic hardware setup, and some understanding of minimum recommended specs. + + + Just like how private keys secure your Ethereum address, you will need to generate keys specifically for your validator. You must understand how to keep any seed phrases or private keys safe and secure. +

Ethereum security and scam prevention

+
+ + Withdrawing staked ETH or rewards from a validator balance is not yet supported. Support for withdrawals are planned for the Shanghai upgrade following The Merge. You should anticipate your ETH being locked for at least one-to-two years. After the Shanghai upgrade you will be able to freely withdraw portions or all of your stake if you wish. + + + Hardware occasionally fails, network connections error out, and client software occasionally needs upgrading. Node maintenance is inevitable and will occasionally require your attention. You'll want to be sure you stay aware of any anticipated network upgrades, or other critical client upgrades. + + + Your rewards are proportional to the time your validator is online and properly attesting. Downtime incurs penalties proportional to how many other validators are offline at the same time, but does not result in slashing. Bandwidth also matters, as rewards are decreased for attestations that are not received in time. Requirements will vary, but a minimum of 10 Mb/s up and down is recommended. + + + Different from inactivity penalties for being offline, slashing is a much more serious penalty reserved for malicious offenses. By running a minority client with your keys loaded on only one machine at time, your risk of being slashed is minimized. That being said, all stakers must be aware of the risks of slashing. + +

More on slashing and validator lifecycle

+
+
+ + + +## How it works {#how-it-works} + + + +If ever desired, you can exit as a validator which eliminates the requirement to be online, and stops any further rewards. Be aware that until the planned Shanghai upgrade _withdrawing_ those funds will not be possible. + +After Shanghai, users will be able to withdraw their rewards as well as their stake if they choose. + +## Get started on the Staking Launchpad {#get-started-on-the-staking-launchpad} + +The Staking Launchpad is an open source application that will help you become a staker. It will guide you through choosing your clients, generate your keys and depositing your ETH to the staking deposit contract. A checklist is provided to make sure you've covered everything to get your validator set up safely. + + + +## What to consider with node and client setup tools {#node-tool-considerations} + +There are a growing number of tools and services to help you solo stake your ETH, but each come with different risks and benefits. + +Attribute indicators are used below to signal notable strengths or weaknesses a listed staking tool may have. Use this section as a reference for how we define these attributes while you’re choosing what tools to help with your staking journey. + + + +## Explore node and client setup tools {#node-and-client-tools} + +There are a variety of options available to help you with your setup. Use the above indicators to help guide you through the tools below. + + +Please note the importance of choosing a minority client as it improves the security of the network, and limits your risk. Tools that allow you to setup minority client are denoted as "multi-client." + + +#### Node tools + + + +#### Key Generators + +These tools can be used as an alternative to the [Staking Deposit CLI](https://github.com/ethereum/staking-deposit-cli/) to help with key generation. + + + +Have a suggestion for a staking tool we missed? Check out our [product listing policy](/contributing/adding-staking-product/) to see if it would be a good fit, and to submit it for review. + +## Explore solo staking guides {#staking-guides} + + + +## FAQ {#faq} + +These are a few of the most common questions about staking that are worth knowing about. + + +A validator is a virtual entity that lives on the Beacon Chain and participates in the consensus of the Ethereum protocol. Validators are represented by a balance, public key, and other properties. A validator client is the software that acts on behalf of the validator by holding and using its private key. A single validator client can hold many key pairs, controlling many validators. + + + +Each key-pair associated with a validator requires exactly 32 ETH to be activated. More ETH deposited to a single set of keys does not increase rewards potential, as each validator is limited to an effective balance of 32 ETH. This means that staking is done in 32 ETH increments, each with it's own set of keys and balance. + +Do not deposit more than 32 ETH for a single validator. It will not increase your rewards, and it will be locked until the planned Shanghai update. + +If solo staking seems too demanding for you, consider using a staking-as-a-service provider, or if you're working with less than 32 ETH, check out the staking pools. + + + +Going offline when the network is finalizing properly will NOT result in slashing. Small inactivity penalties are incurred if your validator is not available to attest for a given epoch (each 6.4 minutes long), but this is very different to slashing. These penalties are slightly less than the reward you would have earned had the validator been available to attest, and losses can be earned back with approximately an equal amount of time back online again. + +Note that penalties for inactivity are proportional to how many validators are offline at the same time. In cases where a large portion of the network is all offline at once, the penalties for each of these validators will be greater than when a single validator is unavailable. + +In extreme cases if the network stops finalizing as a result of more than a third of the validators being offline, these users will suffer what is known as a quadratic inactivity leak, which is an exponential drain of ETH from offline validator accounts. This enables the network to eventually self-heal by burning the ETH of inactive validators until their balance reaches 16 ETH, at which point they will be automatically ejected from the validator pool. The remaining online validators will eventually comprise over 2/3 the network again, satisfying the supermajority needed to once again finalize the chain. + + + +In short, this can never be fully guaranteed, but if you act in good faith, run a minority client and only keep your signing keys on one machine at a time, the risk of getting slashed is nearly zero. + +There are only a few specific ways that can result in a validator getting slashed and ejected from the network. At time of writing, the slashings that have occurred have been exclusively a product of redundant hardware setups where signing keys are stored on two separate machines at once. This can inadvertently result in a double vote from your keys, which is a slashable offense. + +Running a supermajority client (any client used by over 2/3 the network) also holds the risk of potential slashing in the event this client has a bug that results in a chain fork. This can result in a faulty fork that gets finalized. To correct back to the intended chain would require submitting a surround vote by trying to undo a finalized block. This is also a slashable offense and can be avoided simply by running a minority client instead. + +Equivalent bugs in a minority client would never finalize and thus would never result in a surround vote, and would simply result in inactivity penalties, not slashing. + +

Learn more about the important of running a minority client.

+

Learn more about slashing prevention

+
+ + +Yes and no. Staking has been live since December 1, 2020, but until the Merge happens, the proof-of-stake consensus remains isolated on its own chain, while the existing Ethereum network as we know it continues to operate using proof-of-work. These two chains start separate, but with the Merge, proof-of-work will be fully deprecated, and proof-of-stake will become the sole means of consensus from here-on-out. + +This means that staking is currently live for users to deposit their ETH, run a validator client, and start earning rewards. After the Merge, stakers will earn higher rewards as validators begin to process transactions and earn fee tips on top of protocol rewards. After the Shanghai update (planned to follow the Merge by a few months), stakers will then be able to withdraw rewards and funds from their validator balance. + + + +Individual clients may vary slightly in terms of performance and user interface, as each are developed by different teams using a variety of programming languages. That being said, none of them are "best." All production clients are excellent pieces of software, that all perform the same core functions to sync and interact with the blockchain. + +Since all production clients provide the same basic functionality, it is actually very important that you choose a minority client, meaning any client that is NOT currently being used by a majority of validators on the network. This may sound counterintuitive, but running a majority or supermajority client puts you at an increased risk of slashing in the event of a bug in that client. Running a minority client drastically limits these risks. + +Learn more about why client diversity is critical + + + +Although a virtual private server (VPS) can be used as a replacement to home hardware, the physical access and location of your validator client does matter. Centralized cloud solutions such as Amazon Web Services or Digital Ocean allow the convenience of not having to obtain and operate hardware, at the expense of centralizing the network. + +The more validator clients running on a single centralized cloud storage solution, the more dangerous it becomes for these users. Any event that takes these providers offline, whether by an attack, regulatory demands, or just power/internet outages, will result in every validator client that relies on this server to go offline at the same time. + +Offline penalties are proportional to how many others are offline at the same time. Using a VPS greatly increases the risk that offline penalties will be more severe, and increases your risk of quadratic leaking or slashing in the event the outage is large enough. To minimize your own risk, and the risk to the network, users are strongly encouraged to obtain and operate their own hardware. + +More on rewards and penalties + + + +Anyone currently running a CL (Beacon Chain) client will be required to also run an EL client after the merge. This is a result of the new Engine API that will be used to interface between the two layers. Anyone currently running a Beacon Chain without an execution layer client will need to sync the execution layer before the merge to continue being in sync with the network. + +The Merge will also bring unburnt transaction fees to validators. These fees to not accumulate to the balance associated with the validator keys, but instead can be directly to a regular Ethereum address of your choice. In order to properly receive the priority fees/tips from proposed blocks, stakers must update their client settings with the address they would like to receive tips at. See individual client documentation for details. + + +## Further reading {#further-reading} + +- [Ethereum's Client Diversity Problem](https://hackernoon.com/ethereums-client-diversity-problem) - _@emmanuelawosika 2022_ +- [Helping Client Diversity](https://www.attestant.io/posts/helping-client-diversity/) - _Jim McDonald 2022_ +- [Client diversity on Ethereum's consensus layer](https://mirror.xyz/jmcook.eth/S7ONEka_0RgtKTZ3-dakPmAHQNPvuj15nh0YGKPFriA) - _jmcook.eth 2022_ +- [How To: Shop For Ethereum Validator Hardware](https://www.youtube.com/watch?v=C2wwu1IlhDc) - _EthStaker 2022_ +- [Step by Step: How to join the Ethereum 2.0 Testnet](https://kb.beaconcha.in/guides/tutorial-eth2-multiclient) - _Butta_ +- [Eth2 Slashing Prevention Tips](https://medium.com/prysmatic-labs/eth2-slashing-prevention-tips-f6faa5025f50) - _Raul Jordan 2020_ +- [Rewards and Penalties on Ethereum 2.0](https://consensys.net/blog/codefi/rewards-and-penalties-on-ethereum-20-phase-0/) - _James BeckMarch 2020_ diff --git a/src/data/staking-products.json b/src/data/staking-products.json new file mode 100644 index 00000000000..67e9c269989 --- /dev/null +++ b/src/data/staking-products.json @@ -0,0 +1,636 @@ +{ + "nodeTools": [ + { + "name": "Rocket Pool CLI", + "svgPath": "rocket-pool-glyph.svg", + "hue": 379, + "launchDate": "2021-11-08", + "url": "https://rocketpool.net/node-operators", + "audits": [ + { + "name": "Sigma Prime", + "url": "https://rocketpool.net/files/sigma-prime-audit.pdf" + }, + { + "name": "Consensys Diligence", + "url": "https://consensys.net/diligence/audits/2021/04/rocketpool/" + }, + { + "name": "Trail of Bits", + "url": "https://github.com/trailofbits/publications/blob/master/reviews/RocketPool.pdf" + } + ], + "minEth": 16, + "additionalStake": "10", + "additionalStakeUnit": "% RPL", + "tokens": [ + { + "name": "Rocket Pool ETH", + "symbol": "rETH", + "address": "0xae78736Cd615f374D3085123A210448E74Fc6393" + } + ], + "isFoss": true, + "hasBugBounty": true, + "isTrustless": true, + "isPermissionless": true, + "multiClient": true, + "easyClientSwitching": true, + "platforms": ["Linux", "macOS", "Windows"], + "ui": ["CLI"], + "socials": { + "discord": "https://discord.gg/rocketpool", + "twitter": "https://twitter.com/Rocket_Pool", + "github": "https://github.com/rocket-pool" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Rocket Pool go to link" + } + }, + { + "name": "Stereum", + "svgPath": "stereum-glyph.svg", + "hue": 164, + "launchDate": "2021-02-06", + "url": "https://stereum.net/ethereum-node-setup/", + "audits": [ + { + "name": "SBA Research", + "url": "https://stereum.net/stereum-node-setup-security-audit-2021/" + } + ], + "minEth": 32, + "isFoss": true, + "hasBugBounty": false, + "isTrustless": true, + "isPermissionless": true, + "multiClient": true, + "easyClientSwitching": true, + "platforms": ["Linux", "macOS", "Windows"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.com/invite/4vxZnyEDty", + "twitter": "https://twitter.com/stereumdev", + "github": "https://github.com/stereum-dev" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Stereum go to link" + } + }, + { + "name": "DAppNode", + "svgPath": "dappnode-glyph.svg", + "hue": 175, + "launchDate": "2020-12-01", + "url": "https://dappnode.io", + "audits": [], + "minEth": 32, + "isFoss": true, + "hasBugBounty": false, + "isTrustless": true, + "isPermissionless": true, + "multiClient": false, + "easyClientSwitching": false, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.gg/c28an8dA5k", + "twitter": "https://twitter.com/dappnode", + "github": "https://github.com/dappnode" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked DAppNode go to link" + } + }, + { + "name": "Ethereum on Arm", + "svgPath": "default-open-source-glyph.svg", + "hue": 209, + "launchDate": "2021-03-25", + "url": "https://ethereum-on-arm-documentation.readthedocs.io/", + "audits": [], + "minEth": 32, + "isFoss": true, + "hasBugBounty": false, + "isTrustless": true, + "isPermissionless": true, + "multiClient": true, + "easyClientSwitching": false, + "platforms": ["Linux"], + "ui": ["CLI"], + "socials": { + "github": "https://github.com/diglos/ethereumonarm" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Ethereum on Arm go to link" + } + }, + { + "name": "eth-docker", + "svgPath": "docker-icon.svg", + "hue": 221, + "launchDate": "2021-06-05", + "url": "https://eth-docker.net", + "audits": [], + "minEth": 32, + "isFoss": true, + "hasBugBounty": false, + "isTrustless": true, + "isPermissionless": true, + "multiClient": true, + "easyClientSwitching": true, + "platforms": ["Linux"], + "ui": ["CLI"], + "socials": { + "github": "https://github.com/eth-educators/eth-docker" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked eth-docker go to link" + } + }, + { + "name": "Vouch + Dirk", + "svgPath": "default-open-source-glyph.svg", + "hue": 239, + "launchDate": "2020-12-01", + "url": "https://www.attestant.io/posts/introducing-vouch/", + "audits": [ + { + "name": "Leasy Authority", + "url": "https://www.wealdtech.com/articles/ethdo-audit/" + } + ], + "minEth": 32, + "isFoss": true, + "hasBugBounty": false, + "isTrustless": true, + "isPermissionless": true, + "multiClient": true, + "easyClientSwitching": false, + "platforms": ["Linux", "Windows"], + "ui": ["CLI"], + "socials": { + "discord": "https://invite.gg/attestant", + "twitter": "https://twitter.com/attestantio", + "telegram": "https://t.me/attestant", + "github": "https://github.com/attestantio" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Vouch + Dirk go to link" + } + } + ], + "keyGen": [ + { + "name": "Wagyu Key Gen", + "svgPath": "wagyu-glyph.svg", + "hue": 46, + "launchDate": "2021-12-01", + "url": "https://wagyu.gg/", + "audits": [ + { + "name": "HashCloak", + "url": "https://github.com/stake-house/wagyu-key-gen/files/7693548/Wagyu.Key.Gen.Audit.Report.pdf" + } + ], + "isFoss": true, + "hasBugBounty": false, + "isTrustless": true, + "isPermissionless": true, + "isSelfCustody": true, + "platforms": ["Linux", "macOS", "Windows"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.io/ethstaker", + "twitter": "https://twitter.com/wagyutools", + "github": "https://github.com/stake-house/wagyu-key-gen" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Wagyu Key Gen go to link" + } + }, + { + "name": "ethdo", + "svgPath": "default-open-source-glyph.svg", + "hue": 281, + "launchDate": "2020-12-01", + "url": "https://github.com/wealdtech/ethdo", + "audits": [ + { + "name": "Leasy Authority", + "url": "https://www.wealdtech.com/articles/ethdo-audit/" + } + ], + "isFoss": true, + "hasBugBounty": false, + "isTrustless": true, + "isPermissionless": true, + "isSelfCustody": true, + "platforms": ["Linux", "Windows"], + "ui": ["CLI"], + "socials": { + "github": "https://github.com/wealdtech/ethdo" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked ethdo go to link" + } + } + ], + "saas": [ + { + "name": "Allnodes", + "svgPath": "allnodes-glyph.svg", + "hue": 248, + "launchDate": "2020-12-01", + "url": "https://www.allnodes.com/eth2/staking", + "audits": [], + "minEth": 32, + "additionalStake": null, + "additionalStakeUnit": null, + "monthlyFee": 5, + "monthlyFeeUnit": "USD", + "isFoss": false, + "hasBugBounty": false, + "isTrustless": false, + "isPermissionless": false, + "pctMajorityClient": null, + "isSelfCustody": true, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.allnodes.com/", + "twitter": "https://twitter.com/allnodes", + "github": null + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Allnodes go to link" + } + }, + { + "name": "BloxStaking", + "svgPath": "bloxstaking-glyph.svg", + "hue": 341, + "launchDate": "2020-12-08", + "url": "https://www.bloxstaking.com/", + "audits": [ + { + "name": "CoinFabrik", + "url": "https://github.com/bloxapp/ssv-network/blob/main/docs/SSV_Token_Dex%26Vesting_audit.pdf" + } + ], + "minEth": 32, + "additionalStake": null, + "additionalStakeUnit": null, + "monthlyFee": null, + "monthlyFeeUnit": null, + "isFoss": true, + "hasBugBounty": false, + "isTrustless": false, + "isPermissionless": false, + "pctMajorityClient": null, + "isSelfCustody": true, + "platforms": ["Linux", "macOS", "Windows"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.gg/AbYHBfjkDY", + "twitter": "https://twitter.com/ssv_network", + "github": "https://github.com/bloxapp/blox-live" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked BloxStaking go to link" + } + }, + { + "name": "Abyss Finance", + "svgPath": "abyss-glyph.svg", + "hue": 237, + "launchDate": "2020-12-01", + "url": "https://abyss.finance/hosting", + "audits": [], + "minEth": 32, + "additionalStake": "Variable", + "additionalStakeUnit": "ABYSS", + "monthlyFee": null, + "monthlyFeeUnit": null, + "isFoss": false, + "hasBugBounty": false, + "isTrustless": false, + "isPermissionless": true, + "pctMajorityClient": 3.61, + "isSelfCustody": true, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "twitter": "https://twitter.com/AbyssFinance", + "telegram": "https://t.me/abyssfinance", + "github": "https://github.com/abyssfinance" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Abyss Finance go to link" + } + }, + { + "name": "Stakewise Solo", + "svgPath": "stakewise-glyph.svg", + "hue": 220, + "launchDate": "2021-02-01", + "url": "https://app.stakewise.io/solo", + "audits": [], + "minEth": 32, + "additionalStake": null, + "additionalStakeUnit": null, + "monthlyFee": 10, + "monthlyFeeUnit": "USD", + "isFoss": true, + "hasBugBounty": false, + "isTrustless": false, + "isPermissionless": true, + "pctMajorityClient": null, + "isSelfCustody": false, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.gg/8Zf7tKyXeZ", + "twitter": "https://twitter.com/stakewise_io", + "telegram": "https://t.me/stakewise_io", + "github": "https://github.com/stakewise" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Stakewise Solo go to link" + } + } + ], + "pools": [ + { + "name": "Rocket Pool", + "svgPath": "rocket-pool-glyph.svg", + "hue": 14, + "launchDate": "2021-11-08", + "url": "https://rocketpool.net/", + "audits": [ + { + "name": "Sigma Prime", + "url": "https://rocketpool.net/files/sigma-prime-audit.pdf" + }, + { + "name": "Consensys Diligence", + "url": "https://consensys.net/diligence/audits/2021/04/rocketpool/" + }, + { + "name": "Trail of Bits", + "url": "https://github.com/trailofbits/publications/blob/master/reviews/RocketPool.pdf" + } + ], + "minEth": 0.01, + "feePercentage": 15, + "tokens": [ + { + "name": "Rocket Pool ETH", + "symbol": "rETH", + "address": "0xae78736Cd615f374D3085123A210448E74Fc6393" + } + ], + "isFoss": true, + "hasBugBounty": true, + "isTrustless": true, + "hasPermissionlessNodes": true, + "pctMajorityClient": 10.67, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.gg/rocketpool", + "twitter": "https://twitter.com/Rocket_Pool", + "github": "https://github.com/rocket-pool" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Rocket Pool go to link" + } + }, + { + "name": "Lido", + "svgPath": "lido-glyph.svg", + "hue": 210, + "launchDate": "2020-12-17", + "url": "https://lido.fi/", + "audits": [ + { + "name": "Quantstamp", + "url": "https://github.com/lidofinance/audits/blob/main/QSP%20Lido%20Report%2012-2020.pdf" + }, + { + "name": "MixBytes()", + "url": "https://github.com/lidofinance/audits/blob/main/MixBytes%20ETH2%20Oracle%20Security%20Audit%20Report%2004-2021.pdf" + }, + { + "name": "Sigma Prime", + "url": "https://github.com/lidofinance/audits/blob/main/Sigma%20Prime%20-%20Lido%20Finance%20Security%20Assessment%20Report%20v2.1.pdf" + } + ], + "minEth": 0, + "feePercentage": 10, + "tokens": [ + { + "name": "Lido Staked ETH", + "symbol": "stETH", + "address": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + } + ], + "isFoss": true, + "hasBugBounty": true, + "isTrustless": false, + "hasPermissionlessNodes": false, + "pctMajorityClient": 42.83, + "platforms": ["Browser", "Wallet"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.com/invite/vgdPfhZ", + "twitter": "https://twitter.com/lidofinance", + "telegram": "https://t.me/lidofinance", + "reddit": "https://www.reddit.com/r/lidofinance/", + "github": "https://github.com/lidofinance/lido-dao" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Lido go to link" + } + }, + { + "name": "StakeWise Pool", + "svgPath": "stakewise-glyph.svg", + "hue": 220, + "launchDate": "2021-02-01", + "url": "https://app.stakewise.io/", + "audits": [], + "minEth": 0, + "feePercentage": 10, + "tokens": [ + { + "name": "StakeWise Staked ETH2", + "symbol": "sETH2", + "address": "0xFe2e637202056d30016725477c5da089Ab0A043A" + }, + { + "name": "StakeWise Reward ETH2", + "symbol": "rETH2", + "address": "0x20BC832ca081b91433ff6c17f85701B6e92486c5" + } + ], + "isFoss": true, + "hasBugBounty": false, + "isTrustless": false, + "hasPermissionlessNodes": false, + "pctMajorityClient": null, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.com/invite/2BSdr2g", + "twitter": "https://twitter.com/stakewise_io", + "github": "https://github.com/stakewise" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked StakeWise Pool go to link" + } + }, + { + "name": "stakefish", + "svgPath": "stakefish-glyph.svg", + "hue": 73, + "launchDate": "2020-12-01", + "url": "https://stake.fish/ethereum-shared/", + "audits": [ + { + "name": "Solidified", + "url": "https://github.com/solidified-platform/audits/blob/master/Audit%20Report%20-%20Stakefish%20%5B20.09.2021%5D.pdf" + } + ], + "minEth": 0.1, + "feePercentage": 10, + "tokens": [], + "isFoss": false, + "hasBugBounty": false, + "isTrustless": true, + "hasPermissionlessNodes": false, + "pctMajorityClient": 0.0, + "platforms": ["Browser"], + "ui": ["GUI"], + "twitter": "https://twitter.com/stakefish", + "telegram": "https://t.me/stakefish", + "socials": { + "github": null + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked stakefish go to link" + } + }, + { + "name": "Ankr Staking", + "svgPath": "ankr-glyph.svg", + "hue": 221, + "launchDate": "2020-11-20", + "url": "https://stakefi.ankr.com/liquid-staking", + "audits": [], + "minEth": 0.5, + "feePercentage": 15, + "tokens": [ + { + "name": "Ankr ETH2 Reward Earning Bond", + "symbol": "aETHb", + "address": "0xD01ef7C0A5d8c432fc2d1a85c66cF2327362E5C6" + }, + { + "name": "Ankr Eth2 Reward Bearing Certificate", + "symbol": "aETHc", + "address": "0xE95A203B1a91a908F9B9CE46459d101078c2c3cb" + } + ], + "isFoss": true, + "hasBugBounty": false, + "isTrustless": false, + "hasPermissionlessNodes": false, + "pctMajorityClient": 67.94, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "telegram": "https://t.me/stkrsupport", + "twitter": "https://twitter.com/ankr", + "github": "https://github.com/Ankr-network/stkr-smart-contract" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked Ankr Staking go to link" + } + }, + { + "name": "StaFi", + "svgPath": "stafi-glyph.svg", + "hue": 155, + "launchDate": "2020-10-16", + "url": "https://stafi.io", + "audits": [ + { + "name": "PeckShield", + "url": "https://github.com/stafiprotocol/stafi-bootstrap/blob/master/audits/reth/PeckShield-Audit-Report-StaFi-Eth2-Staking-v1.0.pdf" + } + ], + "minEth": 0.01, + "feePercentage": 10, + "tokens": [ + { + "name": "StaFi", + "symbol": "rETH", + "address": "0x9559Aaa82d9649C7A7b220E7c461d2E74c9a3593" + } + ], + "isFoss": true, + "hasBugBounty": false, + "isTrustless": false, + "hasPermissionlessNodes": false, + "pctMajorityClient": null, + "platforms": ["Browser"], + "ui": ["GUI"], + "socials": { + "discord": "https://discord.com/invite/jB77etn", + "telegram": "https://t.co/9MsQSkT34Q", + "twitter": "https://twitter.com/Stafi_Protocol", + "github": "https://github.com/stafiprotocol/stafi-node" + }, + "matomo": { + "eventCategory": "StakingProductCard", + "eventAction": "Clicked", + "eventName": "Clicked StaFi go to link" + } + } + ] +} diff --git a/src/intl/en/common.json b/src/intl/en/common.json index 7f3d966d398..221ba2d2b92 100644 --- a/src/intl/en/common.json +++ b/src/intl/en/common.json @@ -217,6 +217,9 @@ "smart-contracts": "Smart contracts", "stablecoins": "Stablecoins", "staking": "Staking", + "solo": "Solo staking", + "saas": "Staking as a service", + "pools": "Pooled staking", "staking-community-grants": "Staking community grants", "academic-grants-round": "Academic grants round", "summary": "Summary", diff --git a/src/intl/en/page-staking.json b/src/intl/en/page-staking.json index 85bc71fb73f..317173997f0 100644 --- a/src/intl/en/page-staking.json +++ b/src/intl/en/page-staking.json @@ -24,7 +24,7 @@ "page-staking-how-to-stake-desc": "It all depends on how much you are willing to stake. You'll need 32 to become a full validator, but it is possible to stake less.", "page-staking-join": "Join", "page-staking-join-community": "Join the staker community", - "page-staking-join-community-desc": "r/ethstaker is a community for everyone to discuss staking on Ethereum – join for advice, support, and to talk all thing staking.", + "page-staking-join-community-desc": "EthStaker is a community for everyone to discuss and learn about staking on Ethereum. Join tens of thousands of members from around the globe for advice, support, and to talk all thing staking.", "page-staking-less-than": "Less than", "page-staking-link-1": "View backend APIs", "page-staking-meta-description": "An overview of Ethereum staking: the risks, rewards, requirements, and where to do it.", diff --git a/src/pages/run-a-node.js b/src/pages/run-a-node.js index 5cdd4c05e99..24b34fcbcd4 100644 --- a/src/pages/run-a-node.js +++ b/src/pages/run-a-node.js @@ -27,6 +27,7 @@ import { Divider, Page, CardGrid, + InfoGrid, } from "../components/SharedStyledComponents" import ExpandableCard from "../components/ExpandableCard" import ExpandableInfo from "../components/ExpandableInfo" @@ -180,23 +181,6 @@ const StyledExpandableInfo = styled(ExpandableInfo)` } ` -const InfoGrid = styled(CardGrid)` - display: grid; - grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); - gap: 1rem 2rem; - & > div { - height: fit-content; - &:hover { - transition: 0.1s; - transform: scale(1.01); - svg { - transition: 0.1s; - transform: scale(1.1); - } - } - } -` - const ColumnFill = styled.div` line-height: 2; box-sizing: border-box; diff --git a/src/pages/staking/index.js b/src/pages/staking/index.js index a36d20b8287..e52908c4abc 100644 --- a/src/pages/staking/index.js +++ b/src/pages/staking/index.js @@ -1,422 +1,724 @@ -import React, { useState } from "react" -import styled from "styled-components" +import React from "react" import { graphql } from "gatsby" import { useIntl } from "gatsby-plugin-intl" import { getImage } from "gatsby-plugin-image" +import styled from "styled-components" -import { translateMessageId } from "../../utils/translations" -import Translation from "../../components/Translation" -import Breadcrumbs from "../../components/Breadcrumbs" +import ButtonDropdown from "../../components/ButtonDropdown" import ButtonLink from "../../components/ButtonLink" -import Card from "../../components/Card" -import Emoji from "../../components/Emoji" -import GhostCard from "../../components/GhostCard" -import PageHero from "../../components/PageHero" -import InfoBanner from "../../components/InfoBanner" import CalloutBanner from "../../components/CalloutBanner" +import Card from "../../components/Card" import Link from "../../components/Link" - +import PageHero from "../../components/PageHero" import PageMetadata from "../../components/PageMetadata" +import Translation from "../../components/Translation" import { - CardContainer, + CardGrid, Content, - Page, + Page as PageContainer, Divider, } from "../../components/SharedStyledComponents" +import FeedbackCard from "../../components/FeedbackCard" +import ExpandableCard from "../../components/ExpandableCard" +import StakingStatsBox from "../../components/Staking/StakingStatsBox" +import StakingHierarchy from "../../components/Staking/StakingHierarchy" +import StakingHomeTableOfContents from "../../components/Staking/StakingHomeTableOfContents" +import StakingCommunityCallout from "../../components/Staking/StakingCommunityCallout" -const StyledCallout = styled(CalloutBanner)` - margin-left: 0rem; - margin-right: 0rem; +import { translateMessageId } from "../../utils/translations" +import { trackCustomEvent } from "../../utils/matomo" + +const HeroStatsWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + background: ${({ theme }) => theme.colors.layer2Gradient}; + padding-bottom: 2rem; ` -const Row = styled.div` +const Page = styled.div` display: flex; - align-items: flex-start; + justify-content: space-between; + width: 100%; + margin: 0 auto 4rem; + + padding-top: 4rem; @media (max-width: ${(props) => props.theme.breakpoints.l}) { flex-direction: column; } ` -const H2 = styled.h2` - font-size: 1.5rem; - font-style: normal; +const InfoTitle = styled.h2` + font-size: 3rem; font-weight: 700; - line-height: 22px; - letter-spacing: 0px; - margin-top: 0.5rem; + text-align: right; + margin-top: 0rem; + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + text-align: left; + font-size: 2.5rem + display: none; + } ` -const H3 = styled.h3` - margin-top: 0rem; +const StyledButtonDropdown = styled(ButtonDropdown)` + margin-bottom: 2rem; + display: flex; + justify-content: flex-end; + text-align: center; + @media (min-width: ${(props) => props.theme.breakpoints.s}) { + align-self: flex-end; + } ` -const Column = styled.div` - flex: 1 1 33%; - margin-bottom: 1.5rem; - margin-right: 2rem; - width: 100%; +const InfoColumn = styled.aside` + display: flex; + flex-direction: column; + position: sticky; + top: 6.25rem; /* account for navbar */ + height: calc(100vh - 80px); + flex: 0 1 330px; + margin: 0 2rem; @media (max-width: ${(props) => props.theme.breakpoints.l}) { - margin-right: 0rem; - margin-left: 0rem; + display: none; } ` -const StyledCard = styled(Card)` - flex: 1 1 30%; - min-width: 240px; - margin: 1rem; - padding: 1.5rem; +const MobileButton = styled.div` @media (max-width: ${(props) => props.theme.breakpoints.l}) { - flex: 1 1 30%; + background: ${(props) => props.theme.colors.background}; + box-shadow: 0 -1px 0px ${(props) => props.theme.colors.border}; + width: 100%; + bottom: 0; + position: sticky; + padding: 2rem; + z-index: 99; + margin-bottom: 0rem; } ` -const BoxText = styled.div` - font-size: 1.25rem; +const MobileButtonDropdown = styled(StyledButtonDropdown)` + margin-bottom: 0rem; + display: none; + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + display: block; + } ` -const Box = styled.div` - padding: 1.5rem; - border: 1px solid ${(props) => props.theme.colors.border}; - margin: 2rem 4rem; - @media (max-width: ${(props) => props.theme.breakpoints.m}) { - margin: 2rem 0; +// Apply styles for classes within markdown here +const ContentContainer = styled.article` + flex: 1 1 ${(props) => props.theme.breakpoints.l}; + position: relative; + padding: 2rem; + padding-top: 0rem; + gap: 2rem; + display: flex; + flex-direction: column; + align-items: center; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + padding: 0rem; + } + .featured { + padding-left: 1rem; + margin-left: -1rem; + border-left: 1px dotted ${(props) => props.theme.colors.primary}; + } + + .citation { + p { + color: ${(props) => props.theme.colors.text200}; + } + } + h2:first-of-type, + & > div:first-child { + margin-top: 0; + padding-top: 0; } ` -const Vision = styled.div` - margin-top: 4rem; +const ComparisonGrid = styled.div` + display: grid; + grid-column-gap: 3rem; + grid-auto-rows: minmax(64px, auto); + grid-template-columns: repeat(3, 1fr); + grid-template-areas: + "solo-title saas-title pool-title" + "solo-rewards saas-rewards pool-rewards" + "solo-risks saas-risks pool-risks" + "solo-reqs saas-reqs pool-reqs" + "solo-cta saas-cta pool-cta"; + + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + grid-template-columns: 1fr; + grid-template-areas: + "solo-title" + "solo-rewards" + "solo-risks" + "solo-reqs" + "solo-cta" + "saas-title" + "saas-rewards" + "saas-risks" + "saas-reqs" + "saas-cta" + "pool-title" + "pool-rewards" + "pool-risks" + "pool-reqs" + "pool-cta"; + } + + h4 { + color: #787878; + } +` + +const ColorH3 = styled.h3` + grid-area: ${({ color }) => { + switch (color) { + case "stakingGold": + return "solo-title" + case "stakingGreen": + return "saas-title" + case "stakingBlue": + return "pool-title" + default: + return "" + } + }}; + color: ${({ theme, color }) => theme.colors[color]}; ` -const OptionContainer = styled.div` +const ButtonContaier = styled.div` display: flex; - justify-content: center; - margin-bottom: 4rem; - @media (max-width: ${(props) => props.theme.breakpoints.s}) { + gap: 1rem; + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { flex-direction: column; } ` -const Option = styled.div` - border-radius: 32px; - border: 1px solid - ${(props) => - props.isActive ? props.theme.colors.primary : props.theme.colors.text}; - color: ${(props) => - props.isActive ? props.theme.colors.primary : props.theme.colors.text}; - box-shadow: ${(props) => - props.isActive ? props.theme.colors.tableBoxShadow : `none`}; - display: flex; - align-items: center; - padding: 1rem 1.5rem; - margin: 0.5rem; - cursor: pointer; +const StyledButtonLink = styled(ButtonLink)` + @media (max-width: ${({ theme }) => theme.breakpoints.s}) { + width: 100%; + } ` -const OptionText = styled.div` - font-size: 1.5rem; - line-height: 100%; - @media (max-width: ${(props) => props.theme.breakpoints.m}) { - font-size: 1rem; - font-weight: 600; +const StyledCardGrid = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + + @media (max-width: ${({ theme }) => theme.breakpoints.xl}) { + grid-template-columns: 1fr; + } + @media (max-width: ${({ theme }) => theme.breakpoints.l}) { + grid-template-columns: repeat(3, 1fr); + } + @media (max-width: ${({ theme }) => theme.breakpoints.m}) { + grid-template-columns: 1fr; } ` -const StakeContainer = styled.div` - margin: 0 auto; - max-width: ${(props) => props.theme.breakpoints.m}; - display: flex; - flex-direction: column; - text-align: center; +const StyledCard = styled(Card)` + justify-content: flex-start; + h3 { + font-weight: 700; + margin: 0 0 1rem; + } ` -const paths = [ +const benefits = [ { - emoji: ":money_with_wings:", - title: , - description: , + title: "Earn rewards", + emoji: "💰", + description: + "Rewards are given for actions that help the network reach consensus. You'll get rewards for running software that properly batches transactions into new blocks and checks the work of other validators because that's what keeps the chain running securely.", }, { - emoji: ":warning:", - title: , - description: , + title: "Better security", + emoji: ":shield:", + description: + "The network gets stronger against attacks as more ETH is staked, as it then requires more ETH to control a majority of the network. To become a threat, you would need to hold the majority of validators, which means you'd need to control the majority of ETH in the system–that's a lot!", }, { - emoji: ":clipboard:", - title: , - description: , - url: "/developers/docs/apis/backend/#available-libraries", - link: , + title: "More sustainable", + emoji: "🍃", + description: + "Stakers don't need energy-intensive computers to participate in a proof-of-stake system–just a home computer or smartphone. This will make Ethereum better for the environment.", + linkText: "More on Ethereum's energy consumption", + to: "/energy-consumption", }, ] -const StakingPage = ({ data, location }) => { +const StakingPage = ({ data }) => { const intl = useIntl() - const [isSoloStaking, setIsSoloStaking] = useState(true) const heroContent = { - title: translateMessageId("page-staking-title-4", intl), - header: translateMessageId("page-staking-header-1", intl), - subtitle: translateMessageId("page-staking-subtitle", intl), + title: "How to stake your ETH", + header: "Earn rewards while securing Ethereum", + subtitle: + "Staking is a public good for the Ethereum ecosystem. Any user with any amount of ETH can help secure the network and earn rewards in the process.", image: getImage(data.rhino), alt: translateMessageId("page-staking-image-alt", intl), - buttons: [ + buttons: [], + } + + const dropdownLinks = { + text: "Staking Options", + ariaLabel: "Staking options dropdown menu", + items: [ + { + text: "Staking home", + to: "/staking/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked staking home", + }, + }, + { + text: "Solo staking", + to: "/staking/solo/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked solo staking", + }, + }, { - path: "#stake", - content: translateMessageId("page-staking-start", intl), + text: "Staking as a service", + to: "/staking/saas/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked staking as a service", + }, + }, + { + text: "Pooled staking", + to: "/staking/pools/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked pooled staking", + }, }, ], } + // TODO: use translateMessageId() for these strings + const tocItems = { + whatIsStaking: { + id: "what-is-staking", + title: "What is staking?", + }, + whyStakeYourEth: { + id: "why-stake-your-eth", + title: "Why stake your ETH?", + }, + howToStakeYourEth: { + id: "how-to-stake-your-eth", + title: "How to stake your ETH", + }, + comparisonOfOptions: { + id: "comparison-of-options", + title: "Comparison of staking options", + }, + joinTheCommunity: { + id: "join-the-community", + title: translateMessageId("page-staking-join-community", intl), + }, + faq: { + id: "faq", + title: "FAQ", + }, + further: { + id: "further", + title: "Further reading", + }, + } + + const tocArray = Object.keys(tocItems).map((item) => tocItems[item]) + return ( - + - - - - - -

- -

-

- {" "} - - - -

- - {paths.map((path, idx) => ( - - {path.url && {path.link}} - - ))} - -
-
- - - -

- -

-

- {" "} -

-

- -

- - - - - {isSoloStaking && ( - - -

- -

-
- {" "} - - - -
-
-

- -

-

- -

- - - -

- -

-

- -

- - - -
- )} - {!isSoloStaking && ( - -

- -

-

- -

-

- - - -

-

- -

-

- - - -

- -

- -

-
- {" "} -
-
-
- )} -
-
- - -
- - r/ethstaker - -
-
- - - -

- -

+ + + + + + + + Staking with Ethereum + + + + +

+ {tocItems.whatIsStaking.title} +

- {" "} - - - + Staking is the act of locking up ETH to give you the right to + participate in block proposals on the network. Anyone who holds + even a small amount of ETH can consider staking.

-

- + Learn how to get ETH

-

- -

+
+ +

+ {tocItems.whyStakeYourEth.title} +

+ + {benefits.map( + ({ title, description, emoji, linkText, to }, idx) => ( + + {to && {linkText}} + + ) + )} + +
+ +

+ {tocItems.howToStakeYourEth.title} +

- + It all depends on how much you are willing to stake. You'll need + 32 ETH to activate your own validator, but it is possible to stake + less.

-

- -

- + Check out the options below and go for the one that is best for + you, and for the network.

-
- - -

- -

- +
+ + +

+ As you may have noticed, there are many ways to participate in + Ethereum staking. These paths target a wide range of users and + ultimately are each unique and vary in terms of risks, rewards, + and trust assumptions. Some are more decentralized, battle-tested + and/or risky than others. We provide some information on popular + projects in the space, but always do your own research{" "} + before sending ETH anywhere. +

+
+ + +

+ {tocItems.comparisonOfOptions.title} +

+

+ There is no one-size-fits-all solution for staking, and each is + unique. Here we'll compare some of the risks, rewards and + requirements of the different ways you can stake. +

+ + Solo staking +
+

Rewards

+
    +
  • + Maximum rewards - receive full rewards directly from the + protocol +
  • +
  • + You'll get rewards for batching transactions into a new + block or checking the work of other validators to keep the + chain running securely +
  • +
  • + After The Merge you'll receive unburnt transaction fees for + blocks you propose +
  • +
+
+
+

Risks

    +
  • Your ETH is at stake
  • - + There are penalties, which cost ETH, for going offline
  • - + Malicious behavior can result in "slashing" of larger + amounts of ETH and forced ejection from the network +
  • +
+
+
+

Requirements

+
    +
  • You must deposit 32 ETH
  • +
  • + Maintain hardware that runs both an Ethereum execution + client and consensus client while connected to the internet
  • - + The{" "} + + Staking Launchpad + {" "} + will walk you through the process and hardware requirements
  • +
+
+
+ + More on solo staking + +
+ Staking as a service +
+

Rewards

+
  • - + Usually involves full protocol rewards minus monthly fee for + node operations
  • - + Dashboards often available to easily track your validator + client
- - - - -

- -

- - - - - - - - - - - +
+
+

Risks

+
    +
  • + Same risks as solo staking plus counter-party risk of + service provider +
  • +
  • + Use of your signing keys is entrusted to someone else who + could behave maliciously +
  • +
+
+
+

Requirements

+
    +
  • Deposit 32 ETH and generate your keys with assistance
  • +
  • Store your keys securely
  • +
  • + The rest is taken care of, though specific services will + vary +
  • +
+
+
+ + More on staking as a service + +
+ + Pooled staking +
+

Rewards

+
    +
  • + Pooled stakers accrue rewards differently, depending on + which method of pooled staking chosen +
  • +
  • + Many pooled staking services offer one or more liquidity + tokens that represents your staked ETH plus your share of + the validator rewards +
  • +
  • + Liquidity tokens can be held in your own wallet, used in + DeFi and sold if you decide to exit +
  • +
+
+
+

Risks

+
    +
  • Risks vary depending on the method used
  • +
  • + In general, risks consist of a combination of counter-party, + smart contract and execution risk +
  • +
+
+
+

Requirements

+
    +
  • + Lowest ETH requirements, some projects require as little as + 0.01 ETH +
  • +
  • + Deposit directly from your wallet to different pooled + staking platforms or simply trade for one of the staking + liquidity tokens +
  • +
+
+
+ + More on pooled staking + +
+
+
+ + + +

{tocItems.faq.title}

+ + A validator is a virtual entity that lives on the Beacon + Chain, represented by a balance, public key, and other properties. + A validator client is the software that acts on behalf of + the validator by holding and using its private key. A single + validator client can hold many key pairs, controlling many + validators. Each key-pair associated with a validator requires + 32 ETH to be activated. More ETH deposited to a single set of keys + does not increase rewards potential, as each validator is limited + to an{" "} + + effective balance + {" "} + of 32 ETH. This means that staking is done in 32 ETH increments, + each with it's own set of keys and balance. Do not deposit more + than 32 ETH for a single validator. It will be locked until the + planned Shanghai update. If solo staking seems too demanding for + you, consider using a{" "} + staking-as-a-service provider, or + if you're working with less than 32 ETH, check out the{" "} + staking pools. + + + As a validator you have the ability to propose and attest to + blocks for the network. To prevent dishonest behavior users must + have their funds at stake. This allows the protocol to penalize + malicious actors. Staking is a means to keep you honest, as your + actions will have financial consequences. + + +

+ There is no 'Eth2' token native to the protocol, as the native + token ether (ETH) will not change with the transition to + proof-of-stake. +

+

+ There are derivative tokens/tickers that may represent staked + ETH (ie. rETH from Rocket Pool, stETH from Lido, ETH2 from + Coinbase). Learn more about{" "} + staking pools +

+
+
+ +

{tocItems.further.title}

+
    +
  • + + Why Proof of Stake (Nov 2020) + {" "} + - Vitalik Buterin +
  • +
  • + + Serenity Design Rationale + {" "} + - Vitalik Buterin +
  • +
  • + + Proof of Stake FAQ (Dec 2017) + {" "} + - Vitalik Buterin +
  • +
  • + + Eth2 News + {" "} + - Ben Edgington +
  • +
  • + + Finalized no. 33, the Ethereum consensus-layer (Jan 2022) + {" "} + - Danny Ryan +
  • +
  • + + Attestant Posts + +
  • +
  • + + Beaconcha.in Knowledge Base + +
  • +
  • + + Beaconcha.in Community-Contributed Educational Materials + +
  • +
  • + + Ethereum Staking Launchpad FAQ + +
  • +
+
+ + + + + + + +
+ ) } export default StakingPage -export const poolImage = graphql` - fragment poolImage on File { - childImageSharp { - gatsbyImageData( - height: 20 - layout: FIXED - placeholder: BLURRED - quality: 100 - ) - } - } -` - export const query = graphql` { rhino: file(relativePath: { eq: "upgrades/upgrade_rhino.png" }) { @@ -429,16 +731,5 @@ export const query = graphql` ) } } - consensys: file(relativePath: { eq: "projects/consensys.png" }) { - ...poolImage - } - ethhub: file(relativePath: { eq: "projects/ethhub.png" }) { - ...poolImage - } - etherscan: file( - relativePath: { eq: "projects/etherscan-logo-circle.png" } - ) { - ...poolImage - } } ` diff --git a/src/styles/layout.css b/src/styles/layout.css index 64f1d241a67..bfb71166db9 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -197,6 +197,7 @@ html { } * { box-sizing: inherit; + scroll-margin-top: 6rem; } *:before { box-sizing: inherit; diff --git a/src/templates/staking.js b/src/templates/staking.js new file mode 100644 index 00000000000..d4ac0482bd1 --- /dev/null +++ b/src/templates/staking.js @@ -0,0 +1,494 @@ +import React from "react" +import { graphql } from "gatsby" +import { MDXRenderer } from "gatsby-plugin-mdx" +import { GatsbyImage, getImage } from "gatsby-plugin-image" +import { MDXProvider } from "@mdx-js/react" +import styled from "styled-components" + +import ButtonLink from "../components/ButtonLink" +import ButtonDropdown from "../components/ButtonDropdown" +import Card from "../components/Card" +import ExpandableCard from "../components/ExpandableCard" +import DocLink from "../components/DocLink" +import Contributors from "../components/Contributors" +import SharedInfoBanner from "../components/InfoBanner" +import UpgradeStatus from "../components/UpgradeStatus" +import Link from "../components/Link" +import MarkdownTable from "../components/MarkdownTable" +import Logo from "../components/Logo" +import MeetupList from "../components/MeetupList" +import PageMetadata from "../components/PageMetadata" +import Pill from "../components/Pill" +import RandomAppList from "../components/RandomAppList" +import Roadmap from "../components/Roadmap" +import UpgradeTableOfContents from "../components/UpgradeTableOfContents" +import TableOfContents from "../components/TableOfContents" +import TranslationsInProgress from "../components/TranslationsInProgress" +// import Translation from "../components/Translation" +import FeedbackCard from "../components/FeedbackCard" +import SectionNav from "../components/SectionNav" +import { + Divider, + Paragraph, + Header1, + Header4, + InfoGrid, +} from "../components/SharedStyledComponents" +import Emoji from "../components/Emoji" +import YouTube from "../components/YouTube" +import Breadcrumbs from "../components/Breadcrumbs" +import StakingLaunchpadWidget from "../components/Staking/StakingLaunchpadWidget" +import StakingProductsCardGrid from "../components/Staking/StakingProductsCardGrid" +import StakingComparison from "../components/Staking/StakingComparison" +import StakingHowSoloWorks from "../components/Staking/StakingHowSoloWorks" +import StakingConsiderations from "../components/Staking/StakingConsiderations" +import StakingCommunityCallout from "../components/Staking/StakingCommunityCallout" +import StakingGuides from "../components/Staking/StakingGuides" + +import { isLangRightToLeft } from "../utils/translations" + +const Page = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + margin: 0 auto 4rem; + + @media (min-width: ${(props) => props.theme.breakpoints.l}) { + padding-top: 4rem; + } + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + flex-direction: column; + } +` + +const InfoColumn = styled.aside` + display: flex; + flex-direction: column; + position: sticky; + top: 6.25rem; /* account for navbar */ + height: calc(100vh - 80px); + flex: 0 1 330px; + margin: 0 2rem; + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + display: none; + } +` + +const MobileButton = styled.div` + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + background: ${(props) => props.theme.colors.background}; + box-shadow: 0 -1px 0px ${(props) => props.theme.colors.border}; + width: 100%; + bottom: 0; + position: sticky; + padding: 2rem; + z-index: 99; + margin-bottom: 0rem; + } +` + +// Apply styles for classes within markdown here +const ContentContainer = styled.article` + flex: 1 1 ${(props) => props.theme.breakpoints.l}; + position: relative; + padding: 2rem; + padding-top: 0rem; + + @media (min-width: ${({ theme }) => theme.breakpoints.l}) { + h2:first-of-type { + margin-top: 0; + } + } + + .featured { + padding-left: 1rem; + margin-left: -1rem; + border-left: 1px dotted ${(props) => props.theme.colors.primary}; + } + + .citation { + p { + color: ${(props) => props.theme.colors.text200}; + } + } +` + +const Pre = styled.pre` + max-width: 100%; + overflow-x: scroll; + background-color: ${(props) => props.theme.colors.preBackground}; + border-radius: 0.25rem; + padding: 1rem; + border: 1px solid ${(props) => props.theme.colors.preBorder}; + white-space: pre-wrap; +` + +const InfoTitle = styled.h2` + font-size: 3rem; + font-weight: 700; + text-align: right; + margin-top: 0rem; + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + text-align: left; + font-size: 2.5rem + display: none; + } +` + +const H2 = styled.h2` + font-size: 2rem; + font-weight: 700; + margin-top: 4rem; + + a { + display: none; + } + + /* Anchor tag styles */ + + a { + position: relative; + display: initial; + opacity: 0; + margin-left: -1.5em; + padding-right: 0.5rem; + font-size: 1rem; + vertical-align: middle; + &:hover { + display: initial; + fill: ${(props) => props.theme.colors.primary}; + opacity: 1; + } + } + + &:hover { + a { + display: initial; + fill: ${(props) => props.theme.colors.primary}; + opacity: 1; + } + } +` + +const H3 = styled.h3` + font-size: 1.5rem; + font-weight: 700; + + a { + display: none; + } + + /* Anchor tag styles */ + + a { + position: relative; + display: initial; + opacity: 0; + margin-left: -1.5em; + padding-right: 0.5rem; + font-size: 1rem; + vertical-align: middle; + &:hover { + display: initial; + fill: ${(props) => props.theme.colors.primary}; + opacity: 1; + } + } + + &:hover { + a { + display: initial; + fill: ${(props) => props.theme.colors.primary}; + opacity: 1; + } + } +` + +const CardGrid = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + + gap: 2rem; + h3 { + margin-top: 0; + } + + @media (max-width: ${(props) => props.theme.breakpoints.m}) { + grid-template-columns: repeat(1, 1fr); + margin: auto; + } +` + +const Title = styled.h1` + font-size: 2.5rem; + font-weight: 700; + margin-top: 1rem; +` + +const SummaryPoint = styled.li` + color: ${(props) => props.theme.colors.text300}; +` + +const StyledButtonDropdown = styled(ButtonDropdown)` + margin-bottom: 2rem; + display: flex; + justify-content: flex-end; + text-align: center; + @media (min-width: ${(props) => props.theme.breakpoints.s}) { + align-self: flex-end; + } +` + +const MobileButtonDropdown = styled(StyledButtonDropdown)` + margin-bottom: 0rem; + display: none; + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + display: block; + } +` + +const Container = styled.div` + position: relative; +` + +const HeroContainer = styled.div` + display: flex; + align-items: center; + padding: 2rem; + --height: 500px; + max-height: var(--height); + min-height: var(--height); + background: ${({ theme }) => theme.colors.layer2Gradient}; + @media (max-width: ${(props) => props.theme.breakpoints.l}) { + flex-direction: column; + max-height: 100%; + padding-left: 0; + padding-right: 0; + } +` + +const Image = styled(GatsbyImage)` + flex: 1 1 100%; + background-repeat: no-repeat; + right: 0; + bottom: 0; + max-width: 400px; + @media (max-width: ${({ theme }) => theme.breakpoints.l}) { + width: 100%; + height: 100%; + max-height: 340px; + max-width: min(400px, 98%); + overflow: initial; + align-self: center; + margin: 0; + } +` + +const MobileTableOfContents = styled(TableOfContents)` + position: relative; + z-index: 2; +` + +const TitleCard = styled.div` + padding: 2rem; + display: flex; + flex-direction: column; + justify-content: flex-start; + width: 100%; +` + +const InfoBanner = styled(SharedInfoBanner)` + margin: 2rem 0; +` + +// Note: you must pass components to MDXProvider in order to render them in markdown files +// https://www.gatsbyjs.com/plugins/gatsby-plugin-mdx/#mdxprovider +const components = { + a: Link, + h1: Header1, + h2: H2, + h3: H3, + h4: Header4, + p: Paragraph, + pre: Pre, + table: MarkdownTable, + MeetupList, + RandomAppList, + Roadmap, + Logo, + ButtonLink, + Contributors, + InfoBanner, + Card, + CardGrid, + InfoGrid, + Divider, + SectionNav, + Pill, + TranslationsInProgress, + Emoji, + UpgradeStatus, + DocLink, + ExpandableCard, + YouTube, + StakingLaunchpadWidget, + StakingProductsCardGrid, + StakingComparison, + StakingHowSoloWorks, + StakingConsiderations, + StakingGuides, +} + +const StakingPage = ({ data, pageContext, location }) => { + const mdx = data.pageData + const isRightToLeft = isLangRightToLeft(mdx.frontmatter.lang) + const tocItems = mdx.tableOfContents.items + const { summaryPoints } = mdx.frontmatter + + // const { editContentUrl } = data.siteData.siteMetadata + // const { relativePath } = pageContext + // const absoluteEditPath = `${editContentUrl}${relativePath}` + + const dropdownLinks = { + text: "Staking Options", + ariaLabel: "Staking options dropdown menu", + items: [ + { + text: "Staking home", + to: "/staking/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked staking home", + }, + }, + { + text: "Solo staking", + to: "/staking/solo/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked solo staking", + }, + }, + { + text: "Staking as a service", + to: "/staking/saas/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked staking as a service", + }, + }, + { + text: "Pooled staking", + to: "/staking/pools/", + matomo: { + eventCategory: `Staking dropdown`, + eventAction: `Clicked`, + eventName: "clicked pooled staking", + }, + }, + ], + } + + return ( + + + + + {mdx.frontmatter.title} +
    + {summaryPoints.map((point, idx) => ( + {point} + ))} +
+ +
+ {mdx.frontmatter.alt} +
+ + + + + {mdx.frontmatter.title} + + {mdx.frontmatter.sidebar && tocItems && ( + + )} + + + + {mdx.body} + + + + + + + + +
+ ) +} + +export const stakingPageQuery = graphql` + query StakingPageQuery($relativePath: String) { + siteData: site { + siteMetadata { + editContentUrl + } + } + pageData: mdx(fields: { relativePath: { eq: $relativePath } }) { + fields { + slug + } + frontmatter { + title + description + lang + sidebar + emoji + sidebarDepth + summaryPoints + alt + image { + childImageSharp { + gatsbyImageData( + width: 500 + layout: CONSTRAINED + placeholder: BLURRED + quality: 100 + ) + } + } + } + body + tableOfContents + parent { + ... on File { + mtime + fields { + gitLogLatestDate + } + } + } + } + } +` + +export default StakingPage diff --git a/src/theme.js b/src/theme.js index aa521f7eeac..8820f525aca 100644 --- a/src/theme.js +++ b/src/theme.js @@ -122,7 +122,7 @@ const baseColors = { tagOrange: primaryDark100, tagGreen: success100, tagRed: fail100, - tagTurqouise: turquoise, + tagTurquoise: turquoise, tagGray: white700, tagYellow: yellow, tagMint: mint, @@ -237,6 +237,17 @@ const lightColors = { codeBackground: codeBoxLight, rollupDevDocList: primaryLight50, beta: "radial-gradient(25.56% 133.51% at 28.36% 45.54%, rgba(28, 28, 225, 0) 0%, rgba(28, 28, 225, 0.06) 100%)", + offBackground: "#f7f7f7", + stakingPillPlatform: "#cd9df3", + stakingPillUI: "#ebd27a", + stakingGold: "#be8d10", + stakingGoldFill: "#fef9ef", + stakingGreen: "#129e5b", + stakingGreenFill: "#f7faf1", + stakingBlue: "#0b83dc", + stakingBlueFill: "#f1fcf5", + stakingRed: "#a0524c", + stakingRedFill: "#f8fbf9", layer2Gradient: "linear-gradient(85.12deg, rgba(185, 185, 241, 0.2) 0%, rgba(84, 132, 234, 0.2) 56.29%, rgba(58, 142, 137, 0.2) 99.99%)", } @@ -328,6 +339,17 @@ const darkColors = { beta: "background: radial-gradient(25.56% 133.51% at 28.36% 45.54%, rgba(255, 143, 80, 0.72) 0%, rgba(255, 143, 80, 0.22) 100%)", cardGradient: "linear-gradient(49.21deg, rgba(127, 127, 213, 0.2) 19.87%, rgba(134, 168, 231, 0.2) 58.46%, rgba(145, 234, 228, 0.2) 97.05% )", + offBackground: "#181818", + stakingPillPlatform: "#cd9df3", + stakingPillUI: "#ebd27a", + stakingGold: "#F2BB2F", + stakingGoldFill: "#373228", + stakingGreen: "#49DE96", + stakingGreenFill: "#30342b", + stakingBlue: "#A9D3F2", + stakingBlueFill: "#2b352f", + stakingRed: "#D6BBB9", + stakingRedFill: "#313432", layer2Gradient: "linear-gradient(83.46deg, rgba(127, 127, 213, 0.2) 7.03%, rgba(138, 168, 231, 0.2) 52.42%, rgba(145, 234, 228, 0.2) 98.77%), #1E1E1E", } diff --git a/src/utils/calculateStakingRewards.js b/src/utils/calculateStakingRewards.js new file mode 100644 index 00000000000..44cf1e49d94 --- /dev/null +++ b/src/utils/calculateStakingRewards.js @@ -0,0 +1,43 @@ +const calculateStakingRewards = (totalAtStake) => { + const slotTimeInSec = 12 + const slotsInEpoch = 32 + const baseRewardFactor = 64 + const averageNetworkPctOnline = 0.95 + const vaildatorUptime = 0.99 + const validatorDeposit = 32 // ETH + const effectiveBalanceIncrement = 1_000_000_000 // gwei + const weightDenominator = 64 + const proposerWeight = 8 + + // Calculate number of epochs per year + const avgSecInYear = 31556908.8 // 60 * 60 * 24 * 365.242 + const epochPerYear = avgSecInYear / (slotTimeInSec * slotsInEpoch) + const baseRewardPerIncrement = + (effectiveBalanceIncrement * baseRewardFactor) / + (totalAtStake * 10e8) ** 0.5 + + // Calculate base reward for full validator (in gwei) + const baseGweiRewardFullValidator = + ((validatorDeposit * 10e8) / effectiveBalanceIncrement) * + baseRewardPerIncrement + + // Calculate offline per-validator penalty per epoch (in gwei) + // Note: Inactivity penalty is not included in this simple calculation + const offlineEpochGweiPentalty = + baseGweiRewardFullValidator * + ((weightDenominator - proposerWeight) / weightDenominator) + + // Calculate online per-validator reward per epoch (in gwei) + const onlineEpochGweiReward = + baseGweiRewardFullValidator * averageNetworkPctOnline + + // Calculate net yearly staking reward (in gwei) + const reward = onlineEpochGweiReward * vaildatorUptime + const penalty = offlineEpochGweiPentalty * (1 - vaildatorUptime) + const netRewardPerYear = epochPerYear * (reward - penalty) + + // Return net yearly staking reward percentage + return netRewardPerYear / 10e8 / validatorDeposit +} + +export default calculateStakingRewards