Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: CartesiScan networks #196

Merged
merged 3 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/mantine.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ declare module "@mantine/core" {
export { core };
export interface MantineThemeOther {
iconSize: number;
chainIconSize: number;
}
}
1 change: 1 addition & 0 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const nextConfig = {
swcMinify: true,
output: "standalone",
basePath: process.env.BASE_PATH || "",
transpilePackages: ["@ant-design", "antd"],
async headers() {
return [
{
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@ant-design/web3-icons": "^1.6.0",
"@cartesi/rollups-explorer-ui": "*",
"@cartesi/rollups-wagmi": "*",
"@mantine/core": "^7.7.1",
Expand All @@ -28,6 +29,7 @@
"@vercel/analytics": "^1.2.2",
"@vercel/speed-insights": "^1.0.10",
"abitype": "^0.9",
"antd": "^5.17.4",
"encoding": "^0.1",
"graphql": "^16",
"graphql-tag": "^2",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/layout/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { useAccount } from "wagmi";
import CartesiLogo from "../../components/cartesiLogo";
import Footer from "../../components/layout/footer";
import SendTransaction from "../../components/sendTransaction";
import { CartesiScanChains } from "../networks/cartesiScanNetworks";

const Shell: FC<{ children: ReactNode }> = ({ children }) => {
const [opened, { toggle: toggleMobileMenu }] = useDisclosure();
Expand Down Expand Up @@ -87,7 +88,8 @@ const Shell: FC<{ children: ReactNode }> = ({ children }) => {
<Link href="/">
<CartesiLogo height={40} />
</Link>
<Group ml={{ lg: "xl" }} gap="md">
<Group ml={{ lg: "xl" }}>
<CartesiScanChains />
<Button
variant="subtle"
leftSection={<TbArrowsDownUp />}
Expand Down
200 changes: 200 additions & 0 deletions apps/web/src/components/networks/cartesiScanNetworks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"use client";
import {
EthereumCircleColorful,
HardhatColorful,
OptimismCircleColorful,
} from "@ant-design/web3-icons";
import {
Button,
Divider,
Group,
Indicator,
Menu,
Text,
Tooltip,
VisuallyHidden,
useMantineTheme,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { T, cond, includes } from "ramda";
import { FC } from "react";
import { TbCaretUpFilled, TbExternalLink } from "react-icons/tb";
import {
anvil,
mainnet,
optimism,
optimismSepolia,
sepolia,
} from "viem/chains";
import { useConfig } from "wagmi";

const chainIds = [
mainnet.id,
sepolia.id,
optimism.id,
optimismSepolia.id,
anvil.id,
] as const;

type IconType = typeof EthereumCircleColorful;
type SupportedChains = (typeof chainIds)[number];

interface NetworkGroupProps extends NetworkGroup {
currentChain: number;
}

interface NetworkGroup {
Icon: IconType;
text: string;
externalLink: string;
chainId: SupportedChains;
}

const mainnets: NetworkGroup[] = [
{
Icon: EthereumCircleColorful,
text: mainnet.name,
chainId: mainnet.id,
externalLink: "https://cartesiscan.io",
},

{
Icon: OptimismCircleColorful,
text: optimism.name,
chainId: optimism.id,
externalLink: "https://optimism.cartesiscan.io",
},
];

const testnets: NetworkGroup[] = [
{
Icon: EthereumCircleColorful,
text: sepolia.name,
chainId: sepolia.id,
externalLink: "https://sepolia.cartesiscan.io",
},
{
Icon: OptimismCircleColorful,
text: optimismSepolia.name,
chainId: optimismSepolia.id,
externalLink: "https://optimism-sepolia.cartesiscan.io",
},
];

const NetworkGroup: FC<NetworkGroupProps> = ({
Icon,
externalLink,
text,
chainId,
currentChain,
}) => {
const theme = useMantineTheme();
const disabled = currentChain === chainId;
const { chainIconSize } = theme.other;

return (
<Menu.Item
component="a"
target="_blank"
href={externalLink}
disabled={disabled}
>
<Tooltip label={externalLink}>
<Indicator autoContrast disabled={!disabled} processing>
<Group justify="space-between" wrap="nowrap">
<Group justify="flex-start">
<Icon style={{ fontSize: chainIconSize }} />
<Text>{text}</Text>
</Group>
<TbExternalLink />
</Group>
</Indicator>
</Tooltip>
</Menu.Item>
);
};

const CaretIcon: FC<{ up: boolean }> = ({ up }) => {
const styles = {
transform: `rotateZ(${up ? 0 : 180}deg)`,
transitionTimingFunction: "ease-in-out",
transitionDuration: "300ms",
tansitionProperty: "all",
};

return <TbCaretUpFilled style={styles} size={18} />;
};

const getIconByChainId = cond([
[
(id: number) => includes(id, [mainnet.id, sepolia.id]),
() => EthereumCircleColorful,
],
[
(id: number) => includes(id, [optimism.id, optimismSepolia.id]),
() => OptimismCircleColorful,
],
[T, () => HardhatColorful],
]);

const MainIcon: FC<{ chainId: number }> = ({ chainId }) => {
const Icon = getIconByChainId(chainId);
return <Icon style={{ fontSize: 28 }} id={`chain-${chainId}-icon`} />;
};

export const CartesiScanChains = () => {
const [isOpen, { close, open }] = useDisclosure(false);
const config = useConfig();
const chain = config.chains[0];
const chainName = chain.name;

return (
<Menu
withArrow
withinPortal={false}
onClose={close}
onOpen={open}
id="cartesiscan-chain-menu"
>
<Menu.Target>
<Button variant="transparent" p={0}>
<Group gap={3}>
<VisuallyHidden>{chainName}</VisuallyHidden>
<MainIcon chainId={chain.id} />
<CaretIcon up={isOpen} />
</Group>
</Button>
</Menu.Target>

<Menu.Dropdown>
<Menu.Label>
<Divider label="Mainnets" labelPosition="center" />
</Menu.Label>
{mainnets.map((item) => (
<NetworkGroup
key={item.chainId}
chainId={item.chainId}
Icon={item.Icon}
externalLink={item.externalLink}
text={item.text}
currentChain={chain.id}
/>
))}

<Menu.Label>
<Divider label="Testnets" labelPosition="center" />
</Menu.Label>
{testnets.map((item) => (
<NetworkGroup
key={item.chainId}
Icon={item.Icon}
chainId={item.chainId}
externalLink={item.externalLink}
text={item.text}
currentChain={chain.id}
/>
))}
</Menu.Dropdown>
</Menu>
);
};
1 change: 1 addition & 0 deletions apps/web/src/providers/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const themeOverride: MantineThemeOverride = createTheme({
primaryColor: "cyan",
other: {
iconSize: 21,
chainIconSize: 24,
},
components: {
Modal: Modal.extend({
Expand Down
30 changes: 29 additions & 1 deletion apps/web/test/components/layout/shell.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {
fireEvent,
getAllByRole,
getByText,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import { mainnet } from "viem/chains";
import { afterAll, describe, it } from "vitest";
import withMantineTheme from "../../utils/WithMantineTheme";
import Shell from "../../../src/components/layout/shell";
import withMantineTheme from "../../utils/WithMantineTheme";

const Component = withMantineTheme(Shell);

Expand Down Expand Up @@ -74,6 +77,9 @@ vi.mock("@cartesi/rollups-wagmi", async () => {

vi.mock("wagmi", async () => {
return {
useConfig: () => ({
chains: [mainnet],
}),
useContractReads: () => ({
isLoading: false,
isSuccess: true,
Expand Down Expand Up @@ -151,6 +157,28 @@ describe("Shell component", () => {
),
).toThrow("Unable to find an element");
});

it("should display the cartesiscan chain button", async () => {
render(<Component>Children</Component>);

const headerEl = screen.getByTestId("header");

expect(getByText(headerEl, "Ethereum")).toBeInTheDocument();
});

it("should display menu of supported chains after clicking the chain button", async () => {
render(<Component>Children</Component>);

fireEvent.click(screen.getByText("Ethereum"));

const menuEl = await screen.findByRole("menu");

expect(getAllByRole(menuEl, "menuitem")).toHaveLength(4);
expect(getByText(menuEl, "Ethereum")).toBeInTheDocument();
expect(getByText(menuEl, "Sepolia")).toBeInTheDocument();
expect(getByText(menuEl, "OP Mainnet")).toBeInTheDocument();
expect(getByText(menuEl, "OP Sepolia")).toBeInTheDocument();
});
});

describe("Navbar", () => {
Expand Down
Loading
Loading