87 | >(({ className, ...props }, ref) => (
88 | | [role=checkbox]]:translate-y-[2px]',
92 | className,
93 | )}
94 | {...props}
95 | />
96 | ));
97 | TableCell.displayName = 'TableCell';
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | TableCaption.displayName = 'TableCaption';
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | };
121 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@polarzero/savvy",
3 | "url": "https://github.com/0xpolarzero/savvy",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/0xpolarzero/savvy.git"
7 | },
8 | "version": "0.0.1",
9 | "private": false,
10 | "license": "MIT",
11 | "scripts": {
12 | "dev": "next dev",
13 | "build": "next build",
14 | "start": "next start",
15 | "docs:dev": "vocs dev",
16 | "docs:build": "vocs build",
17 | "docs:preview": "vocs preview",
18 | "lint": "next lint",
19 | "format": "prettier --write .",
20 | "typecheck": "tsc --noEmit"
21 | },
22 | "dependencies": {
23 | "@radix-ui/react-accordion": "^1.1.2",
24 | "@radix-ui/react-alert-dialog": "^1.0.5",
25 | "@radix-ui/react-collapsible": "^1.0.3",
26 | "@radix-ui/react-dialog": "^1.0.5",
27 | "@radix-ui/react-dropdown-menu": "^2.0.6",
28 | "@radix-ui/react-hover-card": "^1.0.7",
29 | "@radix-ui/react-icons": "^1.3.0",
30 | "@radix-ui/react-label": "^2.0.2",
31 | "@radix-ui/react-navigation-menu": "^1.1.4",
32 | "@radix-ui/react-popover": "^1.0.7",
33 | "@radix-ui/react-scroll-area": "^1.0.5",
34 | "@radix-ui/react-select": "^2.0.0",
35 | "@radix-ui/react-separator": "^1.0.3",
36 | "@radix-ui/react-slider": "^1.1.2",
37 | "@radix-ui/react-slot": "^1.0.2",
38 | "@radix-ui/react-switch": "^1.0.3",
39 | "@radix-ui/react-tabs": "^1.0.4",
40 | "@radix-ui/react-toggle": "^1.0.3",
41 | "@radix-ui/react-toggle-group": "^1.0.4",
42 | "@radix-ui/react-tooltip": "^1.0.7",
43 | "@shazow/whatsabi": "^0.11.0",
44 | "@tanstack/react-table": "^8.11.8",
45 | "@tevm/bundler": "1.0.0-next.28",
46 | "@tevm/opstack": "1.0.0-next.43",
47 | "@vercel/analytics": "^1.1.2",
48 | "class-variance-authority": "^0.7.0",
49 | "clsx": "^2.1.0",
50 | "cmdk": "^0.2.1",
51 | "lucide-react": "^0.321.0",
52 | "next": "14.1.0",
53 | "next-themes": "^0.2.1",
54 | "react": "^18.2",
55 | "react-dom": "^18.2",
56 | "solady": "^0.0.167",
57 | "sonner": "^1.4.3",
58 | "tailwind-merge": "^2.2.1",
59 | "tailwindcss-animate": "^1.0.7",
60 | "tevm": "1.0.0-next.44",
61 | "uuid": "^9.0.1",
62 | "vaul": "^0.9.0",
63 | "viem": "^2.8.6",
64 | "vocs": "1.0.0-alpha.43",
65 | "web-streams-polyfill": "^4.0.0",
66 | "zustand": "^4.5.0"
67 | },
68 | "devDependencies": {
69 | "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
70 | "@types/node": "^18",
71 | "@types/prettier": "^2.7.3",
72 | "@types/react": "^18.2",
73 | "@types/react-dom": "^18.2",
74 | "@types/uuid": "^9.0.8",
75 | "@typescript-eslint/eslint-plugin": "^7.0.2",
76 | "@typescript-eslint/parser": "^7.0.1",
77 | "autoprefixer": "^10.0.1",
78 | "eslint": "^8.56.0",
79 | "eslint-config-next": "14.1.0",
80 | "eslint-config-prettier": "^9.1.0",
81 | "eslint-plugin-prettier": "^5.1.3",
82 | "eslint-plugin-tailwindcss": "^3.14.3",
83 | "postcss": "^8",
84 | "prettier": "^3.2.5",
85 | "prettier-plugin-tailwindcss": "^0.5.12",
86 | "tailwindcss": "^3.3.0",
87 | "tailwindcss-animate": "^1.0.5",
88 | "ts-plugin": "link:@tevm/plugin/ts-plugin",
89 | "typescript": "^5.2",
90 | "windy-radix-palette": "2.0.0-beta.7"
91 | },
92 | "pnpm": {
93 | "patchedDependencies": {
94 | "@ethereumjs/common@4.1.0": "patches/@ethereumjs__common@4.1.0.patch"
95 | },
96 | "overrides": {
97 | "yaml": "2.3.4"
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { Drawer as DrawerPrimitive } from 'vaul';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | );
17 | Drawer.displayName = 'Drawer';
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger;
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal;
22 |
23 | const DrawerClose = DrawerPrimitive.Close;
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ));
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ));
56 | DrawerContent.displayName = 'DrawerContent';
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | );
67 | DrawerHeader.displayName = 'DrawerHeader';
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | );
78 | DrawerFooter.displayName = 'DrawerFooter';
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ));
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ));
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | };
119 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | import { fontFamily } from 'tailwindcss/defaultTheme';
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ['class'],
6 | content: [
7 | './pages/**/*.{ts,tsx}',
8 | './components/**/*.{ts,tsx}',
9 | './app/**/*.{ts,tsx}',
10 | './src/**/*.{ts,tsx}',
11 | './docs/**/*.{html,md,mdx,tsx,js,jsx}',
12 | ],
13 | prefix: '',
14 | theme: {
15 | container: {
16 | center: true,
17 | padding: '2rem',
18 | screens: {
19 | '2xl': '1400px',
20 | },
21 | },
22 | extend: {
23 | fontFamily: {
24 | sans: ['var(--font-sans)', ...fontFamily.sans],
25 | mono: ['var(--font-mono)', ...fontFamily.mono],
26 | },
27 | colors: {
28 | border: 'hsl(var(--border))',
29 | input: 'hsl(var(--input))',
30 | ring: 'hsl(var(--ring))',
31 | background: 'hsl(var(--background))',
32 | foreground: 'hsl(var(--foreground))',
33 | primary: {
34 | DEFAULT: 'hsl(var(--primary))',
35 | foreground: 'hsl(var(--primary-foreground))',
36 | },
37 | secondary: {
38 | DEFAULT: 'hsl(var(--secondary))',
39 | foreground: 'hsl(var(--secondary-foreground))',
40 | },
41 | destructive: {
42 | DEFAULT: 'hsl(var(--destructive))',
43 | foreground: 'hsl(var(--destructive-foreground))',
44 | },
45 | muted: {
46 | DEFAULT: 'hsl(var(--muted))',
47 | foreground: 'hsl(var(--muted-foreground))',
48 | },
49 | accent: {
50 | DEFAULT: 'hsl(var(--accent))',
51 | foreground: 'hsl(var(--accent-foreground))',
52 | },
53 | popover: {
54 | DEFAULT: 'hsl(var(--popover))',
55 | foreground: 'hsl(var(--popover-foreground))',
56 | },
57 | card: {
58 | DEFAULT: 'hsl(var(--card))',
59 | foreground: 'hsl(var(--card-foreground))',
60 | },
61 | // From Flexoki
62 | // see https://gist.github.com/martin-mael/4b50fa8e55da846f3f73399d84fa1848
63 | red: {
64 | DEFAULT: '#AF3029',
65 | light: '#D14D41',
66 | },
67 | orange: {
68 | DEFAULT: '#BC5215',
69 | light: '#DA702C',
70 | },
71 | yellow: {
72 | DEFAULT: '#AD8301',
73 | light: '#D0A215',
74 | },
75 | green: {
76 | DEFAULT: '#66800B',
77 | light: '#879A39',
78 | },
79 | },
80 | borderRadius: {
81 | lg: 'var(--radius)',
82 | md: 'calc(var(--radius) - 2px)',
83 | sm: 'calc(var(--radius) - 4px)',
84 | },
85 | keyframes: {
86 | 'accordion-down': {
87 | from: { height: '0' },
88 | to: { height: 'var(--radix-accordion-content-height)' },
89 | },
90 | 'accordion-up': {
91 | from: { height: 'var(--radix-accordion-content-height)' },
92 | to: { height: '0' },
93 | },
94 | 'slide-down': {
95 | '0%': { transform: 'translateY(0)' },
96 | '100%': { transform: 'translateY(100%)' },
97 | },
98 | 'slide-up': {
99 | '0%': { transform: 'translateY(100%)' },
100 | '100%': { transform: 'translateY(0)' },
101 | },
102 | },
103 | animation: {
104 | 'accordion-down': 'accordion-down 0.2s ease-out',
105 | 'accordion-up': 'accordion-up 0.2s ease-out',
106 | 'slide-down': 'slide-down 0.5s ease-in forwards',
107 | 'slide-up': 'slide-up 0.5s ease-out forwards',
108 | },
109 | },
110 | },
111 | plugins: [require('tailwindcss-animate'), require('windy-radix-palette')],
112 | };
113 |
--------------------------------------------------------------------------------
/docs/components/progress.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode } from 'react';
2 | import { CheckIcon, StarIcon, XIcon } from 'lucide-react';
3 |
4 | import { cn } from '../utils';
5 | import { Badge, BadgeIntent } from './badge';
6 |
7 | /* -------------------------------------------------------------------------- */
8 | /* TYPES */
9 | /* -------------------------------------------------------------------------- */
10 |
11 | type ProgressListHeadingProps = {
12 | title: string;
13 | caption?: string;
14 | };
15 |
16 | type ProgressListContentProps = {
17 | caption?: string;
18 | children: ReactNode;
19 | };
20 |
21 | type ProgressItemProps = {
22 | completed: boolean;
23 | priority?: boolean;
24 | difficulty?: number;
25 | children: ReactNode;
26 | };
27 |
28 | type DifficultyBadgeProps = {
29 | difficulty: number;
30 | };
31 |
32 | /* -------------------------------------------------------------------------- */
33 | /* CONSTANTS */
34 | /* -------------------------------------------------------------------------- */
35 |
36 | type DifficultyData = {
37 | label: string;
38 | intent: BadgeIntent;
39 | };
40 |
41 | const DifficultyBadge: FC = ({ difficulty }) => {
42 | let data: DifficultyData = { label: '', intent: 'none' };
43 | if (difficulty === -1) data = { label: 'unknown', intent: 'none' };
44 | if (difficulty === 0) data = { label: 'minor', intent: 'none' };
45 | if (difficulty === 1) data = { label: 'moderate', intent: 'primary' };
46 | if (difficulty === 2) data = { label: 'major', intent: 'warning' };
47 | if (difficulty === 3) data = { label: 'extensive', intent: 'fail' };
48 |
49 | return (
50 |
51 | {data.label}
52 |
53 | );
54 | };
55 |
56 | /* -------------------------------------------------------------------------- */
57 | /* COMPONENTS */
58 | /* -------------------------------------------------------------------------- */
59 |
60 | export const ProgressListHeading: FC = ({
61 | title,
62 | caption,
63 | }) => {
64 | return (
65 |
66 | {title}
67 | {caption ? (
68 | {caption}
69 | ) : null}
70 |
71 | );
72 | };
73 |
74 | export const ProgressListContent: FC = ({
75 | children,
76 | }) => {
77 | return (
78 |
79 | {children}
80 |
81 | );
82 | };
83 |
84 | export const ProgressItem: FC = ({
85 | completed,
86 | priority,
87 | difficulty,
88 | children,
89 | }) => {
90 | return (
91 | <>
92 |
98 |
99 | {completed ? (
100 |
101 | ) : priority ? (
102 |
103 | ) : (
104 |
105 | )}
106 |
107 |
108 | {children}
109 | {difficulty !== undefined ? (
110 |
111 | ) : (
112 |
113 | )}
114 | >
115 | );
116 | };
117 |
--------------------------------------------------------------------------------
/src/components/common/currency-amount.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useMemo } from 'react';
2 | import { formatUnits } from 'viem';
3 |
4 | import { DEFAULTS } from '@/lib/constants/defaults';
5 | import { cn } from '@/lib/utils';
6 | import { Icons } from '@/components/common/icons';
7 | import TooltipResponsive from '@/components/common/tooltip-responsive';
8 |
9 | type CurrencyAmountProps = {
10 | amount: string | number | bigint;
11 | symbol?: 'ETH' | 'MATIC' | 'USD' | string;
12 | decimals?: number;
13 | scaled?: boolean;
14 | full?: boolean;
15 | icon?: boolean;
16 | className?: string;
17 | };
18 |
19 | // Opinionated function to scale down the usd amount (easier for precision to use bigint with full decimals)
20 | const scaleDownUsd = (amount: string | number | bigint) =>
21 | formatUnits(BigInt(amount), Number(DEFAULTS.decimalsUsd));
22 |
23 | /**
24 | * @notice A component to display a currency amount with its symbol or icon
25 | * @param amount The amount to display (full number including decimals)
26 | * @param symbol The symbol of the currency (default: 'ETH')
27 | * @param decimals The decimals of the currency (default: 18 for ETH)
28 | * @param scaled Whether the amount is already scaled (default: false)
29 | * @param full Whether to display the full amount (default: false)
30 | * @param icon Whether to display the icon of the currency (default: true)
31 | * @param className Additional classes to apply to the component
32 | */
33 | const CurrencyAmount: FC = ({
34 | amount = 0,
35 | symbol = 'ETH',
36 | decimals = 18,
37 | scaled = false,
38 | full = false,
39 | icon = true,
40 | className,
41 | }) => {
42 | // Format the amount correctly based on the currency
43 | const formatted = useMemo(() => {
44 | return symbol === 'USD'
45 | ? scaled
46 | ? amount
47 | : scaleDownUsd(amount)
48 | : formatUnits(BigInt(amount), decimals);
49 | }, [amount, symbol, decimals, scaled]);
50 |
51 | // Format the displayed amount
52 | const displayedAmount = useMemo(() => {
53 | const tokenDisplayLimit = symbol === 'MATIC' ? BigInt(1e11) : BigInt(1e13);
54 | const tokenDisplayPrecision = symbol === 'MATIC' ? 4 : 5;
55 |
56 | switch (symbol) {
57 | case 'USD':
58 | if (Number(formatted) < 0.01 && Number(formatted) !== 0) return '<0.01';
59 | if (Number(formatted) === 0) return '0';
60 | return `${parseFloat(Number(formatted).toFixed(2))}`;
61 |
62 | default:
63 | if (BigInt(amount) === BigInt(0)) return '0';
64 | // 0 < amount < 0.00001 ETH || 0 < amount < 0.0000001 MATIC
65 | if (BigInt(amount) < tokenDisplayLimit && BigInt(amount) > BigInt(0))
66 | return '<0.00001';
67 | return parseFloat(Number(formatted).toFixed(tokenDisplayPrecision));
68 | }
69 | }, [amount, formatted, symbol]);
70 |
71 | // Does the currency have an icon?
72 | const hasIcon = symbol === 'ETH' || symbol === 'MATIC' || symbol === 'USD';
73 |
74 | if (full)
75 | return (
76 |
77 | {formatted.toString()} {symbol}
78 |
79 | );
80 |
81 | return (
82 |
85 | {icon && symbol === 'ETH' ? (
86 |
87 | ) : icon && symbol === 'MATIC' ? (
88 |
89 | ) : icon && symbol === 'USD' ? (
90 | $
91 | ) : null}
92 | {displayedAmount} {!hasIcon ? symbol : null}
93 |
94 | }
95 | content={`${formatted.toString()} ${symbol}`}
96 | classNameTrigger={cn('w-min whitespace-nowrap', className)}
97 | />
98 | );
99 | };
100 |
101 | export default CurrencyAmount;
102 |
--------------------------------------------------------------------------------
/docs/pages/architecture.mdx:
--------------------------------------------------------------------------------
1 | import {
2 | ArchitectureItem,
3 | ArchitectureWrapper,
4 | } from '../components/architecture';
5 |
6 | # Architecture
7 |
8 | :::note
9 | The structure below can be accessed inside the [`src`](https://github.com/0xpolarzero/savvy/tree/main/src) folder.
10 | :::
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/common/tooltip-responsive.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode, useMemo } from 'react';
2 |
3 | import { useMediaQuery } from '@/lib/hooks/use-media-query';
4 | import { cn } from '@/lib/utils';
5 | import {
6 | Popover,
7 | PopoverContent,
8 | PopoverTrigger,
9 | } from '@/components/ui/popover';
10 | import {
11 | Tooltip,
12 | TooltipContent,
13 | TooltipProvider,
14 | TooltipTrigger,
15 | } from '@/components/ui/tooltip';
16 | import { Icons } from '@/components/common/icons';
17 |
18 | /* ---------------------------- SPECIAL TRIGGERS ---------------------------- */
19 | type SpecialTrigger = 'info' | 'warning' | 'error' | 'success';
20 | const iconClasses =
21 | 'w-4 h-4 text-muted-foreground hover:text-foreground transition-colors';
22 | // Mapping of special triggers to their respective icons
23 | const specialTriggerIcons: Record = {
24 | info: ,
25 | warning: (
26 |
29 | ),
30 | error: (
31 |
32 | ),
33 | success: (
34 |
37 | ),
38 | };
39 |
40 | /* --------------------------------- TOOLTIP -------------------------------- */
41 | type TooltipResponsiveProps = {
42 | trigger: ReactNode | string | SpecialTrigger;
43 | content: ReactNode | string;
44 | classNameTrigger?: string;
45 | classNameContent?: string;
46 | disabled?: boolean;
47 | };
48 |
49 | /**
50 | * @notice A responsive tooltip that switches to a popover on mobile
51 | * @dev We need a popover on touch devices to avoid hover issues; basically it works like a tooltip
52 | * that needs to be explicitly clicked/tapped to show the content
53 | * @param trigger The element that triggers the tooltip; this can be a string for special triggers
54 | * @param content The content to display in the tooltip
55 | * @param classNameTrigger Additional classes to apply to the trigger
56 | * @param classNameContent Additional classes to apply to the content inside the tooltip
57 | * @param disabled Whether to disable the tooltip (default: false)
58 | */
59 | const TooltipResponsive: FC = ({
60 | trigger,
61 | content,
62 | classNameTrigger,
63 | classNameContent,
64 | disabled,
65 | }) => {
66 | const isDesktop = useMediaQuery('(min-width: 768px)'); // md
67 | const triggerMapped = useMemo(() => {
68 | if (typeof trigger === 'string' && trigger in specialTriggerIcons)
69 | return specialTriggerIcons[trigger as SpecialTrigger];
70 |
71 | return trigger;
72 | }, [trigger]);
73 |
74 | /* --------------------------------- DESKTOP -------------------------------- */
75 | if (isDesktop)
76 | return (
77 |
78 |
79 |
80 | {triggerMapped}
81 |
82 | {disabled ? null : (
83 |
84 | {content}
85 |
86 | )}
87 |
88 |
89 | );
90 |
91 | /* -------------------------------- MOBILE --------------------------------- */
92 | return (
93 |
94 |
95 | {triggerMapped}
96 |
97 | {disabled ? null : (
98 |
99 | {content}
100 |
101 | )}
102 |
103 | );
104 | };
105 |
106 | export default TooltipResponsive;
107 |
--------------------------------------------------------------------------------
/src/components/common/shrinked-address.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC, useMemo, useState } from 'react';
4 | import Link from 'next/link';
5 | import { usePathname } from 'next/navigation';
6 | import { Address } from 'tevm/utils';
7 |
8 | import { useMediaQuery } from '@/lib/hooks/use-media-query';
9 | import { Icons } from '@/components/common/icons';
10 |
11 | type ShrinkedAddressProps = {
12 | address: Address;
13 | explorer?: string;
14 | adapt?: boolean;
15 | full?: boolean;
16 | className?: string;
17 | };
18 |
19 | /**
20 | * @notice A component to display a shrinked version of an Ethereum address
21 | * @dev The component will display the first 6 and the last 4 characters of the address,
22 | * with a "..." in between, and a tooltip with the full address.
23 | * Depending on the size of the screen, the full address will be displayed or not.
24 | * @param address The Ethereum address to display
25 | * @param explorer The URL of the explorer to link the address to
26 | * @param adapt Whether to adapt the display to the screen size (default: true)
27 | * @param full Whether to display the full address (default: false)
28 | * @param className Additional classes for the component
29 | */
30 | const ShrinkedAddress: FC = ({
31 | address = '0x', // default to avoid errors
32 | explorer,
33 | adapt = true,
34 | full = false,
35 | className,
36 | }) => {
37 | const pathname = usePathname();
38 | const isLargeScreen = useMediaQuery('(min-width: 1280px)'); // xl
39 | const [copy, setCopy] = useState(false);
40 |
41 | // Display the full address only on large screens (if adapt is true)
42 | const fullAddress = useMemo(() => {
43 | if ((isLargeScreen && adapt) || full) return true;
44 | return false;
45 | }, [isLargeScreen, adapt, full]);
46 |
47 | // Open the address in the explorer for this chain
48 | const openExplorerTab = () => {
49 | if (!explorer) return;
50 | window.open(
51 | `${explorer.endsWith('/') ? explorer : `${explorer}/`}address/${address}`,
52 | '_blank',
53 | );
54 | };
55 |
56 | // Copy the address to the clipboard (and update the icon)
57 | const CopyButton = useMemo(() => {
58 | const copyToClipboard = () => {
59 | navigator.clipboard.writeText(address);
60 | setCopy(true);
61 | setTimeout(() => setCopy(false), 2000);
62 | };
63 |
64 | if (copy) {
65 | return ;
66 | }
67 |
68 | return (
69 |
74 | );
75 | }, [address, copy]);
76 |
77 | if (explorer)
78 | return (
79 |
80 |
86 |
87 | {fullAddress
88 | ? address
89 | : `0x${address.slice(2, 6)}...${address.slice(-4)}`}
90 |
91 |
92 | {CopyButton}
93 |
98 |
99 | );
100 |
101 | return (
102 |
103 |
104 |
105 | {fullAddress
106 | ? address
107 | : `0x${address.slice(2, 6)}...${address.slice(-4)}`}
108 |
109 |
110 | {CopyButton}
111 |
112 | );
113 | };
114 |
115 | export default ShrinkedAddress;
116 |
--------------------------------------------------------------------------------
/src/lib/types/providers.ts:
--------------------------------------------------------------------------------
1 | import { MemoryClient } from 'tevm';
2 | import { PublicClient, Chain as ViemChain } from 'viem';
3 |
4 | import { GasControls } from '@/lib/types/gas';
5 | import { Icon } from '@/components/common/icons';
6 |
7 | /* -------------------------------------------------------------------------- */
8 | /* CHAINS */
9 | /* -------------------------------------------------------------------------- */
10 |
11 | /* ---------------------------------- BASE ---------------------------------- */
12 | /**
13 | * @notice A consensus mechanism
14 | */
15 | type ConsensusMechanism = 'PoW' | 'PoS' | 'DPoS' | 'Optimistic Rollup';
16 |
17 | /**
18 | * @notice A rollup development framework
19 | */
20 | export type RollupFramework = 'op-stack' | 'arbitrum-orbit';
21 |
22 | /**
23 | * @notice The base technical details of a chain
24 | * @param consensusMechanism The consensus mechanism of the chain
25 | * @param avgBlockTime The average time between blocks in seconds
26 | * @param layer The layer of the chain (1, 2, 3)
27 | * @param evmCompatible Whether the chain is EVM compatible or not
28 | * @param hasPriorityFee Whether the chain uses priority fees or not (e.g. Arbitrum One does not)
29 | * @param underlying The underlying chain (as a viem chain) if it's a layer 2+ (optional)
30 | * @param rollup The rollup development framework (optional)
31 | */
32 | type ChainTech = {
33 | consensusMechanism: ConsensusMechanism | undefined;
34 | avgBlockTime: number;
35 | layer: number;
36 | evmCompatible: boolean;
37 | hasPriorityFee: boolean;
38 | underlying?: Chain;
39 | rollup?: RollupFramework;
40 | };
41 |
42 | /**
43 | * @notice The chain configuration
44 | * @param rpcUrl The RPC URL to interact with the chain (used to create a Tevm memory client as well)
45 | * @param provider The public viem client for base chain queries
46 | * @param nativeTokenSlug The Coinmarketcap slug of the native token
47 | * @param gasControls The gas controls for this chain (optional)
48 | * @param icon The icon of the chain (optional)
49 | * @param disabled Whether the chain is disabled or not (optional)
50 | */
51 | type ChainConfig = {
52 | rpcUrl: string;
53 | provider: PublicClient;
54 | nativeTokenSlug: string;
55 | gasControls?: GasControls;
56 | icon?: Icon;
57 | disabled?: boolean;
58 | };
59 |
60 | /* --------------------------------- EXPORTS -------------------------------- */
61 | export type { ViemChain };
62 |
63 | /**
64 | * @type {Chain}
65 | * @notice A Viem chain with custom technical details and configuration
66 | * @dev The rpc url will be used to create a Tevm memory client from a fork.
67 | * @dev The provider is useful to fetch ABIs with WhatsABI.
68 | */
69 | export type Chain = ViemChain & {
70 | custom: {
71 | tech: ChainTech;
72 | config: ChainConfig;
73 | };
74 | };
75 |
76 | /**
77 | * @type {OptimisticRollupBase}
78 | * @notice The base technical details of an optimistic rollup chain
79 | */
80 | export type OptimisticRollupBase = Omit<
81 | ChainTech,
82 | 'avgBlockTime' | 'hasPriorityFee' | 'rollup'
83 | >;
84 |
85 | /* --------------------------------- CUSTOM --------------------------------- */
86 | export type CustomChainOptions = {
87 | name: string;
88 | rpcUrl: string;
89 | chainId: number;
90 | nativeToken: {
91 | name: string;
92 | symbol: string;
93 | decimals: number;
94 | slug: string;
95 | };
96 | layer: number;
97 | evmCompatible: boolean;
98 | hasPriorityFee: boolean;
99 | rollup?: RollupFramework;
100 | underlyingChain?: string;
101 | };
102 |
103 | /* -------------------------------------------------------------------------- */
104 | /* ACCOUNTS */
105 | /* -------------------------------------------------------------------------- */
106 |
107 | /**
108 | * @type {Object} UpdateAccountOptions
109 | * @notice Options for updating the account state (optional)
110 | * @param {boolean} updateAbi Whether to attempt to fetch/refetch the ABI at the provided address
111 | * @param {Chain} chain The chain to target for the abi retrieval
112 | * @param {MemoryClient} client The client to use for the account state
113 | */
114 | export type UpdateAccountOptions = {
115 | updateAbi: boolean;
116 | chain: Chain;
117 | client: MemoryClient;
118 | };
119 |
--------------------------------------------------------------------------------
/src/lib/tevm/account.ts:
--------------------------------------------------------------------------------
1 | import { MemoryClient } from 'tevm';
2 | import { Address, isAddress } from 'tevm/utils';
3 | import { normalize } from 'viem/ens';
4 |
5 | import { Account } from '@/lib/types/config';
6 | import { Chain } from '@/lib/types/providers';
7 |
8 | /* ---------------------------------- TYPES --------------------------------- */
9 | /**
10 | * @type {Function} GetAccount
11 | * @param {MemoryClient} client The client to get the account from
12 | * @param {Chain} chain The chain to get the account from
13 | * @param {Address | string} addressOrENS The address of the account (or ENS)
14 | * @returns {Promise} The account object
15 | */
16 | type GetAccount = (
17 | client: MemoryClient,
18 | chain: Chain,
19 | addressOrENS: Address | string,
20 | ) => Promise;
21 |
22 | /* -------------------------------- FUNCTIONS ------------------------------- */
23 | /**
24 | * @notice Get the account for a given address using a Tevm client
25 | * @dev This will return the account object for the given address.
26 | * @dev If the action fails, an error will be displayed and it will still return an
27 | * account object with the address and an error message.
28 | */
29 | export const getAccount: GetAccount = async (client, chain, addressOrENS) => {
30 | const emptyAccount: Account = {
31 | address: isAddress(addressOrENS) ? addressOrENS : '0x',
32 | ens: isAddress(addressOrENS) ? undefined : addressOrENS,
33 | balance: BigInt(0),
34 | deployedBytecode: '0x',
35 | nonce: BigInt(0),
36 | storageRoot: '0x',
37 | codeHash: '0x',
38 | isContract: false,
39 | isEmpty: false,
40 | errors: [],
41 | };
42 |
43 | try {
44 | // ENS
45 | // Grab the right provider, depending on the chain that can resolve ENS
46 | const resolver = chain.contracts?.ensUniversalResolver;
47 | const provider = resolver
48 | ? chain.custom.config.provider
49 | : chain.custom.tech.underlying?.custom.config.provider;
50 |
51 | // If an ENS was provided and there is no provider, return an error
52 | if (!provider && !isAddress(addressOrENS)) {
53 | return {
54 | ...emptyAccount,
55 | errors: [
56 | {
57 | _tag: 'InvalidRequestError',
58 | name: 'InvalidRequestError',
59 | message: `Could not find an ENS resolver for ${chain.name}`,
60 | },
61 | ],
62 | };
63 | }
64 |
65 | // Find the address if an ENS was provided, and conversely for information purposes
66 | const address = isAddress(addressOrENS)
67 | ? addressOrENS
68 | : await provider?.getEnsAddress({
69 | name: normalize(addressOrENS),
70 | });
71 | const ens = isAddress(addressOrENS)
72 | ? ((await provider?.getEnsName({ address: addressOrENS })) as
73 | | string
74 | | undefined)
75 | : addressOrENS;
76 |
77 | // Make sure the user is clearly notified if there is something wrong with the address
78 | if (!address)
79 | return {
80 | ...emptyAccount,
81 | errors: [
82 | {
83 | _tag: 'InvalidAddressError',
84 | name: 'InvalidAddressError',
85 | message: `Invalid address or ENS: ${address}`,
86 | },
87 | ],
88 | };
89 |
90 | const accountResult = await client.getAccount({ address });
91 | return {
92 | ...accountResult,
93 | // This is a safeguard against an account being mislabeled as invalid
94 | // although very unlikely to happen
95 | address,
96 | ens,
97 | // TODO TEMP fix until RPC providers fix their eth_getProof method
98 | // (some returning address(0), some returning keccak256("no data")...)
99 | isContract: accountResult.deployedBytecode !== '0x',
100 | isEmpty:
101 | accountResult.codeHash ===
102 | '0x0000000000000000000000000000000000000000000000000000000000000000',
103 | };
104 | } catch (err) {
105 | console.error(err);
106 | return {
107 | ...emptyAccount,
108 | errors: [
109 | {
110 | _tag: 'UnexpectedError',
111 | name: 'UnexpectedError',
112 | message:
113 | err instanceof Error
114 | ? err.message.split('.').slice(0, 1).join('.') + '...'
115 | : String(err),
116 | },
117 | ],
118 | };
119 | }
120 | };
121 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import { Cross2Icon } from '@radix-ui/react-icons';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = 'DialogHeader';
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = 'DialogFooter';
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/src/components/core/tx-history/collapsible-mobile.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import {
3 | flexRender,
4 | Row,
5 | Table as TableInterface,
6 | } from '@tanstack/react-table';
7 |
8 | import { cn } from '@/lib/utils';
9 | import {
10 | Accordion,
11 | AccordionContent,
12 | AccordionItem,
13 | AccordionTrigger,
14 | } from '@/components/ui/accordion';
15 | import { Separator } from '@/components/ui/separator';
16 | import DataTablePagination from '@/components/templates/table/pagination';
17 |
18 | /* ---------------------------------- TYPES --------------------------------- */
19 | type TxHistoryCollapsibleMobileProps = {
20 | table: TableInterface;
21 | expandableRender: (row: Row) => ReactNode;
22 | header: ReactNode;
23 | noDataLabel?: string;
24 | pagination?: boolean;
25 | className?: string;
26 | };
27 |
28 | /**
29 | * @notice An alternative to a table for displaying the txs history on mobile (in collapsibles)
30 | * @param table The table instance from tanstack react-table
31 | * @param expandableRender The methode to render the expandable content (row)
32 | * @param header A header displayed above the collapsibles
33 | * @param noDataLabel A label to display when there is no data (default: 'No results.')
34 | * @param pagination Whether to display pagination (default: false)
35 | * @param className Additional classes to apply to the wrapper
36 | */
37 | const TxHistoryCollapsibleMobile = ({
38 | table,
39 | expandableRender,
40 | header,
41 | noDataLabel = 'No results.',
42 | pagination = false,
43 | className,
44 | }: TxHistoryCollapsibleMobileProps) => {
45 | return (
46 |
47 | {header ? {header} : null}
48 |
49 | {table.getRowModel().rows?.length ? (
50 | table.getRowModel().rows.map((row) => (
51 |
64 |
65 | <>
66 | {/* id | function name | status | include | expand button */}
67 | {flexRender(
68 | row.getVisibleCells()[0].column.columnDef.cell,
69 | row.getVisibleCells()[0].getContext(),
70 | )}
71 | {flexRender(
72 | row.getVisibleCells()[2].column.columnDef.cell,
73 | row.getVisibleCells()[2].getContext(),
74 | )}
75 | {flexRender(
76 | row.getVisibleCells()[5].column.columnDef.cell,
77 | row.getVisibleCells()[5].getContext(),
78 | )}
79 | {flexRender(
80 | row.getVisibleCells()[6].column.columnDef.cell,
81 | row.getVisibleCells()[6].getContext(),
82 | )}
83 | >
84 |
85 |
86 | {/* timestamp */}
87 |
88 | {flexRender(
89 | row.getVisibleCells()[4].column.columnDef.cell,
90 | row.getVisibleCells()[3].getContext(),
91 | )}
92 |
93 |
94 |
95 | {/* chain, account, caller, tx value, gas used, data, errors, logs, inputs */}
96 | {expandableRender(row)}
97 |
98 |
99 | ))
100 | ) : (
101 |
102 | {noDataLabel}
103 |
104 | )}
105 |
106 | {pagination ? : null}
107 |
108 | );
109 | };
110 |
111 | export default TxHistoryCollapsibleMobile;
112 |
--------------------------------------------------------------------------------
/src/lib/tevm/op-stack.ts:
--------------------------------------------------------------------------------
1 | import { createGasPriceOracle, createL1Block } from '@tevm/opstack';
2 | import { encodePacked, MemoryClient } from 'tevm';
3 | import { Hex } from 'tevm/utils';
4 | import { toFunctionSelector } from 'viem';
5 |
6 | import { nativeToUsd } from '../gas';
7 |
8 | const DEPOSITOR_ACCOUNT = '0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001';
9 | const GasPriceOracle = createGasPriceOracle();
10 | const L1Block = createL1Block();
11 |
12 | type CalculateL1DataFee = (
13 | client: MemoryClient,
14 | txData: Hex,
15 | baseFee: bigint,
16 | blobBaseFee: bigint,
17 | nativeTokenPrice: number,
18 | nativeTokenDecimals: number,
19 | ) => Promise<{
20 | native: bigint | undefined;
21 | usd: bigint | undefined;
22 | error: string | null;
23 | }>;
24 |
25 | /**
26 | * @notice Calculate the L1 data fee for a transaction on the OP stack
27 | * @dev This will fetch the current base fee and blob base fee scalars from the chain, then set
28 | * them on the L1Block contract and call the GasPriceOracle to calculate the L1 data fee for the transaction data.
29 | * @dev Here we're assuming the chain has activated the Ecotone upgrade. If it was not obvious, we could just check it
30 | * and then call either `setL1BlockValues()` or `setL1BlockValuesEcotone()` depending on the upgrade status.
31 | * @param client The memory client for the chain
32 | * @param txData The serialized transaction data
33 | * @param baseFee The base fee for the transaction on the underlying chain
34 | * @param blobBaseFee The blob base fee for the transaction on the underlying chain
35 | * @param nativeTokenPrice The price of the native token in USD
36 | * @param nativeTokenDecimals The decimals of the native token
37 | */
38 | // see https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L1Block.sol#L101
39 | export const calculateL1DataFee: CalculateL1DataFee = async (
40 | client,
41 | txData,
42 | baseFee,
43 | blobBaseFee,
44 | nativeTokenPrice,
45 | nativeTokenDecimals,
46 | ) => {
47 | // We just want to grab any first error that occurs, since any subsequent call would be inacurate
48 | try {
49 | // Find the scalar values for the base and blob base fees on this chain
50 | const { data: baseFeeScalar } = await client.contract({
51 | ...GasPriceOracle.read.baseFeeScalar(),
52 | });
53 |
54 | const { data: blobBaseFeeScalar } = await client.contract({
55 | ...GasPriceOracle.read.blobBaseFeeScalar(),
56 | });
57 |
58 | // Set the L1Block values
59 | await client.call({
60 | caller: DEPOSITOR_ACCOUNT,
61 | to: L1Block.address,
62 | createTransaction: true,
63 | // We're encoding the data manually because the function doesn't specify inputs,
64 | // instead it uses the calldata directly
65 | data: (toFunctionSelector('setL1BlockValuesEcotone()') +
66 | encodePacked(
67 | [
68 | 'uint32', // _baseFeeScalar
69 | 'uint32', // _blobBaseFeeScalar
70 | 'uint64', // _sequenceNumber
71 | 'uint64', // _timestamp
72 | 'uint64', // _number
73 | 'uint256', // _basefee
74 | 'uint256', // _blobBaseFee
75 | 'bytes32', // _hash
76 | 'bytes32', // _batcherHash
77 | ],
78 | [
79 | Number(baseFeeScalar) || 0,
80 | Number(blobBaseFeeScalar) || 0,
81 | BigInt(0),
82 | BigInt(0),
83 | BigInt(0),
84 | baseFee,
85 | blobBaseFee,
86 | '0x0000000000000000000000000000000000000000000000000000000000000000',
87 | '0x0000000000000000000000000000000000000000000000000000000000000000',
88 | ],
89 | ).slice(2)) as Hex,
90 | });
91 |
92 | // Calculate the L1 data fee
93 | const { data: l1DataFee } = await client.contract({
94 | ...GasPriceOracle.read.getL1Fee(txData),
95 | });
96 |
97 | // Convert the L1 data fee to USD
98 | const l1DataFeeUsd = l1DataFee
99 | ? nativeToUsd(l1DataFee, nativeTokenPrice, nativeTokenDecimals)
100 | : undefined;
101 |
102 | return { native: l1DataFee, usd: l1DataFeeUsd, error: null };
103 | } catch (err) {
104 | return {
105 | native: undefined,
106 | usd: undefined,
107 | error:
108 | err && typeof err === 'object' && 'message' in err
109 | ? (err.message as string)
110 | : err instanceof Error
111 | ? err.toString()
112 | : 'Unknown error',
113 | };
114 | }
115 | };
116 |
--------------------------------------------------------------------------------
/src/components/templates/table/pagination.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ChevronLeftIcon,
3 | ChevronRightIcon,
4 | DoubleArrowLeftIcon,
5 | DoubleArrowRightIcon,
6 | } from '@radix-ui/react-icons';
7 | import { Table } from '@tanstack/react-table';
8 |
9 | import { useMediaQuery } from '@/lib/hooks/use-media-query';
10 | import { Button } from '@/components/ui/button';
11 | import {
12 | Select,
13 | SelectContent,
14 | SelectItem,
15 | SelectTrigger,
16 | SelectValue,
17 | } from '@/components/ui/select';
18 |
19 | interface DataTablePaginationProps {
20 | table: Table;
21 | }
22 |
23 | /* --------------------------------- PARENT --------------------------------- */
24 | /**
25 | * @notice A pagination component for the DataTable
26 | * @dev This component is meant to be displayed alongside the DataTable with the same
27 | * table instance
28 | * @param table The table instance from tanstack react-table
29 | * @dev Modified from shadcn/ui
30 | * @see https://ui.shadcn.com/docs/components/data-table
31 | */
32 | const DataTablePagination = ({
33 | table,
34 | }: DataTablePaginationProps) => {
35 | const isDesktop = useMediaQuery('(min-width: 768px)'); // md
36 |
37 | return (
38 |
39 | {isDesktop ? (
40 | <>
41 |
42 |
43 | >
44 | ) : (
45 | <>
46 |
47 |
48 | >
49 | )}
50 |
51 | );
52 | };
53 |
54 | /* ------------------------- ROWS PER PAGE CONTROLS ------------------------- */
55 | const RowsPerPageControls = ({
56 | table,
57 | }: DataTablePaginationProps) => {
58 | return (
59 |
60 | Rows per page
61 |
78 |
79 | );
80 | };
81 |
82 | /* ------------------------ NAVIGATION CONTROLS ------------------------ */
83 | const NavigationControls = ({
84 | table,
85 | }: DataTablePaginationProps) => {
86 | return (
87 |
88 |
89 | Page {table.getState().pagination.pageIndex + 1} of{' '}
90 | {table.getPageCount()}
91 |
92 |
93 |
102 |
111 |
120 |
129 |
130 |
131 | );
132 | };
133 |
134 | export default DataTablePagination;
135 |
--------------------------------------------------------------------------------
/src/components/core/tx-history/total-fees.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | import { TotalFees } from '@/lib/types/tx';
4 | import { useProviderStore } from '@/lib/store/use-provider';
5 | import { Separator } from '@/components/ui/separator';
6 | import { Skeleton } from '@/components/ui/skeleton';
7 | import CurrencyAmount from '@/components/common/currency-amount';
8 |
9 | type TotalFeesTableProps = {
10 | totalFees: TotalFees[number];
11 | txAmount: number;
12 | hydrating: boolean;
13 | };
14 |
15 | /**
16 | * @notice A table to display the total fees across all (included) transactions
17 | * @param totalFees The total fees on the chain (+ submission if relevant), in native tokens and usd
18 | * @param txAmount The amount of transactions on this chain
19 | * @param hydrating Whether the component is still hydrating
20 | */
21 | const TotalFeesTable: FC = ({
22 | totalFees,
23 | txAmount,
24 | hydrating,
25 | }) => {
26 | // The current chain
27 | const chain = useProviderStore((state) => state.chain);
28 | const underlying = chain.custom.tech.underlying?.name;
29 |
30 | if (hydrating) return ;
31 | return (
32 |
33 |
34 |
35 | Fees
36 | {txAmount ? (
37 |
38 | {totalFees.gasUsed} gas{' '}
39 | across {txAmount} transaction{txAmount > 1 ? 's' : ''}
40 |
41 | ) : null}
42 |
43 |
44 |
45 | {underlying && txAmount ? (
46 |
47 | Total
48 |
49 | ) : null}
50 |
59 |
67 |
68 | {underlying && txAmount ? (
69 | <>
70 |
71 |
72 | L2 ({chain.name})
73 |
74 |
80 |
85 |
86 |
87 |
88 | L1 submission ({underlying})
89 |
90 |
96 |
101 |
102 | {txAmount ? (
103 |
104 | On average, the L1 submission accounted for{' '}
105 |
106 | {(
107 | (Number(totalFees.costNative.l1Submission) * 100) /
108 | (Number(totalFees.costNative.root) +
109 | Number(totalFees.costNative.l1Submission))
110 | ).toFixed(2)}{' '}
111 | %
112 | {' '}
113 | of the fees.
114 |
115 | ) : null}
116 | >
117 | ) : null}
118 |
119 |
120 | );
121 | };
122 |
123 | export default TotalFeesTable;
124 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SheetPrimitive from '@radix-ui/react-dialog';
5 | import { Cross2Icon } from '@radix-ui/react-icons';
6 | import { cva, type VariantProps } from 'class-variance-authority';
7 |
8 | import { cn } from '@/lib/utils';
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const SheetClose = SheetPrimitive.Close;
15 |
16 | const SheetPortal = SheetPrimitive.Portal;
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32 |
33 | const sheetVariants = cva(
34 | 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
35 | {
36 | variants: {
37 | side: {
38 | top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
39 | bottom:
40 | 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
41 | left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
42 | right:
43 | 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
44 | },
45 | },
46 | defaultVariants: {
47 | side: 'right',
48 | },
49 | },
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = 'right', className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ));
75 | SheetContent.displayName = SheetPrimitive.Content.displayName;
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | );
89 | SheetHeader.displayName = 'SheetHeader';
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | );
103 | SheetFooter.displayName = 'SheetFooter';
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ));
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | };
141 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
5 |
6 | import { cn } from '@/lib/utils';
7 | import { buttonVariants } from '@/components/ui/button';
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root;
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal;
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ));
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | );
60 | AlertDialogHeader.displayName = 'AlertDialogHeader';
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | );
74 | AlertDialogFooter.displayName = 'AlertDialogFooter';
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ));
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ));
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName;
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ));
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | };
142 |
--------------------------------------------------------------------------------
/src/components/core/selection/config-menu/mobile.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { Settings } from 'lucide-react';
5 |
6 | import { CustomChainOptions } from '@/lib/types/providers';
7 | import { useMediaQuery } from '@/lib/hooks/use-media-query';
8 | import { useConfigStore } from '@/lib/store/use-config';
9 | import { useProviderStore } from '@/lib/store/use-provider';
10 | import { cn } from '@/lib/utils';
11 | import {
12 | Breadcrumb,
13 | BreadcrumbItem,
14 | BreadcrumbLink,
15 | BreadcrumbList,
16 | BreadcrumbPage,
17 | BreadcrumbSeparator,
18 | } from '@/components/ui/breadcrumb';
19 | import { Button } from '@/components/ui/button';
20 | import {
21 | Sheet,
22 | SheetContent,
23 | SheetDescription,
24 | SheetFooter,
25 | SheetHeader,
26 | SheetTrigger,
27 | } from '@/components/ui/sheet';
28 | import GweiAmount from '@/components/common/gwei-amount';
29 | import CallerSelection from '@/components/core/selection/caller';
30 | import ChainSelection from '@/components/core/selection/chain';
31 | import CustomChainCreation, {
32 | CustomChainCreationButtons,
33 | } from '@/components/core/selection/custom-chain';
34 | import GasPriceSelection from '@/components/core/selection/gas-price';
35 | import NativePriceSelection from '@/components/core/selection/native-price';
36 |
37 | /**
38 | * @notice The configuration menu on mobile to define parameters for the calls/estimations
39 | * @dev This will display a bottom-sliding menu on mobile and a side menu on desktop.
40 | * @dev This includes the chain, gas price, native price, and caller selection.
41 | */
42 | const ConfigMenuMobile = () => {
43 | const [open, setOpen] = useState(false);
44 | const [chainCreation, setChainCreation] = useState(false);
45 | const [customChainOptions, setCustomChainOptions] = useState<
46 | CustomChainOptions | undefined
47 | >(undefined);
48 | const isTablet = useMediaQuery('(max-width: 1024px)');
49 |
50 | const chain = useProviderStore((state) => state.chain);
51 | const gasFeesConfig = useConfigStore((state) => state.gasFeesConfig);
52 |
53 | if (!isTablet) return null;
54 | return (
55 | setOpen(o)}>
56 |
57 |
58 |
59 |
60 |
61 |
71 |
72 |
73 |
74 |
75 |
76 | {chain.name}
77 |
78 |
79 |
80 |
81 |
82 |
85 |
86 |
87 |
88 |
89 |
90 | {/* Creating a new chain, replace the layour with the form */}
91 | {chainCreation ? (
92 |
93 |
94 | Custom chain
95 |
96 |
97 |
98 |
101 |
102 |
103 |
104 |
108 |
109 |
110 | ) : (
111 |
112 |
113 | Config
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | )}
126 |
127 | );
128 | };
129 |
130 | export default ConfigMenuMobile;
131 |
--------------------------------------------------------------------------------
/src/components/core/account-state.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC, useEffect } from 'react';
4 |
5 | import { useConfigStore } from '@/lib/store/use-config';
6 | import { useProviderStore } from '@/lib/store/use-provider';
7 | import { Badge } from '@/components/ui/badge';
8 | import { Skeleton } from '@/components/ui/skeleton';
9 | import CurrencyAmount from '@/components/common/currency-amount';
10 | import ShrinkedAddress from '@/components/common/shrinked-address';
11 | import TooltipResponsive from '@/components/common/tooltip-responsive';
12 |
13 | type AccountStateProps = {
14 | initialSearchedAccount: string;
15 | };
16 |
17 | /**
18 | * @notice The state of the current account
19 | * @dev This will display the data about the current account.
20 | * @dev This will be updated after searching for an address or making a call.
21 | */
22 | const AccountState: FC = ({ initialSearchedAccount }) => {
23 | /* ---------------------------------- STATE --------------------------------- */
24 | const { chain, client, forkTime, initializing } = useProviderStore(
25 | (state) => ({
26 | chain: state.chain,
27 | client: state.client,
28 | forkTime: state.forkTime,
29 | initializing: state.initializing,
30 | }),
31 | );
32 |
33 | const { account, fetchingAccount, updateAccount } = useConfigStore(
34 | (state) => ({
35 | account: state.account,
36 | fetchingAccount: state.fetchingAccount,
37 | updateAccount: state.updateAccount,
38 | }),
39 | );
40 |
41 | const loading = initializing || fetchingAccount;
42 |
43 | /* --------------------------------- EFFECTS -------------------------------- */
44 | // Update the account:
45 | // - on chain change/reset
46 | // - on refresh (direct mount or search)
47 | useEffect(() => {
48 | if (client) {
49 | updateAccount(initialSearchedAccount, {
50 | updateAbi: true,
51 | chain,
52 | client,
53 | });
54 | }
55 | // eslint-disable-next-line react-hooks/exhaustive-deps
56 | }, [chain, client, forkTime[chain.id]]);
57 |
58 | /* --------------------------------- RENDER --------------------------------- */
59 | if (!account && !fetchingAccount) return null;
60 |
61 | // We know that if account is undefined but loading is true, the components
62 | // accessing account will be rendered as skeletons; if it's false, this won't be rendered at all
63 | return (
64 |
65 |
66 |
67 | {!account || account.isEmpty
68 | ? 'Account'
69 | : account.isContract
70 | ? 'Contract'
71 | : 'EOA'}
72 |
73 | {account?.errors && account.errors.length > 0 ? (
74 |
78 | ) : null}
79 |
80 | {loading || !account ? (
81 |
82 | ) : (
83 |
84 |
88 | {account.isEmpty ? (
89 |
92 | not initialized
93 | empty
94 |
95 | }
96 | content="This account has never been initialized (0 balance, 0 nonce, no deployed bytecode)"
97 | classNameTrigger="flex items-center"
98 | />
99 | ) : account.ens ? (
100 |
104 | {account.ens}
105 |
106 | ) : null}
107 |
108 | )}
109 | Balance
110 | {loading || !account ? (
111 |
112 | ) : (
113 |
118 | )}
119 | {!account?.isContract && !account?.isEmpty ? (
120 | <>
121 |
122 | Transactions
123 |
127 |
128 | {loading || !account ? (
129 |
130 | ) : (
131 | {account.nonce.toString()}
132 | )}
133 | >
134 | ) : null}
135 |
136 | );
137 | };
138 |
139 | export default AccountState;
140 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { type DialogProps } from '@radix-ui/react-dialog';
5 | import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
6 | import { Command as CommandPrimitive } from 'cmdk';
7 |
8 | import { cn } from '@/lib/utils';
9 | import { Dialog, DialogContent } from '@/components/ui/dialog';
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ));
24 | Command.displayName = CommandPrimitive.displayName;
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | );
38 | };
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ));
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName;
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ));
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName;
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ));
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ));
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName;
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ));
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName;
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | );
142 | };
143 | CommandShortcut.displayName = 'CommandShortcut';
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | };
156 |
--------------------------------------------------------------------------------
|