From 92a4d56c7f3b42151b913b053e6717fca3adc347 Mon Sep 17 00:00:00 2001 From: jeff <113397187+cyberhorsey@users.noreply.github.com> Date: Tue, 16 May 2023 18:06:11 -0700 Subject: [PATCH] feat(status-page): handle multiple layers (#13770) --- packages/status-page/.default.env | 23 +- packages/status-page/src/App.svelte | 23 +- packages/status-page/src/domain/layer.ts | 4 + .../status-page/src/pages/home/Home.svelte | 396 ++---------------- packages/status-page/src/store/layer.ts | 4 + .../src/utils/buildStatusIndicators.ts | 318 ++++++++++++++ .../src/utils/getStateVariables.ts | 9 +- packages/status-page/src/utils/initConfig.ts | 48 +++ .../src/utils/layerToDisplayName.ts | 4 + 9 files changed, 441 insertions(+), 388 deletions(-) create mode 100644 packages/status-page/src/domain/layer.ts create mode 100644 packages/status-page/src/store/layer.ts create mode 100644 packages/status-page/src/utils/buildStatusIndicators.ts create mode 100644 packages/status-page/src/utils/initConfig.ts create mode 100644 packages/status-page/src/utils/layerToDisplayName.ts diff --git a/packages/status-page/.default.env b/packages/status-page/.default.env index 871254df358..ff979910cfc 100644 --- a/packages/status-page/.default.env +++ b/packages/status-page/.default.env @@ -1,10 +1,15 @@ -VITE_NODE_ENV=dev -VITE_L1_RPC_URL="https://l1rpc.a1.taiko.xyz" -VITE_L2_RPC_URL="https://l2rpc.a1.taiko.xyz" -VITE_TAIKO_L2_ADDRESS="0x0000777700000000000000000000000000000001" -VITE_TAIKO_L1_ADDRESS="0x7B3AF414448ba906f02a1CA307C56c4ADFF27ce7" -VITE_L1_EXPLORER_URL="https://l1explorer.a1.taiko.xyz" -VITE_L2_EXPLORER_URL="https://l2explorer.a1.taiko.xyz" -VITE_FEE_TOKEN_SYMBOL="TKO"; +VITE_NODE_ENV=production +VITE_L1_RPC_URL="https://l1rpc.internal.taiko.xyz" +VITE_L2_RPC_URL="https://l2rpc.internal.taiko.xyz" +VITE_L3_RPC_URL="https://l2rpc.internal.taiko.xyz" +VITE_L2_TAIKO_L2_ADDRESS="0x1000777700000000000000000000000000000001" +VITE_L2_TAIKO_L1_ADDRESS="0x0B306BF915C4d645ff596e518fAf3F9669b97016" +VITE_L3_TAIKO_L2_ADDRESS="0x1000777700000000000000000000000000000001" +VITE_L3_TAIKO_L1_ADDRESS="0x0B306BF915C4d645ff596e518fAf3F9669b97016" +VITE_L1_EXPLORER_URL="https://l1explorer.internal.taiko.xyz" +VITE_L2_EXPLORER_URL="https://l2explorer.internal.taiko.xyz" +VITE_L3_EXPLORER_URL="https://l3explorer.internal.taiko.xyz" +VITE_FEE_TOKEN_SYMBOL=TTKO VITE_ORACLE_PROVER_ADDRESS="0x1567CDAb5F7a69154e61A16D8Ff5eE6A3e991b39" -VITE_EVENT_INDEXER_API_URL="http://localhost:4100" \ No newline at end of file +VITE_L2_EVENT_INDEXER_API_URL="http://localhost:4100" +VITE_L3_EVENT_INDEXER_API_URL="http://localhost:4100" \ No newline at end of file diff --git a/packages/status-page/src/App.svelte b/packages/status-page/src/App.svelte index 0bba71fdce1..90493e6b219 100644 --- a/packages/status-page/src/App.svelte +++ b/packages/status-page/src/App.svelte @@ -6,32 +6,13 @@ import { setupI18n } from "./i18n"; import Navbar from "./components/Navbar.svelte"; import { ethers } from "ethers"; + import { layer } from "./store/layer"; + import { Layer } from "./domain/layer"; setupI18n({ withLocale: "en" }); - const l1Provider = new ethers.providers.JsonRpcProvider( - import.meta.env.VITE_L1_RPC_URL - ); - const l2Provider = new ethers.providers.JsonRpcProvider( - import.meta.env.VITE_L2_RPC_URL - ); - const routes = { "/": wrap({ component: Home, - props: { - l1Provider: l1Provider, - l1TaikoAddress: import.meta.env.VITE_TAIKO_L1_ADDRESS, - l2Provider: l2Provider, - l2TaikoAddress: import.meta.env.VITE_TAIKO_L2_ADDRESS, - l1ExplorerUrl: import.meta.env.VITE_L1_EXPLORER_URL, - l2ExplorerUrl: import.meta.env.VITE_L2_EXPLORER_URL, - feeTokenSymbol: import.meta.env.VITE_FEE_TOKEN_SYMBOL || "TKO", - oracleProverAddress: - import.meta.env.ORACLE_PROVER_ADDRESS || - "0x1567CDAb5F7a69154e61A16D8Ff5eE6A3e991b39", - eventIndexerApiUrl: import.meta.env.VITE_EVENT_INDEXER_API_URL, - }, - userData: {}, }), }; diff --git a/packages/status-page/src/domain/layer.ts b/packages/status-page/src/domain/layer.ts new file mode 100644 index 00000000000..47935ccc366 --- /dev/null +++ b/packages/status-page/src/domain/layer.ts @@ -0,0 +1,4 @@ +export enum Layer { + Two, + Three, +} diff --git a/packages/status-page/src/pages/home/Home.svelte b/packages/status-page/src/pages/home/Home.svelte index 98248f1092f..7f5d6ea4869 100644 --- a/packages/status-page/src/pages/home/Home.svelte +++ b/packages/status-page/src/pages/home/Home.svelte @@ -1,379 +1,61 @@

Taiko Protocol Status

+

+ {layerToDisplayName($layer)} +

- {#await getNumProvers(eventIndexerApiUrl) then provers} + {#await getNumProvers(config.eventIndexerApiUrl) then provers} {#each provers.provers as prover} @@ -424,10 +106,10 @@ class="grid grid-cols-2 gap-4 text-center my-10 max-h-96 overflow-y-auto" slot="body" > - {#await getNumProposers(eventIndexerApiUrl) then proposers} + {#await getNumProposers(config.eventIndexerApiUrl) then proposers} {#each proposers.proposers as proposer} diff --git a/packages/status-page/src/store/layer.ts b/packages/status-page/src/store/layer.ts new file mode 100644 index 00000000000..c8842876b06 --- /dev/null +++ b/packages/status-page/src/store/layer.ts @@ -0,0 +1,4 @@ +import { Layer } from "../domain/layer"; +import { writable } from "svelte/store"; + +export const layer = writable(Layer.Two); diff --git a/packages/status-page/src/utils/buildStatusIndicators.ts b/packages/status-page/src/utils/buildStatusIndicators.ts new file mode 100644 index 00000000000..2b880929c60 --- /dev/null +++ b/packages/status-page/src/utils/buildStatusIndicators.ts @@ -0,0 +1,318 @@ +import { BigNumber, Contract, ethers } from "ethers"; +import TaikoL1 from "../constants/abi/TaikoL1"; +import type { Status, StatusIndicatorProp } from "../domain/status"; +import { getAvailableSlots } from "./getAvailableSlots"; +import { getBlockFee } from "./getBlockFee"; +import { getEthDeposits } from "./getEthDeposits"; +import { getGasPrice } from "./getGasPrice"; +import { getLastVerifiedBlockId } from "./getLastVerifiedBlockId"; +import { getLatestSyncedHeader } from "./getLatestSyncedHeader"; +import { getNextBlockId } from "./getNextBlockId"; +import { getNextEthDepositToProcess } from "./getNextEthDepositToProcess"; +import { getNumProposers } from "./getNumProposers"; +import { getNumProvers } from "./getNumProvers"; +import { getPendingBlocks } from "./getPendingBlocks"; +import { getPendingTransactions } from "./getPendingTransactions"; +import { getProofReward } from "./getProofReward"; +import { getQueuedTransactions } from "./getQueuedTransactions"; +import { getStateVariables } from "./getStateVariables"; +import type { initConfig } from "./initConfig"; +import { watchHeaderSynced } from "./watchHeaderSynced"; + +export function buildStatusIndicators( + config: ReturnType, + onProverClick: (value: Status) => void, + onProposerClick: (value: Status) => void +) { + const indicators: StatusIndicatorProp[] = [ + { + statusFunc: async ( + provider: ethers.providers.JsonRpcProvider, + address: string + ) => (await getNumProvers(config.eventIndexerApiUrl)).uniqueProvers, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Unique Provers", + intervalInMs: 0, + colorFunc: (value: Status) => { + return "green"; + }, + onClick: onProverClick, + tooltip: + "The number of unique provers who successfully submitted a proof to the TaikoL1 smart contract.", + }, + { + statusFunc: async ( + provider: ethers.providers.JsonRpcProvider, + address: string + ) => (await getNumProposers(config.eventIndexerApiUrl)).uniqueProposers, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Unique Proposers", + intervalInMs: 0, + colorFunc: (value: Status) => { + return "green"; + }, + onClick: onProposerClick, + tooltip: + "The number of unique proposers who successfully submitted a proposed block to the TaikoL1 smart contract.", + }, + { + statusFunc: getLatestSyncedHeader, + watchStatusFunc: watchHeaderSynced, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "L1 Latest Synced Header", + intervalInMs: 0, + colorFunc: (value: Status) => { + return "green"; + }, + onClick: (value: Status) => { + window.open( + `${config.l2ExplorerUrl}/block/${value.toString()}`, + "_blank" + ); + }, + tooltip: + "The most recent Layer 2 Header that has been synchronized with the TaikoL1 smart contract.", + }, + { + statusFunc: getLatestSyncedHeader, + watchStatusFunc: watchHeaderSynced, + provider: config.l2Provider, + contractAddress: config.l2TaikoAddress, + header: "L2 Latest Synced Header", + intervalInMs: 0, + colorFunc: (value: Status) => { + return "green"; + }, + onClick: (value: Status) => { + window.open( + `${config.l1ExplorerUrl}/block/${value.toString()}`, + "_blank" + ); + }, + tooltip: + "The most recent Layer 1 Header that has been synchronized with the TaikoL2 smart contract. The headers are synchronized with every L2 block.", + }, + { + statusFunc: getPendingTransactions, + watchStatusFunc: null, + provider: config.l2Provider, + contractAddress: "", + header: "Tx Mempool (pending)", + intervalInMs: 20000, + colorFunc: (value: Status) => { + if (BigNumber.from(value).gt(4000)) return "red"; + return "green"; + }, + tooltip: + "The current processable transactions in the mempool that have not been added to a block yet.", + }, + { + statusFunc: getQueuedTransactions, + watchStatusFunc: null, + provider: config.l2Provider, + contractAddress: "", + header: "Tx Mempool (queued)", + intervalInMs: 20000, + colorFunc: (value: Status) => { + if (BigNumber.from(value).gt(4000)) return "red"; + return "green"; + }, + tooltip: + "The current transactions in the mempool where the transaction nonce is not in sequence. They are currently non-processable.", + }, + { + statusFunc: getAvailableSlots, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Available Slots", + intervalInMs: 20000, + colorFunc: (value: Status) => { + if (BigNumber.from(value).eq(0)) return "red"; + return "green"; + }, + tooltip: + "The amount of slots for proposed blocks on the TaikoL1 smart contract. When this number is 0, no blocks can be proposed until a block has been proven.", + }, + { + statusFunc: getLastVerifiedBlockId, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Last Verified Block ID", + intervalInMs: 20000, + colorFunc: (value: Status) => { + return "green"; + }, + tooltip: + "The most recently verified Layer 2 block on the TaikoL1 smart contract.", + }, + { + statusFunc: getNextBlockId, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Next Block ID", + intervalInMs: 20000, + colorFunc: (value: Status) => { + return "green"; + }, + tooltip: + "The ID that the next proposed block on the TaikoL1 smart contract will receive.", + }, + { + statusFunc: getPendingBlocks, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Unverified Blocks", + intervalInMs: 20000, + colorFunc: (value: Status) => { + if (BigNumber.from(value).eq(0)) { + return "red"; + } else if (BigNumber.from(value).lt(5)) { + return "yellow"; + } else { + return "green"; + } + }, + tooltip: + "The amount of pending proposed blocks that have not been proven on the TaikoL1 smart contract.", + }, + { + statusFunc: getEthDeposits, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "ETH Deposits", + intervalInMs: 20000, + colorFunc: (value: Status) => { + if (BigNumber.from(value).eq(0)) { + return "green"; + } else if (BigNumber.from(value).lt(32)) { + return "yellow"; + } else { + return "red"; + } + }, + tooltip: "The number of pending ETH deposits for L1 => L2", + }, + { + statusFunc: getNextEthDepositToProcess, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Next ETH Deposit", + intervalInMs: 20000, + colorFunc: (value: Status) => { + return "green"; + }, + tooltip: "The next ETH deposit that will be processed", + }, + { + statusFunc: getGasPrice, + watchStatusFunc: null, + provider: config.l2Provider, + contractAddress: "", + header: "Gas Price (gwei)", + intervalInMs: 20000, + colorFunc: (value: Status) => { + return "green"; + }, + tooltip: + "The current recommended gas price for a transaction on Layer 2.", + }, + ]; + + try { + indicators.push({ + statusFunc: getBlockFee, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Block Fee", + intervalInMs: 15000, + colorFunc: function (status: Status) { + return "green"; // todo: whats green, yellow, red? + }, + tooltip: + "The current fee to propose a block to the TaikoL1 smart contract.", + }); + indicators.push({ + statusFunc: getProofReward, + watchStatusFunc: null, + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Proof Reward", + intervalInMs: 15000, + colorFunc: function (status: Status) { + return "green"; // todo: whats green, yellow, red? + }, + tooltip: + "The current reward for successfully submitting a proof for a proposed block on the TaikoL1 smart contract.", + }); + indicators.push({ + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + header: "Latest Proof", + intervalInMs: 0, + status: "0", + watchStatusFunc: async ( + provider: ethers.providers.JsonRpcProvider, + address: string, + onEvent: (value: Status) => void + ) => { + const contract = new Contract(address, TaikoL1, provider); + contract.on( + "BlockProven", + ( + id, + parentHash, + blockHash, + signalRoot, + prover, + provenAt, + ...args + ) => { + // ignore oracle prover + if ( + prover.toLowerCase() !== config.oracleProverAddress.toLowerCase() + ) { + onEvent(new Date().getTime().toString()); + } + } + ); + }, + colorFunc: function (status: Status) { + return "green"; // todo: whats green, yellow, red? + }, + tooltip: "The most recent block proof submitted on TaikoL1 contract.", + }); + + indicators.push({ + provider: config.l1Provider, + contractAddress: config.l1TaikoAddress, + statusFunc: async ( + provider: ethers.providers.JsonRpcProvider, + address: string + ) => { + const stateVars = await getStateVariables(provider, address); + return `${stateVars.proofTimeIssued.toNumber() / 1000} seconds`; + }, + colorFunc: function (status: Status) { + return "green"; // todo: whats green, yellow, red? + }, + header: "Average Proof Time", + intervalInMs: 5 * 1000, + tooltip: + "The current average proof time, updated when a block is successfully proven.", + }); + } catch (e) { + console.error(e); + } + + return indicators; +} diff --git a/packages/status-page/src/utils/getStateVariables.ts b/packages/status-page/src/utils/getStateVariables.ts index d0b83e0b7b5..e2d60cc4e09 100644 --- a/packages/status-page/src/utils/getStateVariables.ts +++ b/packages/status-page/src/utils/getStateVariables.ts @@ -5,6 +5,7 @@ const cacheTime = 1000 * 15; // 15 seconds type StateVarsCache = { cachedAt: number; stateVars: any; + chainId: number; }; let stateVarsCache: StateVarsCache; @@ -13,7 +14,12 @@ export const getStateVariables = async ( provider: ethers.providers.JsonRpcProvider, contractAddress: string ) => { - if (stateVarsCache && stateVarsCache.cachedAt + cacheTime > Date.now()) { + const { chainId } = await provider.getNetwork(); + if ( + stateVarsCache && + stateVarsCache.chainId === chainId && + stateVarsCache.cachedAt + cacheTime > Date.now() + ) { return stateVarsCache.stateVars; } @@ -22,6 +28,7 @@ export const getStateVariables = async ( stateVarsCache = { stateVars: vars, cachedAt: Date.now(), + chainId: chainId, }; return vars; }; diff --git a/packages/status-page/src/utils/initConfig.ts b/packages/status-page/src/utils/initConfig.ts new file mode 100644 index 00000000000..d7625fbe487 --- /dev/null +++ b/packages/status-page/src/utils/initConfig.ts @@ -0,0 +1,48 @@ +import { ethers } from "ethers"; +import { Layer } from "../domain/layer"; + +export function initConfig(layer: Layer) { + const l1Provider = new ethers.providers.JsonRpcProvider( + layer === Layer.Two + ? import.meta.env.VITE_L1_RPC_URL + : import.meta.env.VITE_L2_RPC_URL + ); + const l2Provider = new ethers.providers.JsonRpcProvider( + layer === Layer.Two + ? import.meta.env.VITE_L2_RPC_URL + : import.meta.env.VITE_L3_RPC_URL + ); + + const l1TaikoAddress = + layer === Layer.Two + ? import.meta.env.VITE_L2_TAIKO_L1_ADDRESS + : import.meta.env.VITE_L3_TAIKO_L1_ADDRESS; + const l2TaikoAddress = + layer === Layer.Two + ? import.meta.env.VITE_L2_TAIKO_L2_ADDRESS + : import.meta.env.VITE_L3_TAIKO_L2_ADDRESS; + const l1ExplorerUrl = import.meta.env.VITE_L1_EXPLORER_URL; + const l2ExplorerUrl = + layer === Layer.Two + ? import.meta.env.VITE_L2_EXPLORER_URL + : import.meta.env.VITE_L3_EXPLORER_URL; + const feeTokenSymbol = import.meta.env.VITE_FEE_TOKEN_SYMBOL || "TKO"; + const oracleProverAddress = + import.meta.env.ORACLE_PROVER_ADDRESS || + "0x1567CDAb5F7a69154e61A16D8Ff5eE6A3e991b39"; + const eventIndexerApiUrl = + layer === Layer.Two + ? import.meta.env.VITE_L2_EVENT_INDEXER_API_URL + : import.meta.env.VITE_L3_EVENT_INDEXER_API_URL; + return { + l1Provider, + l2Provider, + l1TaikoAddress, + l2TaikoAddress, + l1ExplorerUrl, + l2ExplorerUrl, + feeTokenSymbol, + oracleProverAddress, + eventIndexerApiUrl, + }; +} diff --git a/packages/status-page/src/utils/layerToDisplayName.ts b/packages/status-page/src/utils/layerToDisplayName.ts new file mode 100644 index 00000000000..886c0accff0 --- /dev/null +++ b/packages/status-page/src/utils/layerToDisplayName.ts @@ -0,0 +1,4 @@ +import { Layer } from "../domain/layer"; + +export const layerToDisplayName = (layer: Layer) => + layer === Layer.Two ? "Taiko L2" : "Taiko L3";