diff --git a/web/src/components/CurrencyValue.js b/web/src/components/CurrencyValue.js index c4dd01e..e553f2e 100644 --- a/web/src/components/CurrencyValue.js +++ b/web/src/components/CurrencyValue.js @@ -26,6 +26,7 @@ const typeVariantBySize = { }; export default function CurrencyValue({ value = ethers.constants.Zero, + valueColor = (theme) => theme.palette.text.primary, prefix = "", currency = "eth", perCurrency = null, @@ -62,10 +63,7 @@ export default function CurrencyValue({ spacing={typeVariants.spacing} {...props} > - theme.palette.text.primary} - > + {prefix} {valueText} diff --git a/web/src/components/NodeRewardsSummaryCard.js b/web/src/components/NodeRewardsSummaryCard.js index 64dab4f..3c92219 100644 --- a/web/src/components/NodeRewardsSummaryCard.js +++ b/web/src/components/NodeRewardsSummaryCard.js @@ -11,6 +11,7 @@ import { CircularProgress, Divider, FormHelperText, + Grid, IconButton, Stack, Tooltip, @@ -36,8 +37,6 @@ import { useEnsName } from "wagmi"; import { Link } from "react-router-dom"; import { AllInclusive, - Done, - Error, EventRepeat, Help, OpenInNew, @@ -200,19 +199,11 @@ function SummaryCardHeader({ asLink, nodeAddress }) { , + under: , close: , - over: , }[rplStatus] } label={ @@ -584,155 +575,109 @@ function RplPriceRangeAxis({ sx, nodeAddress }) { const theme = useTheme(); const { data: details } = useNodeDetails({ nodeAddress }); const rplEthPrice = useRplEthPrice(); - let { rplStake, minimumRPLStake, maximumRPLStake } = details || { + const rplStatus = useNodeRplStatus({ nodeAddress }); + let { rplStake, minimumRPLStake } = details || { rplStake: ethers.constants.Zero, minimumRPLStake: ethers.constants.Zero, - maximumRPLStake: ethers.constants.Zero, }; let rplStakeOrOne = rplStake.isZero() ? ethers.constants.One : rplStake; return ( - - + + <-> - - - ETH - - / - - RPL - - + - + + + ETH + + / + + RPL + + ); } function RplStakeEthRangeAxis({ sx, nodeAddress }) { - const theme = useTheme(); - const minipools = useMinipoolDetails(nodeAddress); const { data: details } = useNodeDetails({ nodeAddress }); let rplStatus = useNodeRplStatus({ nodeAddress }); - let { ethMatched } = details || { + const rplEthPrice = useRplEthPrice(); + let { minimumRPLStake, ethMatched } = details || { + minimumRPLStake: ethers.constants.Zero, ethMatched: ethers.constants.Zero, - minipoolCount: ethers.constants.Zero, }; - const ethSupplied = bnSum( - (minipools || []) - .filter((mp) => !mp.isFinalized) - .map((mp) => mp.nodeDepositBalance) - ); + let decayThresholdRPLStake = + minimumRPLStake?.mul(3).div(2) || ethers.constants.Zero; + const decayThresholdRPLStakeEth = decayThresholdRPLStake + ?.mul(rplEthPrice) + .div(ethers.constants.WeiPerEther); return ( - <> - - - - 10% of{" "} - - {trimValue(ethers.utils.formatUnits(ethMatched), { - maxDecimals: 0, - })} - - - - borrowed - - - - ETH + + + + + borrowed - - - 150% of{" "} - - {trimValue(ethers.utils.formatUnits(ethSupplied), { - maxDecimals: 0, - })} - - - - supplied - - {/**/} - - + @@ -763,21 +708,102 @@ function RplStakeEthRangeAxis({ sx, nodeAddress }) { sx={{ width: 95, opacity: 0.8, - ...(rplStatus === "over" + ...(rplStatus === "excess" ? { opacity: 1, } : {}), }} - justifyContent="end" + justifyContent="start" key="max" - value={ethSupplied.mul(3).div(2)} - maxDecimals={0} + value={decayThresholdRPLStakeEth} + maxDecimals={2} currency="eth" size="small" /> - + + + 10% + + + 15% + + + + ); +} + +function decayedRplWeight({ rplStake, ethMatched, rplPrice }) { + // See RPIP-30 + const weight = + (13.6137 + 2 * Math.log((100 * rplStake * rplPrice) / ethMatched - 13)) * + ethMatched; + return weight; +} + +function DecayingRplYieldCurve(props) { + const { + x, + y, + width, + height, + fill, + opacity, + decayFill, + rplStake, + minRplPrice, + maxRplPrice, + ethMatched, + } = props; + const weightAtPrice = (rplPrice) => + decayedRplWeight({ + ethMatched: ethMatched.div(ethers.constants.WeiPerEther).toNumber(), + rplStake: rplStake.div(ethers.constants.WeiPerEther).toNumber(), + rplPrice, + }); + const maxWeight = weightAtPrice(minRplPrice); + const yAtPrice = (price) => + ((weightAtPrice(price) - maxWeight) / maxWeight) * height + y; + const xAtPrice = (price) => + ((price - minRplPrice) / (maxRplPrice - minRplPrice)) * width + x; + const curvePointCount = 20; // enough to make it smooth-ish + const curvePricePoints = Array(curvePointCount) + .fill(minRplPrice) + .map( + (price, i) => + price + ((maxRplPrice - minRplPrice) * i) / (curvePointCount - 1) + ); + return ( + + `L ${xAtPrice(price)} ${yAtPrice(price)}`) + ) + .concat([`L ${xAtPrice(maxRplPrice)} ${yAtPrice(minRplPrice)}`, "Z"]) + .join(" ")} + fill={decayFill} + opacity={opacity} + /> + `L ${xAtPrice(price)} ${yAtPrice(price)}`) + ) + .concat([`L ${xAtPrice(maxRplPrice)} ${y + height}`, "Z"]) + .join(" ")} + fill={fill} + opacity={opacity} + /> + ); } @@ -786,14 +812,16 @@ function RplStakeChart({ sx, nodeAddress }) { const { data: details } = useNodeDetails({ nodeAddress }); const rplEthPrice = useRplEthPrice(); let rplStatus = useNodeRplStatus({ nodeAddress }); - let { rplStake, minimumRPLStake, maximumRPLStake } = details || { + let { ethMatched, rplStake, minimumRPLStake } = details || { + ethMatched: ethers.constants.Zero, rplStake: ethers.constants.Zero, minimumRPLStake: ethers.constants.Zero, - maximumRPLStake: ethers.constants.Zero, }; const rplStakeEth = rplStake ?.mul(rplEthPrice) .div(ethers.constants.WeiPerEther); + let decayThresholdRPLStake = + minimumRPLStake?.mul(3).div(2) || ethers.constants.Zero; const minRplPrice = rplStake.isZero() ? 0 : Number( @@ -803,12 +831,22 @@ function RplStakeChart({ sx, nodeAddress }) { .div(rplStake.isZero() ? ethers.constants.One : rplStake) ) ); + const decayThresholdRplPrice = rplStake.isZero() + ? 0 + : Number( + ethers.utils.formatUnits( + decayThresholdRPLStake + .mul(rplEthPrice) + .div(rplStake.isZero() ? ethers.constants.One : rplStake) + ) + ); const rplPrice = Number(ethers.utils.formatUnits(rplEthPrice)) || 0; + const maxRplStake = decayThresholdRPLStake.mul(20).div(12); const maxRplPrice = rplStake.isZero() ? 0 : Number( ethers.utils.formatUnits( - maximumRPLStake + (maxRplStake.lt(rplStake) ? rplStake : maxRplStake) .mul(rplEthPrice) .div(rplStake.isZero() ? ethers.constants.One : rplStake) ) @@ -824,6 +862,11 @@ function RplStakeChart({ sx, nodeAddress }) { rplPrice: rplPrice, value: 1, }, + { + name: "decay", + rplPrice: decayThresholdRplPrice, + value: 1, + }, { name: "max", rplPrice: maxRplPrice, @@ -840,9 +883,22 @@ function RplStakeChart({ sx, nodeAddress }) { ethers.utils.formatUnits(rplStakeEth), { maxDecimals: 2 } ); + const percentEffective = { + under: () => 0, + close: () => 1, + excess: () => + decayedRplWeight({ + ethMatched: ethMatched.div(ethers.constants.WeiPerEther).toNumber(), + rplStake: rplStake.div(ethers.constants.WeiPerEther).toNumber(), + rplPrice, + }) / + 100 / + (rplStakeEth.div(ethers.constants.WeiPerEther).toNumber() || 0.00001), + optimal: () => 1, + }[rplStatus]?.(); return ( - + ≈ } value={rplStakeEth} @@ -863,37 +919,41 @@ function RplStakeChart({ sx, nodeAddress }) { size="small" /> - , - close: , - over: , - effective: , - }[rplStatus] - } - label={ - - {rplStatus} - - } - /> - - + + + + + + + below RPL reward threshold + + ) : ( + + {(100 * percentEffective).toFixed(0)}% effective + + ) + } + /> + + + + + + - - + + + + @@ -1000,6 +1057,12 @@ function RplStakeChart({ sx, nodeAddress }) { stroke={theme.palette.text.secondary} opacity={0.3} /> + + } /> {/**/} diff --git a/web/src/hooks/useNodeRplStatus.js b/web/src/hooks/useNodeRplStatus.js index 39023f8..a84fe87 100644 --- a/web/src/hooks/useNodeRplStatus.js +++ b/web/src/hooks/useNodeRplStatus.js @@ -3,19 +3,17 @@ import { ethers } from "ethers"; export default function useNodeRplStatus({ nodeAddress }) { const { data: details } = useNodeDetails({ nodeAddress }); - let { rplStake, minimumRPLStake, maximumRPLStake } = details || { + let { rplStake, minimumRPLStake } = details || { rplStake: ethers.constants.Zero, minimumRPLStake: ethers.constants.Zero, - maximumRPLStake: ethers.constants.Zero, }; - // rplStake = rplStake.div(ethers.BigNumber.from(2)); return !rplStake - ? "effective" + ? "optimal" : rplStake?.lte(minimumRPLStake) ? "under" - : rplStake?.lte(minimumRPLStake.mul(3).div(2)) + : rplStake?.lte(minimumRPLStake.mul(11).div(10)) ? "close" - : rplStake?.gt(maximumRPLStake) - ? "over" - : "effective"; + : rplStake?.gt(minimumRPLStake.mul(3).div(2)) + ? "excess" + : "optimal"; }