diff --git a/.github/workflows/auto_check.yml b/.github/workflows/auto_check.yml index 40f3473..3d9ff39 100644 --- a/.github/workflows/auto_check.yml +++ b/.github/workflows/auto_check.yml @@ -5,13 +5,18 @@ on: - cron: "00 */4 * * *" push: branches: - - "master" + - "main" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: npx @dappnode/dappnodesdk github-action bump-upstream env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/build/api/src/index.ts b/build/api/src/index.ts index 3a21cd9..950dba3 100644 --- a/build/api/src/index.ts +++ b/build/api/src/index.ts @@ -3,7 +3,10 @@ import dotenv from "dotenv"; import shelljs from "shelljs"; import cors from "cors"; import appConfig from "./AppConfig"; -const { API_PORT = 3000 } = process.env; +const { API_PORT = 3000, ROCKETPOOL_API_URL = "http://127.0.0.1:8280" } = process.env; + +const ROCKETPOOL_SETTINGS = "/app/rocketpool/user-settings.yml"; +const WEI_PER_ETH = BigInt("1000000000000000000"); dotenv.config(); @@ -46,41 +49,193 @@ app.get("/api/v1/w3s-status", async (req, res) => { } }); -app.get("/api/v1/version", (req: Request, res: Response) => { - var version = shelljs.exec(`/usr/local/bin/rocketpoold --version`).stdout; - res.send(version); +app.get("/api/v1/version", async (req: Request, res: Response) => { + res.send(await callRocketpoolApi("/api/version")); +}); + +app.get("/api/v1/megapool/next-validator-bond", async (req: Request, res: Response) => { + try { + const status = await callRocketpoolApi("/api/megapool/status"); + if (status.status !== "success") { + res.send(status); + return; + } + + const megapool = status.megapoolDetails ?? {}; + const activeValidatorCount = Number(megapool.activeValidatorCount ?? 0); + const nodeBond = BigInt(megapool.nodeBond ?? 0); + const nodeQueuedBond = BigInt(megapool.nodeQueuedBond ?? 0); + const bondedEth = nodeBond + nodeQueuedBond; + + const bondRequirement = await callRocketpoolApi( + "/api/node/get-bond-requirement", + { numValidators: String(activeValidatorCount + 1) } + ); + if (bondRequirement.status !== "success") { + res.send(bondRequirement); + return; + } + + let nextValidatorBond = BigInt(bondRequirement.bondRequirement ?? 0) - bondedEth; + if (nextValidatorBond < WEI_PER_ETH) { + nextValidatorBond = WEI_PER_ETH; + } + if (nextValidatorBond > BigInt(32) * WEI_PER_ETH) { + nextValidatorBond = BigInt(32) * WEI_PER_ETH; + } + + res.send({ + status: "success", + error: "", + bondRequirement: nextValidatorBond.toString(), + activeValidatorCount, + nodeBond: nodeBond.toString(), + nodeQueuedBond: nodeQueuedBond.toString(), + megapoolDeployed: megapool.deployed ?? false, + }); + } catch (error) { + console.log(error); + res.send({ status: "error", error: String(error) }); + } }); // POST /api/v1/rocketpool-command-custom -app.post("/api/v1/rocketpool-command-custom", (req: Request, res: Response) => { +app.post("/api/v1/rocketpool-command-custom", async (req: Request, res: Response) => { console.log(req.body.cmd); + // Keep the advanced shell as an explicit escape hatch for now, but use the + // v1.20 CLI (no removed `api` subcommand). Main UI calls use the HTTP API. var result = shelljs.exec( - `/usr/local/bin/rocketpoold --settings /app/rocketpool/user-settings.yml api ${req.body.cmd}` + `/usr/local/bin/rocketpool --settings ${ROCKETPOOL_SETTINGS} ${req.body.cmd}` ).stdout; res.send(result); }); // POST /api/v1/rocketpool-command -app.post("/api/v1/rocketpool-command", (req: Request, res: Response) => { +app.post("/api/v1/rocketpool-command", async (req: Request, res: Response) => { console.log(req.body.cmd); - res.send(executeCommand(req.body.cmd)); + res.send(await executeCommand(req.body.cmd)); }); -// function that executes the command using shelljs -function executeCommand(cmd: string) { - var result = shelljs.exec( - `/usr/local/bin/rocketpoold --settings /app/rocketpool/user-settings.yml api ${cmd}` - ).stdout; - // conver result to json and check if result response "status":"success" - var resultJson = JSON.parse(result); +async function executeCommand(cmd: string) { + const resultJson = await executeRocketpoolCommand(cmd); if (resultJson.status == "success") { if (cmd.startsWith("node deposit")) { - // {"status":"success","error":"","txHash":"0x72162c8ac6b6fd9afe7c5b95166b7dc9c20df10031404fb09ad32db4bd9400c9","minipoolAddress":"0x14866919e7043288676eca918f6fc40d6f4616e0","validatorPubkey":"8eaccddb3ff58d68be44c1302b58d747350ef7e63c6c5fd555d356b8ab41f7cd61bb94f315897558c5d193324eec6ebc","scrubPeriod":3600000000000} - executeCommand(`wait ${resultJson.txHash}`); - importKey(`0x${resultJson.validatorPubkey}`); + await executeCommand(`wait ${resultJson.txHash}`); + const validatorPubkeys = resultJson.validatorPubkeys ?? []; + for (const validatorPubkey of validatorPubkeys) { + await importKey(ensureHexPrefix(validatorPubkey)); + } } } - return result; + return resultJson; +} + +async function executeRocketpoolCommand(cmd: string) { + const parts = splitCommand(cmd); + const [group, action, ...args] = parts; + + if (group === "wait") { + return callRocketpoolApi("/api/wait", { txHash: action }); + } + + if (group === "wallet") { + if (action === "status") return callRocketpoolApi("/api/wallet/status"); + if (action === "init") return callRocketpoolApi("/api/wallet/init", {}, "POST"); + if (action === "recover") return callRocketpoolApi("/api/wallet/recover", { mnemonic: args.join(" ") }, "POST"); + } + + if (group === "node") { + if (action === "status") return callRocketpoolApi("/api/node/status"); + if (action === "sync") return callRocketpoolApi("/api/node/sync"); + if (action === "can-register") return callRocketpoolApi("/api/node/can-register", { timezoneLocation: args[0] }); + if (action === "register") return callRocketpoolApi("/api/node/register", { timezoneLocation: args[0] }, "POST"); + if (action === "can-set-primary-withdrawal-address") return callRocketpoolApi("/api/node/can-set-primary-withdrawal-address", { address: args[0], confirm: args[1] }); + if (action === "set-primary-withdrawal-address") return callRocketpoolApi("/api/node/set-primary-withdrawal-address", { address: args[0], confirm: args[1] }, "POST"); + if (action === "can-set-smoothing-pool-status") return callRocketpoolApi("/api/node/can-set-smoothing-pool-status", { status: args[0] }); + if (action === "set-smoothing-pool-status") return callRocketpoolApi("/api/node/set-smoothing-pool-status", { status: args[0] }, "POST"); + if (action === "stake-rpl-allowance") return callRocketpoolApi("/api/node/stake-rpl-allowance"); + if (action === "stake-rpl-approve-rpl") return callRocketpoolApi("/api/node/stake-rpl-approve-rpl", { amountWei: args[0] }, "POST"); + if (action === "can-stake-rpl") return callRocketpoolApi("/api/node/can-stake-rpl", { amountWei: args[0] }); + if (action === "stake-rpl") return callRocketpoolApi("/api/node/stake-rpl", { amountWei: args[0] }, "POST"); + if (action === "can-deposit") return callRocketpoolApi("/api/node/can-deposit", depositParams(args)); + if (action === "deposit") return callRocketpoolApi("/api/node/deposit", depositParams(args, true), "POST"); + if (action === "rewards") return callRocketpoolApi("/api/node/rewards"); + if (action === "get-rewards-info") return callRocketpoolApi("/api/node/get-rewards-info"); + if (action === "can-claim-rewards") return callRocketpoolApi("/api/node/can-claim-rewards", { indices: args[0] }); + if (action === "claim-rewards") return callRocketpoolApi("/api/node/claim-rewards", { indices: args[0] }, "POST"); + if (action === "can-claim-and-stake-rewards") return callRocketpoolApi("/api/node/can-claim-and-stake-rewards", { indices: args[0], stakeAmount: args[1] }); + if (action === "claim-and-stake-rewards") return callRocketpoolApi("/api/node/claim-and-stake-rewards", { indices: args[0], stakeAmount: args[1] }, "POST"); + } + + if (group === "network") { + if (action === "node-fee") return callRocketpoolApi("/api/network/node-fee"); + if (action === "rpl-price") return callRocketpoolApi("/api/network/rpl-price"); + } + + if (group === "minipool" && action === "status") { + return callRocketpoolApi("/api/minipool/status"); + } + + if (group === "megapool") { + if (action === "status") return callRocketpoolApi("/api/megapool/status"); + if (action === "get-new-validator-bond-requirement") return callRocketpoolApi("/api/megapool/get-new-validator-bond-requirement"); + } + + return { status: "error", error: `Unsupported Rocket Pool command: ${cmd}` }; +} + +function depositParams(args: string[], includeExecuteParams = false): Record { + const params: Record = { + amountWei: args[0], + minFee: args[1], + salt: args[2], + expressTickets: args[3], + count: args[6] ?? args[4] ?? "1", + }; + if (includeExecuteParams) { + params.useCreditBalance = args[3]; + params.expressTickets = args[4]; + params.submit = args[5]; + params.count = args[6] ?? "1"; + } + return params; +} + +async function callRocketpoolApi(path: string, params: Record = {}, method = "GET") { + const url = new URL(path, ROCKETPOOL_API_URL); + const init: RequestInit = { method }; + + if (method === "GET") { + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } + } else { + init.body = new URLSearchParams(params).toString(); + init.headers = { "Content-Type": "application/x-www-form-urlencoded" }; + } + + const response = await fetch(url, init); + const text = await response.text(); + try { + return parseRocketpoolJson(text); + } catch (error) { + return { status: response.ok ? "success" : "error", error: response.ok ? "" : text, output: text }; + } +} + +function parseRocketpoolJson(text: string) { + // Rocket Pool returns wei amounts as JSON numbers. Preserve large integers as + // strings so Node/React never round transaction amounts beyond MAX_SAFE_INTEGER. + return JSON.parse(text.replace(/(:\s*)(-?\d{16,})(\s*[,}])/g, '$1"$2"$3')); +} + +function splitCommand(cmd: string): string[] { + const matches = cmd.match(/"([^"]*)"|'([^']*)'|\S+/g) ?? []; + return matches.map((part) => part.replace(/^['"]|['"]$/g, "")); +} + +function ensureHexPrefix(value: string): string { + return value.startsWith("0x") ? value : `0x${value}`; } // POST /api/v1/minipool/import @@ -134,10 +289,6 @@ app.listen(API_PORT, () => { console.log(`⚡️[server]: Server is running at http://localhost:${API_PORT}`); }); -String.prototype.startsWith = function (str) { - return this.indexOf(str) === 0; -}; - interface ImportKeyResponseData { data: ImportKeyResponse[]; } diff --git a/build/rocketpool-start.sh b/build/rocketpool-start.sh index 335fe40..5c9d676 100644 --- a/build/rocketpool-start.sh +++ b/build/rocketpool-start.sh @@ -16,6 +16,9 @@ fi case $NETWORK in "mainnet") echo "Mainnet network" + # Smartnode's internal network identifier (mainnet|testnet); the dappnode + # package keeps using $NETWORK (mainnet/hoodi) for its own identity. + SMARTNODE_NETWORK="mainnet" # Assign proper value to _DAPPNODE_GLOBAL_EXECUTION_CLIENT_MAINNET. case $_DAPPNODE_GLOBAL_EXECUTION_CLIENT_MAINNET in @@ -84,8 +87,13 @@ case $NETWORK in esac ;; -"testnet") +"testnet"|"hoodi") echo "Hoodi network" + # Smartnode only accepts mainnet|testnet|devnet; "testnet" IS Hoodi + # (chainId 560048). Mapping hoodi -> testnet here so the daemon can resolve + # the RocketStorage address; without it every on-chain call fails with + # "The Rocket Pool storage contract was not found". + SMARTNODE_NETWORK="testnet" # https://github.com/dappnode/DAppNodePackage-SSV-Shifu/blob/775dfbc2190b8c3bc7384a2e4c62d83892071001/build/entrypoint.sh#L3 # Assign proper value to _DAPPNODE_GLOBAL_EXECUTION_CLIENT_HOODI. @@ -180,6 +188,7 @@ export BEACON_NODE_CLIENT=$_BEACON_NODE_CLIENT # BEACON_NODE_API_4000="http://beacon-chain.prysm-hoodi.dappnode:4000" NETWORK="${NETWORK}" \ +SMARTNODE_NETWORK="${SMARTNODE_NETWORK}" \ EXECUTION_NODE_CLIENT="${EXECUTION_NODE_CLIENT}" \ BEACON_NODE_CLIENT="${BEACON_NODE_CLIENT}" \ EXECUTION_LAYER_HTTP="${EXECUTION_LAYER_HTTP}" \ @@ -197,8 +206,27 @@ if [ -f "/rocketpool/data/wallet" ]; then echo "${INFO} Wallet already created" fi if [ ! -f /rocketpool/data/password ]; then + echo "${INFO} Initializing Rocketpool service before setting password" + /usr/local/bin/rocketpoold --settings /app/rocketpool/user-settings.yml node & + ROCKETPOOL_PID=$! + + echo "${INFO} Waiting for Rocketpool HTTP API" + for i in $(seq 1 60); do + if curl -fsS http://127.0.0.1:8280/api/version >/dev/null 2>&1; then + break + fi + if ! kill -0 "$ROCKETPOOL_PID" >/dev/null 2>&1; then + echo "${ERROR} Rocketpool service exited before HTTP API became available" + wait "$ROCKETPOOL_PID" + exit $? + fi + sleep 1 + done + echo "${INFO} set-password" - /usr/local/bin/rocketpoold --settings /app/rocketpool/user-settings.yml api wallet set-password "${WALLET_PASSWORD}" + curl -fsS -X POST --data-urlencode "password=${WALLET_PASSWORD}" http://127.0.0.1:8280/api/wallet/set-password || true + wait "$ROCKETPOOL_PID" + exit $? fi echo "${INFO} Initializing Rocketpool service" exec /usr/local/bin/rocketpoold --settings /app/rocketpool/user-settings.yml node diff --git a/build/ui/src/components/Setup/CreateMinipool.tsx b/build/ui/src/components/Setup/CreateMinipool.tsx index 9c2dce0..da5d736 100644 --- a/build/ui/src/components/Setup/CreateMinipool.tsx +++ b/build/ui/src/components/Setup/CreateMinipool.tsx @@ -14,10 +14,11 @@ import { CanDeposit } from "../../types/CanDeposit"; import { StakeRplApprove } from "../../types/StakeRplApprove"; import { StakeResponse } from "../../types/StakeResponse"; import { DepositResponse } from "../../types/DepositResponse"; -import MinipoolEthToggle from "./MinipoolEthToggle"; +import MinipoolEthToggle, { ValidatorDepositMode } from "./MinipoolEthToggle"; import "./minipool.css"; import TxsLinksBox from "./TxsLinksBox"; import { WaitResponse } from "../../types/WaitResponse"; +import { NextValidatorBond } from "../../types/NextValidatorBond"; interface CreateMinipoolProps { data?: RocketpoolData; @@ -38,36 +39,57 @@ const CreateMinipool: React.FC = ({ const [w3sStatusResponse, setW3sStatusResponse] = useState(); const [canDeposit, setCanDeposit] = useState(); const [nodeFee, setNodeFee] = useState(0); - const [minipoolEth, setMinipoolEth] = useState<8 | 16>(8); + const [depositMode, setDepositMode] = useState("megapool"); + const [nextValidatorBond, setNextValidatorBond] = useState(); - const minimumRpl = data?.networkRplPrice?.minPer8EthMinipoolRplStake ?? 0; - const ethBalance = data?.nodeStatus?.accountBalances.eth ?? 0; const rplBalance = data?.nodeStatus?.accountBalances.rpl ?? 0; const appService = new AppService(); - async function refreshData(selectedEth: number) { - setIsDepositLoading(true); - var canDeposit = await appService.canDeposit(selectedEth, nodeFee); - setCanDeposit(canDeposit); - setIsDepositLoading(false); - - // if ((data?.nodeStatus?.rplStake ?? 0) < minimumRpl) { - // setIsDepositETHEnabled(false); - // var allowance = await appService.getNodeStakeRplAllowance(); - // setIsApproveRPLEnabled(allowance < (data?.nodeStatus?.accountBalances.rpl ?? 0)); - // setIsStakeRPLEnabled(allowance >= (data?.nodeStatus?.accountBalances.rpl ?? 0)); - // } else { - // setIsApproveRPLEnabled(false); - // setIsStakeRPLEnabled(false); + async function getDepositAmountWei(selectedMode: ValidatorDepositMode): Promise { + if (selectedMode === "megapool") { + const bond = await appService.getMegapoolNextValidatorBond(); + setNextValidatorBond(bond); + if (bond.status !== "success") { + throw new Error(bond.error || "Unable to get the next megapool validator bond requirement"); + } + return bond.bondRequirement; + } + return selectedMode === "8" ? "8000000000000000000" : "16000000000000000000"; + } - // setIsDepositETHEnabled(false); - // } + async function refreshData(selectedMode: ValidatorDepositMode) { + setIsDepositLoading(true); + try { + const amountWei = await getDepositAmountWei(selectedMode); + const canDeposit = await appService.canDepositAmountWei(amountWei, nodeFee); + setCanDeposit(canDeposit); + } catch (error) { + setCanDeposit({ + status: "error", + error: String(error), + canDeposit: false, + creditBalance: 0, + depositBalance: 0, + canUseCredit: false, + nodeBalance: 0, + insufficientBalance: false, + insufficientBalanceWithoutCredit: false, + insufficientRplStake: false, + invalidAmount: false, + depositDisabled: false, + inConsensus: false, + isAtlasDeployed: false, + gasInfo: { estGasLimit: 0, safeGasLimit: 0 }, + }); + } finally { + setIsDepositLoading(false); + } } async function fetchData() { - var networkNodeFee = await appService.getNetworkNodeFee(); + const networkNodeFee = await appService.getNetworkNodeFee(); setNodeFee(networkNodeFee.nodeFee); - refreshData(minipoolEth); + refreshData(depositMode); } useEffect(() => { @@ -80,9 +102,9 @@ const CreateMinipool: React.FC = ({ try { setStakeTxs([]); setIsStakeLoading(true); - var allowance = await appService.getNodeStakeRplAllowance(); + const allowance = await appService.getNodeStakeRplAllowance(); if (allowance < rplBalance) { - var approveResponse = await appService.stakeRplApprove(rplBalance); + const approveResponse = await appService.stakeRplApprove(rplBalance); setStakeTxs([...stakeTxs, approveResponse.approveTxHash]); setApprovalResponse(approveResponse); if (approveResponse.status !== "success") { @@ -90,11 +112,11 @@ const CreateMinipool: React.FC = ({ } await appService.wait(approveResponse.approveTxHash); } - var canStakeRpl = await appService.getNodeCanStakeRpl(rplBalance); + const canStakeRpl = await appService.getNodeCanStakeRpl(rplBalance); if (!canStakeRpl.canStake) { return; } - var stakeResponse = await appService.nodeStakeRpl(rplBalance); + const stakeResponse = await appService.nodeStakeRpl(rplBalance); setStakeTxs([...txs, stakeResponse.stakeTxHash]); setStakeResponse(stakeResponse); if (stakeResponse.status !== "success") { @@ -103,7 +125,7 @@ const CreateMinipool: React.FC = ({ await appService.wait(stakeResponse.stakeTxHash); } finally { setIsStakeLoading(false); - refreshData(minipoolEth); + refreshData(depositMode); } }; @@ -116,24 +138,25 @@ const CreateMinipool: React.FC = ({ if (w3sStatus.status !== "success") { return; } - var despositResponse = await appService.nodeDeposit( - minipoolEth, + const amountWei = await getDepositAmountWei(depositMode); + const depositResponse = await appService.nodeDepositAmountWei( + amountWei, nodeFee, canDeposit?.canUseCredit ?? false ); - setTxs([...txs, despositResponse.txHash]); - setDepositResponse(despositResponse); - if (despositResponse.status !== "success") { + setTxs([...txs, depositResponse.txHash]); + setDepositResponse(depositResponse); + if (depositResponse.status !== "success") { return; } - var wait = await appService.wait(despositResponse.txHash); + const wait = await appService.wait(depositResponse.txHash); if (wait.status !== "success") { return; } onAddMinipoolClick(false); } finally { setIsDepositLoading(false); - refreshData(minipoolEth); + refreshData(depositMode); } }; @@ -154,17 +177,28 @@ const CreateMinipool: React.FC = ({ ); } + const depositLabel = depositMode === "megapool" ? "Megapool validator" : `${depositMode} ETH minipool`; + const depositAmountLabel = depositMode === "megapool" + ? `${toEtherString(nextValidatorBond?.bondRequirement ?? "4000000000000000000")} ETH` + : `${depositMode} ETH`; + return (
- Create minipool + Create validator + {depositMode === "megapool" && ( + + Uses the stable Smartnode v1.20 HTTP API to create a Saturn megapool validator. The UI asks Smartnode for the current bond requirement before depositing. + + )}
- +
Stake {toEtherString(rplBalance)} RPL, all you have in your wallet @@ -174,7 +208,6 @@ const CreateMinipool: React.FC = ({ variant="contained" onClick={() => handleStakeRPLClick()} > - {" "} {isStakeLoading ? ( ) : ( @@ -199,7 +232,7 @@ const CreateMinipool: React.FC = ({
- Deposit {minipoolEth} ETH to create the minipool (validator key will be + Deposit {depositAmountLabel} to create the {depositLabel} (validator key will be imported and configured automatically) @@ -208,11 +241,10 @@ const CreateMinipool: React.FC = ({ variant="contained" onClick={() => handleDepositRPLClick()} > - {" "} {isDepositLoading ? ( ) : ( - `Deposit ${minipoolEth} ETH` + `Deposit ${depositAmountLabel}` )} {(data?.nodeStatus?.minipoolCounts.total ?? 0) > 0 && ( @@ -231,6 +263,16 @@ const CreateMinipool: React.FC = ({ {w3sStatusResponse?.error} )} + {canDeposit?.error && ( + + {canDeposit?.error} + + )} + {canDeposit?.nodeHasDebt && ( + + The node has megapool debt. Repay debt before creating a new validator. + + )} {depositResponse?.error && ( {depositResponse?.error} diff --git a/build/ui/src/components/Setup/MinipoolEthToggle.tsx b/build/ui/src/components/Setup/MinipoolEthToggle.tsx index de8a704..16d9579 100644 --- a/build/ui/src/components/Setup/MinipoolEthToggle.tsx +++ b/build/ui/src/components/Setup/MinipoolEthToggle.tsx @@ -1,53 +1,70 @@ import { ToggleButton, ToggleButtonGroup } from "@mui/material"; import { CanDeposit } from "../../types/CanDeposit"; +export type ValidatorDepositMode = "megapool" | "8" | "16"; + function MinipoolEthToggle({ - minipoolEth, - setMinipoolEth, + depositMode, + setDepositMode, setCanDeposit, refreshData, + includeMegapool = true, + includeLegacyMinipools = true, }: { - minipoolEth: 8 | 16; - setMinipoolEth: (minipoolEth: 8 | 16) => void; + depositMode: ValidatorDepositMode; + setDepositMode: (depositMode: ValidatorDepositMode) => void; setCanDeposit?: React.Dispatch>; - refreshData?: (selectedEth: number) => void; + refreshData?: (selectedMode: ValidatorDepositMode) => void; + includeMegapool?: boolean; + includeLegacyMinipools?: boolean; }): JSX.Element { const handleMinipoolEthChange = ( event: React.MouseEvent, - newMinipoolEth: string + newDepositMode: ValidatorDepositMode | null ) => { - const minipoolEth = Number(newMinipoolEth); - - if (minipoolEth === 8 || minipoolEth === 16) { - setMinipoolEth(minipoolEth); + if (newDepositMode) { + setDepositMode(newDepositMode); setCanDeposit && setCanDeposit(undefined); - refreshData && refreshData(minipoolEth); + refreshData && refreshData(newDepositMode); } }; return ( - - 8 ETH - - - 16 ETH - + {includeMegapool && ( + + 4 ETH Megapool + + )} + {includeLegacyMinipools && ( + + 8 ETH Minipool + + )} + {includeLegacyMinipools && ( + + 16 ETH Minipool + + )} ); } diff --git a/build/ui/src/components/Setup/RegisterNode.tsx b/build/ui/src/components/Setup/RegisterNode.tsx index 35f6068..b0851c7 100644 --- a/build/ui/src/components/Setup/RegisterNode.tsx +++ b/build/ui/src/components/Setup/RegisterNode.tsx @@ -22,7 +22,7 @@ import { RocketpoolContext } from "../Providers/Context"; import { TxResponse } from "../../types/TxResponse"; import RequiredBalanceInfo from "./RequiredBalanceInfo"; import "./registerNode.css"; -import MinipoolEthToggle from "./MinipoolEthToggle"; +import MinipoolEthToggle, { ValidatorDepositMode } from "./MinipoolEthToggle"; import TxsLinksBox from "./TxsLinksBox"; interface RegisterNodeProps { @@ -43,7 +43,7 @@ const RegisterNode: React.FC = ({ useState(); const [addressEntered, setAddressEntered] = useState(""); const [addressError, setAddressError] = useState(""); - const [minipoolEth, setMinipoolEth] = useState<8 | 16>(8); + const [minipoolEth, setMinipoolEth] = useState("8"); const { rocketpoolValue, updateRocketpoolValue } = React.useContext(RocketpoolContext); @@ -234,8 +234,9 @@ const RegisterNode: React.FC = ({ Info
@@ -256,7 +257,7 @@ const RegisterNode: React.FC = ({

- +

diff --git a/build/ui/src/components/Setup/RequiredBalanceInfo.tsx b/build/ui/src/components/Setup/RequiredBalanceInfo.tsx index 19bdeec..4850f9c 100644 --- a/build/ui/src/components/Setup/RequiredBalanceInfo.tsx +++ b/build/ui/src/components/Setup/RequiredBalanceInfo.tsx @@ -2,18 +2,25 @@ import React from "react"; import { Typography, Box } from "@mui/material"; import { RocketpoolData } from "../../types/RocketpoolData"; import { toEther } from "../../utils/Utils"; +import { ValidatorDepositMode } from "./MinipoolEthToggle"; interface RequiredBalanceInfoProps { - minipoolEth: 8 | 16; + depositMode: ValidatorDepositMode; + requiredBondWei?: string; data?: RocketpoolData; } const RequiredBalanceInfo: React.FC = ({ data, - minipoolEth, + depositMode, + requiredBondWei, }): JSX.Element => { - const minRpl = - minipoolEth === 8 + const isMegapool = depositMode === "megapool"; + const minipoolEth = depositMode === "16" ? 16 : 8; + const requiredEth = isMegapool ? toEther(requiredBondWei ?? "4000000000000000000") : minipoolEth; + const minRpl = isMegapool + ? data?.nodeStatus?.minimumRplStake ?? 0 + : minipoolEth === 8 ? data?.networkRplPrice?.minPer8EthMinipoolRplStake ?? 0 : data?.networkRplPrice?.minPer16EthMinipoolRplStake ?? 0; @@ -22,10 +29,14 @@ const RequiredBalanceInfo: React.FC = ({ return ( - 1. At least {minipoolEth} ETH + 0.2 ETH (we recommend{" "} + 1. At least {requiredEth.toFixed(2)} ETH + 0.2 ETH (we recommend{" "} 0.5 ETH) for gas costs
- {maxRpl === 0 ? ( + {isMegapool ? ( + <> + 2. Enough RPL collateral for the node's current megapool bond requirement + + ) : maxRpl === 0 ? ( <> 2. At least {Math.ceil(toEther(minRpl))} RPL for {minipoolEth} ETH minipool diff --git a/build/ui/src/services/AppService.ts b/build/ui/src/services/AppService.ts index 3458b55..e982920 100644 --- a/build/ui/src/services/AppService.ts +++ b/build/ui/src/services/AppService.ts @@ -10,7 +10,7 @@ import { WaitResponse } from "../types/WaitResponse"; import { NodeCanSetWithdrawalAddress } from "../types/NodeCanSetWithdrawalAddress"; import { NodeCanSetSmoothingPool } from "../types/NodeCanSetSmoothingPool"; import { CanDeposit } from "../types/CanDeposit"; -import { toWei, toWeiString } from "../utils/Utils"; +import { ethToWeiString, toWeiString } from "../utils/Utils"; import { NodeFee } from "../types/NodeFee"; import { StakeRplApprove } from "../types/StakeRplApprove"; import { CanStake } from "../types/CanStake"; @@ -21,6 +21,7 @@ import { GetRewardsInfo } from "../types/GetRewardsInfo"; import { CanClaimRewards } from "../types/CanClaimRewards"; import apiBaseUrl, { Config } from "../types/AppConfig"; import { ImportKeyResponseData } from "../types/ImportKeyResponse"; +import { NextValidatorBond } from "../types/NextValidatorBond"; export class AppService { public api = axios.create({ @@ -163,17 +164,25 @@ export class AppService { }); return response.data; } + public async getMegapoolNextValidatorBond(): Promise { + const response = await this.api.get(`/api/v1/megapool/next-validator-bond`); + return response.data; + } public async canDeposit(ethPool: number, nodeFee: number): Promise { - const amount = toWei(ethPool); + return this.canDepositAmountWei(ethToWeiString(ethPool), nodeFee); + } + public async canDepositAmountWei(amountWei: string, nodeFee: number): Promise { const response = await this.api.post(`/api/v1/rocketpool-command`, { - cmd: `node can-deposit ${amount} ${nodeFee} 0 false`, + cmd: `node can-deposit ${amountWei} ${nodeFee} 0 0`, }); return response.data; } public async nodeDeposit(ethPool: number, nodeFee: number, useCreditBalance: boolean): Promise { - const amount = toWei(ethPool); + return this.nodeDepositAmountWei(ethToWeiString(ethPool), nodeFee, useCreditBalance); + } + public async nodeDepositAmountWei(amountWei: string, nodeFee: number, useCreditBalance: boolean): Promise { const response = await this.api.post(`/api/v1/rocketpool-command`, { - cmd: `node deposit ${amount} ${nodeFee} 0 ${useCreditBalance} false true`, + cmd: `node deposit ${amountWei} ${nodeFee} 0 ${useCreditBalance} 0 true`, }); return response.data; } diff --git a/build/ui/src/types/CanDeposit.ts b/build/ui/src/types/CanDeposit.ts index 75ec580..238d38f 100644 --- a/build/ui/src/types/CanDeposit.ts +++ b/build/ui/src/types/CanDeposit.ts @@ -15,10 +15,13 @@ export interface CanDeposit { insufficientBalanceWithoutCredit: boolean; insufficientRplStake: boolean; invalidAmount: boolean; - unbondedMinipoolsAtMax: boolean; + unbondedMinipoolsAtMax?: boolean; depositDisabled: boolean; inConsensus: boolean; isAtlasDeployed: boolean; - minipoolAddress: string; + nodeHasDebt?: boolean; + minipoolAddress?: string; + megapoolAddress?: string; + validatorPubkeys?: string[]; gasInfo: GasInfo; } \ No newline at end of file diff --git a/build/ui/src/types/DepositResponse.ts b/build/ui/src/types/DepositResponse.ts index eec94ba..95ece21 100644 --- a/build/ui/src/types/DepositResponse.ts +++ b/build/ui/src/types/DepositResponse.ts @@ -6,7 +6,8 @@ export interface DepositResponse { status: Status; error: string; txHash: string; - minipoolAddress: string; - validatorPubkey: string; + minipoolAddress?: string; + validatorPubkey?: string; + validatorPubkeys?: string[]; scrubPeriod: number; } \ No newline at end of file diff --git a/build/ui/src/types/NextValidatorBond.ts b/build/ui/src/types/NextValidatorBond.ts new file mode 100644 index 0000000..d5cc82e --- /dev/null +++ b/build/ui/src/types/NextValidatorBond.ts @@ -0,0 +1,11 @@ +import { Status } from './Status'; + +export interface NextValidatorBond { + status: Status; + error: string; + bondRequirement: string; + activeValidatorCount: number; + nodeBond: string; + nodeQueuedBond: string; + megapoolDeployed: boolean; +} diff --git a/build/ui/src/utils/Utils.ts b/build/ui/src/utils/Utils.ts index 1762f6b..8585596 100644 --- a/build/ui/src/utils/Utils.ts +++ b/build/ui/src/utils/Utils.ts @@ -6,15 +6,19 @@ export function toWei(ether: number): number { return ether * 10 ** 18; } -export function toWeiString(ether: number): string { - return BigInt(ether).toString(); +export function toWeiString(wei: number | string): string { + return BigInt(wei).toString(); } -export function toEther(wei: number): number { - return wei / 10 ** 18; +export function ethToWeiString(ether: number): string { + return (BigInt(Math.round(ether * 1000000)) * BigInt(10 ** 12)).toString(); } -export function toEtherString(wei: number): string { +export function toEther(wei: number | string): number { + return Number(wei) / 10 ** 18; +} + +export function toEtherString(wei: number | string): string { return toEther(wei).toFixed(4); } diff --git a/build/user-settings_template.yml b/build/user-settings_template.yml index 2bd3683..c955a2e 100644 --- a/build/user-settings_template.yml +++ b/build/user-settings_template.yml @@ -138,6 +138,7 @@ lodestar: additionalVcFlags: "" containerTag: chainsafe/lodestar:v1.33.0 maxPeers: "100" + p2pQuicPort: "8001" mevBoost: additionalFlags: "" aestusEnabled: "false" @@ -207,6 +208,7 @@ prysm: vcContainerTag: gcr.io/offchainlabs/prysm/validator:v6.0.4 root: bnMetricsPort: "9100" + enableIPv6: "false" consensusClient: nimbus consensusClientMode: external ecMetricsPort: "9105" @@ -224,16 +226,17 @@ root: rpDir: /app/rocketpool/ useFallbackClients: "false" vcMetricsPort: "9101" - version: v1.17.3 + version: v1.20.3 watchtowerMetricsPort: "9104" smartnode: + apiPort: "8280" archiveECUrl: "" autoInitVPThreshold: "5" dataPath: /rocketpool/data distributeThreshold: "1" manualMaxFee: "0" minipoolStakeGasThreshold: "150" - network: ${NETWORK} + network: ${SMARTNODE_NETWORK} priceBalanceSubmissionReferenceTimestamp: "1713420000" priorityFee: "2" projectName: rocketpool diff --git a/dappnode_package.json b/dappnode_package.json index ff11a44..b2e9cb0 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -1,7 +1,7 @@ { "name": "rocketpool-testnet.public.dappnode.eth", - "version": "0.1.11", - "upstreamVersion": "v1.20.2", + "version": "0.1.12", + "upstreamVersion": "v1.20.3", "upstreamRepo": "rocket-pool/smartnode", "architectures": ["linux/amd64"], "description": "How Rocket Pool Works. Unlike solo stakers, who are required to put 32 ETH up for deposit to create a new validator, Rocket Pool nodes only need to deposit 8/16 ETH per validator. This will be coupled with 16 ETH from the staking pool (which stakers deposited in exchange for rETH) to create a new Ethereum validator. This new validator is called a minipool.", diff --git a/docker-compose.yml b/docker-compose.yml index 631c61e..b98e33c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: ./build args: - UPSTREAM_VERSION: v1.20.2 + UPSTREAM_VERSION: v1.20.3 NETWORK: hoodi volumes: - rocketpool-testnet:/rocketpool