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";