diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md new file mode 100644 index 0000000000..cc0f2c5528 --- /dev/null +++ b/.agents/AGENTS.md @@ -0,0 +1,26 @@ +# Frontend application for Blockscout + +## Architecture + +See `./rules/architecture.mdc`. + +## Design System Rules + +See `./rules/design-system.mdc`. + +## Code Style & Quality + +See `./rules/code-quality.mdc`. + +## TypeScript Conventions + +See `./rules/typescript.mdc`. + +## Environment Variables + +See `./rules/env-vars.mdc`. + +## Testing + +- Unit tests (`*.spec.ts` / `*.spec.tsx`): See `./rules/tests-unit.mdc`. +- Visual component tests (`*.pw.tsx`): See `./rules/tests-visual.mdc`. diff --git a/.agents/rules/architecture.mdc b/.agents/rules/architecture.mdc new file mode 100644 index 0000000000..818eea2644 --- /dev/null +++ b/.agents/rules/architecture.mdc @@ -0,0 +1,84 @@ +--- +description: Project overview, tech stack, directory layout, data flow, and ongoing client/ architecture migration +globs: +alwaysApply: true +--- +# Project Architecture + +## What this is + +Blockscout frontend — a blockchain explorer UI. Distributed as a Docker image; configured entirely via environment variables at runtime (see `.agents/rules/env-vars.mdc`). + +## Tech stack + +| Layer | Technology | +|---|---| +| Framework | Next.js 16 — **Pages Router only** (not App Router) | +| UI | React 19 | +| Component library | Chakra UI v3 (see `.agents/rules/design-system.mdc`) | +| Server state | React Query 5 | +| Web3 | Wagmi 2 / Viem 2 | +| Schema validation | Valibot | +| Unit tests | Vitest | +| Visual tests | Playwright | +| Package manager | pnpm (Node >=22.14.0) | + +## Domain terminology + +Feature and product codenames used throughout the codebase are defined in `docs/GLOSSARY.md`. Consult it whenever you encounter an unfamiliar term — e.g. `tac`, `bens`, `cctx`, `kettle`, `epoch`, `blobs`. + +## Directory layout + +``` +pages/ Next.js file-based routes — thin wrappers only (dynamic import + getServerSideProps) +ui/ Legacy React UI components organized by feature (~65 subdirectories) → being migrated to client/ +lib/ Legacy business logic, API utilities, custom hooks, context providers → being migrated to client/ +client/ New home for all product code (see Migration section below) +toolkit/ Design system — Chakra wrappers, theme tokens, shared hooks/components (stays here permanently) +configs/app/ Runtime app configuration; must not import from client/ +nextjs/ Next.js config utilities: headers, rewrites, redirects, type-safe routes +mocks/ Shared mock data for tests → being co-located under client/ slices/features +deploy/ Docker scripts, env validator, build tools +docs/ ENVS.md, GLOSSARY.md, CONTRIBUTING.md +``` + +## Data flow + +- **Getting data:** pages fetch data via React Query. Query keys and fetcher functions live in `lib/api/` (moving to `client/api/`). +- **Global UI state:** React Context providers initialized in `pages/_app.tsx` — `AppContextProvider`, `SettingsContextProvider`, `MarketplaceContextProvider`, etc. +- **Real-time data:** WebSocket via `SocketProvider` (moving to `client/api/socket/`). +- **App config:** always read via `configs/app/` (which reads `window.__envs` at runtime) — never `process.env.*` directly in component code. + +--- + +## Ongoing migration: `client/` architecture + +The codebase is being progressively restructured. The long-term target moves all product code from `ui/` and `lib/` into a new `client/` directory with a clear domain-based layout. + +**Blueprint:** `client/ARCH_REDESIGN.md` +**Task backlog and current status:** `client/MIGRATION_TASKS.md` + +### Target layout inside `client/` + +| Directory | Contents | +|---|---| +| `client/shell/` | App chrome: layout, header, footer, navigation, top-bar, root contexts | +| `client/api/` | API transport, fetch utilities, query client, WebSocket; migrated from `lib/api/` and `lib/socket/` | +| `client/slices/` | Core explorer entities always present on any EVM chain (tx, block, address, token, contract, search, …) | +| `client/features/` | Optional / config-gated areas: rollups, chain variants, account, rewards, marketplace, stats, … | +| `client/shared/` | Cross-cutting utilities with no single domain owner (analytics, errors, hooks, router helpers, text utils, …) | + +### Slice vs feature + +- **Slice** — present on every vanilla EVM chain, no feature flag required. Examples: `tx`, `block`, `address`, `token`, `contract`. +- **Feature** — config-gated or chain-specific. Examples: `rollup/optimism`, `chain-variants/celo`, `account`, `stats`, `gas-tracker`. + +### Key rules for migrated code + +- **No barrel `index.ts` files** inside `client/` unless the file defines a genuine public facade (not just `export * from ...`). +- **`client/api` must not have runtime imports** from `client/slices/*` or `client/features/*`; `import type` is allowed. +- **`configs/` never imports `client/`.** +- **`pages/` files are thin wrappers** — a dynamic import and optionally `getServerSideProps`; no UI components or business logic inline. +- **Naming:** kebab-case for directories and non-component modules; `PascalCase.tsx` for React components; `useCamelCase.ts` for hooks. +- **Types:** each slice/feature owns its API response types in `/types/api.ts`. Do not import from deeper internal paths across domain boundaries. +- **When editing legacy `lib/` or `ui/` code:** check `client/MIGRATION_TASKS.md` to see if that area has a migration task in progress or planned. If so, note the target destination in your PR description. diff --git a/.agents/rules/code-quality.mdc b/.agents/rules/code-quality.mdc new file mode 100644 index 0000000000..9971165cb5 --- /dev/null +++ b/.agents/rules/code-quality.mdc @@ -0,0 +1,152 @@ +--- +description: Code quality rules for the Blockscout frontend +globs: *.tsx,*.ts +alwaysApply: false +--- +# Code Quality + +We use ESLint, cSpell and Typescript to maintain code quality and consistency across the project. +In order to check code quality run the following commands: +```bash +pnpm lint:eslint:fix +pnpm lint:tsc +pnpm lint:cspell +``` + +Moreover, please find below the general sense rules that linters do not cover. + +## Code Style and Structure + +### General Principles + +- Write concise, readable TypeScript code +- Use functional and declarative programming patterns; avoid classes +- Prefer iteration and modularization over code duplication +- Implement early returns for better readability +- Structure components logically: exports, subcomponents, helpers, types + +### Naming Conventions + +- Prefer descriptive names with auxiliary verbs (isLoading, hasError) +- Prefix event handlers with "handle" (handleClick, handleSubmit) +- Favor default exports for React components + +Names should be specific and self-documenting. Vague names hide intent and make the codebase harder to navigate. + + +```ts +// BAD +const useWidgets = () => { ... }; + +// GOOD +const useAddress3rdPartyWidgets = () => { ... }; +``` + +This applies to hooks, components, functions, and variables alike.. + +## Specific rules + +### Magic numbers + +Extract magic numbers into named `UPPER_SNAKE_CASE` constants, placed above the component or function that uses them. This makes intent clear and avoids silent duplication. + +```ts +// BAD +const visibleItems = items.slice(0, 4); + +// GOOD +const MAX_VISIBLE_ITEMS = 4; +const visibleItems = items.slice(0, MAX_VISIBLE_ITEMS); +``` + +The same applies to magic strings used as discriminators, keys, or thresholds. In tests, unexplained magic values should also be extracted into named constants so their meaning is clear. + +### Static empty defaults + +Never define empty arrays or objects inline as default values — a new reference is created on every render, causing unnecessary re-renders or stale hook dependencies. + +```ts +// BAD +const items = data ?? []; + +// GOOD +const EMPTY_ITEMS: Array = []; +const items = data ?? EMPTY_ITEMS; +``` + +Define the constant outside the component or hook. + +### useMemo for derived arrays + +Wrap `.filter()`, `.map()`, or `.reduce()` results in `useMemo` when the result is passed as a prop or used as a hook dependency. Without memoisation, a new array reference is produced on every render. + +```ts +// BAD +const filtered = items.filter(isActive); +return ; + +// GOOD +const filtered = useMemo(() => items.filter(isActive), [items]); +return ; +``` + +### eslint-disable comments + +Every `eslint-disable` comment must include an explanation. Without one, the reason for the suppression is lost and the comment becomes a maintenance hazard. + +```ts +// BAD +// eslint-disable-next-line @typescript-eslint/no-explicit-any + +// GOOD +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- API response shape is dynamic and validated at runtime +``` + +### File path string interpolation + +Prefer explicit file names over template literals in asset paths. Explicit names are easier to locate with search tools and eliminate accidental runtime errors. + +```ts +// BAD +const src = `streak_${days}.png`; + +// GOOD — map variants explicitly +const STREAK_IMAGES: Record = { + 30: 'streak_30.png', + 60: 'streak_60.png', +}; +``` + +### Prefer es-toolkit utilities + +Before writing custom utility logic (clamping, deep cloning, grouping, etc.), check whether `es-toolkit` already provides it. Prefer the library function over a manual implementation. Documentation: https://es-toolkit.dev/llms-full.txt + +### Commented-out code + +Remove commented-out code blocks. The git history preserves anything that might be needed later. TODOs are acceptable only when paired with a clear follow-up plan or issue reference. + +### Links + +- Use `toolkit/chakra/link` instead of `next/link`. Never import `Link` from `next/link` directly. +- When links to **application pages** are constructed, verify that `nextjs-routes` or `nextjs/routes` utilities are used instead of string concatenation or template literals. The full list of application routes is available in `nextjs/nextjs-routes.d.ts`. + +### Date and time + +- Import `dayjs` only via `lib/date/dayjs.ts` — never directly from the `dayjs` package. +- Render all dates and times through the shared `Time` or `TimeWithTooltip` components. Do not format timestamps inline. + +### Strict comparison + +Use strict equality (`===`, `!==`) only — never loose equality (`==`, `!=`). Strict operators avoid type coercion surprises and align with TypeScript expectations. + +```ts +// BAD +if (count == 0) { ... } +if (status != 'ok') { ... } + +// GOOD +if (count === 0) { ... } +if (status !== 'ok') { ... } +``` + +When you need to treat both `null` and `undefined` as missing, write `value === null || value === undefined` instead of `value == null`. \ No newline at end of file diff --git a/.agents/rules/design-system.mdc b/.agents/rules/design-system.mdc new file mode 100644 index 0000000000..9a054b1097 --- /dev/null +++ b/.agents/rules/design-system.mdc @@ -0,0 +1,124 @@ +--- +description: Design system and styling rules for the Blockscout frontend +globs: *.tsx,*.ts +alwaysApply: false +--- +# Design System + +## Stack + +The app uses **Chakra UI v3** as its component and styling foundation. + +**Documentation:** https://chakra-ui.com/llms.txt — consult this to understand what components Chakra provides and how its styling system works. + +## Project configuration + +The design system is layered on top of Chakra UI inside `toolkit/`: + +| Path | Purpose | +|---|---| +| `toolkit/chakra/` | Custom wrappers for Chakra components — always prefer these over bare Chakra imports | +| `toolkit/theme/theme.ts` | Theme entry point; uses Chakra v3's `createSystem` API to merge defaults with project config | +| `toolkit/theme/foundations/semanticTokens.ts` | Full list of semantic color tokens (text, bg, border, icon, component-level tokens) | +| `toolkit/theme/foundations/colors.ts` | Raw color palette referenced by semantic tokens | +| `toolkit/theme/recipes/` | Component style recipes (slot recipes and simple recipes) | +| `toolkit/components/` | Custom business components (forms, charts, tabs, etc.) built on top of Chakra | +| `toolkit/hooks/` | Shared React hooks (useDisclosure, useClipboard, etc.) | + +The `Provider` component at `toolkit/chakra/provider.tsx` wraps `ChakraProvider` with the custom theme and color mode support. It must be mounted at the app root. + +## Component import priority + +Always check `toolkit/chakra/` before importing from Chakra UI directly. If a custom wrapper exists there, use it — never the bare Chakra component. + +```ts +// BAD +import { Button } from '@chakra-ui/react'; + +// GOOD +import { Button } from 'toolkit/chakra/button'; +``` + + +## Colors + +Never use raw color values (hex, RGB, HSL). Always reference a token. Three sources are valid: + +1. **Semantic tokens** — context-aware, light/dark aware. Full list in `toolkit/theme/foundations/semanticTokens.ts`. Prefer these whenever a semantic meaning exists. + + ```tsx + + + ``` + + Common groups: `text.*`, `bg.*`, `border.*`, `icon.*`, `link.*`, `button.*`, `badge.*`. + +2. **Project color palette** — scale and alpha colors defined in `toolkit/theme/foundations/colors.ts`: `gray`, `blue`, `red`, `orange`, `yellow`, `green`, `teal`, `cyan`, `purple`, `pink` (steps 50–900), `black`, `white`, `whiteAlpha.*`, `blackAlpha.*`. + + ```tsx + + ``` + +3. **Brand colors** — also defined in `toolkit/theme/foundations/colors.ts`: `github`, `telegram`, `linkedin`, `discord`, `slack`, `twitter`, `opensea`, `facebook`, `medium`, `reddit`, `celo`, `clusters`. + + ```tsx + + ``` + +If a raw color value is truly unavoidable (e.g. a third-party embed), leave a comment explaining why. + +## Design tokens + +The project customizes the following Chakra token categories in `toolkit/theme/`. Always use these tokens instead of raw CSS values: + +| Token type | File | Example | +|---|---|---| +| Border radius | `foundations/borders.ts` | `borderRadius="md"` instead of `borderRadius="12px"` | +| Shadows | `foundations/shadows.ts` | `boxShadow="size.md"` instead of a custom `box-shadow` | +| Z-index | `foundations/zIndex.ts` | `zIndex="modal"` instead of a raw number | +| Font weights | `theme.ts` (inline) | `fontWeight="semibold"` instead of `fontWeight={600}` | +| Durations | `foundations/durations.ts` | Use duration tokens for CSS transitions | +| Keyframes | `foundations/animations.ts` | Reference named keyframes for custom animations | + +Available `radii` tokens: `none`, `sm` (4px), `base` (8px), `md` (12px), `lg` (16px), `xl` (24px), `full`. + +Available `shadows` tokens: `size.xs`, `size.sm`, `size.base`, `size.md`, `size.lg`, `size.xl`, `size.2xl`, `action_bar`, `dark-lg`. + +## Text styles + +Do not set `fontSize` or `lineHeight` directly. Apply the appropriate `textStyle` token instead — it encodes both properties together along with `fontWeight` and `fontFamily`. + +```tsx +// BAD +Label + +// GOOD +Label +``` + +Available text styles (defined in `toolkit/theme/foundations/typography.ts`): + +| Token | fontSize / lineHeight | +|---|---| +| `heading.xl` | 32px / 40px | +| `heading.lg` | 24px / 32px | +| `heading.md` | 18px / 24px | +| `heading.sm` | 16px / 24px | +| `heading.xs` | 14px / 20px | +| `text.xl` | 20px / 28px | +| `text.md` | 16px / 24px | +| `text.sm` | 14px / 20px | +| `text.xs` | 12px / 16px | + +For a regular text block, the `text.` prefix can be omitted. + +## Compound component spacing + +Do not override the default spacing of **internal parts** of compound components (e.g. adding custom padding to `DialogHeader` inside a `Dialog`, or to a `MenuList` item). The root component itself may be spaced freely; its sub-parts may not. + +This rule applies to all components from `toolkit/chakra/`. + +## Duplicated style props + +Before adding a style prop, check whether the same property is already set by an inherited style or a parent component. Flag and remove redundant re-declarations. + diff --git a/.agents/rules/env-vars.mdc b/.agents/rules/env-vars.mdc new file mode 100644 index 0000000000..61b3145578 --- /dev/null +++ b/.agents/rules/env-vars.mdc @@ -0,0 +1,83 @@ +--- +description: Environment variables — where they live, how they're delivered at runtime, validated, and how to add or deprecate them +globs: +alwaysApply: false +--- +# Environment Variables + +## Reference + +All environment variables are documented in `docs/ENVS.md`. This is the authoritative list — every variable must be present there. + +## Runtime delivery (not build-time) + +Almost all variables are injected at **runtime**, not baked into the Next.js build. Only three variables are true build-time constants, compiled into the image via Docker `ARG`: `NEXT_PUBLIC_GIT_COMMIT_SHA`, `NEXT_PUBLIC_GIT_TAG`, `NEXT_OPEN_TELEMETRY_ENABLED`. + +Everything else follows this pipeline: + +**1. Build time — `deploy/scripts/collect_envs.sh`** + +Scans `docs/ENVS.md` for every `NEXT_PUBLIC_*` name and writes `.env.registry` (a list of expected variable names with placeholder values). This registry is copied into the Docker image and tells the runtime which variables the app expects. **If a variable is not in `docs/ENVS.md`, it will not appear in the registry and will not be delivered to the browser.** + +**2. Container startup — `deploy/scripts/entrypoint.sh`** + +Runs in sequence: +1. Optionally loads a named preset from `configs/envs/` +2. Downloads external asset files (images, JSON configs) via `download_assets.sh` +3. Validates environment values against the schema (`validate_envs.sh` → `deploy/tools/envs-validator/`) +4. Writes `public/assets/envs.js` via `make_envs_script.sh`: + ```js + window.__envs = { + NEXT_PUBLIC_API_HOST: "...", + ... + } + ``` + +**3. In the app — `configs/app/utils.ts` `getEnvValue()`** + +Reads `window.__envs` when running in the browser, and `process.env` during SSR / build-time evaluation. + +## Accessing variables in code + +Never read `process.env.NEXT_PUBLIC_*` directly in component or library code. Always go through `configs/app/`, which exposes a frozen, typed config object. The config is split into sections: + +| Section | Purpose | +|---|---| +| `app` | App-level settings (host, protocol, env flags) | +| `apis` | Main API and related service endpoints | +| `chain` | Blockchain parameters | +| `UI` | Visual customizations (colors, fonts, layout) | +| `features` | Feature flags | +| `services` | Third-party service integrations | +| `meta` | SEO and meta-tag settings | + +## Validation + +At container startup, `deploy/tools/envs-validator/` validates the current environment against a [Valibot](https://valibot.dev/) schema defined in `deploy/tools/envs-validator/schema.ts`. The app will not start if validation fails (unless `SKIP_ENVS_VALIDATION=true`). + +For complex config shapes, define a **separate sub-schema** (see `tacSchema`, `beaconChainSchema` for examples) rather than inlining everything. + +## How to add a new variable + +Follow all steps — skipping any one of them will cause the variable to be missing at runtime or fail CI validation. + +1. **`docs/ENVS.md`** — document the variable: name, expected type, required/optional, default value, example value. This step is mandatory; without it the runtime script will not deliver the value to the browser. + +2. **`configs/app/`** — add a property in the appropriate section file (`app.ts`, `apis.ts`, `chain.ts`, `ui.ts`, `features/`, `services.ts`, `meta.ts`). Read the value via `getEnvValue('NEXT_PUBLIC_...')`. Never use the env var name directly outside `configs/app/`. + +3. **`deploy/tools/envs-validator/schema.ts`** — add or update the validation rule. For complex structures, create a dedicated sub-schema. + +4. **`deploy/tools/envs-validator/test/.env.base`** — add the variable to the base test preset. If the variable supports alternative configurations, add examples to `.env.alt` as well. Verify locally: + ```bash + cd deploy/tools/envs-validator && pnpm test + ``` + +5. **CSP** (if the variable holds an external non-asset URL) — add the domain to the appropriate policy in `nextjs/csp/policies/`. Only add the policy when the relevant config option is enabled. + +6. **`deploy/scripts/download_assets.sh`** (if the variable holds an asset URL — image or JSON config) — extend the `ASSETS_ENVS` array with the variable name. + +7. **JSON config URL only** — add an example file to `deploy/tools/envs-validator/test/assets/configs/` (filename derived by stripping `NEXT_PUBLIC_` prefix and `_URL` suffix, lowercased — e.g. `NEXT_PUBLIC_MARKETPLACE_CONFIG_URL` → `marketplace_config.json`) and add the variable name to the `envsWithJsonConfig` array in `deploy/tools/envs-validator/index.ts`. + +## TBD: Deprecating a variable + +> This section is not yet defined. Topics to cover: how to announce deprecation, the grace period before removal, how to update `docs/ENVS.md` and `schema.ts`, and how to handle backwards compatibility for existing deployments. diff --git a/.agents/rules/glossary.mdc b/.agents/rules/glossary.mdc new file mode 100644 index 0000000000..fd04c18e02 --- /dev/null +++ b/.agents/rules/glossary.mdc @@ -0,0 +1,8 @@ +--- +description: Project domain terms and ubiquitous language — consult docs/GLOSSARY.md +alwaysApply: true +--- + +# Ubiquitous language (Glossary) + +Product and feature codenames used in this codebase are defined in `docs/GLOSSARY.md`. Consult it when you encounter an unfamiliar term (e.g. `tac`, `bens`, `cctx`, `kettle`, `epoch`). diff --git a/.agents/rules/tests-unit.mdc b/.agents/rules/tests-unit.mdc new file mode 100644 index 0000000000..b866de4d6d --- /dev/null +++ b/.agents/rules/tests-unit.mdc @@ -0,0 +1,75 @@ +--- +description: Vitest unit tests — purpose, setup, utilities, and conventions +globs: "*.spec.ts,*.spec.tsx" +alwaysApply: false +--- +# Unit Tests (Vitest) + +## Purpose + +Unit tests cover logic that is independent of visual presentation: utility functions, custom hooks, and component behavior (state transitions, conditional rendering, event handling). If a change has no visual output to verify, prefer a Vitest test over a Playwright one — it is faster and cheaper. + +## File naming and location + +Test files must be named `*.spec.ts` or `*.spec.tsx` and placed alongside the code they test. Run all tests: + +```bash +pnpm test:vitest +``` + +Run a single file: + +```bash +pnpm test:vitest path/to/file.spec.ts +``` + +## Setup + +`vitest/setup.ts` runs before each test file and provides: +- **Environment variables** — loaded from `.env.vitest` via dotenv; accessible as `window.__envs` in test code. +- **Fetch mocking** — `vitest-fetch-mock` is initialized globally; all `fetch` calls are interceptable. + +`vitest/global-setup.ts` runs once before the entire test suite and loads `.env.vitest`. + +## Rendering components + +**Never import from `@testing-library/react` directly.** Use the project's custom wrapper instead: + +```tsx +import { render, screen } from 'vitest/lib'; +``` + +`vitest/lib.tsx` re-exports everything from `@testing-library/react` and replaces `render` with a custom version that wraps the component in the full app context: +- `QueryClientProvider` (no retry, no window-focus refetch) +- `AppContextProvider` +- `GrowthBookProvider` +- `SocketProvider` + +The `wrapper` export is also available if you need to pass it separately to RTL hooks: + +```tsx +import { wrapper } from 'vitest/lib'; +const { result } = renderHook(() => useMyHook(), { wrapper }); +``` + +## Utilities + +**`vitest/utils/flushPromises.ts`** — call after an action that triggers async effects (e.g. a state update followed by a data fetch) to flush all pending microtasks before asserting: + +```tsx +import flushPromises from 'vitest/utils/flushPromises'; + +await userEvent.click(button); +await flushPromises(); +expect(screen.getByText('Loaded')).toBeInTheDocument(); +``` + +## Mocking fetch responses + +`vitest-fetch-mock` is active globally. Mock responses before the code under test runs: + +```tsx +fetchMock.mockResponseOnce(JSON.stringify({ data: 'value' })); +``` + +Reset mocks between tests using `beforeEach` / `afterEach` if needed. diff --git a/.agents/rules/tests-visual.mdc b/.agents/rules/tests-visual.mdc new file mode 100644 index 0000000000..81d902b473 --- /dev/null +++ b/.agents/rules/tests-visual.mdc @@ -0,0 +1,189 @@ +--- +description: Playwright component visual tests — purpose, setup, fixtures, and conventions +globs: "*.pw.tsx" +alwaysApply: false +--- +# Visual Component Tests (Playwright) + +## Purpose + +Playwright tests verify the visual presentation and interactive behavior of UI components across multiple viewports and color modes. Use them when a change affects what the user sees — layout, styling, conditional UI states. + +## File naming and location + +Test files must be named `*.pw.tsx` and placed alongside the component they test. Run all tests: + +```bash +pnpm test:pw # locally (fast, no Docker) +pnpm test:pw:docker # in Docker (required for screenshot updates) +pnpm test:pw:docker path/to/Component.pw.ts --update-snapshots # Run single file in Docker and update all corresponding screenshots +``` + +**Never commit screenshots generated by `pnpm test:pw` (local).** Screenshots used in CI must be generated via `pnpm test:pw:docker`, otherwise tests will fail in CI due to rendering differences. + +## Test projects + +Three projects run against every test file: + +| Project | Tag | Browser | Viewport | +|---|---|---|---| +| `default` | _(runs unless excluded with `-@default`)_ | Desktop Chrome | 1200×750 | +| `mobile` | `+@mobile` | iPhone 13 Pro (Safari) | 375×812 | +| `dark-color-mode` | `+@dark-mode` | Desktop Chrome | 1200×750, forced dark | + +Tag a test to include or exclude it from a project: + +```tsx +test('renders correctly +@mobile', async ({ render }) => { ... }); +test('mobile only -@default +@mobile', async ({ render }) => { ... }); +test('dark mode variant +@dark-mode', async ({ render }) => { ... }); +``` + +## Imports + +**Never import directly from `@playwright/experimental-ct-react`.** Use the project's extended test: + +```tsx +import { test, expect } from 'playwright/index'; +``` + +`playwright/index.ts` extends Playwright's `test` with all project fixtures and re-exports everything from `@playwright/experimental-ct-react`. + +## Component mounting + +Use the `render` fixture — it mounts the component inside `TestApp`, which provides the full provider stack: ChakraProvider, QueryClientProvider, SocketProvider, AppContextProvider, SettingsContextProvider, GrowthBookProvider, WagmiProvider, RewardsContextProvider, and CsvExportContextProvider. + +```tsx +test('example', async ({ render }) => { + const component = await render(); + await expect(component).toHaveScreenshot(); +}); +``` + +`render` accepts optional second and third arguments to customize `hooksConfig` (mainly used for mocking Next.js router object), `appContext` and `marketplaceContext` props passed to `TestApp`. + +## Fixtures + +All fixtures are injected via the `test` function. Available fixtures: + +### `mockApiResponse` +Intercepts Blockscout API calls. Returns the mocked URL. + +```tsx +await mockApiResponse('token', tokenMock, { + pathParams: { hash: '0x...' }, + queryParams: { tab: 'holders' }, + status: 200, // optional, default 200 + times: 1, // optional, intercept N times +}); +``` + +### `mockEnvs` +Sets environment variables for the test (stored in localStorage, read by `getEnvValue`). Use the built-in `ENVS_MAP` for common configs: + +```tsx +await mockEnvs([ + ['NEXT_PUBLIC_ROLLUP_TYPE', 'optimistic'], + ['NEXT_PUBLIC_API_HOST', 'localhost'], +]); +``` + +### `mockFeatures` +Sets feature flags in localStorage: + +```tsx +await mockFeatures([ + ['account', true], + ['rewards', false], +]); +``` + +### `mockAssetResponse` +Routes any URL to serve a file from disk: + +```tsx +await mockAssetResponse('https://example.com/logo.png', './playwright/mocks/duck.png'); +``` + +### `mockConfigResponse` +Routes an external config asset URL (JSON or image) derived from an env var name: + +```tsx +await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://example.com/config.json', content); +``` + +### `mockRpcResponse` +Mocks RPC calls (typed against viem's `PublicRpcSchema`): + +```tsx +await mockRpcResponse([{ method: 'eth_blockNumber', result: '0x1' }]); +``` + +### `mockContractReadResponse` +Mocks `eth_call` for a specific contract read: + +```tsx +await mockContractReadResponse({ + abiItem: erc20ABI[0], + args: ['0x123'], + address: '0x00ab', + result: '1000000', +}); +``` + +### `createSocket` +Creates a local WebSocket server on port 3200. Resolves when a client connects: + +```tsx +const socket = await createSocket(); +// push events via socket +``` + +### `mockTextAd` +Mocks the text ad provider (Sevio + duck ad data). Call this in tests that render ad-containing layouts to avoid network requests and visual noise. + +### `injectMetaMaskProvider` +Injects `window.ethereum` with `isMetaMask: true`: + +```tsx +await injectMetaMaskProvider(); +``` + +### `auth` helpers +For tests requiring an authenticated user, use the `contextWithAuth` fixture or call `authenticateUser(context)` to set the API token cookie. + +### `mockMultichainConfig` +Injects `window.__multichainConfig` with 5 mock chains (chainA–E). Use in tests that render multichain UI. + +## Selectors + +Always use semantic selectors — never CSS class names. CSS classes are implementation details and break when styling changes. + +```tsx +// BAD +page.locator('.tx-hash-link') + +// GOOD +page.getByRole('link', { name: /transaction/i }) +page.getByLabel('Address menu') +page.getByText('0x1234…') +page.getByTestId('tx-hash') +``` + +## Environment and date + +`playwright/lib.tsx` runs before every component mount and: +- Mocks `next/router.useRouter()` with a configurable router object. +- Fixes the current date to `2022-11-11T12:00:00Z` via MockDate — all `new Date()` / `Date.now()` calls return this value. + +## Mock data + +Use existing files from `mocks/` and `playwright/mocks/` rather than hardcoding values inline. The `mocks/` directory contains shared mock data (API responses, entities) used across both Vitest and Playwright tests. `playwright/mocks/` contains Playwright-specific assets (images, JSON files, module mocks). + +Extract any non-obvious test values (IDs, amounts, hashes) into named constants above the test suite. + +## Conventions + +- Do not use the `testFn: TestFixture<...>` pattern. It makes refactoring harder and can bypass Playwright lint rules. Use it only when genuinely sharing fixture logic across multiple suites. +- Keep each test focused on one visual state or interaction. +- Screenshots are the contract — keep them up to date and review diffs carefully in PRs. diff --git a/.cursor/rules/typescript.mdc b/.agents/rules/typescript.mdc similarity index 84% rename from .cursor/rules/typescript.mdc rename to .agents/rules/typescript.mdc index 632369e7e6..dcfb6acd3b 100644 --- a/.cursor/rules/typescript.mdc +++ b/.agents/rules/typescript.mdc @@ -8,7 +8,6 @@ alwaysApply: false - Use TypeScript for all code - Prefer interfaces over types - Implement proper type safety and inference -- Use `satisfies` operator for type validation ## Import types @@ -80,7 +79,21 @@ const youSayGoodbyeISayHello = < }; ``` -Outside of generic functions, use `any` extremely sparingly. +Outside of generic functions, use `any` extremely sparingly. Prefer `unknown` with proper narrowing instead: + +```ts +// BAD +function process(value: any) { + return value.name; +} + +// GOOD +function process(value: unknown) { + if (typeof value === 'object' && value !== null && 'name' in value) { + return (value as { name: string }).name; + } +} +``` ## Default exports @@ -444,3 +457,54 @@ if (result.ok) { console.error(result.error); } ``` + + +## Type assertions and satisfies + +Prefer `satisfies` over `as` assertions for type validation. `as` silently widens or narrows the type; `satisfies` validates without losing inference. + +```ts +// BAD +const items = response.data as MyType[]; + +// GOOD +const items = response.data satisfies Array; +``` + +Avoid double-cast coercions (`as unknown as MyType`). If a cast is unavoidable, add a comment explaining why it is safe: + +```ts +// BAD +const el = ref.current as unknown as HTMLInputElement; + +// GOOD — explain why the double cast is safe +// The portal always renders an , so this cast is guaranteed at runtime. +const el = ref.current as unknown as HTMLInputElement; +``` + + +## Window globals + +Never access third-party globals via `(window as any)`. Declare the property inside the existing `declare global { interface Window { ... } }` block in `global.d.ts`: + +```ts +// BAD +const analytics = (window as any).analytics; + +// GOOD — in global.d.ts +declare global { + interface Window { + analytics: SegmentAnalytics.AnalyticsJS; + } +} +``` + + +## Module type declarations + +Use `decs.d.ts` exclusively for untyped third-party module declarations. Do not put other type definitions there. + +```ts +// decs.d.ts +declare module 'some-untyped-package'; +``` diff --git a/.agents/skills/arch-migrate/SKILL.md b/.agents/skills/arch-migrate/SKILL.md new file mode 100644 index 0000000000..5170d5ce10 --- /dev/null +++ b/.agents/skills/arch-migrate/SKILL.md @@ -0,0 +1,117 @@ +--- +name: arch-migrate +description: Execute a migration task from a GitHub issue through to a PR, or fix unresolved review comments on an existing migration PR. Reads scope and acceptance criteria from the issue body. +--- + +You are executing a step of the Blockscout frontend client architecture migration. + +## Invocation +The skill can **ONLY** be invoked as: `/arch-migrate ("execute" mode)` or `/arch-migrate fix ("fix review comments" mode)`. + +## GitHub tool selection + +Use whichever GitHub access method is available in your environment — in order of preference: + +1. **`gh` CLI** — if available and authenticated (`gh auth status` succeeds), use it for all GitHub operations. +2. **GitHub MCP server tools** — if `gh` is unavailable, use the MCP tools provided by the GitHub MCP server. +3. **REST API via `curl`** — if neither above is available, use the GitHub REST API directly with `curl` and a `GITHUB_TOKEN` env var. + +Detect availability once at the start and stick with that method throughout. Do not mix methods. + +## Mode + +**Execute** (`/arch-migrate `): fetch the GitHub issue and execute the migration through to a PR. + +**Fix** (`/arch-migrate fix `): address unresolved review comments on an existing PR. + +--- + +## Execute mode + +### 1. Read context + +Fetch the issue body from `blockscout/frontend` using the available GitHub tool. The issue number is the argument passed to the skill. + +Also read: +- `client/ARCH_REDESIGN.md` — naming conventions (§2), dependency rules (§8), execution rules (§10) +- `docs/GLOSSARY.md` — domain terminology + +The issue body contains the **Scope**, **Findings**, and **Acceptance criteria** for this task. That is your working spec. + +### 2. Explore before touching +Read all source files listed in the issue **Scope**. Understand what they export and what tests exist. + +### 3. Move and delete files +- Destination comes from the issue Scope and `ARCH_REDESIGN.md §6` migration map. +- Rename files to kebab-case **at move time** — no separate rename PR. +- Component files stay PascalCase. Hook files stay `useCamelCase.ts`. Everything else: kebab-case. +- Extract types/interfaces flagged in the issue **Findings** section into their new homes. +- **After writing each file at its destination, delete the source.** Use the `## Files to delete` list in the issue as your checklist — work through it item by item. If an entry is a folder, delete the entire folder (`rm -rf`). If an entry is a file, delete that file. +- **Verify deletions before moving on.** Once all moves are done, check that every path listed under `## Files to delete` is gone. For any that still exist, delete them now. + +### 4. Update all imports in the same PR +- Update every import path across the entire repo. +- For high-fanout files, write a codemod script, run it, then **delete the script** — do not commit it to the repo. Commit only the resulting file changes. +- No re-export shims. No long-lived compatibility aliases. + +### 5. Run checks +``` +pnpm lint:tsc # must pass +pnpm lint:eslint:fix # must pass +``` + +Fix errors if any (warnings are acceptable). + +### 6. Cross-slice dependencies left at old paths +If a dependency is not yet migrated, leave its import at the old path and list it explicitly in the PR description as a follow-up for the relevant future task. + +### 7. Branch and PR + +- Extract the task ID from the issue title (format: `[Migration] : ...`). +- Branch name: `migration/-` (e.g. `migration/1-1-client-api`) +- Branch from `main`. +- PR title: `[Migration ] ` +- PR targets `main`. +- Create the PR using the available GitHub tool (see **GitHub tool selection** above). +- PR body must include: + - `Closes #` — on the first line, so GitHub auto-closes the issue on merge + - What moved and where + - Any codemods run (include the command / script body used, but do not commit the script itself) + - Cross-slice deps left at legacy paths (list file + old import path) + - Checklist: `pnpm lint:tsc` passing, `pnpm lint:eslint` clean within `client/`, all source files/folders deleted from old paths +- PR labels: `refactoring` + +--- + +## Fix mode + +`/arch-migrate fix ` — address unresolved review comments only. + +1. Fetch unresolved review threads for the PR from `blockscout/frontend` using the available GitHub tool. If using `gh`: + +```bash +gh api graphql -f query=' +{ + repository(owner: "blockscout", name: "frontend") { + pullRequest(number: ) { + reviewThreads(first: 50) { + nodes { + isResolved + path + line + comments(first: 5) { + nodes { body author { login } } + } + } + } + } + } +}' +``` + +If using the GitHub MCP server, call `list_review_comments_on_pull_request` (for inline comments) and `get_pull_request_reviews` (for review-level comments), then filter to unresolved threads manually. If using the REST API, `GET /repos/blockscout/frontend/pulls//comments` returns inline comments; filter by `in_reply_to_id` to group threads. + +Keep only threads where `isResolved: false`. Skip everything else — the reviewer marks threads resolved when they are no longer relevant. + +2. Address each unresolved thread. Push fixes to the existing branch. +3. Summarise which threads were addressed and what changed. diff --git a/.agents/skills/arch-research/SKILL.md b/.agents/skills/arch-research/SKILL.md new file mode 100644 index 0000000000..a5386b2529 --- /dev/null +++ b/.agents/skills/arch-research/SKILL.md @@ -0,0 +1,201 @@ +--- +name: arch-research +description: Research a migration task, create a GitHub sub-issue with scope and acceptance criteria, and update the task backlog. Takes a task ID from docs/MIGRATION_TASKS.md, explores the codebase, creates an issue in blockscout/frontend, links it to the parent migration issue, and commits the status update. +--- + +You are preparing a migration task for execution by exploring the codebase and producing a well-scoped GitHub issue. + +## Invocation +The skill can **ONLY** be invoked as: `/arch-research ` (e.g. `/arch-research 1-1`) + +## Prerequisites + +Follow the **check-github-cli** skill first to ensure `gh` is available and authenticated. + +## Steps + +### 1. Find the task + +Read `client/MIGRATION_TASKS.md`. Locate the task by the given ID (e.g. `1-1`). Read its **Scope** section. + +If the task already has `[~]` or `[x]` status, stop and report that it has already been researched or completed. + +Also read: +- `client/ARCH_REDESIGN.md` — conventions and migration map (§2, §4, §6) +- `docs/GLOSSARY.md` — domain terminology + +### 2. Plan the areas + +Do a quick top-level scan of the task Scope and the standard source directories to understand what exists. Then present the user with the list of areas you will cover, in order, with a one-line description of what each contains for this specific task. Skip any area where you found nothing relevant. + +**Standard areas (always in this order):** + +1. **Types** — `types/api/*.ts`, `types/client/*.ts`, `types/views/*.ts`, and any related param/shared type files +2. **Stubs and mocks** — `stubs/*.ts`, `mocks/*.ts` +3. **Hooks, utilities, and contexts** — `lib/hooks/`, `lib//`, `lib/contexts/`, and any `utils/` files within `ui//` +4. **Shared components** — `ui/shared/entities//`, `ui/shared//` +5. **Pages** — `ui/pages/*.tsx`, `ui//` (the detail page and all its tabs, plus any index/list pages) +6. **Feature impact** — all features identified as affected across the previous areas + +Present as a short numbered list. For each area, note the rough file count or key directories found. End with: *"I'll work through them one at a time. Let me know if you want to skip or reorder any, otherwise I'll start with Area 1."* + +Wait for a go-ahead (or redirect) before proceeding. + +### 3. Analyze each area, one at a time + +Work through each area in sequence. For each one: + +**a. Analyze** — read the relevant files and apply the rules below that apply to this area. + +**b. Present a mini-plan** in this format: + +``` +### Area [N/total]: [Name] + +[One sentence describing what this area covers for this task.] + +**Plan** +| Source | Destination | +|--------|-------------| +| ... | ... | + +**Findings** +- [extractions, splits, mixed-concern files, conflicts, or "None."] +``` + +**c. Wait for explicit approval.** End each area with: *"Any corrections, or shall I move to Area [N+1]: [name]?"* + +Apply any corrections. If the changes are significant, re-show the updated mini-plan before proceeding. Do not move to the next area until the user approves the current one. + +--- + +**Rules to apply per area:** + +**Types (Area 1)** +- Two strictly separate type layers: + - `types/api.ts` destination: API/DTO shapes from `types/api/*.ts`. For each optional field group that maps to a feature (e.g. `celo?`, `arbitrum?`), extract it to `client/features//types/api.ts`. The slice's own `types/api.ts` composes them via `interface Entity extends FeatureTypeA, FeatureTypeB, ...`. + - `types/client.ts` destination: frontend-only derived types from `types/client/*.ts` and `types/views/*.ts`. Same feature decomposition applies — feature-specific client types go to `client/features//types/client.ts`. Never mix API shapes and client types in the same file. +- Verify the path exists for each source file; note any missing files. +- Check whether destination `types/api.ts` / `types/client.ts` already exists in `client/` (conflict check). + +**Stubs and mocks (Area 2)** +- Identify which stubs entries are feature-specific vs. entity-core. Feature-specific entries go to `client/features//stubs/.ts` (named after the slice, not the feature). Slice keeps only entity-core entries. +- Flag any high-fanout stub constants (e.g. `ENTITY_HASH`, `ENTITY_PARAMS`) that are imported across many test files — these require a codemod; note this in Findings. + +**Hooks, utilities, and contexts (Area 3)** +- For each hook file: check if it reads `config.features.`. If yes → `client/features//hooks/`. If no → `client/slices//hooks/` (or wherever the slice keeps its hooks). +- `lib/contexts/` scan: look for files whose name contains the entity name. Destination is `client/slices//contexts/` or `client/features//contexts/` — a dedicated sub-folder, not the root. +- Mixed-concern utility files: open any `utils.ts` in scope and check if it mixes logic from different domains. If yes, split rather than move; list what each new file should contain. + +**Shared components (Area 4)** +- `ui/shared/` sweep: search for components whose filename begins with the slice entity prefix. These are slice-owned components that ended up in the shared bucket. +- For each component file with a sibling `.pw.tsx` or `__screenshots__/` directory, include those siblings in the mapping. +- Classify each component: core slice → `client/slices//components/`; feature/chain-specific → `client/features//components/`. + +**Pages (Area 5)** +- `ui/pages/` sweep: search for page components whose filename begins with the entity prefix. **Also** scan the Next.js `pages/` directory for entries that `dynamic(() => import('ui/pages/'))` a component related to the entity — page filenames may not match the route (e.g. `Accounts.tsx` → `/accounts`). +- For a tabbed detail page, each tab becomes a named sub-folder under `pages/details/` (e.g. `info/`, `txs/`, `token-transfers/`). Feature-owned tab components go to `client/features//pages//` — never as sub-folders inside the slice's `pages/` tree. +- For index/list pages, all related list-item and table components go flat into `pages/index/`. +- Include `.pw.tsx` siblings and `__screenshots__/` directories for every page component. +- For each Next.js entry file, note the import path update required (the entry file itself stays in `pages/`). + +**Feature impact (Area 6)** +- Compile all features identified in Areas 1–5. +- For each feature, list: feature path, files to create or merge (`types/api.ts`, `types/client.ts`, `stubs/.ts`, page components), and what each file should contain. +- If no features are affected, state that explicitly. + +### 4. Compile and confirm + +Once all areas are approved, assemble the full issue body: + +```markdown +## Scope + +[All approved source → destination file mappings, organised by slice and feature. One mapping table per section.] + +## Feature impact + +[For each affected feature: + - Feature path + - Files to create or merge, and what they should contain +If none: "No feature-side files required."] + +## Findings + +[All Findings from the individual areas, consolidated. Remove duplicates.] + +If nothing non-obvious was found: "No conflicts or extractions identified — straightforward move." + +## Files to delete + +[Flat list of every source path that must be removed once its contents have been moved. Use a folder path when the entire folder moves; list individual files only when part of the folder stays. Paths that stay in place (e.g. Next.js `pages/` entry files) must NOT appear here.] + +## Acceptance criteria + +- [ ] All files moved to target paths per `ARCH_REDESIGN.md §6` +- [ ] All import paths updated repo-wide (no references to old paths remain) +- [ ] Extracted API types live in their new slice/feature `types/api.ts` +- [ ] Extracted client/UI types live in their new slice/feature `types/client.ts` +- [ ] `pnpm lint:tsc` passes +- [ ] `pnpm lint:eslint:fix` clean within `client/` (warnings in legacy paths acceptable) +- [ ] All source files/folders deleted from old paths (none remain) +- [ ] Cross-slice deps left at old paths are explicitly listed in the PR description +``` + +Present the full draft with the issue title (`[Migration] : `) and ask: *"Shall I create this issue? You can request changes before I proceed."* + +Wait for explicit confirmation. Apply any requested edits and re-confirm if the changes are significant. Do not proceed to step 5 until the user approves. + +### 5. Create the GitHub issue + +```bash +gh issue create \ + --repo blockscout/frontend \ + --title "[Migration] : " \ + --body "" \ + --label "Task for agent" +``` + +Capture the issue number from the output URL (e.g. `https://github.com/blockscout/frontend/issues/42` → number is `42`). + +### 6. Link to the parent issue + +Get the internal numeric `id` of the new issue (this is different from the issue number): + +```bash +gh api repos/blockscout/frontend/issues/ --jq '.id' +``` + +Link it as a sub-issue of the parent (parent issue number is in the `client/MIGRATION_TASKS.md` header): + +```bash +gh api \ + --method POST \ + repos/blockscout/frontend/issues//sub_issues \ + --field sub_issue_id= +``` + +### 7. Update client/MIGRATION_TASKS.md + +In the task entry, make two changes: +- Change status from `[ ]` to `[~]` +- Append the issue link to the task heading line + +Example — before: +``` +### 1-1 · [ ] Migrate `client/api/` +``` +After: +``` +### 1-1 · [~] Migrate `client/api/` · [#42](https://github.com/blockscout/frontend/issues/42) +``` + +Commit directly to `main`: + +```bash +git add client/MIGRATION_TASKS.md +git commit -m "track: 1-1 in progress — blockscout/frontend#42" +git push origin main +``` + +Replace `1-1` and `42` with the actual task ID and issue number. diff --git a/.cursor/skills/check-github-cli/SKILL.md b/.agents/skills/check-github-cli/SKILL.md similarity index 100% rename from .cursor/skills/check-github-cli/SKILL.md rename to .agents/skills/check-github-cli/SKILL.md diff --git a/.cursor/skills/create-issue-from-slack-thread/SKILL.md b/.agents/skills/create-issue-from-slack-thread/SKILL.md similarity index 100% rename from .cursor/skills/create-issue-from-slack-thread/SKILL.md rename to .agents/skills/create-issue-from-slack-thread/SKILL.md diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md new file mode 100644 index 0000000000..86b88310a1 --- /dev/null +++ b/.agents/skills/create-pr/SKILL.md @@ -0,0 +1,61 @@ +--- +name: create-pr +description: Create a well-structured pull request with proper description, labels, and reviewers +--- +# Create PR + +## Prerequisites: GitHub CLI + +This workflow uses `gh` to check for existing PRs, fetch issue details, and create the PR. **Follow the check-github-cli skill** first (ensure `gh auth status` succeeds; if not, guide the user to install/configure `gh` and do not proceed). The account needs read access to the repo and write access to create PRs and manage labels. + +## Overview + +Create a well-structured pull request with proper description, labels, and reviewers. + +## Steps + +_Note:_ In the command output, format all URLs as clickable Markdown links: `[Link Text](URL)`. + +### 1. Check that a PR is not already open for this branch + +- Get the current branch: `git branch --show-current`. +- List open PRs for that branch: `gh pr list --head --state open`. +- If a PR exists, do **only** the PR summary (see step 3: write the description content as if for the PR, but do not create or update the PR). Skip steps 2, 3 (create/update), and 4. Tell the user the PR is already open and link to it. + +### 2. Prepare the branch + +- Ensure all changes are committed: + - `git status` to see uncommitted changes. + - If there are changes: `git add` (as appropriate), then `git commit -m "..."` with a clear message. +- Push the branch: `git push -u origin ` (or `git push` if upstream is already set). +- Verify the branch is up to date with main: + - `git fetch origin main` then compare: e.g. `git rev-list --left-right --count origin/main...HEAD` (expect `0 N` for “main has nothing we don’t have”) or merge/rebase if the user wants: `git merge origin/main`. +- Resolve any merge conflicts before continuing. + +### 3. Write the PR description + +- Use the template from `./docs/PULL_REQUEST_TEMPLATE.md` as the base. Read it and fill in each section. +- **Issue number from branch name:** If the branch name matches the pattern `issue-\d+` (e.g. `issue-123`), extract the number (e.g. `git branch --show-current | grep -oE 'issue-[0-9]+' | grep -oE '[0-9]+'`), then: + - Fetch the issue: `gh issue view `. + - At the very beginning of the **"Description and Related Issue(s)"** section, include: `Resolves #`. +- **Summary of changes:** Summarize the changes clearly and concisely in no more than two paragraphs. Use bullet points if needed. Be precise; keep the description short. +- **Environment variable changes:** If any env vars were added, changed, or documented: + - Compare or read `./docs/ENVS.md` (and the validator/ENVS docs if relevant) to list what changed. + - Add a separate section listing each variable change and the **purpose** of each (why it was added/changed). + - **Bad:** "Added `NEXT_PUBLIC_VIEWS_TX_GROUPED_FEES` environment variable to the documentation." + - **Good:** "Added `NEXT_PUBLIC_VIEWS_TX_GROUPED_FEES` to group transaction fees into one section on the transaction page." + - **Good:** "Extended possible values for `NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS` with set_max_gas_limit to display the maximum gas price set by the transaction sender." + - **Good:** "Introduced a new option, `"fee reception"`, for the `NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE` variable." +- **Checklist:** Keep the "Checklist for PR Author" section from the template and check the items that apply (e.g. tested locally, tests added, ENVS/docs/validator updated if env vars changed). +- When the description is ready, **ask the user for confirmation or changes** before creating the PR (step 4). + +### 4. Create the PR and set labels + +- Create the pull request with a descriptive title. Use draft mode if the user asked for it: + - `gh pr create --title "Your descriptive title" --body-file /path/to/body.md` (and add `--draft` if draft). + - Or paste the body inline if preferred; ensure the filled template is used. +- Add labels: + - If anything was added or changed in `./docs/ENVS.md`, add the **"ENVs"** label: `gh pr edit --add-label "ENVs"` (or add labels during `gh pr create` if your `gh` version supports it). + - If `package.json` has changed (check with `git diff origin/main -- package.json`), add the **"dependencies"** label. + - If the branch name contained an issue number (pattern `issue-\d+`), copy all labels from that issue onto the PR: get labels with `gh issue view --json labels -q '.labels[].name'`, then add each: `gh pr edit --add-label " diff --git a/ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx b/client/features/tx-interpretation/noves/components/NovesTokenTooltipContent.tsx similarity index 80% rename from ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx rename to client/features/tx-interpretation/noves/components/NovesTokenTooltipContent.tsx index 9e65de6522..0205b9bb50 100644 --- a/ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx +++ b/client/features/tx-interpretation/noves/components/NovesTokenTooltipContent.tsx @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Text } from '@chakra-ui/react'; import type { FC } from 'react'; import React from 'react'; import type { NovesNft, NovesToken } from 'types/api/noves'; +import shortenString from 'client/shared/text/shorten-string'; + import { HEX_REGEXP } from 'toolkit/utils/regexp'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; @@ -21,7 +25,7 @@ const NovesTokenTooltipContent: FC = ({ token, amount }) => { const showTokenAddress = HEX_REGEXP.test(token.address); return ( - + { amount } @@ -40,9 +44,9 @@ const NovesTokenTooltipContent: FC = ({ token, amount }) => { { showTokenAddress && ( - { token.address } + { shortenString(token.address) } - + ) } diff --git a/client/features/tx-interpretation/noves/components/TxTranslationType.tsx b/client/features/tx-interpretation/noves/components/TxTranslationType.tsx new file mode 100644 index 0000000000..719f39b0c3 --- /dev/null +++ b/client/features/tx-interpretation/noves/components/TxTranslationType.tsx @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +import type { TransactionType } from 'client/slices/tx/types/api'; + +import TxType from 'client/slices/tx/components/TxType'; + +import { Badge } from 'toolkit/chakra/badge'; + +import { camelCaseToSentence } from '../utils/translation'; + +export interface Props { + txTypes: Array; + isLoading?: boolean; + type: string | undefined; +} + +const FILTERED_TYPES = [ 'unclassified' ]; + +const TxTranslationType = ({ txTypes, isLoading, type }: Props) => { + + if (!type || FILTERED_TYPES.includes(type.toLowerCase())) { + return ; + } + + return ( + + { camelCaseToSentence(type) } + + ); + +}; + +export default TxTranslationType; diff --git a/client/features/tx-interpretation/noves/hooks/useDescribeTxs.ts b/client/features/tx-interpretation/noves/hooks/useDescribeTxs.ts new file mode 100644 index 0000000000..8949e8b52a --- /dev/null +++ b/client/features/tx-interpretation/noves/hooks/useDescribeTxs.ts @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { uniq, chunk } from 'es-toolkit'; +import React from 'react'; + +import type { Transaction } from 'client/slices/tx/types/api'; + +import type { ReturnType } from 'client/api/hooks/useApiQueries'; +import useApiQueries from 'client/api/hooks/useApiQueries'; + +import config from 'configs/app'; + +const feature = config.features.txInterpretation; + +const translateEnabled = feature.isEnabled && feature.provider === 'noves'; + +export type TxsTranslationQuery = ReturnType<'general:noves_describe_txs'> | undefined; + +export default function useDescribeTxs( + items: Array | undefined, + viewAsAccountAddress: string | undefined, + isPlaceholderData: boolean, +): TxsTranslationQuery { + const enabled = translateEnabled && !isPlaceholderData; + const chunks = React.useMemo(() => { + if (!enabled) { + return []; + } + + const txsHash = items ? uniq(items.map(({ hash }) => hash)) : []; + return chunk(txsHash, 10); + }, [ items, enabled ]); + + const query = useApiQueries( + 'general:noves_describe_txs', + chunks.map((hashes) => { + return { + queryParams: { + viewAsAccountAddress, + hashes, + }, + }; + }), + { enabled }, + ); + + return enabled ? query : undefined; +} diff --git a/ui/address/AddressAccountHistory.tsx b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistory.tsx similarity index 90% rename from ui/address/AddressAccountHistory.tsx rename to client/features/tx-interpretation/noves/pages/address/AddressAccountHistory.tsx index c09795797e..c5aa762cfe 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistory.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; @@ -5,21 +7,22 @@ import React from 'react'; import type { NovesHistoryFilterValue } from 'types/api/noves'; import { NovesHistoryFilterValues } from 'types/api/noves'; -import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; -import useIsMounted from 'lib/hooks/useIsMounted'; -import getQueryParamString from 'lib/router/getQueryParamString'; +import useIsMounted from 'client/shared/hooks/useIsMounted'; +import getFilterValueFromQuery from 'client/shared/router/get-filter-value-from-query'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { generateListStub } from 'stubs/utils'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; -import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAccountHistoryTableItem'; import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import { getFromToValue } from 'ui/shared/Noves/utils'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; -import AddressAccountHistoryListItem from './accountHistory/AddressAccountHistoryListItem'; import AccountHistoryFilter from './AddressAccountHistoryFilter'; +import AddressAccountHistoryListItem from './AddressAccountHistoryListItem'; +import AddressAccountHistoryTableItem from './AddressAccountHistoryTableItem'; const getFilterValue = (getFilterValueFromQuery).bind(null, NovesHistoryFilterValues); diff --git a/ui/address/AddressAccountHistoryFilter.tsx b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryFilter.tsx similarity index 89% rename from ui/address/AddressAccountHistoryFilter.tsx rename to client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryFilter.tsx index 7c202f0e99..3123b1bf7b 100644 --- a/ui/address/AddressAccountHistoryFilter.tsx +++ b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryFilter.tsx @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { createListCollection } from '@chakra-ui/react'; import React from 'react'; import type { NovesHistoryFilterValue } from 'types/api/noves'; -import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import useIsInitialLoading from 'client/shared/hooks/useIsInitialLoading'; + import PopoverFilterRadio from 'ui/shared/filters/PopoverFilterRadio'; const OPTIONS = [ diff --git a/ui/address/accountHistory/AddressAccountHistoryListItem.tsx b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryListItem.tsx similarity index 97% rename from ui/address/accountHistory/AddressAccountHistoryListItem.tsx rename to client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryListItem.tsx index 4a5690029c..f218e4a1f9 100644 --- a/ui/address/accountHistory/AddressAccountHistoryListItem.tsx +++ b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryListItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, Text } from '@chakra-ui/react'; import React, { useMemo } from 'react'; diff --git a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryTableItem.tsx similarity index 97% rename from ui/address/accountHistory/AddressAccountHistoryTableItem.tsx rename to client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryTableItem.tsx index 4e016f6d73..195ded5a5f 100644 --- a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx +++ b/client/features/tx-interpretation/noves/pages/address/AddressAccountHistoryTableItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React, { useMemo } from 'react'; diff --git a/ui/tx/TxAssetFlows.tsx b/client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlows.tsx similarity index 90% rename from ui/tx/TxAssetFlows.tsx rename to client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlows.tsx index 182bf8bfd9..adf65d1128 100644 --- a/ui/tx/TxAssetFlows.tsx +++ b/client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlows.tsx @@ -1,21 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Text } from '@chakra-ui/react'; import { chunk } from 'es-toolkit'; import React, { useMemo, useState } from 'react'; import type { PaginationParams } from 'ui/shared/pagination/types'; -import useApiQuery from 'lib/api/useApiQuery'; +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import Pagination from 'ui/shared/pagination/Pagination'; -import TxAssetFlowsListItem from './assetFlows/TxAssetFlowsListItem'; -import TxAssetFlowsTableItem from './assetFlows/TxAssetFlowsTableItem'; -import { generateFlowViewData } from './assetFlows/utils/generateFlowViewData'; +import { generateFlowViewData } from '../../utils/generateFlowViewData'; +import TxAssetFlowsListItem from './TxAssetFlowsListItem'; +import TxAssetFlowsTableItem from './TxAssetFlowsTableItem'; interface FlowViewProps { hash: string; diff --git a/ui/tx/assetFlows/TxAssetFlowsListItem.tsx b/client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlowsListItem.tsx similarity index 85% rename from ui/tx/assetFlows/TxAssetFlowsListItem.tsx rename to client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlowsListItem.tsx index 64da979123..297574953a 100644 --- a/ui/tx/assetFlows/TxAssetFlowsListItem.tsx +++ b/client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlowsListItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Text } from '@chakra-ui/react'; import React from 'react'; @@ -6,8 +8,8 @@ import IconSvg from 'ui/shared/IconSvg'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; -import NovesActionSnippet from './components/NovesActionSnippet'; -import type { NovesFlowViewItem } from './utils/generateFlowViewData'; +import NovesActionSnippet from '../../components/NovesActionSnippet'; +import type { NovesFlowViewItem } from '../../utils/generateFlowViewData'; type Props = { isPlaceholderData: boolean; diff --git a/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx b/client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlowsTableItem.tsx similarity index 80% rename from ui/tx/assetFlows/TxAssetFlowsTableItem.tsx rename to client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlowsTableItem.tsx index a64b742e3b..1b72fa9458 100644 --- a/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx +++ b/client/features/tx-interpretation/noves/pages/tx-asset-flows/TxAssetFlowsTableItem.tsx @@ -1,10 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { TableRow, TableCell } from 'toolkit/chakra/table'; import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; -import NovesActionSnippet from './components/NovesActionSnippet'; -import type { NovesFlowViewItem } from './utils/generateFlowViewData'; +import NovesActionSnippet from '../../components/NovesActionSnippet'; +import type { NovesFlowViewItem } from '../../utils/generateFlowViewData'; type Props = { isPlaceholderData: boolean; diff --git a/ui/tx/assetFlows/utils/createNovesSummaryObject.spec.ts b/client/features/tx-interpretation/noves/utils/createNovesSummaryObject.spec.ts similarity index 100% rename from ui/tx/assetFlows/utils/createNovesSummaryObject.spec.ts rename to client/features/tx-interpretation/noves/utils/createNovesSummaryObject.spec.ts diff --git a/ui/tx/assetFlows/utils/createNovesSummaryObject.ts b/client/features/tx-interpretation/noves/utils/createNovesSummaryObject.ts similarity index 98% rename from ui/tx/assetFlows/utils/createNovesSummaryObject.ts rename to client/features/tx-interpretation/noves/utils/createNovesSummaryObject.ts index 81df1eabea..867bb05ae2 100644 --- a/ui/tx/assetFlows/utils/createNovesSummaryObject.ts +++ b/client/features/tx-interpretation/noves/utils/createNovesSummaryObject.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { NovesResponseData } from 'types/api/noves'; import type { TxInterpretationSummary } from 'types/api/txInterpretation'; diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.spec.ts b/client/features/tx-interpretation/noves/utils/generateFlowViewData.spec.ts similarity index 100% rename from ui/tx/assetFlows/utils/generateFlowViewData.spec.ts rename to client/features/tx-interpretation/noves/utils/generateFlowViewData.spec.ts diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.ts b/client/features/tx-interpretation/noves/utils/generateFlowViewData.ts similarity index 97% rename from ui/tx/assetFlows/utils/generateFlowViewData.ts rename to client/features/tx-interpretation/noves/utils/generateFlowViewData.ts index 5024942104..c06046b7fe 100644 --- a/ui/tx/assetFlows/utils/generateFlowViewData.ts +++ b/client/features/tx-interpretation/noves/utils/generateFlowViewData.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/api/noves'; export interface NovesAction { diff --git a/ui/tx/assetFlows/utils/getAddressValues.spec.ts b/client/features/tx-interpretation/noves/utils/getAddressValues.spec.ts similarity index 100% rename from ui/tx/assetFlows/utils/getAddressValues.spec.ts rename to client/features/tx-interpretation/noves/utils/getAddressValues.spec.ts diff --git a/ui/tx/assetFlows/utils/getAddressValues.ts b/client/features/tx-interpretation/noves/utils/getAddressValues.ts similarity index 98% rename from ui/tx/assetFlows/utils/getAddressValues.ts rename to client/features/tx-interpretation/noves/utils/getAddressValues.ts index a680f068ee..c4221b7c57 100644 --- a/ui/tx/assetFlows/utils/getAddressValues.ts +++ b/client/features/tx-interpretation/noves/utils/getAddressValues.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { NovesResponseData } from 'types/api/noves'; import type { SummaryAddress, SummaryValues } from './createNovesSummaryObject'; diff --git a/ui/tx/assetFlows/utils/getTokensData.spec.ts b/client/features/tx-interpretation/noves/utils/getTokensData.spec.ts similarity index 100% rename from ui/tx/assetFlows/utils/getTokensData.spec.ts rename to client/features/tx-interpretation/noves/utils/getTokensData.spec.ts diff --git a/ui/tx/assetFlows/utils/getTokensData.ts b/client/features/tx-interpretation/noves/utils/getTokensData.ts similarity index 95% rename from ui/tx/assetFlows/utils/getTokensData.ts rename to client/features/tx-interpretation/noves/utils/getTokensData.ts index 37fbb161ae..0a0edd1b67 100644 --- a/ui/tx/assetFlows/utils/getTokensData.ts +++ b/client/features/tx-interpretation/noves/utils/getTokensData.ts @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { groupBy, mapValues } from 'es-toolkit'; +import type { TokenInfo } from 'client/slices/token/types/api'; import type { NovesResponseData } from 'types/api/noves'; -import type { TokenInfo } from 'types/api/token'; import { HEX_REGEXP } from 'toolkit/utils/regexp'; diff --git a/client/features/tx-interpretation/noves/utils/translation.ts b/client/features/tx-interpretation/noves/utils/translation.ts new file mode 100644 index 0000000000..e39494f951 --- /dev/null +++ b/client/features/tx-interpretation/noves/utils/translation.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import capitalizeFirstLetter from 'client/shared/text/capitalize-first-letter'; + +export function camelCaseToSentence(camelCaseString: string | undefined) { + if (!camelCaseString) { + return ''; + } + + let sentence = camelCaseString.replace(/([a-z])([A-Z])/g, '$1 $2'); + sentence = sentence.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2'); + sentence = capitalizeFirstLetter(sentence); + + return sentence; +} diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestUserOp.tsx b/client/features/user-ops/components/SearchBarSuggestUserOp.tsx similarity index 77% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestUserOp.tsx rename to client/features/user-ops/components/SearchBarSuggestUserOp.tsx index 11c56a01b3..bdbc4cfdda 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestUserOp.tsx +++ b/client/features/user-ops/components/SearchBarSuggestUserOp.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Flex } from '@chakra-ui/react'; import React from 'react'; -import type { ItemsProps } from './types'; -import type { SearchResultUserOp } from 'types/api/search'; +import type { SearchResultUserOp } from 'client/features/user-ops/types/api'; +import type { ItemsProps } from 'client/slices/search/components/search-bar/SearchBarSuggest/types'; + +import * as UserOpEntity from 'client/features/user-ops/components/entity/UserOpEntity'; -import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import Time from 'ui/shared/time/Time'; diff --git a/ui/shared/userOps/UserOpSponsorType.tsx b/client/features/user-ops/components/UserOpSponsorType.tsx similarity index 80% rename from ui/shared/userOps/UserOpSponsorType.tsx rename to client/features/user-ops/components/UserOpSponsorType.tsx index e8a82a5826..526280b2d8 100644 --- a/ui/shared/userOps/UserOpSponsorType.tsx +++ b/client/features/user-ops/components/UserOpSponsorType.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { UserOpSponsorType as TUserOpSponsorType } from 'types/api/userOps'; +import type { UserOpSponsorType as TUserOpSponsorType } from 'client/features/user-ops/types/api'; import { Badge } from 'toolkit/chakra/badge'; diff --git a/ui/shared/userOps/UserOpStatus.tsx b/client/features/user-ops/components/UserOpStatus.tsx similarity index 89% rename from ui/shared/userOps/UserOpStatus.tsx rename to client/features/user-ops/components/UserOpStatus.tsx index 28b17bef37..34902b66a2 100644 --- a/ui/shared/userOps/UserOpStatus.tsx +++ b/client/features/user-ops/components/UserOpStatus.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import StatusTag from 'ui/shared/statusTag/StatusTag'; diff --git a/ui/shared/entities/userOp/UserOpEntity.pw.tsx b/client/features/user-ops/components/entity/UserOpEntity.pw.tsx similarity index 100% rename from ui/shared/entities/userOp/UserOpEntity.pw.tsx rename to client/features/user-ops/components/entity/UserOpEntity.pw.tsx diff --git a/ui/shared/entities/userOp/UserOpEntity.tsx b/client/features/user-ops/components/entity/UserOpEntity.tsx similarity index 95% rename from ui/shared/entities/userOp/UserOpEntity.tsx rename to client/features/user-ops/components/entity/UserOpEntity.tsx index 66faaa25ca..32907f18ea 100644 --- a/ui/shared/entities/userOp/UserOpEntity.tsx +++ b/client/features/user-ops/components/entity/UserOpEntity.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; @@ -5,10 +7,9 @@ import { route } from 'nextjs/routes'; import { useMultichainContext } from 'lib/contexts/multichain'; import * as EntityBase from 'ui/shared/entities/base/components'; +import { distributeEntityProps } from 'ui/shared/entities/base/utils'; import getChainTooltipText from 'ui/shared/externalChains/getChainTooltipText'; -import { distributeEntityProps } from '../base/utils'; - type LinkProps = EntityBase.LinkBaseProps & Pick; const Link = chakra((props: LinkProps) => { diff --git a/ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_dark-color-mode_with-no-copy-dark-mode-1.png b/client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_dark-color-mode_with-no-copy-dark-mode-1.png similarity index 100% rename from ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_dark-color-mode_with-no-copy-dark-mode-1.png rename to client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_dark-color-mode_with-no-copy-dark-mode-1.png diff --git a/ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_customization-1.png b/client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_customization-1.png similarity index 100% rename from ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_customization-1.png rename to client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_customization-1.png diff --git a/ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_loading-1.png b/client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_loading-1.png similarity index 100% rename from ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_loading-1.png rename to client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_loading-1.png diff --git a/ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_variant-content-1.png b/client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_variant-content-1.png similarity index 100% rename from ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_variant-content-1.png rename to client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_variant-content-1.png diff --git a/ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_variant-subheading-1.png b/client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_variant-subheading-1.png similarity index 100% rename from ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_variant-subheading-1.png rename to client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_variant-subheading-1.png diff --git a/ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_with-no-copy-dark-mode-1.png b/client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_with-no-copy-dark-mode-1.png similarity index 100% rename from ui/shared/entities/userOp/__screenshots__/UserOpEntity.pw.tsx_default_with-no-copy-dark-mode-1.png rename to client/features/user-ops/components/entity/__screenshots__/UserOpEntity.pw.tsx_default_with-no-copy-dark-mode-1.png diff --git a/client/features/user-ops/mocks/search.ts b/client/features/user-ops/mocks/search.ts new file mode 100644 index 0000000000..9e44a825cf --- /dev/null +++ b/client/features/user-ops/mocks/search.ts @@ -0,0 +1,8 @@ +import type { SearchResultUserOp } from 'client/features/user-ops/types/api'; + +export const userOp1: SearchResultUserOp = { + timestamp: '2024-01-11T14:15:48.000000Z', + type: 'user_operation', + user_operation_hash: '0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', + url: '/op/0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', +}; diff --git a/client/features/user-ops/mocks/user-op.ts b/client/features/user-ops/mocks/user-op.ts new file mode 100644 index 0000000000..158084498e --- /dev/null +++ b/client/features/user-ops/mocks/user-op.ts @@ -0,0 +1,111 @@ +/* eslint-disable max-len */ +import type { UserOp } from 'client/features/user-ops/types/api'; + +export const userOpData: UserOp = { + timestamp: '2024-01-19T12:42:12.000000Z', + transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a', + user_logs_start_index: 40, + fee: '187125856691380', + call_gas_limit: '26624', + gas: '258875', + status: true, + aggregator_signature: null, + block_hash: '0xff5f41ec89e5fb3dfcf103bbbd67469fed491a7dd7cffdf00bd9e3bf45d8aeab', + pre_verification_gas: '48396', + factory: null, + signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b', + verification_gas_limit: '61285', + max_fee_per_gas: '1575000898', + aggregator: null, + hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83', + gas_price: '1575000898', + user_logs_count: 1, + block_number: '10399597', + gas_used: '118810', + sender: { + ens_domain_name: null, + hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + nonce: '0x000000000000000000000000000000000000000000000000000000000000004f', + entry_point: { + ens_domain_name: null, + hash: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + sponsor_type: 'paymaster_sponsor', + raw: { + + call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', + call_gas_limit: '26624', + init_code: '0x', + max_fee_per_gas: '1575000898', + max_priority_fee_per_gas: '1575000898', + nonce: '79', + paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b', + pre_verification_gas: '48396', + sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b', + verification_gas_limit: '61285', + }, + max_priority_fee_per_gas: '1575000898', + revert_reason: null, + bundler: { + ens_domain_name: null, + hash: '0xd53Eb5203e367BbDD4f72338938224881Fc501Ab', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + }, + call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', + execute_call_data: '0x3cf80e6c', + execute_target: { + ens_domain_name: null, + hash: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + implementations: null, + is_contract: true, + is_verified: true, + name: 'FiatTokenProxy', + }, + decoded_call_data: { + method_call: 'execute(address dest, uint256 value, bytes func)', + method_id: 'b61d27f6', + parameters: [ + { + name: 'dest', + type: 'address', + value: '0xb0ccffd05f5a87c4c3ceffaa217900422a249915', + }, + { + name: 'value', + type: 'uint256', + value: '0', + }, + { + name: 'func', + type: 'bytes', + value: '0x3cf80e6c', + }, + ], + }, + decoded_execute_call_data: { + method_call: 'advanceEpoch()', + method_id: '3cf80e6c', + parameters: [], + }, + paymaster: { + ens_domain_name: null, + hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, +}; diff --git a/client/features/user-ops/mocks/user-ops.ts b/client/features/user-ops/mocks/user-ops.ts new file mode 100644 index 0000000000..c16b7e4693 --- /dev/null +++ b/client/features/user-ops/mocks/user-ops.ts @@ -0,0 +1,58 @@ +import type { UserOpsResponse } from 'client/features/user-ops/types/api'; + +export const userOpsData: UserOpsResponse = { + items: [ + { + address: { + ens_domain_name: null, + hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399597', + fee: '187125856691380', + hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83', + status: true, + timestamp: '2022-01-19T12:42:12.000000Z', + transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a', + }, + { + address: + { ens_domain_name: null, + hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399596', + fee: '381895502291373', + hash: '0xcb945ae86608bdc88c3318245403c81a880fcb1e49fef18ac59477761c056cea', + status: false, + timestamp: '2022-01-19T12:42:00.000000Z', + transaction_hash: '0x558d699e7cbc235461d48ed04b8c3892d598a4000f20851760d00dc3513c2e48', + }, + { + address: { + ens_domain_name: null, + hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399560', + fee: '165019501210143', + hash: '0x84c1270b12af3f0ffa204071f1bf503ebf9b1ccf6310680383be5a2b6fd1d8e5', + status: true, + timestamp: '2022-01-19T12:32:00.000000Z', + transaction_hash: '0xc4c1c38680ec63139411aa2193275e8de44be15217c4256db9473bf0ea2b6264', + }, + ], + next_page_params: { + page_size: 50, + page_token: '10396582,0x9bf4d2a28813c5c244884cb20cdfe01dabb3f927234ae961eab6e38502de7a28', + }, +}; diff --git a/ui/address/AddressUserOps.tsx b/client/features/user-ops/pages/address/AddressUserOps.tsx similarity index 76% rename from ui/address/AddressUserOps.tsx rename to client/features/user-ops/pages/address/AddressUserOps.tsx index e958a80682..e6f034c894 100644 --- a/ui/address/AddressUserOps.tsx +++ b/client/features/user-ops/pages/address/AddressUserOps.tsx @@ -1,12 +1,16 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import React from 'react'; -import useIsMounted from 'lib/hooks/useIsMounted'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import { USER_OPS_ITEM } from 'stubs/userOps'; +import UserOpsContent from 'client/features/user-ops/pages/index/UserOpsContent'; +import { USER_OPS_ITEM } from 'client/features/user-ops/stubs'; + +import useIsMounted from 'client/shared/hooks/useIsMounted'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import { generateListStub } from 'stubs/utils'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; -import UserOpsContent from 'ui/userOps/UserOpsContent'; type Props = { scrollRef?: React.RefObject; diff --git a/ui/pages/UserOp.pw.tsx b/client/features/user-ops/pages/details/UserOp.pw.tsx similarity index 94% rename from ui/pages/UserOp.pw.tsx rename to client/features/user-ops/pages/details/UserOp.pw.tsx index 9b4a5cc0b9..a4c15489a7 100644 --- a/ui/pages/UserOp.pw.tsx +++ b/client/features/user-ops/pages/details/UserOp.pw.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { userOpData } from 'mocks/userOps/userOp'; +import { userOpData } from 'client/features/user-ops/mocks/user-op'; + import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect, devices } from 'playwright/lib'; diff --git a/client/features/user-ops/pages/details/UserOp.tsx b/client/features/user-ops/pages/details/UserOp.tsx new file mode 100644 index 0000000000..56fc2dff6c --- /dev/null +++ b/client/features/user-ops/pages/details/UserOp.tsx @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { inRange } from 'es-toolkit'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { TransactionLog } from 'client/slices/log/types/api'; +import type { TokenTransfer } from 'client/slices/token-transfer/types/api'; +import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import TxTokenTransfer from 'client/slices/token-transfer/pages/tx/TxTokenTransfer'; +import useTxQuery from 'client/slices/tx/hooks/useTxQuery'; +import TxLogs from 'client/slices/tx/pages/details/logs/TxLogs'; + +import { USER_OP } from 'client/features/user-ops/stubs'; + +import throwOnAbsentParamError from 'client/shared/errors/throw-on-absent-param-error'; +import throwOnResourceLoadError from 'client/shared/errors/throw-on-resource-load-error'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; +import TextAd from 'ui/shared/ad/TextAd'; +import PageTitle from 'ui/shared/Page/PageTitle'; + +import UserOpDetails from './UserOpDetails'; +import UserOpRaw from './UserOpRaw'; +import UserOpSubHeading from './UserOpSubHeading'; + +const UserOp = () => { + const router = useRouter(); + const hash = getQueryParamString(router.query.hash); + + const userOpQuery = useApiQuery('general:user_op', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash), + placeholderData: USER_OP, + }, + }); + + const txQuery = useTxQuery({ hash: userOpQuery.data?.transaction_hash, isEnabled: !userOpQuery.isPlaceholderData }); + + const filterTokenTransfersByLogIndex = React.useCallback((tt: TokenTransfer) => { + if (!userOpQuery.data) { + return true; + } else { + if (!userOpQuery.data.user_logs_start_index || !userOpQuery.data.user_logs_count) { + return false; + } + if (inRange( + Number(tt.log_index), + userOpQuery.data?.user_logs_start_index, + userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count, + )) { + return true; + } + return false; + } + }, [ userOpQuery.data ]); + + const filterLogsByLogIndex = React.useCallback((log: TransactionLog) => { + if (!userOpQuery.data) { + return true; + } else { + if (!userOpQuery.data.user_logs_start_index || !userOpQuery.data.user_logs_count) { + return false; + } + if (inRange(log.index, userOpQuery.data?.user_logs_start_index, userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count)) { + return true; + } + return false; + } + }, [ userOpQuery.data ]); + + const tabs: Array = React.useMemo(() => ([ + { id: 'index', title: 'Details', component: }, + { + id: 'token_transfers', + title: 'Token transfers', + component: , + }, + { id: 'logs', title: 'Logs', component: }, + { id: 'raw', title: 'Raw', component: }, + ]), [ userOpQuery, txQuery, filterTokenTransfersByLogIndex, filterLogsByLogIndex ]); + + throwOnAbsentParamError(hash); + throwOnResourceLoadError(userOpQuery); + + const titleSecondRow = ; + + return ( + <> + + + + + ); +}; + +export default UserOp; diff --git a/ui/userOp/UserOpCallData.tsx b/client/features/user-ops/pages/details/UserOpCallData.tsx similarity index 87% rename from ui/userOp/UserOpCallData.tsx rename to client/features/user-ops/pages/details/UserOpCallData.tsx index bcc0b2aee5..c4406fea06 100644 --- a/ui/userOp/UserOpCallData.tsx +++ b/client/features/user-ops/pages/details/UserOpCallData.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { UserOp } from 'types/api/userOps'; +import type { UserOp } from 'client/features/user-ops/types/api'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import RawInputData from 'ui/shared/RawInputData'; @@ -37,7 +39,7 @@ const UserOpDecodedCallData = ({ data }: Props) => { return ( <> { labelText } diff --git a/ui/userOp/UserOpCallDataSwitch.tsx b/client/features/user-ops/pages/details/UserOpCallDataSwitch.tsx similarity index 96% rename from ui/userOp/UserOpCallDataSwitch.tsx rename to client/features/user-ops/pages/details/UserOpCallDataSwitch.tsx index 1e3913013c..f751a2b530 100644 --- a/ui/userOp/UserOpCallDataSwitch.tsx +++ b/client/features/user-ops/pages/details/UserOpCallDataSwitch.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Flex } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/userOp/UserOpDecodedCallData.tsx b/client/features/user-ops/pages/details/UserOpDecodedCallData.tsx similarity index 86% rename from ui/userOp/UserOpDecodedCallData.tsx rename to client/features/user-ops/pages/details/UserOpDecodedCallData.tsx index cc5835fbf0..c11c97a67c 100644 --- a/ui/userOp/UserOpDecodedCallData.tsx +++ b/client/features/user-ops/pages/details/UserOpDecodedCallData.tsx @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; -import type { UserOp } from 'types/api/userOps'; +import type { UserOp } from 'client/features/user-ops/types/api'; + +import LogDecodedInputData from 'client/slices/log/components/LogDecodedInputData'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; -import useIsMobile from 'lib/hooks/useIsMobile'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; -import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData'; import UserOpCallDataSwitch from './UserOpCallDataSwitch'; diff --git a/ui/userOp/UserOpDetails.tsx b/client/features/user-ops/pages/details/UserOpDetails.tsx similarity index 93% rename from ui/userOp/UserOpDetails.tsx rename to client/features/user-ops/pages/details/UserOpDetails.tsx index 568fee3d14..2ff1de8be4 100644 --- a/ui/userOp/UserOpDetails.tsx +++ b/client/features/user-ops/pages/details/UserOpDetails.tsx @@ -1,13 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { GridItem } from '@chakra-ui/react'; import type { UseQueryResult } from '@tanstack/react-query'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { UserOp } from 'types/api/userOps'; +import type { UserOp } from 'client/features/user-ops/types/api'; + +import type { ResourceError } from 'client/api/resources'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import AddressStringOrParam from 'client/slices/address/components/entity/AddressStringOrParam'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + +import UserOpEntity from 'client/features/user-ops/components/entity/UserOpEntity'; +import UserOpSponsorType from 'client/features/user-ops/components/UserOpSponsorType'; +import UserOpStatus from 'client/features/user-ops/components/UserOpStatus'; + +import throwOnResourceLoadError from 'client/shared/errors/throw-on-resource-load-error'; import config from 'configs/app'; -import type { ResourceError } from 'lib/api/resources'; -import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import { CollapsibleDetails } from 'toolkit/chakra/collapsible'; import { Skeleton } from 'toolkit/chakra/skeleton'; import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; @@ -15,13 +28,6 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import DetailedInfoNativeCoinValue from 'ui/shared/DetailedInfo/DetailedInfoNativeCoinValue'; import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrParam'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; -import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; -import UserOpSponsorType from 'ui/shared/userOps/UserOpSponsorType'; -import UserOpStatus from 'ui/shared/userOps/UserOpStatus'; import Utilization from 'ui/shared/Utilization/Utilization'; import GasPriceValue from 'ui/shared/value/GasPriceValue'; diff --git a/ui/userOp/UserOpDetailsActions.tsx b/client/features/user-ops/pages/details/UserOpDetailsActions.tsx similarity index 86% rename from ui/userOp/UserOpDetailsActions.tsx rename to client/features/user-ops/pages/details/UserOpDetailsActions.tsx index 0e91ad5d9d..2a5a6628f6 100644 --- a/ui/userOp/UserOpDetailsActions.tsx +++ b/client/features/user-ops/pages/details/UserOpDetailsActions.tsx @@ -1,10 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import useApiQuery from 'lib/api/useApiQuery'; +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import TxInterpretation from 'client/features/tx-interpretation/common/components/TxInterpretation'; + import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import DetailedInfoActionsWrapper from 'ui/shared/DetailedInfo/DetailedInfoActionsWrapper'; -import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation'; interface Props { hash?: string; diff --git a/ui/userOp/UserOpRaw.tsx b/client/features/user-ops/pages/details/UserOpRaw.tsx similarity index 89% rename from ui/userOp/UserOpRaw.tsx rename to client/features/user-ops/pages/details/UserOpRaw.tsx index ebb066e7aa..2edb1dbd22 100644 --- a/ui/userOp/UserOpRaw.tsx +++ b/client/features/user-ops/pages/details/UserOpRaw.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { UserOp } from 'types/api/userOps'; +import type { UserOp } from 'client/features/user-ops/types/api'; import { Skeleton } from 'toolkit/chakra/skeleton'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; diff --git a/ui/userOp/UserOpSubHeading.tsx b/client/features/user-ops/pages/details/UserOpSubHeading.tsx similarity index 87% rename from ui/userOp/UserOpSubHeading.tsx rename to client/features/user-ops/pages/details/UserOpSubHeading.tsx index ed7bc356ff..c25378f8de 100644 --- a/ui/userOp/UserOpSubHeading.tsx +++ b/client/features/user-ops/pages/details/UserOpSubHeading.tsx @@ -1,18 +1,22 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; // import type { UseQueryResult } from '@tanstack/react-query'; import React from 'react'; -// import type { UserOp } from 'types/api/userOps'; +// import type { UserOp } from 'client/features/user-ops/types/api'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import TxInterpretation from 'client/features/tx-interpretation/common/components/TxInterpretation'; +import UserOpEntity from 'client/features/user-ops/components/entity/UserOpEntity'; import config from 'configs/app'; -// import type { ResourceError } from 'lib/api/resources'; -import useApiQuery from 'lib/api/useApiQuery'; +// import type { ResourceError } from 'client/api/resources'; import { useMultichainContext } from 'lib/contexts/multichain'; import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import { Link } from 'toolkit/chakra/link'; import { TX_ACTIONS_BLOCK_ID } from 'ui/shared/DetailedInfo/DetailedInfoActionsWrapper'; -import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; -import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation'; type Props = { hash: string; diff --git a/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png b/client/features/user-ops/pages/details/__screenshots__/UserOp.pw.tsx_default_base-view-1.png similarity index 100% rename from ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png rename to client/features/user-ops/pages/details/__screenshots__/UserOp.pw.tsx_default_base-view-1.png diff --git a/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png b/client/features/user-ops/pages/details/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png similarity index 100% rename from ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png rename to client/features/user-ops/pages/details/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png diff --git a/ui/pages/UserOps.pw.tsx b/client/features/user-ops/pages/index/UserOps.pw.tsx similarity index 87% rename from ui/pages/UserOps.pw.tsx rename to client/features/user-ops/pages/index/UserOps.pw.tsx index 5870bc7052..41b8331305 100644 --- a/ui/pages/UserOps.pw.tsx +++ b/client/features/user-ops/pages/index/UserOps.pw.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { userOpsData } from 'mocks/userOps/userOps'; +import { userOpsData } from 'client/features/user-ops/mocks/user-ops'; + import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect } from 'playwright/lib'; diff --git a/ui/pages/UserOps.tsx b/client/features/user-ops/pages/index/UserOps.tsx similarity index 83% rename from ui/pages/UserOps.tsx rename to client/features/user-ops/pages/index/UserOps.tsx index 274090dcbb..b09c4934da 100644 --- a/ui/pages/UserOps.tsx +++ b/client/features/user-ops/pages/index/UserOps.tsx @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; +import { USER_OPS_ITEM } from 'client/features/user-ops/stubs'; + import config from 'configs/app'; -import { USER_OPS_ITEM } from 'stubs/userOps'; import { generateListStub } from 'stubs/utils'; import PageTitle from 'ui/shared/Page/PageTitle'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; -import UserOpsContent from 'ui/userOps/UserOpsContent'; + +import UserOpsContent from './UserOpsContent'; const UserOps = () => { const query = useQueryWithPages({ diff --git a/ui/userOps/UserOpsContent.tsx b/client/features/user-ops/pages/index/UserOpsContent.tsx similarity index 92% rename from ui/userOps/UserOpsContent.tsx rename to client/features/user-ops/pages/index/UserOpsContent.tsx index ea67f39163..86868a4f23 100644 --- a/ui/userOps/UserOpsContent.tsx +++ b/client/features/user-ops/pages/index/UserOpsContent.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; @@ -6,8 +8,9 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataListDisplay from 'ui/shared/DataListDisplay'; import Pagination from 'ui/shared/pagination/Pagination'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; -import UserOpsListItem from 'ui/userOps/UserOpsListItem'; -import UserOpsTable from 'ui/userOps/UserOpsTable'; + +import UserOpsListItem from './UserOpsListItem'; +import UserOpsTable from './UserOpsTable'; type Props = { query: QueryWithPagesResult<'general:user_ops'>; diff --git a/ui/userOps/UserOpsListItem.tsx b/client/features/user-ops/pages/index/UserOpsListItem.tsx similarity index 86% rename from ui/userOps/UserOpsListItem.tsx rename to client/features/user-ops/pages/index/UserOpsListItem.tsx index b520ba4049..f0d0a6fc35 100644 --- a/ui/userOps/UserOpsListItem.tsx +++ b/client/features/user-ops/pages/index/UserOpsListItem.tsx @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { UserOpsItem } from 'types/api/userOps'; +import type { UserOpsItem } from 'client/features/user-ops/types/api'; import type { ClusterChainConfig } from 'types/multichain'; +import AddressStringOrParam from 'client/slices/address/components/entity/AddressStringOrParam'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + +import UserOpEntity from 'client/features/user-ops/components/entity/UserOpEntity'; +import UserOpStatus from 'client/features/user-ops/components/UserOpStatus'; + import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; -import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrParam'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; -import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; -import UserOpStatus from 'ui/shared/userOps/UserOpStatus'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; type Props = { diff --git a/ui/userOps/UserOpsTable.tsx b/client/features/user-ops/pages/index/UserOpsTable.tsx similarity index 91% rename from ui/userOps/UserOpsTable.tsx rename to client/features/user-ops/pages/index/UserOpsTable.tsx index e198599659..2891e00d64 100644 --- a/ui/userOps/UserOpsTable.tsx +++ b/client/features/user-ops/pages/index/UserOpsTable.tsx @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { UserOpsItem } from 'types/api/userOps'; +import type { UserOpsItem } from 'client/features/user-ops/types/api'; + +import { AddressHighlightProvider } from 'client/slices/address/contexts/address-highlight'; import config from 'configs/app'; -import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; import { useMultichainContext } from 'lib/contexts/multichain'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle'; diff --git a/ui/userOps/UserOpsTableItem.tsx b/client/features/user-ops/pages/index/UserOpsTableItem.tsx similarity index 81% rename from ui/userOps/UserOpsTableItem.tsx rename to client/features/user-ops/pages/index/UserOpsTableItem.tsx index 1569d2ee04..2b886400d4 100644 --- a/ui/userOps/UserOpsTableItem.tsx +++ b/client/features/user-ops/pages/index/UserOpsTableItem.tsx @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { UserOpsItem } from 'types/api/userOps'; +import type { UserOpsItem } from 'client/features/user-ops/types/api'; import type { ClusterChainConfig } from 'types/multichain'; +import AddressStringOrParam from 'client/slices/address/components/entity/AddressStringOrParam'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + +import UserOpEntity from 'client/features/user-ops/components/entity/UserOpEntity'; +import UserOpStatus from 'client/features/user-ops/components/UserOpStatus'; + import config from 'configs/app'; import { TableCell, TableRow } from 'toolkit/chakra/table'; -import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrParam'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; -import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import ChainIcon from 'ui/shared/externalChains/ChainIcon'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; -import UserOpStatus from 'ui/shared/userOps/UserOpStatus'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; type Props = { diff --git a/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png b/client/features/user-ops/pages/index/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png rename to client/features/user-ops/pages/index/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png diff --git a/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png b/client/features/user-ops/pages/index/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png rename to client/features/user-ops/pages/index/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png diff --git a/client/features/user-ops/pages/tx/TxUserOps.tsx b/client/features/user-ops/pages/tx/TxUserOps.tsx new file mode 100644 index 0000000000..40e08fe4d1 --- /dev/null +++ b/client/features/user-ops/pages/tx/TxUserOps.tsx @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +import TxPendingAlert from 'client/slices/tx/components/TxPendingAlert'; +import TxSocketAlert from 'client/slices/tx/components/TxSocketAlert'; +import type { TxQuery } from 'client/slices/tx/hooks/useTxQuery'; + +import UserOpsContent from 'client/features/user-ops/pages/index/UserOpsContent'; +import { USER_OPS_ITEM } from 'client/features/user-ops/stubs'; + +import { generateListStub } from 'stubs/utils'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +interface Props { + txQuery: TxQuery; +} + +const TxUserOps = ({ txQuery }: Props) => { + const userOpsQuery = useQueryWithPages({ + resourceName: 'general:user_ops', + options: { + enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.status && txQuery.data?.hash), + // most often there is only one user op in one tx + placeholderData: generateListStub<'general:user_ops'>(USER_OPS_ITEM, 1, { next_page_params: null }), + }, + filters: { transaction_hash: txQuery.data?.hash }, + }); + + if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) { + return txQuery.socketStatus ? : ; + } + + if (txQuery.isError) { + return ; + } + + return ; +}; + +export default TxUserOps; diff --git a/client/features/user-ops/stubs.ts b/client/features/user-ops/stubs.ts new file mode 100644 index 0000000000..978a3718cd --- /dev/null +++ b/client/features/user-ops/stubs.ts @@ -0,0 +1,69 @@ +import type { UserOpsItem, UserOp, UserOpsAccount } from 'client/features/user-ops/types/api'; + +import { ADDRESS_HASH } from 'client/slices/address/stubs/address-params'; +import { BLOCK_HASH } from 'client/slices/block/stubs/block'; +import { TX_HASH } from 'client/slices/tx/stubs/tx'; + +const USER_OP_HASH = '0xb94fab8f31f83001a23e20b2ce3cdcfb284c57a64b9a073e0e09c018bc701978'; + +export const USER_OPS_ITEM: UserOpsItem = { + hash: USER_OP_HASH, + block_number: '10356381', + transaction_hash: TX_HASH, + address: ADDRESS_HASH, + timestamp: '2023-12-18T10:48:49.000000Z', + status: true, + fee: '48285720012071430', +}; + +export const USER_OP: UserOp = { + hash: USER_OP_HASH, + sender: ADDRESS_HASH, + nonce: '0x00b', + call_data: '0x123', + execute_call_data: null, + decoded_call_data: null, + decoded_execute_call_data: null, + call_gas_limit: '71316', + verification_gas_limit: '91551', + pre_verification_gas: '53627', + max_fee_per_gas: '100000020', + max_priority_fee_per_gas: '100000000', + signature: '0x000', + aggregator: null, + aggregator_signature: null, + entry_point: ADDRESS_HASH, + transaction_hash: TX_HASH, + block_number: '10358181', + block_hash: BLOCK_HASH, + bundler: ADDRESS_HASH, + factory: null, + paymaster: ADDRESS_HASH, + status: true, + revert_reason: null, + gas: '399596', + gas_price: '1575000898', + gas_used: '118810', + sponsor_type: 'paymaster_sponsor', + fee: '17927001792700', + timestamp: '2023-12-18T10:48:49.000000Z', + user_logs_count: 1, + user_logs_start_index: 2, + raw: { + sender: ADDRESS_HASH, + nonce: '1', + init_code: '0x', + call_data: '0x345', + call_gas_limit: '29491', + verification_gas_limit: '80734', + pre_verification_gas: '3276112', + max_fee_per_gas: '309847206', + max_priority_fee_per_gas: '100000000', + paymaster_and_data: '0x', + signature: '0x000', + }, +}; + +export const USER_OPS_ACCOUNT: UserOpsAccount = { + total_ops: 1, +}; diff --git a/client/features/user-ops/types/api.ts b/client/features/user-ops/types/api.ts new file mode 100644 index 0000000000..fc49241032 --- /dev/null +++ b/client/features/user-ops/types/api.ts @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { AddressParamBasic } from 'client/slices/address/types/api'; +import type { DecodedInput } from 'client/slices/log/types/api'; + +export interface SearchResultUserOp { + type: 'user_operation'; + user_operation_hash: string; + timestamp: string; + url?: string; +} + +export type UserOpsItem = { + hash: string; + block_number: string; + transaction_hash: string; + address: string | AddressParamBasic; + timestamp: string; + status: boolean; + fee: string; +}; + +export type UserOpsResponse = { + items: Array; + next_page_params: { + page_token: string; + page_size: number; + } | null; +}; + +export type UserOpSponsorType = 'paymaster_hybrid' | 'paymaster_sponsor' | 'wallet_balance' | 'wallet_deposit'; + +export type UserOp = { + hash: string; + sender: string | AddressParamBasic; + status: boolean; + revert_reason: string | null; + timestamp: string | null; + fee: string; + gas: string; + transaction_hash: string; + block_number: string; + block_hash: string; + entry_point: string | AddressParamBasic; + call_gas_limit: string; + verification_gas_limit: string; + pre_verification_gas: string; + max_fee_per_gas: string; + max_priority_fee_per_gas: string; + aggregator: string | null; + aggregator_signature: string | null; + bundler: string | AddressParamBasic; + factory: string | null; + paymaster: string | AddressParamBasic | null; + sponsor_type: UserOpSponsorType; + signature: string; + nonce: string; + call_data: string; + decoded_call_data: DecodedInput | null; + execute_call_data: string | null; + execute_target?: AddressParamBasic | null; + decoded_execute_call_data: DecodedInput | null; + user_logs_start_index: number; + user_logs_count: number; + raw: { + account_gas_limits?: string; + call_data: string; + call_gas_limit: string; + gas_fees?: string; + init_code: string; + max_fee_per_gas: string; + max_priority_fee_per_gas: string; + nonce: string; + paymaster_and_data: string; + pre_verification_gas: string; + sender: string; + signature: string; + verification_gas_limit: string; + }; + gas_price: string; + gas_used: string; +}; + +export type UserOpsFilters = { + transaction_hash?: string; + sender?: string; +}; + +export type UserOpsAccount = { + total_ops: number; +}; diff --git a/client/features/verified-tokens/pages/token/TokenProjectInfo.tsx b/client/features/verified-tokens/pages/token/TokenProjectInfo.tsx new file mode 100644 index 0000000000..4674fa9549 --- /dev/null +++ b/client/features/verified-tokens/pages/token/TokenProjectInfo.tsx @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +import type { TokenVerifiedInfo } from 'client/features/verified-tokens/types/api'; + +import InfoButton from 'ui/shared/InfoButton'; + +import Content, { hasContent } from './project-info/Content'; + +interface Props { + data: TokenVerifiedInfo; +} + +const TokenProjectInfo = ({ data }: Props) => { + if (!hasContent(data)) { + return null; + } + + return ( + + + + ); +}; + +export default React.memo(TokenProjectInfo); diff --git a/ui/token/TokenVerifiedInfo.tsx b/client/features/verified-tokens/pages/token/TokenVerifiedInfo.tsx similarity index 86% rename from ui/token/TokenVerifiedInfo.tsx rename to client/features/verified-tokens/pages/token/TokenVerifiedInfo.tsx index 483fdc49fa..d57a74543d 100644 --- a/ui/token/TokenVerifiedInfo.tsx +++ b/client/features/verified-tokens/pages/token/TokenVerifiedInfo.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { UseQueryResult } from '@tanstack/react-query'; import React from 'react'; -import type { TokenVerifiedInfo as TTokenVerifiedInfo } from 'types/api/token'; +import type { TokenVerifiedInfo as TTokenVerifiedInfo } from 'client/features/verified-tokens/types/api'; + +import type { ResourceError } from 'client/api/resources'; import config from 'configs/app'; -import type { ResourceError } from 'lib/api/resources'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; diff --git a/ui/token/TokenProjectInfo/Content.tsx b/client/features/verified-tokens/pages/token/project-info/Content.tsx similarity index 96% rename from ui/token/TokenProjectInfo/Content.tsx rename to client/features/verified-tokens/pages/token/project-info/Content.tsx index 086b72a636..48b7bf0d5f 100644 --- a/ui/token/TokenProjectInfo/Content.tsx +++ b/client/features/verified-tokens/pages/token/project-info/Content.tsx @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Text, Grid } from '@chakra-ui/react'; import React from 'react'; -import type { TokenVerifiedInfo } from 'types/api/token'; +import type { TokenVerifiedInfo } from 'client/features/verified-tokens/types/api'; import DocsLink from './DocsLink'; import type { Props as ServiceLinkProps } from './ServiceLink'; diff --git a/ui/token/TokenProjectInfo/DocsLink.tsx b/client/features/verified-tokens/pages/token/project-info/DocsLink.tsx similarity index 90% rename from ui/token/TokenProjectInfo/DocsLink.tsx rename to client/features/verified-tokens/pages/token/project-info/DocsLink.tsx index a1d3fee013..2ca3ed87ca 100644 --- a/ui/token/TokenProjectInfo/DocsLink.tsx +++ b/client/features/verified-tokens/pages/token/project-info/DocsLink.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { Link } from 'toolkit/chakra/link'; diff --git a/ui/token/TokenProjectInfo/ServiceLink.tsx b/client/features/verified-tokens/pages/token/project-info/ServiceLink.tsx similarity index 83% rename from ui/token/TokenProjectInfo/ServiceLink.tsx rename to client/features/verified-tokens/pages/token/project-info/ServiceLink.tsx index 3f2f7b8387..7217e0483a 100644 --- a/ui/token/TokenProjectInfo/ServiceLink.tsx +++ b/client/features/verified-tokens/pages/token/project-info/ServiceLink.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { TokenVerifiedInfo } from 'types/api/token'; +import type { TokenVerifiedInfo } from 'client/features/verified-tokens/types/api'; import { Link } from 'toolkit/chakra/link'; import type { IconName } from 'ui/shared/IconSvg'; diff --git a/ui/token/TokenProjectInfo/SupportLink.tsx b/client/features/verified-tokens/pages/token/project-info/SupportLink.tsx similarity index 92% rename from ui/token/TokenProjectInfo/SupportLink.tsx rename to client/features/verified-tokens/pages/token/project-info/SupportLink.tsx index 8024b2f259..eeb618c763 100644 --- a/ui/token/TokenProjectInfo/SupportLink.tsx +++ b/client/features/verified-tokens/pages/token/project-info/SupportLink.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { Link } from 'toolkit/chakra/link'; diff --git a/client/features/verified-tokens/types/api.ts b/client/features/verified-tokens/types/api.ts new file mode 100644 index 0000000000..729a01f3e8 --- /dev/null +++ b/client/features/verified-tokens/types/api.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { TokenInfoApplication } from 'client/features/account/types/api'; + +export type TokenVerifiedInfo = Omit; diff --git a/client/features/web3-wallet/components/TokenAddToWallet.tsx b/client/features/web3-wallet/components/TokenAddToWallet.tsx new file mode 100644 index 0000000000..9e9dbdda25 --- /dev/null +++ b/client/features/web3-wallet/components/TokenAddToWallet.tsx @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Box, chakra } from '@chakra-ui/react'; +import React from 'react'; +import type { WatchAssetParams } from 'viem'; + +import type { TokenInfo } from 'client/slices/token/types/api'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import useProvider from 'client/shared/web3/useProvider'; +import useSwitchOrAddChain from 'client/shared/web3/useSwitchOrAddChain'; +import { WALLETS_INFO } from 'client/shared/web3/wallets'; + +import config from 'configs/app'; +import useRewardsActivity from 'lib/hooks/useRewardsActivity'; +import { IconButton } from 'toolkit/chakra/icon-button'; +import { Skeleton } from 'toolkit/chakra/skeleton'; +import { toaster } from 'toolkit/chakra/toaster'; +import { Tooltip } from 'toolkit/chakra/tooltip'; +import IconSvg from 'ui/shared/IconSvg'; + +function getRequestParams(token: TokenInfo, tokenId?: string): WatchAssetParams | undefined { + switch (token.type) { + case 'ERC-20': + return { + type: 'ERC20', + options: { + address: token.address_hash, + symbol: token.symbol || '', + decimals: Number(token.decimals ?? '18'), + image: token.icon_url || '', + }, + }; + case 'ERC-721': + case 'ERC-1155': { + if (!tokenId) { + return; + } + + return { + type: token.type === 'ERC-721' ? 'ERC721' : 'ERC1155', + options: { + address: token.address_hash, + tokenId: tokenId, + }, + } as never; // There is no official EIP, and therefore no typings for these token types. + } + default: + return; + } +} + +interface Props { + className?: string; + token: TokenInfo; + tokenId?: string; + isLoading?: boolean; + variant?: 'icon' | 'button'; + iconSize?: number; + chainConfig?: typeof config; +} + +const TokenAddToWallet = ({ className, token, tokenId, isLoading, variant = 'icon', iconSize = 6, chainConfig }: Props) => { + const { data: { wallet, provider } = {} } = useProvider(); + const switchOrAddChain = useSwitchOrAddChain({ chainConfig }); + const isMobile = useIsMobile(); + const { trackUsage } = useRewardsActivity(); + + const feature = (chainConfig ?? config).features.web3Wallet; + + const handleClick = React.useCallback(async() => { + if (!wallet) { + return; + } + + try { + const params = getRequestParams(token, tokenId); + + if (!params) { + throw new Error('Unsupported token type'); + } + + // switch to the correct network otherwise the token will be added to the wrong one + await switchOrAddChain(); + + const wasAdded = await provider?.request?.({ + method: 'wallet_watchAsset', + params, + }); + + if (wasAdded) { + toaster.success({ + title: 'Success', + description: 'Successfully added token to your wallet', + }); + + await trackUsage('add_token'); + + mixpanel.logEvent(mixpanel.EventTypes.ADD_TO_WALLET, { + Target: 'token', + Wallet: wallet, + Token: token.symbol || '', + }); + } + } catch (error) { + toaster.error({ + title: 'Error', + description: (error as Error)?.message || 'Something went wrong', + }); + } + }, [ wallet, token, tokenId, switchOrAddChain, provider, trackUsage ]); + + if (!provider || !wallet) { + return null; + } + + if (isLoading) { + return ; + } + + const canBeAdded = ( + // MetaMask can add NFTs now, but this is still experimental feature, and doesn't work on mobile devices + // https://docs.metamask.io/wallet/how-to/display/tokens/#display-nfts + wallet === 'metamask' && + [ 'ERC-721', 'ERC-1155' ].includes(token.type) && + tokenId && + !isMobile + ) || token.type === 'ERC-20'; + + if (!feature.isEnabled || !canBeAdded) { + return null; + } + + if (variant === 'button') { + return ( + + + + + + ); + } + + return ( + + + + + + ); +}; + +export default React.memo(chakra(TokenAddToWallet)); diff --git a/client/shared/analytics/mixpanel/get-page-type.ts b/client/shared/analytics/mixpanel/get-page-type.ts new file mode 100644 index 0000000000..503eab14d0 --- /dev/null +++ b/client/shared/analytics/mixpanel/get-page-type.ts @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { Route } from 'nextjs-routes'; + +export const PAGE_TYPE_DICT: Record = { + '/': 'Homepage', + '/txs': 'Transactions', + '/internal-txs': 'Internal transactions', + '/txs/kettle/[hash]': 'Kettle transactions', + '/tx/[hash]': 'Transaction details', + '/blocks': 'Blocks', + '/block/[height_or_hash]': 'Block details', + '/block/countdown': 'Block countdown search', + '/block/countdown/[height]': 'Block countdown', + '/accounts': 'Top accounts', + '/accounts/label/[slug]': 'Addresses search by label', + '/address/[hash]': 'Address details', + '/verified-contracts': 'Verified contracts', + '/contract-verification': 'Contract verification', + '/address/[hash]/contract-verification': 'Contract verification for address', + '/tokens': 'Tokens', + '/token/[hash]': 'Token details', + '/token/[hash]/instance/[id]': 'Token Instance', + '/apps': 'DApps', + '/apps/[id]': 'DApp', + '/essential-dapps/[id]': 'Essential dapps', + '/stats': 'Stats', + '/stats/[id]': 'Stats chart', + '/uptime': 'Uptime', + '/hot-contracts': 'Hot contracts', + '/api-docs': 'REST API', + '/search-results': 'Search results', + '/auth/profile': 'Profile', + '/account/merits': 'Merits', + '/account/watchlist': 'Watchlist', + '/account/api-key': 'API keys', + '/account/custom-abi': 'Custom ABI', + '/account/tag-address': 'Private tags', + '/account/verified-addresses': 'Verified addresses', + '/public-tags/submit': 'Submit public tag', + '/withdrawals': 'Withdrawals', + '/txn-withdrawals': 'Txn withdrawals', + '/visualize/sol2uml': 'Solidity UML diagram', + '/deposits': 'Deposits', + '/output-roots': 'Output roots', + '/dispute-games': 'Dispute games', + '/batches': 'Txn batches', + '/batches/[number]': 'L2 txn batch details', + '/batches/celestia/[height]/[commitment]': 'L2 txn batch details', + '/blobs/[hash]': 'Blob details', + '/ops': 'User operations', + '/op/[hash]': 'User operation details', + '/404': '404', + '/name-services': 'Domains search and resolve', + '/name-services/domains/[name]': 'Domain details', + '/name-services/clusters/[name]': 'Cluster details', + '/validators': 'Validators list', + '/validators/[id]': 'Validator details', + '/epochs': 'Epochs', + '/epochs/[number]': 'Epoch details', + '/gas-tracker': 'Gas tracker', + '/mud-worlds': 'MUD worlds', + '/token-transfers': 'Token transfers', + '/advanced-filter': 'Advanced filter', + '/pools': 'DEX pools', + '/pools/[hash]': 'Pool details', + '/interop-messages': 'Interop messages', + '/operations': 'Operations', + '/operation/[id]': 'Operation details', + '/cc/tx/[hash]': 'Cross-chain transaction details', + '/cross-chain-tx/[id]': 'Cross-chain transaction details', + '/ictt-users': 'ICTT users', + + // multichain routes + '/chain/[chain_slug_or_id]/accounts/label/[slug]': 'Chain addresses search by label', + '/chain/[chain_slug_or_id]/advanced-filter': 'Chain advanced filter', + '/chain/[chain_slug_or_id]/block/[height_or_hash]': 'Chain block details', + '/chain/[chain_slug_or_id]/block/countdown': 'Chain block countdown index', + '/chain/[chain_slug_or_id]/block/countdown/[height]': 'Chain block countdown', + '/chain/[chain_slug_or_id]/op/[hash]': 'Chain user operation details', + '/chain/[chain_slug_or_id]/token/[hash]': 'Chain token details', + '/chain/[chain_slug_or_id]/token/[hash]/instance/[id]': 'Chain token NFT instance', + '/chain/[chain_slug_or_id]/tx/[hash]': 'Chain transaction details', + '/chain/[chain_slug_or_id]/visualize/sol2uml': 'Chain Solidity UML diagram', + '/ecosystems': 'Ecosystems', + + // service routes, added only to make typescript happy + '/login': 'Login', + '/sprite': 'Sprite', + '/chakra': 'Chakra UI showcase', + '/api/metrics': 'Node API: Prometheus metrics', + '/api/monitoring/invalid-api-schema': 'Node API: Prometheus metrics', + '/api/log': 'Node API: Request log', + '/api/tokens/[hash]/instances/[id]/media-type': 'Node API: Token instance media type', + '/api/proxy': 'Node API: Proxy', + '/api/csrf': 'Node API: CSRF token', + '/api/healthz': 'Node API: Health check', + '/api/config': 'Node API: App config', + '/api/monitoring/pageview': 'Node API: Pageview', +}; + +export default function getPageType(pathname: Route['pathname']) { + return PAGE_TYPE_DICT[pathname] || 'Unknown page'; +} diff --git a/client/shared/analytics/mixpanel/get-tab-name.ts b/client/shared/analytics/mixpanel/get-tab-name.ts new file mode 100644 index 0000000000..3358638167 --- /dev/null +++ b/client/shared/analytics/mixpanel/get-tab-name.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { capitalize } from 'es-toolkit'; + +export default function getTabName(tab: string) { + return tab !== '' ? capitalize(tab.replaceAll('_', ' ')) : 'Default'; +} diff --git a/client/shared/analytics/mixpanel/index.ts b/client/shared/analytics/mixpanel/index.ts new file mode 100644 index 0000000000..0fa9d8b449 --- /dev/null +++ b/client/shared/analytics/mixpanel/index.ts @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import getPageType, { PAGE_TYPE_DICT } from './get-page-type'; +import logEvent from './log-event'; +import reset from './reset'; +import useLogPageView from './useLogPageView'; +import useInit from './useMixpanelInit'; +import * as userProfile from './user-profile'; +export * from './utils'; + +export { + useInit, + useLogPageView, + logEvent, + getPageType, + userProfile, + reset, + PAGE_TYPE_DICT, +}; diff --git a/client/shared/analytics/mixpanel/log-event.ts b/client/shared/analytics/mixpanel/log-event.ts new file mode 100644 index 0000000000..42d70507bf --- /dev/null +++ b/client/shared/analytics/mixpanel/log-event.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import mixpanel from 'mixpanel-browser'; + +import config from 'configs/app'; + +import type { EventTypes, EventPayload } from './utils'; + +type TrackFnArgs = Parameters; + +export default function logEvent( + type: EventType, + properties?: EventPayload, + optionsOrCallback?: TrackFnArgs[2], + callback?: TrackFnArgs[3], +) { + if (!config.features.mixpanel.isEnabled) { + return; + } + mixpanel.track(type, properties, optionsOrCallback, callback); +} diff --git a/lib/mixpanel/reset.ts b/client/shared/analytics/mixpanel/reset.ts similarity index 79% rename from lib/mixpanel/reset.ts rename to client/shared/analytics/mixpanel/reset.ts index 489646d725..d0244d9e41 100644 --- a/lib/mixpanel/reset.ts +++ b/client/shared/analytics/mixpanel/reset.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import mixpanel from 'mixpanel-browser'; import config from 'configs/app'; diff --git a/lib/mixpanel/useLogPageView.tsx b/client/shared/analytics/mixpanel/useLogPageView.tsx similarity index 84% rename from lib/mixpanel/useLogPageView.tsx rename to client/shared/analytics/mixpanel/useLogPageView.tsx index fc142e837e..14c9c489a7 100644 --- a/lib/mixpanel/useLogPageView.tsx +++ b/client/shared/analytics/mixpanel/useLogPageView.tsx @@ -1,18 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { usePathname } from 'next/navigation'; import { useRouter } from 'next/router'; import React from 'react'; import type { ColorThemeId } from 'types/settings'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; +import * as cookies from 'client/shared/storage/cookies'; + import config from 'configs/app'; -import * as cookies from 'lib/cookies'; -import getQueryParamString from 'lib/router/getQueryParamString'; import { getDefaultColorTheme } from 'lib/settings/colorTheme'; import { useColorMode } from 'toolkit/chakra/color-mode'; -import getPageType from './getPageType'; -import getTabName from './getTabName'; -import logEvent from './logEvent'; +import getPageType from './get-page-type'; +import getTabName from './get-tab-name'; +import logEvent from './log-event'; import { EventTypes } from './utils'; export default function useLogPageView(isInitialized: boolean) { diff --git a/client/shared/analytics/mixpanel/useMixpanelInit.tsx b/client/shared/analytics/mixpanel/useMixpanelInit.tsx new file mode 100644 index 0000000000..8edca32e5b --- /dev/null +++ b/client/shared/analytics/mixpanel/useMixpanelInit.tsx @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { capitalize } from 'es-toolkit'; +import type { Config } from 'mixpanel-browser'; +import mixpanel from 'mixpanel-browser'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { deviceType } from 'react-device-detect'; + +import getQueryParamString from 'client/shared/router/get-query-param-string'; +import * as cookies from 'client/shared/storage/cookies'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; + +import * as userProfile from './user-profile'; + +const multichainFeature = config.features.multichain; + +export default function useMixpanelInit() { + const [ isInitialized, setIsInitialized ] = React.useState(false); + const router = useRouter(); + const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug)); + + React.useEffect(() => { + const feature = config.features.mixpanel; + if (!feature.isEnabled) { + return; + } + + const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG); + + const mixpanelConfig: Partial = { + debug: Boolean(debugFlagQuery.current || debugFlagCookie), + persistence: 'localStorage', + ...feature.configOverrides, + }; + const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); + + const uuid = cookies.get(cookies.NAMES.UUID); + + mixpanel.init(feature.projectToken, mixpanelConfig); + mixpanel.register({ + 'Chain id': config.chain.id, + Environment: config.app.isDev ? 'Dev' : 'Prod', + Authorized: isAuth, + 'Viewport width': window.innerWidth, + 'Viewport height': window.innerHeight, + Language: window.navigator.language, + 'Device type': capitalize(deviceType), + 'User id': uuid, + ...(multichainFeature.isEnabled ? { 'Cluster name': multichainFeature.cluster } : {}), + }); + mixpanel.identify(uuid); + userProfile.set({ + 'Device Type': capitalize(deviceType), + ...(isAuth ? { 'With Account': true } : {}), + }); + userProfile.setOnce({ + 'First Time Join': dayjs().toISOString(), + }); + + setIsInitialized(true); + if (debugFlagQuery.current && !debugFlagCookie) { + cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true'); + } + }, [ ]); + + return isInitialized; +} diff --git a/client/shared/analytics/mixpanel/user-profile.ts b/client/shared/analytics/mixpanel/user-profile.ts new file mode 100644 index 0000000000..b1e2d12ae0 --- /dev/null +++ b/client/shared/analytics/mixpanel/user-profile.ts @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import mixpanel from 'mixpanel-browser'; + +import type { PickByType } from 'types/utils'; + +interface UserProfileProperties { + 'With Account': boolean; + 'With Connected Wallet': boolean; + 'Device Type': string; + 'First Time Join': string; +} + +type UserProfilePropertiesNumerable = PickByType; + +export function set(props: Partial) { + mixpanel.people.set(props); +} + +export function setOnce(props: Partial) { + mixpanel.people.set_once(props); +} + +export function increment(props: UserProfilePropertiesNumerable) { + mixpanel.people.increment(props); +} diff --git a/client/shared/analytics/mixpanel/utils.ts b/client/shared/analytics/mixpanel/utils.ts new file mode 100644 index 0000000000..269a0eb768 --- /dev/null +++ b/client/shared/analytics/mixpanel/utils.ts @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { WalletType } from 'types/client/wallets'; +import type { ColorThemeId } from 'types/settings'; + +export enum EventTypes { + PAGE_VIEW = 'Page view', + SEARCH_QUERY = 'Search query', + LOCAL_SEARCH = 'Local search', + ADD_TO_WALLET = 'Add to wallet', + ACCOUNT_ACCESS = 'Account access', + LOGIN = 'Login', + ACCOUNT_LINK_INFO = 'Account link info', + PRIVATE_TAG = 'Private tag', + VERIFY_ADDRESS = 'Verify address', + VERIFY_TOKEN = 'Verify token', + WALLET_CONNECT = 'Wallet connect', + WALLET_ACTION = 'Wallet action', + CONTRACT_INTERACTION = 'Contract interaction', + CONTRACT_VERIFICATION = 'Contract verification', + QR_CODE = 'QR code', + PAGE_WIDGET = 'Page widget', + TX_INTERPRETATION_INTERACTION = 'Transaction interpretation interaction', + EXPERIMENT_STARTED = 'Experiment started', + FILTERS = 'Filters', + BUTTON_CLICK = 'Button click', + PROMO_BANNER = 'Promo banner', + APP_FEEDBACK = 'App feedback', + ADDRESS_WIDGET = 'Address widget', +} + +/* eslint-disable @stylistic/indent */ +export type EventPayload = +Type extends EventTypes.PAGE_VIEW ? +{ + 'Page type': string; + Tab: string; + Page?: string; + Source?: string; + 'Color mode': 'light' | 'dark'; + 'Color theme': ColorThemeId | undefined; +} : +Type extends EventTypes.SEARCH_QUERY ? { + 'Search query': string; + 'Source page type': string; + 'Result URL': string; +} : +Type extends EventTypes.LOCAL_SEARCH ? { + 'Search query': string; + Source: 'Marketplace'; +} : +Type extends EventTypes.ADD_TO_WALLET ? ( + { + Wallet: WalletType; + Target: 'network'; + Source: 'Footer' | 'Top bar' | 'Chain widget'; + } | { + Wallet: WalletType; + Target: 'token'; + Token: string; + } +) : +Type extends EventTypes.ACCOUNT_ACCESS ? { + Action: 'Dropdown open' | 'Logged out'; +} : +Type extends EventTypes.LOGIN ? ( + { + Action: 'Started'; + Source: string; + } | { + Action: 'Wallet' | 'Email'; + Source: 'Options selector'; + } | { + Action: 'OTP sent'; + Source: 'Email'; + } | { + Action: 'Success'; + Source: 'Email' | 'Wallet' | 'Dynamic'; + } +) : +Type extends EventTypes.ACCOUNT_LINK_INFO ? { + Source: 'Profile' | 'Login modal' | 'Profile dropdown' | 'Merits'; + Status: 'Started' | 'OTP sent' | 'Finished'; + Type: 'Email' | 'Wallet'; +} : +Type extends EventTypes.PRIVATE_TAG ? { + Action: 'Form opened' | 'Submit'; + 'Page type': string; + 'Tag type': 'Address' | 'Tx'; +} : +Type extends EventTypes.VERIFY_ADDRESS ? ( + { + Action: 'Form opened' | 'Address entered'; + 'Page type': string; + } | { + Action: 'Sign ownership'; + 'Page type': string; + 'Sign method': 'wallet' | 'manual'; + } +) : +Type extends EventTypes.VERIFY_TOKEN ? { + Action: 'Form opened' | 'Submit'; +} : +Type extends EventTypes.WALLET_CONNECT ? { + Source: 'Header' | 'Login' | 'Profile' | 'Profile dropdown' | 'Smart contracts' | 'Swap button' | 'Merits' | 'Essential dapps'; + Status: 'Started' | 'Connected'; +} : +Type extends EventTypes.WALLET_ACTION ? ( + { + Action: 'Open' | 'Address click'; + } | { + Action: 'Send Transaction' | 'Sign Message' | 'Sign Typed Data'; + Source: 'Dappscout' | 'Essential dapps'; + AppId: string; + Address?: string; + ChainId?: string; + } +) : +Type extends EventTypes.CONTRACT_INTERACTION ? { + 'Method type': 'Read' | 'Write'; + 'Method name': string; +} : +Type extends EventTypes.CONTRACT_VERIFICATION ? { + Method: string; + Status: 'Method selected' | 'Finished'; +} : +Type extends EventTypes.QR_CODE ? { + 'Page type': string; +} : +Type extends EventTypes.PAGE_WIDGET ? ( + { + Type: 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; + } | { + Type: 'Favorite app' | 'More button'; + Info: string; + Source: 'Discovery view' | 'App modal' | 'App page' | 'Banner'; + } | { + Type: 'Action button'; + Info: string; + Source: 'Txn' | 'NFT collection' | 'NFT item'; + } | { + Type: 'Address tag'; + Info: string; + URL: string; + } | { + Type: 'Share chart'; + Info: string; + } | { + Type: 'Chain switch'; + Info: string; + Source: 'Revoke essential dapp'; + } | { + Type: 'Txn view switch'; + Info: 'Table view' | 'List view'; + Source: 'Address page'; + } +) : +Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { + Type: 'Address click' | 'Token click' | 'Domain click'; +} : +Type extends EventTypes.EXPERIMENT_STARTED ? { + 'Experiment name': string; + 'Variant name': string; + Source: 'growthbook'; +} : +Type extends EventTypes.FILTERS ? { + Source: 'Marketplace'; + 'Filter name': string; +} : +Type extends EventTypes.BUTTON_CLICK ? { + Content: string; + Source: string; +} : +Type extends EventTypes.PROMO_BANNER ? { + Source: 'Marketplace'; + Link: string; +} : +Type extends EventTypes.APP_FEEDBACK ? { + Action: 'Rating'; + Source: 'Discovery' | 'App modal' | 'App page'; + AppId: string; + Score: number; +} : +Type extends EventTypes.ADDRESS_WIDGET ? { + Name: string; +} : +undefined; +/* eslint-enable @stylistic/indent */ diff --git a/client/shared/auth/decode-jwt.ts b/client/shared/auth/decode-jwt.ts new file mode 100644 index 0000000000..ba306e2fea --- /dev/null +++ b/client/shared/auth/decode-jwt.ts @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +interface JWTHeader { + alg: string; + typ?: string; + [key: string]: unknown; +} + +interface JWTPayload { + [key: string]: unknown; +} + +const base64UrlDecode = (str: string): string => { + // Replace characters according to Base64Url standard + str = str.replace(/-/g, '+').replace(/_/g, '/'); + + // Add padding '=' characters for correct decoding + const pad = str.length % 4; + if (pad) { + str += '='.repeat(4 - pad); + } + + // Decode from Base64 to string + const decodedStr = atob(str); + + return decodedStr; +}; + +export default function decodeJWT(token: string): { header: JWTHeader; payload: JWTPayload; signature: string } | null { + try { + const parts = token.split('.'); + + if (parts.length !== 3) { + throw new Error('Invalid JWT format'); + } + + const [ encodedHeader, encodedPayload, signature ] = parts; + + const headerJson = base64UrlDecode(encodedHeader); + const payloadJson = base64UrlDecode(encodedPayload); + + const header = JSON.parse(headerJson) as JWTHeader; + const payload = JSON.parse(payloadJson) as JWTPayload; + + return { header, payload, signature }; + } catch (error) { + return null; + } +} diff --git a/client/shared/chain/get-chain-title.ts b/client/shared/chain/get-chain-title.ts new file mode 100644 index 0000000000..6e39883bc9 --- /dev/null +++ b/client/shared/chain/get-chain-title.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import config from 'configs/app'; + +// TODO delete when page descriptions is refactored +export default function getChainTitle() { + return config.chain.name + (config.chain.shortName ? ` (${ config.chain.shortName })` : '') + ' Explorer'; +} diff --git a/client/shared/chain/get-chain-utilization-params.ts b/client/shared/chain/get-chain-utilization-params.ts new file mode 100644 index 0000000000..e3b6b88a19 --- /dev/null +++ b/client/shared/chain/get-chain-utilization-params.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function getChainUtilizationParams(value: number) { + const load = (() => { + if (value > 80) { + return 'high'; + } + + if (value > 50) { + return 'medium'; + } + + return 'low'; + })(); + + const colors = { + high: 'red.600', + medium: 'orange.600', + low: 'green.600', + }; + const color = colors[load]; + + return { + load, + color, + }; +} diff --git a/client/shared/chain/get-chain-validation-action-text.ts b/client/shared/chain/get-chain-validation-action-text.ts new file mode 100644 index 0000000000..dd8e33d0ad --- /dev/null +++ b/client/shared/chain/get-chain-validation-action-text.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import config from 'configs/app'; + +export default function getChainValidationActionText(chainConfig = config) { + switch (chainConfig.chain.verificationType) { + case 'validation': { + return 'validated'; + } + case 'mining': { + return 'mined'; + } + case 'posting': { + return 'posted'; + } + case 'fee reception': { + return 'validated'; + } + default: { + return 'mined'; + } + } +} diff --git a/client/shared/chain/get-chain-validator-title.ts b/client/shared/chain/get-chain-validator-title.ts new file mode 100644 index 0000000000..9cc2c218a0 --- /dev/null +++ b/client/shared/chain/get-chain-validator-title.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import config from 'configs/app'; + +export default function getChainValidatorTitle() { + switch (config.chain.verificationType) { + case 'validation': { + return 'validator'; + } + case 'mining': { + return 'miner'; + } + case 'posting': { + return 'poster'; + } + case 'fee reception': { + return 'fee recipient'; + } + default: { + return 'miner'; + } + } +} diff --git a/lib/units.ts b/client/shared/chain/units.ts similarity index 87% rename from lib/units.ts rename to client/shared/chain/units.ts index c7f58bbea6..fc090de445 100644 --- a/lib/units.ts +++ b/client/shared/chain/units.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import config from 'configs/app'; import type { Unit } from 'ui/shared/value/utils'; diff --git a/lib/hooks/useTimeAgoIncrement.tsx b/client/shared/date-and-time/useTimeAgoIncrement.tsx similarity index 97% rename from lib/hooks/useTimeAgoIncrement.tsx rename to client/shared/date-and-time/useTimeAgoIncrement.tsx index 56e478fec2..9a025755b8 100644 --- a/lib/hooks/useTimeAgoIncrement.tsx +++ b/client/shared/date-and-time/useTimeAgoIncrement.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import dayjs from 'lib/date/dayjs'; diff --git a/client/shared/errors/get-error-cause-status-code.ts b/client/shared/errors/get-error-cause-status-code.ts new file mode 100644 index 0000000000..920b576863 --- /dev/null +++ b/client/shared/errors/get-error-cause-status-code.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import getErrorCause from './get-error-cause'; + +export default function getErrorCauseStatusCode(error: Error | undefined): number | undefined { + const cause = getErrorCause(error); + return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined; +} diff --git a/client/shared/errors/get-error-cause.ts b/client/shared/errors/get-error-cause.ts new file mode 100644 index 0000000000..21c465d9f8 --- /dev/null +++ b/client/shared/errors/get-error-cause.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function getErrorCause(error: Error | undefined): Record | undefined { + return ( + error && 'cause' in error && + typeof error.cause === 'object' && error.cause !== null && + error.cause as Record + ) || + undefined; +} diff --git a/client/shared/errors/get-error-message.ts b/client/shared/errors/get-error-message.ts new file mode 100644 index 0000000000..3749d79963 --- /dev/null +++ b/client/shared/errors/get-error-message.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import getErrorObj from './get-error-obj'; + +export default function getErrorMessage(error: unknown): string | undefined { + const errorObj = getErrorObj(error); + return errorObj && 'message' in errorObj && typeof errorObj.message === 'string' ? errorObj.message : undefined; +} diff --git a/client/shared/errors/get-error-obj-payload.ts b/client/shared/errors/get-error-obj-payload.ts new file mode 100644 index 0000000000..7f1e2ca849 --- /dev/null +++ b/client/shared/errors/get-error-obj-payload.ts @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import getErrorObj from './get-error-obj'; + +export default function getErrorObjPayload(error: unknown): Payload | undefined { + const errorObj = getErrorObj(error); + + if (!errorObj || !('payload' in errorObj)) { + return; + } + + if (typeof errorObj.payload !== 'object') { + return; + } + + if (errorObj === null) { + return; + } + + if (Array.isArray(errorObj)) { + return; + } + + return errorObj.payload as Payload; +} diff --git a/client/shared/errors/get-error-obj-status-code.ts b/client/shared/errors/get-error-obj-status-code.ts new file mode 100644 index 0000000000..84e8c4d481 --- /dev/null +++ b/client/shared/errors/get-error-obj-status-code.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import getErrorObj from './get-error-obj'; + +export default function getErrorObjStatusCode(error: unknown) { + const errorObj = getErrorObj(error); + + if (!errorObj || !('status' in errorObj) || typeof errorObj.status !== 'number') { + return; + } + + return errorObj.status; +} diff --git a/client/shared/errors/get-error-obj.ts b/client/shared/errors/get-error-obj.ts new file mode 100644 index 0000000000..385c4c97c1 --- /dev/null +++ b/client/shared/errors/get-error-obj.ts @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function getErrorObj(error: unknown) { + if (typeof error !== 'object') { + return; + } + + if (Array.isArray(error)) { + return; + } + + if (error === null) { + return; + } + + return error; +} diff --git a/client/shared/errors/get-error-prop.ts b/client/shared/errors/get-error-prop.ts new file mode 100644 index 0000000000..4c56833d45 --- /dev/null +++ b/client/shared/errors/get-error-prop.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import getErrorObj from './get-error-obj'; + +export default function getErrorProp(error: unknown, prop: string): T | undefined { + const errorObj = getErrorObj(error); + return errorObj && prop in errorObj ? + (errorObj[prop as keyof typeof errorObj] as T) : + undefined; +} diff --git a/client/shared/errors/get-error-stack.ts b/client/shared/errors/get-error-stack.ts new file mode 100644 index 0000000000..60f6241614 --- /dev/null +++ b/client/shared/errors/get-error-stack.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function getErrorStack(error: Error | undefined): string | undefined { + return ( + error && 'stack' in error && + typeof error.stack === 'string' && error.stack !== null && + error.stack + ) || + undefined; +} diff --git a/client/shared/errors/get-resource-error-payload.ts b/client/shared/errors/get-resource-error-payload.ts new file mode 100644 index 0000000000..1ea755dacc --- /dev/null +++ b/client/shared/errors/get-resource-error-payload.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { ResourceError } from 'client/api/resources'; + +import getErrorCause from './get-error-cause'; + +export default function getResourceErrorPayload | string>(error: Error | undefined): +ResourceError['payload'] | undefined { + const cause = getErrorCause(error); + return cause && 'payload' in cause ? cause.payload as ResourceError['payload'] : undefined; +} diff --git a/client/shared/errors/throw-on-absent-param-error.ts b/client/shared/errors/throw-on-absent-param-error.ts new file mode 100644 index 0000000000..471fc00254 --- /dev/null +++ b/client/shared/errors/throw-on-absent-param-error.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export const ABSENT_PARAM_ERROR_MESSAGE = 'Required param not provided'; + +export default function throwOnAbsentParamError(param: unknown) { + if (!param) { + throw new Error(ABSENT_PARAM_ERROR_MESSAGE, { cause: { status: 404 } }); + } +} diff --git a/client/shared/errors/throw-on-resource-load-error.ts b/client/shared/errors/throw-on-resource-load-error.ts new file mode 100644 index 0000000000..c521c32f9d --- /dev/null +++ b/client/shared/errors/throw-on-resource-load-error.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { ResourceError, ResourceName } from 'client/api/resources'; + +type Params = ({ + isError: true; + error: ResourceError; +} | { + isError: false; + error: null; +}) & { + resource?: ResourceName; +}; + +export const RESOURCE_LOAD_ERROR_MESSAGE = 'Resource load error'; + +export default function throwOnResourceLoadError({ isError, error, resource }: Params) { + if (isError) { + throw Error(RESOURCE_LOAD_ERROR_MESSAGE, { cause: { ...error, resource } as unknown as Error }); + } +} diff --git a/client/shared/feature-flags/consts.ts b/client/shared/feature-flags/consts.ts new file mode 100644 index 0000000000..91b2abe4a0 --- /dev/null +++ b/client/shared/feature-flags/consts.ts @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export const STORAGE_KEY = 'growthbook:experiments'; +export const STORAGE_LIMIT = 20; diff --git a/lib/growthbook/init.ts b/client/shared/feature-flags/init.ts similarity index 93% rename from lib/growthbook/init.ts rename to client/shared/feature-flags/init.ts index 655edc5b31..86d7f6afaa 100644 --- a/lib/growthbook/init.ts +++ b/client/shared/feature-flags/init.ts @@ -1,7 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { GrowthBook } from '@growthbook/growthbook-react'; +import * as mixpanel from 'client/shared/analytics/mixpanel'; + import config from 'configs/app'; -import * as mixpanel from 'lib/mixpanel'; import { STORAGE_KEY, STORAGE_LIMIT } from './consts'; diff --git a/lib/growthbook/useFeatureValue.ts b/client/shared/feature-flags/useFeatureValue.ts similarity index 90% rename from lib/growthbook/useFeatureValue.ts rename to client/shared/feature-flags/useFeatureValue.ts index 1b30b85006..4f686e1806 100644 --- a/lib/growthbook/useFeatureValue.ts +++ b/client/shared/feature-flags/useFeatureValue.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useFeatureValue, useGrowthBook } from '@growthbook/growthbook-react'; import type { GrowthBookFeatures } from './init'; diff --git a/lib/growthbook/useLoadFeatures.ts b/client/shared/feature-flags/useLoadFeatures.ts similarity index 91% rename from lib/growthbook/useLoadFeatures.ts rename to client/shared/feature-flags/useLoadFeatures.ts index 193928d5a9..2bc19bf487 100644 --- a/lib/growthbook/useLoadFeatures.ts +++ b/client/shared/feature-flags/useLoadFeatures.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { GrowthBook } from '@growthbook/growthbook-react'; import React from 'react'; diff --git a/lib/hooks/useDebounce.tsx b/client/shared/hooks/useDebounce.tsx similarity index 79% rename from lib/hooks/useDebounce.tsx rename to client/shared/hooks/useDebounce.tsx index 5dfc71c141..21eb41de8e 100644 --- a/lib/hooks/useDebounce.tsx +++ b/client/shared/hooks/useDebounce.tsx @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; +// REFACTOR: replace with useDebounce from es-toolkit export default function useDebounce(value: string, delay: number) { const [ debouncedValue, setDebouncedValue ] = React.useState(value); React.useEffect( diff --git a/lib/hooks/useGradualIncrement.tsx b/client/shared/hooks/useGradualIncrement.tsx similarity index 96% rename from lib/hooks/useGradualIncrement.tsx rename to client/shared/hooks/useGradualIncrement.tsx index e1bab30e27..2532f3baaa 100644 --- a/lib/hooks/useGradualIncrement.tsx +++ b/client/shared/hooks/useGradualIncrement.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; const DURATION = 300; diff --git a/lib/hooks/useIsInitialLoading.tsx b/client/shared/hooks/useIsInitialLoading.tsx similarity index 86% rename from lib/hooks/useIsInitialLoading.tsx rename to client/shared/hooks/useIsInitialLoading.tsx index de07b15fd6..b443553e42 100644 --- a/lib/hooks/useIsInitialLoading.tsx +++ b/client/shared/hooks/useIsInitialLoading.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; export default function useIsInitialLoading(isLoading: boolean | undefined) { diff --git a/lib/hooks/useIsMobile.tsx b/client/shared/hooks/useIsMobile.tsx similarity index 92% rename from lib/hooks/useIsMobile.tsx rename to client/shared/hooks/useIsMobile.tsx index 670f1066ba..e910b999b5 100644 --- a/lib/hooks/useIsMobile.tsx +++ b/client/shared/hooks/useIsMobile.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useBreakpointValue } from '@chakra-ui/react'; // The default behavior of useBreakpointValue was changed in the commit - https://github.com/chakra-ui/chakra-ui/commit/7f30a7b7eebae236b55fe639a202bbf354677143 diff --git a/lib/hooks/useIsMounted.tsx b/client/shared/hooks/useIsMounted.tsx similarity index 80% rename from lib/hooks/useIsMounted.tsx rename to client/shared/hooks/useIsMounted.tsx index d14880ae1b..eb9589d72c 100644 --- a/lib/hooks/useIsMounted.tsx +++ b/client/shared/hooks/useIsMounted.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; export default function useIsMounted() { diff --git a/lib/hooks/usePreventFocusAfterModalClosing.tsx b/client/shared/hooks/usePreventFocusAfterModalClosing.tsx similarity index 81% rename from lib/hooks/usePreventFocusAfterModalClosing.tsx rename to client/shared/hooks/usePreventFocusAfterModalClosing.tsx index ec9e22fd14..fc2330bac8 100644 --- a/lib/hooks/usePreventFocusAfterModalClosing.tsx +++ b/client/shared/hooks/usePreventFocusAfterModalClosing.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; // prevent set focus on button when closing modal diff --git a/lib/hooks/useTableViewValue.tsx b/client/shared/hooks/useTableViewValue.tsx similarity index 84% rename from lib/hooks/useTableViewValue.tsx rename to client/shared/hooks/useTableViewValue.tsx index 69cede86c6..994463de9a 100644 --- a/lib/hooks/useTableViewValue.tsx +++ b/client/shared/hooks/useTableViewValue.tsx @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import * as cookies from 'lib/cookies'; -import useFeatureValue from 'lib/growthbook/useFeatureValue'; -import * as mixpanel from 'lib/mixpanel'; +import * as mixpanel from 'client/shared/analytics/mixpanel'; +import useFeatureValue from 'client/shared/feature-flags/useFeatureValue'; +import * as cookies from 'client/shared/storage/cookies'; export default function useTableViewValue() { const cookieValue = cookies.get(cookies.NAMES.TABLE_VIEW_ON_MOBILE); diff --git a/lib/hooks/useUpdateValueEffect.tsx b/client/shared/hooks/useUpdateValueEffect.tsx similarity index 94% rename from lib/hooks/useUpdateValueEffect.tsx rename to client/shared/hooks/useUpdateValueEffect.tsx index 57763f11fb..cd7868a7f3 100644 --- a/lib/hooks/useUpdateValueEffect.tsx +++ b/client/shared/hooks/useUpdateValueEffect.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; // run effect only if value is updated since initial mount diff --git a/client/shared/i18n/set-locale.ts b/client/shared/i18n/set-locale.ts new file mode 100644 index 0000000000..ee7a591994 --- /dev/null +++ b/client/shared/i18n/set-locale.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +const old = Number.prototype.toLocaleString; +Number.prototype.toLocaleString = function(locale, ...args) { + return old.call(this, 'en', ...args); +}; + +export {}; diff --git a/client/shared/links/utils/strip-utm-params.ts b/client/shared/links/utils/strip-utm-params.ts new file mode 100644 index 0000000000..8e42ffca1a --- /dev/null +++ b/client/shared/links/utils/strip-utm-params.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import config from 'configs/app'; + +const UTM_PARAMS = [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content' ]; + +export default function stripUtmParams(url: string): string { + if (!config.app.isPrivateMode) { + return url; + } + + try { + const urlObj = new URL(url); + UTM_PARAMS.forEach((param) => { + urlObj.searchParams.delete(param); + }); + return urlObj.toString(); + } catch (error) { + // If URL parsing fails, return original URL + return url; + } +} diff --git a/client/shared/lists/get-item-index.ts b/client/shared/lists/get-item-index.ts new file mode 100644 index 0000000000..7222204089 --- /dev/null +++ b/client/shared/lists/get-item-index.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +const DEFAULT_PAGE_SIZE = 50; + +export default function getItemIndex(index: number, page: number, pageSize: number = DEFAULT_PAGE_SIZE) { + return (page - 1) * pageSize + index + 1; +}; diff --git a/lib/hooks/useInitialList.tsx b/client/shared/lists/useInitialList.tsx similarity index 94% rename from lib/hooks/useInitialList.tsx rename to client/shared/lists/useInitialList.tsx index e8f5aab9a3..007e5f9dab 100644 --- a/lib/hooks/useInitialList.tsx +++ b/client/shared/lists/useInitialList.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; type Id = string | number; diff --git a/lib/hooks/useLazyRenderedList.tsx b/client/shared/lists/useLazyRenderedList.tsx similarity index 93% rename from lib/hooks/useLazyRenderedList.tsx rename to client/shared/lists/useLazyRenderedList.tsx index 4ebd630b18..a6f22818e4 100644 --- a/lib/hooks/useLazyRenderedList.tsx +++ b/client/shared/lists/useLazyRenderedList.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { clamp } from 'es-toolkit'; import React from 'react'; import { useInView } from 'react-intersection-observer'; diff --git a/lib/metadata/__snapshots__/generate.spec.ts.snap b/client/shared/metadata/__snapshots__/generate.spec.ts.snap similarity index 88% rename from lib/metadata/__snapshots__/generate.spec.ts.snap rename to client/shared/metadata/__snapshots__/generate.spec.ts.snap index 302323c7f6..3d5da97c6a 100644 --- a/lib/metadata/__snapshots__/generate.spec.ts.snap +++ b/client/shared/metadata/__snapshots__/generate.spec.ts.snap @@ -50,7 +50,7 @@ exports[`generates correct metadata for: > dynamic route with API data 1`] = ` exports[`generates correct metadata for: > static route 1`] = ` { "canonical": "http://localhost:3000/txs", - "description": "Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the Blockscout (Blockscout) Explorer network.", + "description": "Scan Blockscout (Blockscout) Explorer with Blockscout. Search transactions, verify smart contracts, analyze addresses, and access blockchain data through explorer APIs.", "jsonLd": undefined, "opengraph": { "description": "", diff --git a/client/shared/metadata/compile-value.ts b/client/shared/metadata/compile-value.ts new file mode 100644 index 0000000000..2a66d00aee --- /dev/null +++ b/client/shared/metadata/compile-value.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function compileValue(template: string, params: Record | undefined>) { + const PLACEHOLDER_REGEX = /%(\w+)%/g; + return template.replaceAll(PLACEHOLDER_REGEX, (match, p1) => { + const value = params[p1]; + + if (Array.isArray(value)) { + return value.join(', '); + } + + if (value === undefined) { + return ''; + } + + return value; + }); +} diff --git a/client/shared/metadata/generate-product-schema.ts b/client/shared/metadata/generate-product-schema.ts new file mode 100644 index 0000000000..87ca7e12c2 --- /dev/null +++ b/client/shared/metadata/generate-product-schema.ts @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { ProductSchema, ApiData } from './types'; +import type { RouteParams } from 'nextjs/types'; + +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; + +/** + * Generates Product schema (JSON-LD) for token pages + * Returns undefined for non-token pages or when data is not available + */ +export default function generateProductSchema( + route: RouteParams, + apiData: ApiData, +): ProductSchema | undefined { + // Only generate for token pages + if (route.pathname !== '/token/[hash]') { + return undefined; + } + + // Only generate if we have token data + if (!apiData || typeof apiData !== 'object') { + return undefined; + } + + const tokenData = apiData as ApiData<'/token/[hash]'>; + if (!tokenData) { + return undefined; + } + + const hash = typeof route.query?.hash === 'string' ? route.query.hash : undefined; + if (!hash) { + return undefined; + } + + const baseUrl = config.app.baseUrl; + const tokenUrl = `${ baseUrl }/token/${ hash }`; + + const schema: ProductSchema = { + '@context': 'https://schema.org', + '@type': 'Product', + name: tokenData.name || tokenData.symbol || undefined, + description: tokenData.description || undefined, + image: tokenData.icon_url || undefined, + url: tokenUrl, + productID: tokenData.address_hash, + }; + + if (tokenData.projectName) { + schema.brand = { + '@type': 'Brand', + name: tokenData.projectName, + }; + } + + // Only include offers if we have a valid price + // Schema.org requires valid price data when including an Offer + if (tokenData.exchange_rate) { + schema.offers = { + '@type': 'Offer', + price: tokenData.exchange_rate, + priceCurrency: 'USD', + priceValidUntil: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(), + availability: 'InStock', + }; + } + + return schema; +} diff --git a/lib/metadata/generate.spec.ts b/client/shared/metadata/generate.spec.ts similarity index 100% rename from lib/metadata/generate.spec.ts rename to client/shared/metadata/generate.spec.ts diff --git a/lib/metadata/generate.ts b/client/shared/metadata/generate.ts similarity index 76% rename from lib/metadata/generate.ts rename to client/shared/metadata/generate.ts index e89348fd3c..341c22c50f 100644 --- a/lib/metadata/generate.ts +++ b/client/shared/metadata/generate.ts @@ -1,16 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { ApiData, Metadata } from './types'; import type { RouteParams } from 'nextjs/types'; import type { Route } from 'nextjs-routes'; +import getChainTitle from 'client/shared/chain/get-chain-title'; +import { currencyUnits } from 'client/shared/chain/units'; + import config from 'configs/app'; -import getNetworkTitle from 'lib/networks/getNetworkTitle'; -import { currencyUnits } from 'lib/units'; -import compileValue from './compileValue'; -import generateProductSchema from './generateProductSchema'; -import getCanonicalUrl from './getCanonicalUrl'; -import getPageOgType from './getPageOgType'; +import compileValue from './compile-value'; +import generateProductSchema from './generate-product-schema'; +import getCanonicalUrl from './get-canonical-url'; +import getPageOgType from './get-page-og-type'; import * as templates from './templates'; export default function generate(route: RouteParams, apiData: ApiData = null): Metadata { @@ -21,7 +24,7 @@ export default function generate(route: Rout ...route.query, ...apiData, network_name: config.chain.name, - network_title: getNetworkTitle(), + network_title: getChainTitle(), network_gwei: currencyUnits.gwei, id_cap: idCap, }; diff --git a/client/shared/metadata/get-canonical-url.ts b/client/shared/metadata/get-canonical-url.ts new file mode 100644 index 0000000000..937b00732a --- /dev/null +++ b/client/shared/metadata/get-canonical-url.ts @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; + +const CANONICAL_ROUTES: Array = [ + '/', + '/txs', + '/ops', + '/verified-contracts', + '/name-services', + '/withdrawals', + '/tokens', + '/stats', + '/api-docs', + '/gas-tracker', + '/apps', +]; + +export default function getCanonicalUrl(pathname: Route['pathname']) { + if (CANONICAL_ROUTES.includes(pathname)) { + return config.app.baseUrl + pathname; + } +} diff --git a/client/shared/metadata/get-page-og-type.ts b/client/shared/metadata/get-page-og-type.ts new file mode 100644 index 0000000000..3000304f4a --- /dev/null +++ b/client/shared/metadata/get-page-og-type.ts @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { Route } from 'nextjs-routes'; + +type OGPageType = 'Homepage' | 'Root page' | 'Regular page'; + +const OG_TYPE_DICT: Record = { + '/': 'Homepage', + '/txs': 'Root page', + '/internal-txs': 'Root page', + '/txs/kettle/[hash]': 'Regular page', + '/tx/[hash]': 'Regular page', + '/blocks': 'Root page', + '/block/[height_or_hash]': 'Regular page', + '/block/countdown': 'Regular page', + '/block/countdown/[height]': 'Regular page', + '/accounts': 'Root page', + '/accounts/label/[slug]': 'Root page', + '/address/[hash]': 'Regular page', + '/verified-contracts': 'Root page', + '/contract-verification': 'Root page', + '/address/[hash]/contract-verification': 'Regular page', + '/tokens': 'Root page', + '/token/[hash]': 'Regular page', + '/token/[hash]/instance/[id]': 'Regular page', + '/apps': 'Root page', + '/apps/[id]': 'Regular page', + '/essential-dapps/[id]': 'Regular page', + '/stats': 'Root page', + '/stats/[id]': 'Regular page', + '/uptime': 'Root page', + '/hot-contracts': 'Root page', + '/api-docs': 'Regular page', + '/search-results': 'Regular page', + '/auth/profile': 'Root page', + '/account/merits': 'Regular page', + '/account/watchlist': 'Regular page', + '/account/api-key': 'Regular page', + '/account/custom-abi': 'Regular page', + '/account/tag-address': 'Regular page', + '/account/verified-addresses': 'Root page', + '/public-tags/submit': 'Regular page', + '/withdrawals': 'Root page', + '/txn-withdrawals': 'Root page', + '/visualize/sol2uml': 'Regular page', + '/deposits': 'Root page', + '/output-roots': 'Root page', + '/dispute-games': 'Root page', + '/batches': 'Root page', + '/batches/[number]': 'Regular page', + '/batches/celestia/[height]/[commitment]': 'Regular page', + '/blobs/[hash]': 'Regular page', + '/ops': 'Root page', + '/op/[hash]': 'Regular page', + '/404': 'Regular page', + '/name-services': 'Root page', + '/name-services/domains/[name]': 'Regular page', + '/name-services/clusters/[name]': 'Regular page', + '/validators': 'Root page', + '/validators/[id]': 'Regular page', + '/epochs': 'Root page', + '/epochs/[number]': 'Regular page', + '/gas-tracker': 'Root page', + '/mud-worlds': 'Root page', + '/token-transfers': 'Root page', + '/advanced-filter': 'Root page', + '/pools': 'Root page', + '/pools/[hash]': 'Regular page', + '/interop-messages': 'Root page', + '/operations': 'Root page', + '/operation/[id]': 'Regular page', + '/cc/tx/[hash]': 'Regular page', + '/cross-chain-tx/[id]': 'Regular page', + '/ictt-users': 'Root page', + + // multichain routes + '/chain/[chain_slug_or_id]/accounts/label/[slug]': 'Root page', + '/chain/[chain_slug_or_id]/advanced-filter': 'Regular page', + '/chain/[chain_slug_or_id]/block/[height_or_hash]': 'Regular page', + '/chain/[chain_slug_or_id]/block/countdown': 'Regular page', + '/chain/[chain_slug_or_id]/block/countdown/[height]': 'Regular page', + '/chain/[chain_slug_or_id]/op/[hash]': 'Regular page', + '/chain/[chain_slug_or_id]/token/[hash]': 'Regular page', + '/chain/[chain_slug_or_id]/token/[hash]/instance/[id]': 'Regular page', + '/chain/[chain_slug_or_id]/tx/[hash]': 'Regular page', + '/chain/[chain_slug_or_id]/visualize/sol2uml': 'Regular page', + '/ecosystems': 'Root page', + + // service routes, added only to make typescript happy + '/login': 'Regular page', + '/sprite': 'Regular page', + '/chakra': 'Regular page', + '/api/metrics': 'Regular page', + '/api/monitoring/invalid-api-schema': 'Regular page', + '/api/log': 'Regular page', + '/api/tokens/[hash]/instances/[id]/media-type': 'Regular page', + '/api/proxy': 'Regular page', + '/api/csrf': 'Regular page', + '/api/healthz': 'Regular page', + '/api/config': 'Regular page', + '/api/monitoring/pageview': 'Regular page', +}; + +export default function getPageOgType(pathname: Route['pathname']) { + return OG_TYPE_DICT[pathname]; +} diff --git a/client/shared/metadata/index.ts b/client/shared/metadata/index.ts new file mode 100644 index 0000000000..003a8fe31f --- /dev/null +++ b/client/shared/metadata/index.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export { default as generate } from './generate'; +export { default as update } from './update'; +export * from './types'; diff --git a/lib/metadata/templates/description.ts b/client/shared/metadata/templates/description.ts similarity index 82% rename from lib/metadata/templates/description.ts rename to client/shared/metadata/templates/description.ts index e405a66c21..aeeab9668d 100644 --- a/lib/metadata/templates/description.ts +++ b/client/shared/metadata/templates/description.ts @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + /* eslint-disable max-len */ import type { Route } from 'nextjs-routes'; // equal og:description -const DEFAULT_TEMPLATE = 'Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the %network_title% network.'; +const DEFAULT_TEMPLATE = 'Scan %network_title% with Blockscout. Search transactions, verify smart contracts, analyze addresses, and access blockchain data through explorer APIs.'; // FIXME all page descriptions will be updated later const TEMPLATE_MAP: Record = { @@ -44,7 +46,6 @@ const TEMPLATE_MAP: Record = { '/withdrawals': DEFAULT_TEMPLATE, '/txn-withdrawals': DEFAULT_TEMPLATE, '/visualize/sol2uml': DEFAULT_TEMPLATE, - '/csv-export': DEFAULT_TEMPLATE, '/deposits': DEFAULT_TEMPLATE, '/output-roots': DEFAULT_TEMPLATE, '/dispute-games': DEFAULT_TEMPLATE, @@ -73,19 +74,19 @@ const TEMPLATE_MAP: Record = { '/operation/[id]': DEFAULT_TEMPLATE, '/cc/tx/[hash]': DEFAULT_TEMPLATE, '/cross-chain-tx/[id]': DEFAULT_TEMPLATE, + '/ictt-users': DEFAULT_TEMPLATE, // multichain routes - '/chain/[chain_slug]/accounts/label/[slug]': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/advanced-filter': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/block/[height_or_hash]': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/block/countdown': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/block/countdown/[height]': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/csv-export': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/op/[hash]': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/token/[hash]': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/token/[hash]/instance/[id]': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/tx/[hash]': DEFAULT_TEMPLATE, - '/chain/[chain_slug]/visualize/sol2uml': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/accounts/label/[slug]': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/advanced-filter': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/block/[height_or_hash]': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/block/countdown': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/block/countdown/[height]': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/op/[hash]': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/token/[hash]': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/token/[hash]/instance/[id]': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/tx/[hash]': DEFAULT_TEMPLATE, + '/chain/[chain_slug_or_id]/visualize/sol2uml': DEFAULT_TEMPLATE, '/ecosystems': DEFAULT_TEMPLATE, // service routes, added only to make typescript happy diff --git a/client/shared/metadata/templates/index.ts b/client/shared/metadata/templates/index.ts new file mode 100644 index 0000000000..d1c412add2 --- /dev/null +++ b/client/shared/metadata/templates/index.ts @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export * as title from './title'; +export * as description from './description'; diff --git a/lib/metadata/templates/title.ts b/client/shared/metadata/templates/title.ts similarity index 84% rename from lib/metadata/templates/title.ts rename to client/shared/metadata/templates/title.ts index eaa77f6af5..c2a63c0720 100644 --- a/lib/metadata/templates/title.ts +++ b/client/shared/metadata/templates/title.ts @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { getFeaturePayload } from 'configs/app/features/types'; import type { Route } from 'nextjs-routes'; +import { layerLabels } from 'client/features/rollup/common/utils/layer'; + import config from 'configs/app'; -import { layerLabels } from 'lib/rollups/utils'; const dappEntityName = (getFeaturePayload(config.features.marketplace)?.titles.entity_name ?? '').toLowerCase(); @@ -46,7 +49,6 @@ const TEMPLATE_MAP: Record = { '/withdrawals': '%network_name% withdrawals - track on %network_name% explorer', '/txn-withdrawals': `${ layerLabels.current } to ${ layerLabels.parent } message relayer`, '/visualize/sol2uml': '%network_name% Solidity UML diagram', - '/csv-export': '%network_name% export data to CSV', '/deposits': '%network_name% deposits - track on %network_name% explorer', '/output-roots': '%network_name% output roots', '/dispute-games': '%network_name% dispute games', @@ -75,19 +77,19 @@ const TEMPLATE_MAP: Record = { '/operation/[id]': '%network_name% operation %id%', '/cc/tx/[hash]': '%network_name% cross-chain transaction %hash% details', '/cross-chain-tx/[id]': '%network_name% cross-chain transaction %id% details', + '/ictt-users': '%network_name% ICTT users', // multichain routes - '/chain/[chain_slug]/accounts/label/[slug]': '%network_name% addresses search by label', - '/chain/[chain_slug]/advanced-filter': '%network_name% advanced filter', - '/chain/[chain_slug]/block/[height_or_hash]': '%network_name% block %height_or_hash% details', - '/chain/[chain_slug]/block/countdown': '%network_name% block countdown index', - '/chain/[chain_slug]/block/countdown/[height]': '%network_name% block %height% countdown', - '/chain/[chain_slug]/csv-export': '%network_name% export data to CSV', - '/chain/[chain_slug]/op/[hash]': '%network_name% user operation %hash% details', - '/chain/[chain_slug]/token/[hash]': '%network_name% token details', - '/chain/[chain_slug]/token/[hash]/instance/[id]': '%network_name% token NFT instance', - '/chain/[chain_slug]/tx/[hash]': '%network_name% transaction %hash% details', - '/chain/[chain_slug]/visualize/sol2uml': '%network_name% Solidity UML diagram', + '/chain/[chain_slug_or_id]/accounts/label/[slug]': '%network_name% addresses search by label', + '/chain/[chain_slug_or_id]/advanced-filter': '%network_name% advanced filter', + '/chain/[chain_slug_or_id]/block/[height_or_hash]': '%network_name% block %height_or_hash% details', + '/chain/[chain_slug_or_id]/block/countdown': '%network_name% block countdown index', + '/chain/[chain_slug_or_id]/block/countdown/[height]': '%network_name% block %height% countdown', + '/chain/[chain_slug_or_id]/op/[hash]': '%network_name% user operation %hash% details', + '/chain/[chain_slug_or_id]/token/[hash]': '%network_name% token details', + '/chain/[chain_slug_or_id]/token/[hash]/instance/[id]': '%network_name% token NFT instance', + '/chain/[chain_slug_or_id]/tx/[hash]': '%network_name% transaction %hash% details', + '/chain/[chain_slug_or_id]/visualize/sol2uml': '%network_name% Solidity UML diagram', '/ecosystems': '%network_name% ecosystems', // service routes, added only to make typescript happy diff --git a/client/shared/metadata/types.ts b/client/shared/metadata/types.ts new file mode 100644 index 0000000000..89555efc4d --- /dev/null +++ b/client/shared/metadata/types.ts @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { LineChart } from '@blockscout/stats-types'; +import type { TokenInfo } from 'client/slices/token/types/api'; + +import type { Route } from 'nextjs-routes'; + +/* eslint-disable @stylistic/indent */ +export type ApiData = +( + Pathname extends '/address/[hash]' ? { domain_name: string } : + Pathname extends '/token/[hash]' ? TokenInfo & { symbol_or_name: string; description?: string; projectName?: string } : + Pathname extends '/token/[hash]/instance/[id]' ? { symbol_or_name: string } : + Pathname extends '/apps/[id]' ? { app_name: string } : + Pathname extends '/stats/[id]' ? LineChart['info'] : + never +) | null; + +export interface ProductSchema { + '@context': string; + '@type': 'Product'; + name?: string; + description?: string; + image?: string; + url?: string; + productID?: string; + offers?: { + '@type': 'Offer'; + price?: string; + priceCurrency?: string; + priceValidUntil?: string; + availability?: string; + }; + brand?: { + '@type': 'Brand'; + name?: string; + }; +} + +export interface Metadata { + title: string; + description: string; + opengraph: { + title: string; + description?: string; + imageUrl?: string; + }; + canonical: string | undefined; + jsonLd?: ProductSchema; +} diff --git a/lib/metadata/update.ts b/client/shared/metadata/update.ts similarity index 96% rename from lib/metadata/update.ts rename to client/shared/metadata/update.ts index d37c2c16ce..458fcc04bb 100644 --- a/lib/metadata/update.ts +++ b/client/shared/metadata/update.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { ApiData } from './types'; import type { RouteParams } from 'nextjs/types'; diff --git a/client/shared/monitoring/rollbar/index.tsx b/client/shared/monitoring/rollbar/index.tsx new file mode 100644 index 0000000000..9a3d9a6507 --- /dev/null +++ b/client/shared/monitoring/rollbar/index.tsx @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Provider as DefaultProvider, useRollbar as useRollbarDefault } from '@rollbar/react'; +import type React from 'react'; +import type { Configuration } from 'rollbar'; + +import { ABSENT_PARAM_ERROR_MESSAGE } from 'client/shared/errors/throw-on-absent-param-error'; +import { RESOURCE_LOAD_ERROR_MESSAGE } from 'client/shared/errors/throw-on-resource-load-error'; + +import config from 'configs/app'; + +import { isBot, isHeadlessBrowser, isNextJsChunkError, getRequestInfo, getExceptionClass, getExceptionOriginFileName } from './utils'; + +const feature = config.features.rollbar; + +const FallbackProvider = ({ children }: { children: React.ReactNode }) => children; + +const useRollbarFallback = (): undefined => {}; + +export const Provider = feature.isEnabled ? DefaultProvider : FallbackProvider; +export const useRollbar = feature.isEnabled ? useRollbarDefault : useRollbarFallback; + +export const clientConfig: Configuration | undefined = feature.isEnabled ? { + accessToken: feature.clientToken, + environment: feature.environment, + payload: { + code_version: feature.codeVersion, + app_instance: feature.instance, + }, + checkIgnore(_isUncaught, _args, item) { + if (isBot(window.navigator.userAgent)) { + return true; + } + + if (isHeadlessBrowser(window.navigator.userAgent)) { + return true; + } + + if (isNextJsChunkError(getRequestInfo(item)?.url)) { + return true; + } + + const exceptionClass = getExceptionClass(item); + const IGNORED_EXCEPTION_CLASSES = [ + // these are React errors - "NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node." + // they could be caused by browser extensions + // one of the examples - https://github.com/facebook/react/issues/11538 + // we can ignore them for now + 'NotFoundError', + + 'AbortError', + ]; + + if (exceptionClass && IGNORED_EXCEPTION_CLASSES.includes(exceptionClass)) { + return true; + } + + const originFileName = getExceptionOriginFileName(item); + const IGNORED_ORIGIN_FILE_NAMES_CHUNKS = [ + '/node_modules/@walletconnect', + '/node_modules/@reown', + 'chrome-extension://', + ]; + + if (originFileName && IGNORED_ORIGIN_FILE_NAMES_CHUNKS.some((chunk) => originFileName.includes(chunk))) { + return true; + } + + return false; + }, + hostSafeList: [ config.app.host ].filter(Boolean), + ignoredMessages: [ + // these are errors that we throw on when make a call to the API + RESOURCE_LOAD_ERROR_MESSAGE, + ABSENT_PARAM_ERROR_MESSAGE, + + // Filter out network-related errors that are usually not actionable + 'Network Error', + 'Failed to fetch', + + // Filter out CORS errors from third-party extensions + 'cross-origin', + + // Filter out client-side navigation cancellations + 'cancelled navigation', + ], + maxItems: 10, // Max items per page load + captureUncaught: true, + captureUnhandledRejections: true, +} : undefined; diff --git a/client/shared/monitoring/rollbar/utils.ts b/client/shared/monitoring/rollbar/utils.ts new file mode 100644 index 0000000000..7515b10768 --- /dev/null +++ b/client/shared/monitoring/rollbar/utils.ts @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { get } from 'es-toolkit/compat'; +import type { Dictionary } from 'rollbar'; + +import { castToString } from 'toolkit/utils/guards'; + +export function isBot(userAgent: string | undefined) { + if (!userAgent) return false; + + const botPatterns = [ + 'Googlebot', // Google + 'Baiduspider', // Baidu + 'bingbot', // Bing + 'YandexBot', // Yandex + 'DuckDuckBot', // DuckDuckGo + 'Slurp', // Yahoo + 'Applebot', // Apple + 'facebookexternalhit', // Facebook + 'Twitterbot', // Twitter + 'rogerbot', // Moz + 'Alexa', // Alexa + 'AhrefsBot', // Ahrefs + 'SemrushBot', // Semrush + 'spider', // Generic spiders + 'crawler', // Generic crawlers + ]; + + return botPatterns.some(pattern => + userAgent.toLowerCase().includes(pattern.toLowerCase()), + ); +} + +export function isHeadlessBrowser(userAgent: string | undefined) { + if (!userAgent) return false; + + if ( + userAgent.includes('headless') || + userAgent.includes('phantomjs') || + userAgent.includes('selenium') || + userAgent.includes('puppeteer') + ) { + return true; + } +} + +export function isNextJsChunkError(url: unknown) { + if (typeof url !== 'string') return false; + return url.includes('/_next/'); +} + +export function getRequestInfo(item: Dictionary): { url: string } | undefined { + if ( + !item.request || + item.request === null || + typeof item.request !== 'object' || + !('url' in item.request) || + typeof item.request.url !== 'string' + ) { + return undefined; + } + return { url: item.request.url }; +} + +export function getExceptionClass(item: Dictionary) { + const exceptionClass = get(item, 'body.trace.exception.class'); + + return castToString(exceptionClass); +} + +export function getExceptionOriginFileName(item: Dictionary) { + const originFileName = get(item, 'body.trace.frames[0].filename'); + + return castToString(originFileName); +} diff --git a/client/shared/router/get-filter-value-from-query.ts b/client/shared/router/get-filter-value-from-query.ts new file mode 100644 index 0000000000..8da0d286a3 --- /dev/null +++ b/client/shared/router/get-filter-value-from-query.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined): FilterType | undefined { + if (typeof val === 'string' && filterValues.includes(val as FilterType)) { + return val as FilterType; + } +} diff --git a/client/shared/router/get-filter-values-from-query.ts b/client/shared/router/get-filter-values-from-query.ts new file mode 100644 index 0000000000..f0113bd957 --- /dev/null +++ b/client/shared/router/get-filter-values-from-query.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import getValuesArrayFromQuery from './get-values-array-from-query'; + +export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined) { + const valArray = getValuesArrayFromQuery(val); + + if (!valArray) { + return; + } + + return valArray.filter(el => filterValues.includes(el as unknown as FilterType)) as unknown as Array; +} diff --git a/client/shared/router/get-query-param-string.ts b/client/shared/router/get-query-param-string.ts new file mode 100644 index 0000000000..6e79f1ca8b --- /dev/null +++ b/client/shared/router/get-query-param-string.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function getQueryParamString(param: string | Array | undefined): string { + if (Array.isArray(param)) { + return param.join(','); + } + + return param || ''; +} diff --git a/client/shared/router/get-values-array-from-query.ts b/client/shared/router/get-values-array-from-query.ts new file mode 100644 index 0000000000..d066ef802a --- /dev/null +++ b/client/shared/router/get-values-array-from-query.ts @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function getValuesArrayFromQuery(val: string | Array | undefined) { + if (val === undefined) { + return; + } + + const valArray = []; + if (typeof val === 'string') { + valArray.push(...val.split(',')); + } + if (Array.isArray(val)) { + if (!val.length) { + return; + } + val.forEach(el => valArray.push(...el.split(','))); + } + + return valArray; +} diff --git a/client/shared/router/remove-query-param.ts b/client/shared/router/remove-query-param.ts new file mode 100644 index 0000000000..10a83bb14a --- /dev/null +++ b/client/shared/router/remove-query-param.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { NextRouter } from 'next/router'; + +export default function removeQueryParam(router: NextRouter, param: string) { + const { pathname, query, asPath } = router; + const newQuery = { ...query }; + delete newQuery[param]; + + const hashIndex = asPath.indexOf('#'); + const hash = hashIndex !== -1 ? asPath.substring(hashIndex) : ''; + + router.replace({ pathname, query: newQuery, hash }, undefined, { shallow: true }); +} diff --git a/client/shared/router/types.ts b/client/shared/router/types.ts new file mode 100644 index 0000000000..4d277e78be --- /dev/null +++ b/client/shared/router/types.ts @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export type NextJsQueryParam = string | Array | undefined; diff --git a/client/shared/router/update-query-param.ts b/client/shared/router/update-query-param.ts new file mode 100644 index 0000000000..f4d3c7cac3 --- /dev/null +++ b/client/shared/router/update-query-param.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { NextRouter } from 'next/router'; + +export default function updateQueryParam(router: NextRouter, param: string, newValue: string) { + const { pathname, query, asPath } = router; + const newQuery = { ...query }; + newQuery[param] = newValue; + + const hashIndex = asPath.indexOf('#'); + const hash = hashIndex !== -1 ? asPath.substring(hashIndex) : ''; + + router.replace({ pathname, query: newQuery, hash }, undefined, { shallow: true }); +} diff --git a/lib/router/useEtherscanRedirects.ts b/client/shared/router/useEtherscanRedirects.ts similarity index 97% rename from lib/router/useEtherscanRedirects.ts rename to client/shared/router/useEtherscanRedirects.ts index b41258ee83..f1273b5751 100644 --- a/lib/router/useEtherscanRedirects.ts +++ b/client/shared/router/useEtherscanRedirects.ts @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import React from 'react'; -import getQueryParamString from './getQueryParamString'; +import getQueryParamString from './get-query-param-string'; export default function useEtherscanRedirects() { const router = useRouter(); diff --git a/lib/router/useQueryParams.ts b/client/shared/router/useQueryParams.ts similarity index 85% rename from lib/router/useQueryParams.ts rename to client/shared/router/useQueryParams.ts index dcec65c385..22ef42fc72 100644 --- a/lib/router/useQueryParams.ts +++ b/client/shared/router/useQueryParams.ts @@ -1,10 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import { useCallback } from 'react'; export function useQueryParams() { const router = useRouter(); - const updateQuery = useCallback((updates: Record, replace = false) => { + const updateQuery = useCallback((updates: Record>, replace = false) => { const newQuery = { ...router.query }; Object.entries(updates).forEach(([ key, value ]) => { diff --git a/lib/cookies.ts b/client/shared/storage/cookies.ts similarity index 96% rename from lib/cookies.ts rename to client/shared/storage/cookies.ts index fb09e0128f..fcefa5b3a3 100644 --- a/lib/cookies.ts +++ b/client/shared/storage/cookies.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import Cookies from 'js-cookie'; import config from 'configs/app'; @@ -66,7 +68,7 @@ function isDisallowedInPrivateMode(name: NAMES): boolean { /** * Checks if the app is currently in private mode by reading the APP_PROFILE cookie. */ -function isPrivateMode(serverCookie?: string): boolean { +export function isPrivateMode(serverCookie?: string): boolean { const appProfile = get(NAMES.APP_PROFILE, serverCookie); return appProfile === 'private'; } diff --git a/client/shared/text/capitalize-first-letter.ts b/client/shared/text/capitalize-first-letter.ts new file mode 100644 index 0000000000..a129fb6d28 --- /dev/null +++ b/client/shared/text/capitalize-first-letter.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function capitalizeFirstLetter(text: string) { + if (!text || !text.length) { + return ''; + } + + return text.charAt(0).toUpperCase() + text.slice(1); +} diff --git a/client/shared/text/escape-reg-exp.ts b/client/shared/text/escape-reg-exp.ts new file mode 100644 index 0000000000..0cfdec5885 --- /dev/null +++ b/client/shared/text/escape-reg-exp.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} diff --git a/client/shared/text/highlight-text.ts b/client/shared/text/highlight-text.ts new file mode 100644 index 0000000000..686dfe4888 --- /dev/null +++ b/client/shared/text/highlight-text.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import xss from 'xss'; + +import escapeRegExp from 'client/shared/text/escape-reg-exp'; + +export default function highlightText(text: string, query: string) { + const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi'); + return xss(text.replace(regex, '$1')); +} diff --git a/client/shared/text/shorten-string.ts b/client/shared/text/shorten-string.ts new file mode 100644 index 0000000000..9da82937f6 --- /dev/null +++ b/client/shared/text/shorten-string.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function shortenString(string: string | null, charNumber: number | undefined = 8) { + if (!string) { + return ''; + } + + if (string.length <= charNumber) { + return string; + } + + const tailLength = charNumber < 8 ? 2 : 4; + + return string.slice(0, charNumber - tailLength) + '...' + string.slice(-tailLength); +} diff --git a/client/shared/transformers/base64-to-hex.ts b/client/shared/transformers/base64-to-hex.ts new file mode 100644 index 0000000000..7631238d43 --- /dev/null +++ b/client/shared/transformers/base64-to-hex.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import bytesToHex from './bytes-to-hex'; + +export default function base64ToHex(base64: string): string { + const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); + return bytesToHex(bytes, false); +} diff --git a/client/shared/transformers/bytes-to-base64.ts b/client/shared/transformers/bytes-to-base64.ts new file mode 100644 index 0000000000..bb6f5c3f68 --- /dev/null +++ b/client/shared/transformers/bytes-to-base64.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function bytesToBase64(bytes: Uint8Array) { + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + + const base64String = btoa(binary); + + return base64String; +} diff --git a/client/shared/transformers/bytes-to-hex.ts b/client/shared/transformers/bytes-to-hex.ts new file mode 100644 index 0000000000..a5e649f12e --- /dev/null +++ b/client/shared/transformers/bytes-to-hex.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function bytesToBase64(bytes: Uint8Array, addPrefix = true) { + let result = ''; + for (const byte of bytes) { + result += Number(byte).toString(16).padStart(2, '0'); + } + + return addPrefix ? `0x${ result }` : result; +} diff --git a/client/shared/transformers/hex-to-address.ts b/client/shared/transformers/hex-to-address.ts new file mode 100644 index 0000000000..3c917a594e --- /dev/null +++ b/client/shared/transformers/hex-to-address.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function hexToAddress(hex: string) { + const shortenHex = hex.slice(0, 66); + return shortenHex.slice(0, 2) + shortenHex.slice(26); +} diff --git a/client/shared/transformers/hex-to-base64.ts b/client/shared/transformers/hex-to-base64.ts new file mode 100644 index 0000000000..a3553b3f7e --- /dev/null +++ b/client/shared/transformers/hex-to-base64.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import bytesToBase64 from './bytes-to-base64'; +import hexToBytes from './hex-to-bytes'; + +export default function hexToBase64(hex: string) { + const bytes = hexToBytes(hex); + + return bytesToBase64(bytes); +} diff --git a/client/shared/transformers/hex-to-bytes.ts b/client/shared/transformers/hex-to-bytes.ts new file mode 100644 index 0000000000..0104aeda7f --- /dev/null +++ b/client/shared/transformers/hex-to-bytes.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +// hex can be with prefix - `0x{string}` - or without it - `{string}` +export default function hexToBytes(hex: string) { + const bytes = []; + const startIndex = hex.startsWith('0x') ? 2 : 0; + for (let c = startIndex; c < hex.length; c += 2) { + bytes.push(parseInt(hex.substring(c, c + 2), 16)); + } + return new Uint8Array(bytes); +} diff --git a/client/shared/transformers/hex-to-decimal.ts b/client/shared/transformers/hex-to-decimal.ts new file mode 100644 index 0000000000..716c693476 --- /dev/null +++ b/client/shared/transformers/hex-to-decimal.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function hetToDecimal(hex: string) { + const strippedHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return parseInt(strippedHex, 16); +} diff --git a/client/shared/transformers/hex-to-utf8.ts b/client/shared/transformers/hex-to-utf8.ts new file mode 100644 index 0000000000..abb05d1e24 --- /dev/null +++ b/client/shared/transformers/hex-to-utf8.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import hexToBytes from 'client/shared/transformers/hex-to-bytes'; + +export default function hexToUtf8(hex: string) { + const utf8decoder = new TextDecoder(); + const bytes = hexToBytes(hex); + + return utf8decoder.decode(bytes); +} diff --git a/client/shared/utils/delay.ts b/client/shared/utils/delay.ts new file mode 100644 index 0000000000..49cca92e39 --- /dev/null +++ b/client/shared/utils/delay.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export default function delay(time: number) { + return new Promise((resolve) => window.setTimeout(resolve, time)); +} diff --git a/client/shared/utils/is-meta-key.ts b/client/shared/utils/is-meta-key.ts new file mode 100644 index 0000000000..26852e4224 --- /dev/null +++ b/client/shared/utils/is-meta-key.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type React from 'react'; + +export default function isMetaKey(event: React.KeyboardEvent) { + return event.metaKey || event.getModifierState('Meta'); +} diff --git a/client/shared/web3/detect-wallet.ts b/client/shared/web3/detect-wallet.ts new file mode 100644 index 0000000000..303e4018d8 --- /dev/null +++ b/client/shared/web3/detect-wallet.ts @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { WalletType } from 'types/client/wallets'; +import type { WalletProvider } from 'types/web3'; + +const isWalletProvider = (wallet: WalletType) => (provider: WalletProvider): boolean | undefined => { + switch (wallet) { + case 'rabby': { + return provider.isRabby; + } + case 'coinbase': { + return provider.isCoinbaseWallet; + } + case 'token_pocket': { + return provider.isTokenPocket; + } + case 'okx': { + return provider.isOkxWallet; + } + case 'trust': { + return provider.isTrustWallet; + } + case 'metamask': { + // some wallets (e.g TokenPocket, Liquality, etc) try to look like MetaMask but they are not (not even close) + // found this hack in wagmi repo + // https://github.com/wevm/wagmi/blob/8c35b7adccb4f92c4e0e36a76b970e9126053772/packages/core/src/connectors/injected.ts#L553 + if (!provider.isMetaMask) { + return false; + } + if (!provider._events || !provider._state) { + return false; + } + // Other wallets that try to look like MetaMask + const flags = [ + 'isApexWallet', + 'isAvalanche', + 'isBitKeep', + 'isBlockWallet', + 'isKuCoinWallet', + 'isMathWallet', + 'isOkxWallet', + 'isOKExWallet', + 'isOneInchIOSWallet', + 'isOneInchAndroidWallet', + 'isOpera', + 'isPhantom', + 'isPortal', + 'isRabby', + 'isRainbow', + 'isTokenPocket', + 'isTokenary', + 'isUniswapWallet', + 'isZerion', + ] ; + for (const flag of flags) { + if (provider[flag as keyof WalletProvider]) { + return false; + } + } + return true; + } + default: + return false; + } +}; + +export default function detectWallet(wallet: WalletType): { wallet: WalletType; provider: WalletProvider } | undefined { + if (!('ethereum' in window && window.ethereum)) { + return; + } + + // if user has multiple wallets installed, they all are injected in the window.ethereum.providers array + // if user has only one wallet, the provider is injected in the window.ethereum directly + const providers = Array.isArray(window.ethereum.providers) ? window.ethereum.providers : [ window.ethereum ]; + const provider = providers.find(isWalletProvider(wallet)); + + if (provider) { + return { wallet, provider }; + } +} diff --git a/lib/web3/useAddChain.tsx b/client/shared/web3/useAddChain.tsx similarity index 93% rename from lib/web3/useAddChain.tsx rename to client/shared/web3/useAddChain.tsx index 33503443f2..e90ec3ae86 100644 --- a/lib/web3/useAddChain.tsx +++ b/client/shared/web3/useAddChain.tsx @@ -1,11 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { AddEthereumChainParameter } from 'viem'; import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; +import useRewardsActivity from 'lib/hooks/useRewardsActivity'; import { SECOND } from 'toolkit/utils/consts'; -import useRewardsActivity from '../hooks/useRewardsActivity'; import useProvider from './useProvider'; import { getHexadecimalChainId } from './utils'; diff --git a/lib/web3/useAddChainClick.ts b/client/shared/web3/useAddChainClick.ts similarity index 91% rename from lib/web3/useAddChainClick.ts rename to client/shared/web3/useAddChainClick.ts index fddb2f29a3..f493c36948 100644 --- a/lib/web3/useAddChainClick.ts +++ b/client/shared/web3/useAddChainClick.ts @@ -1,6 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import * as mixpanel from 'lib/mixpanel/index'; +import * as mixpanel from 'client/shared/analytics/mixpanel'; + import { toaster } from 'toolkit/chakra/toaster'; import useAddChain from './useAddChain'; diff --git a/lib/web3/useDetectWalletEip6963.ts b/client/shared/web3/useDetectWalletEip6963.ts similarity index 97% rename from lib/web3/useDetectWalletEip6963.ts rename to client/shared/web3/useDetectWalletEip6963.ts index 5bf1d1d25e..fe21d84508 100644 --- a/lib/web3/useDetectWalletEip6963.ts +++ b/client/shared/web3/useDetectWalletEip6963.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + // https://eips.ethereum.org/EIPS/eip-6963 import React from 'react'; diff --git a/lib/web3/useProvider.tsx b/client/shared/web3/useProvider.tsx similarity index 96% rename from lib/web3/useProvider.tsx rename to client/shared/web3/useProvider.tsx index ac95440b78..b867bea654 100644 --- a/lib/web3/useProvider.tsx +++ b/client/shared/web3/useProvider.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useQuery } from '@tanstack/react-query'; import { getFeaturePayload } from 'configs/app/features/types'; @@ -5,7 +7,7 @@ import { getFeaturePayload } from 'configs/app/features/types'; import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; -import detectWallet from './detectWallet'; +import detectWallet from './detect-wallet'; import useDetectWalletEip6963 from './useDetectWalletEip6963'; export default function useProvider() { diff --git a/lib/web3/useSwitchChain.tsx b/client/shared/web3/useSwitchChain.tsx similarity index 95% rename from lib/web3/useSwitchChain.tsx rename to client/shared/web3/useSwitchChain.tsx index 294d97b1b3..0d140ada0e 100644 --- a/lib/web3/useSwitchChain.tsx +++ b/client/shared/web3/useSwitchChain.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import config from 'configs/app'; diff --git a/lib/web3/useSwitchOrAddChain.tsx b/client/shared/web3/useSwitchOrAddChain.tsx similarity index 90% rename from lib/web3/useSwitchOrAddChain.tsx rename to client/shared/web3/useSwitchOrAddChain.tsx index 00536e6f33..d65eff80b6 100644 --- a/lib/web3/useSwitchOrAddChain.tsx +++ b/client/shared/web3/useSwitchOrAddChain.tsx @@ -1,8 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { get } from 'es-toolkit/compat'; import React from 'react'; +import getErrorObj from 'client/shared/errors/get-error-obj'; + import type config from 'configs/app'; -import getErrorObj from 'lib/errors/getErrorObj'; import useAddChain from './useAddChain'; import useProvider from './useProvider'; diff --git a/client/shared/web3/utils.ts b/client/shared/web3/utils.ts new file mode 100644 index 0000000000..753ed6b126 --- /dev/null +++ b/client/shared/web3/utils.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export function getHexadecimalChainId(chainId: number) { + return '0x' + Number(chainId).toString(16); +} diff --git a/lib/web3/wallets.ts b/client/shared/web3/wallets.ts similarity index 93% rename from lib/web3/wallets.ts rename to client/shared/web3/wallets.ts index d9a8031813..a127aa6645 100644 --- a/lib/web3/wallets.ts +++ b/client/shared/web3/wallets.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { WalletType, WalletInfo } from 'types/client/wallets'; export const WALLETS_INFO: Record, WalletInfo> = { diff --git a/ui/shared/entities/address/AddressEntity.pw.tsx b/client/slices/address/components/entity/AddressEntity.pw.tsx similarity index 96% rename from ui/shared/entities/address/AddressEntity.pw.tsx rename to client/slices/address/components/entity/AddressEntity.pw.tsx index 2f467bd4bd..4660f93ee7 100644 --- a/ui/shared/entities/address/AddressEntity.pw.tsx +++ b/client/slices/address/components/entity/AddressEntity.pw.tsx @@ -2,11 +2,13 @@ import { Box } from '@chakra-ui/react'; import type { BrowserContext } from '@playwright/test'; import React from 'react'; +import { AddressHighlightProvider } from 'client/slices/address/contexts/address-highlight'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as implementationsMock from 'client/slices/address/mocks/implementations'; + +import * as cookies from 'client/shared/storage/cookies'; + import config from 'configs/app'; -import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; -import * as cookies from 'lib/cookies'; -import * as addressMock from 'mocks/address/address'; -import * as implementationsMock from 'mocks/address/implementations'; import * as metadataMock from 'mocks/metadata/address'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect } from 'playwright/lib'; diff --git a/ui/shared/entities/address/AddressEntity.tsx b/client/slices/address/components/entity/AddressEntity.tsx similarity index 94% rename from ui/shared/entities/address/AddressEntity.tsx rename to client/slices/address/components/entity/AddressEntity.tsx index ee05c3982d..2a7a7cd8d4 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/client/slices/address/components/entity/AddressEntity.tsx @@ -1,24 +1,27 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, chakra, VStack } from '@chakra-ui/react'; import React from 'react'; -import type { AddressParam } from 'types/api/addressParams'; +import type { AddressParam } from 'client/slices/address/types/api'; import { route } from 'nextjs/routes'; -import { toBech32Address } from 'lib/address/bech32'; -import { useAddressHighlightContext } from 'lib/contexts/addressHighlight'; +import { useAddressHighlightContext } from 'client/slices/address/contexts/address-highlight'; +import { toBech32Address } from 'client/slices/address/utils/bech32'; + import { useSettingsContext } from 'lib/contexts/settings'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; import * as EntityBase from 'ui/shared/entities/base/components'; +import { distributeEntityProps, getContentProps, getIconProps } from 'ui/shared/entities/base/utils'; import { getTagName } from 'ui/shared/EntityTags/utils'; import getChainTooltipText from 'ui/shared/externalChains/getChainTooltipText'; import type { IconName } from 'ui/shared/IconSvg'; -import { distributeEntityProps, getContentProps, getIconProps } from '../base/utils'; +import AddressIconDelegated from '../icon/AddressIconDelegated'; +import AddressIdenticon from '../icon/AddressIdenticon'; import AddressEntityContentProxy from './AddressEntityContentProxy'; -import AddressIconDelegated from './AddressIconDelegated'; -import AddressIdenticon from './AddressIdenticon'; type LinkProps = EntityBase.LinkBaseProps & Pick; diff --git a/ui/shared/entities/address/AddressEntityContentProxy.tsx b/client/slices/address/components/entity/AddressEntityContentProxy.tsx similarity index 97% rename from ui/shared/entities/address/AddressEntityContentProxy.tsx rename to client/slices/address/components/entity/AddressEntityContentProxy.tsx index d196ff742e..6ee9a445f9 100644 --- a/ui/shared/entities/address/AddressEntityContentProxy.tsx +++ b/client/slices/address/components/entity/AddressEntityContentProxy.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/entities/address/AddressEntityExternal.tsx b/client/slices/address/components/entity/AddressEntityExternal.tsx similarity index 95% rename from ui/shared/entities/address/AddressEntityExternal.tsx rename to client/slices/address/components/entity/AddressEntityExternal.tsx index ca0ab45a21..d11f7cbb93 100644 --- a/ui/shared/entities/address/AddressEntityExternal.tsx +++ b/client/slices/address/components/entity/AddressEntityExternal.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { JsxStyleProps } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/entities/address/AddressEntityInterchain.tsx b/client/slices/address/components/entity/AddressEntityInterchain.tsx similarity index 94% rename from ui/shared/entities/address/AddressEntityInterchain.tsx rename to client/slices/address/components/entity/AddressEntityInterchain.tsx index 1b1268d936..720b0187ed 100644 --- a/ui/shared/entities/address/AddressEntityInterchain.tsx +++ b/client/slices/address/components/entity/AddressEntityInterchain.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { JsxStyleProps } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/entities/address/AddressEntityWithTokenFilter.tsx b/client/slices/address/components/entity/AddressEntityWithTokenFilter.tsx similarity index 95% rename from ui/shared/entities/address/AddressEntityWithTokenFilter.tsx rename to client/slices/address/components/entity/AddressEntityWithTokenFilter.tsx index 44f859621b..647539737c 100644 --- a/ui/shared/entities/address/AddressEntityWithTokenFilter.tsx +++ b/client/slices/address/components/entity/AddressEntityWithTokenFilter.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/entities/address/AddressStringOrParam.tsx b/client/slices/address/components/entity/AddressStringOrParam.tsx similarity index 80% rename from ui/shared/entities/address/AddressStringOrParam.tsx rename to client/slices/address/components/entity/AddressStringOrParam.tsx index 6005339e90..bd6491228c 100644 --- a/ui/shared/entities/address/AddressStringOrParam.tsx +++ b/client/slices/address/components/entity/AddressStringOrParam.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { AddressParamBasic } from 'types/api/addressParams'; +import type { AddressParamBasic } from 'client/slices/address/types/api'; import AddressEntity from './AddressEntity'; import type { EntityProps } from './AddressEntity'; diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_dark-color-mode_delegated-address-dark-mode-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_dark-color-mode_delegated-address-dark-mode-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_dark-color-mode_delegated-address-dark-mode-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_dark-color-mode_delegated-address-dark-mode-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_bech32-format-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_bech32-format-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_bech32-format-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_bech32-format-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-verified-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_contract-verified-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-verified-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_contract-verified-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_customization-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_customization-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_customization-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_customization-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_delegated-address-dark-mode-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_delegated-address-dark-mode-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_delegated-address-dark-mode-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_delegated-address-dark-mode-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_external-link-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_loading-with-alias-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_loading-with-alias-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_loading-with-alias-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_loading-with-alias-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_loading-without-alias-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_loading-without-alias-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_loading-without-alias-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_loading-without-alias-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_no-link-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_no-link-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_no-link-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_no-link-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-implementation-name-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-implementation-name-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-implementation-name-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-implementation-name-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-multiple-implementations-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-multiple-implementations-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-multiple-implementations-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-multiple-implementations-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-name-tag-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-name-tag-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-name-tag-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-name-tag-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-any-name-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-any-name-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-any-name-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-any-name-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-implementation-name-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-implementation-name-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-implementation-name-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-implementation-name-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_shield-contract-with-icon-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_shield-contract-with-icon-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_shield-contract-with-icon-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_shield-contract-with-icon-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_shield-regular-address-with-image-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_shield-regular-address-with-image-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_shield-regular-address-with-image-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_shield-regular-address-with-image-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_variant-content-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_variant-content-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_variant-content-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_variant-content-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_variant-subheading-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_variant-subheading-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_variant-subheading-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_variant-subheading-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-ENS-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_with-ENS-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-ENS-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_with-ENS-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-cex-deposit-tag-default-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_with-cex-deposit-tag-default-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-cex-deposit-tag-default-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_with-cex-deposit-tag-default-1.png diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-name-tag-1.png b/client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_with-name-tag-1.png similarity index 100% rename from ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-name-tag-1.png rename to client/slices/address/components/entity/__screenshots__/AddressEntity.pw.tsx_default_with-name-tag-1.png diff --git a/ui/shared/address/AddressFromTo.pw.tsx b/client/slices/address/components/from-to/AddressFromTo.pw.tsx similarity index 95% rename from ui/shared/address/AddressFromTo.pw.tsx rename to client/slices/address/components/from-to/AddressFromTo.pw.tsx index d8ca3ed9fa..8f24518ddf 100644 --- a/ui/shared/address/AddressFromTo.pw.tsx +++ b/client/slices/address/components/from-to/AddressFromTo.pw.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import * as addressMock from 'mocks/address/address'; +import * as addressMock from 'client/slices/address/mocks/address'; + import { test, expect } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; diff --git a/ui/shared/address/AddressFromTo.tsx b/client/slices/address/components/from-to/AddressFromTo.tsx similarity index 89% rename from ui/shared/address/AddressFromTo.tsx rename to client/slices/address/components/from-to/AddressFromTo.tsx index 75b41c30a4..79718fd9bb 100644 --- a/ui/shared/address/AddressFromTo.tsx +++ b/client/slices/address/components/from-to/AddressFromTo.tsx @@ -1,14 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { ConditionalValue } from '@chakra-ui/react'; import { Flex, Grid, chakra, useBreakpointValue } from '@chakra-ui/react'; import React from 'react'; -import type { EntityProps } from 'ui/shared/entities/address/AddressEntity'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import AddressEntityWithTokenFilter from 'ui/shared/entities/address/AddressEntityWithTokenFilter'; +import type { EntityProps } from 'client/slices/address/components/entity/AddressEntity'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import AddressEntityWithTokenFilter from 'client/slices/address/components/entity/AddressEntityWithTokenFilter'; +import { getTxCourseType } from 'client/slices/address/utils/tx'; + +import AddressEntityZetaChain from 'client/features/chain-variants/zeta-chain/components/AddressEntityZetaChain'; -import AddressEntityZetaChain from '../entities/address/AddressEntityZetaChain'; import AddressFromToIcon from './AddressFromToIcon'; -import { getTxCourseType } from './utils'; type Mode = 'compact' | 'long'; diff --git a/ui/shared/address/AddressFromToIcon.pw.tsx b/client/slices/address/components/from-to/AddressFromToIcon.pw.tsx similarity index 88% rename from ui/shared/address/AddressFromToIcon.pw.tsx rename to client/slices/address/components/from-to/AddressFromToIcon.pw.tsx index 16f6732fa6..e8ca465b91 100644 --- a/ui/shared/address/AddressFromToIcon.pw.tsx +++ b/client/slices/address/components/from-to/AddressFromToIcon.pw.tsx @@ -1,10 +1,11 @@ import { Box } from '@chakra-ui/react'; import React from 'react'; +import type { TxCourseType } from 'client/slices/address/utils/tx'; + import { test, expect } from 'playwright/lib'; import AddressFromToIcon from './AddressFromToIcon'; -import type { TxCourseType } from './utils'; test.use({ viewport: { width: 36, height: 36 } }); diff --git a/ui/shared/address/AddressFromToIcon.tsx b/client/slices/address/components/from-to/AddressFromToIcon.tsx similarity index 92% rename from ui/shared/address/AddressFromToIcon.tsx rename to client/slices/address/components/from-to/AddressFromToIcon.tsx index 52a0c8538b..80fb139a4e 100644 --- a/ui/shared/address/AddressFromToIcon.tsx +++ b/client/slices/address/components/from-to/AddressFromToIcon.tsx @@ -1,11 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; +import type { TxCourseType } from 'client/slices/address/utils/tx'; + import { Tooltip } from 'toolkit/chakra/tooltip'; import IconSvg from 'ui/shared/IconSvg'; -import type { TxCourseType } from './utils'; - interface Props { isLoading?: boolean; type: TxCourseType; diff --git a/ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_compact-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_compact-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_compact-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_compact-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_incoming-txn-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_incoming-txn-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_incoming-txn-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_incoming-txn-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_loading-state-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_loading-state-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_loading-state-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_loading-state-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_outgoing-txn-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_outgoing-txn-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromTo.pw.tsx_default_outgoing-txn-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromTo.pw.tsx_default_outgoing-txn-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_in-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_in-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_in-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_in-txn-type-dark-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_out-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_out-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_out-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_out-txn-type-dark-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_self-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_self-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_self-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_self-txn-type-dark-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_unspecified-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_unspecified-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_unspecified-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_dark-color-mode_unspecified-txn-type-dark-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_in-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_in-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_in-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_in-txn-type-dark-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_out-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_out-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_out-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_out-txn-type-dark-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_self-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_self-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_self-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_self-txn-type-dark-mode-1.png diff --git a/ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_unspecified-txn-type-dark-mode-1.png b/client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_unspecified-txn-type-dark-mode-1.png similarity index 100% rename from ui/shared/address/__screenshots__/AddressFromToIcon.pw.tsx_default_unspecified-txn-type-dark-mode-1.png rename to client/slices/address/components/from-to/__screenshots__/AddressFromToIcon.pw.tsx_default_unspecified-txn-type-dark-mode-1.png diff --git a/ui/shared/entities/address/AddressIconDelegated.tsx b/client/slices/address/components/icon/AddressIconDelegated.tsx similarity index 94% rename from ui/shared/entities/address/AddressIconDelegated.tsx rename to client/slices/address/components/icon/AddressIconDelegated.tsx index 6e06431238..674f53003e 100644 --- a/ui/shared/entities/address/AddressIconDelegated.tsx +++ b/client/slices/address/components/icon/AddressIconDelegated.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, useToken } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/entities/address/AddressIdenticon.tsx b/client/slices/address/components/icon/AddressIdenticon.tsx similarity index 84% rename from ui/shared/entities/address/AddressIdenticon.tsx rename to client/slices/address/components/icon/AddressIdenticon.tsx index 4197708f12..8bdf896ff7 100644 --- a/ui/shared/entities/address/AddressIdenticon.tsx +++ b/client/slices/address/components/icon/AddressIdenticon.tsx @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import dynamic from 'next/dynamic'; import React from 'react'; +import * as cookies from 'client/shared/storage/cookies'; + import config from 'configs/app'; -import * as cookies from 'lib/cookies'; import { Image } from 'toolkit/chakra/image'; -import IdenticonGithub from 'ui/shared/IdenticonGithub'; + +import AddressIdenticonGithub from './AddressIdenticonGithub'; interface IconProps { hash: string; @@ -18,7 +22,7 @@ const Icon = dynamic( switch (type) { case 'github': { - return (props: IconProps) => ; + return (props: IconProps) => ; } case 'blockie': { @@ -58,7 +62,7 @@ const Icon = dynamic( } case 'nouns': { - const NounsIdenticon = (await import('./NounsIdenticon')).default; + const NounsIdenticon = (await import('./AddressIdenticonNouns')).default; return (props: IconProps) => { return ; diff --git a/client/slices/address/components/icon/AddressIdenticonGithub.tsx b/client/slices/address/components/icon/AddressIdenticonGithub.tsx new file mode 100644 index 0000000000..09d07b1999 --- /dev/null +++ b/client/slices/address/components/icon/AddressIdenticonGithub.tsx @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Box, chakra } from '@chakra-ui/react'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import { Skeleton } from 'toolkit/chakra/skeleton'; + +const Identicon = dynamic<{ bg: string; string: string; size: number }>( + async() => { + const lib = await import('react-identicons'); + return typeof lib === 'object' && 'default' in lib ? lib.default : lib; + }, + { + loading: () => , + ssr: false, + }, +); + +interface Props { + className?: string; + iconSize: number; + seed: string; +} + +const AddressIdenticonGithub = ({ iconSize, seed }: Props) => { + return ( + + + + ); +}; + +export default React.memo(chakra(AddressIdenticonGithub)); diff --git a/client/slices/address/components/icon/AddressIdenticonNouns.tsx b/client/slices/address/components/icon/AddressIdenticonNouns.tsx new file mode 100644 index 0000000000..4c28e6e589 --- /dev/null +++ b/client/slices/address/components/icon/AddressIdenticonNouns.tsx @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { ImageData, getNounData, getNounSeedFromBlockHash } from '@nouns/assets'; +import { buildSVG } from '@nouns/sdk'; +import CryptoJS from 'crypto-js'; +import React from 'react'; + +import { Image } from 'toolkit/chakra/image'; + +const { palette } = ImageData; + +interface Props { + hash: string; + size: number; +} + +const MAGIC_HASH = '0xAC1CA184579A254E4F289319E42FF4A67BF5698AD26C6E7C68769D0D21FAFCB1'; + +// analog of getNumberFromString from deleted @cloudnoun package +export const getNumberFromString = (input: string): number => { + const str = input.trim(); + if (!str) return 0; + + const combined = [ + str, + CryptoJS.MD5(str).toString(CryptoJS.enc.Hex), + CryptoJS.SHA1(str).toString(CryptoJS.enc.Base64), + CryptoJS.SHA256(str).toString(CryptoJS.enc.Hex), + CryptoJS.SHA512(str).toString(CryptoJS.enc.Base64), + CryptoJS.RIPEMD160(str).toString(CryptoJS.enc.Base64), + ].join(''); + + let sum = 0; + for (let i = 0; i < combined.length; i++) { + sum += combined.charCodeAt(i); + } + return sum * str.length; +}; + +const AddressIdenticonNouns: React.FC = ({ hash, size }) => { + const id = getNumberFromString(hash); + const seed = getNounSeedFromBlockHash(id, MAGIC_HASH); + + const { parts, background } = getNounData(seed); + const svg = buildSVG(parts, palette, background); + const svgData = btoa(svg); + if (!svgData) { + return null; + } + + return ( + { + ); +}; + +export default React.memo(AddressIdenticonNouns); diff --git a/client/slices/address/contexts/address-highlight.tsx b/client/slices/address/contexts/address-highlight.tsx new file mode 100644 index 0000000000..a533a03482 --- /dev/null +++ b/client/slices/address/contexts/address-highlight.tsx @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +interface AddressHighlightProviderProps { + children: React.ReactNode; +} + +interface TAddressHighlightContext { + onMouseEnter: (event: React.MouseEvent) => void; + onMouseLeave: (event: React.MouseEvent) => void; +} + +export const AddressHighlightContext = React.createContext(null); + +export function AddressHighlightProvider({ children }: AddressHighlightProviderProps) { + const timeoutId = React.useRef(null); + const hashRef = React.useRef(null); + + const onMouseEnter = React.useCallback((event: React.MouseEvent) => { + const hash = event.currentTarget.getAttribute('data-hash'); + if (hash) { + hashRef.current = hash; + timeoutId.current = window.setTimeout(() => { + // for better performance we update DOM-nodes directly bypassing React reconciliation + const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`); + for (const node of nodes) { + node.classList.add('address-entity_highlighted'); + } + }, 100); + } + }, []); + + const onMouseLeave = React.useCallback(() => { + if (hashRef.current) { + const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`); + for (const node of nodes) { + node.classList.remove('address-entity_highlighted'); + } + hashRef.current = null; + } + typeof timeoutId.current === 'number' && window.clearTimeout(timeoutId.current); + }, []); + + const value = React.useMemo(() => { + return { + onMouseEnter, + onMouseLeave, + }; + }, [ onMouseEnter, onMouseLeave ]); + + React.useEffect(() => { + return () => { + typeof timeoutId.current === 'number' && window.clearTimeout(timeoutId.current); + }; + }, []); + + return ( + + { children } + + ); +} + +export function useAddressHighlightContext(disabled?: boolean) { + const context = React.useContext(AddressHighlightContext); + if (context === undefined || disabled) { + return null; + } + return context; +} diff --git a/ui/address/utils/useAddressCountersQuery.ts b/client/slices/address/hooks/useAddressCountersQuery.ts similarity index 84% rename from ui/address/utils/useAddressCountersQuery.ts rename to client/slices/address/hooks/useAddressCountersQuery.ts index 165b108394..43690e182c 100644 --- a/ui/address/utils/useAddressCountersQuery.ts +++ b/client/slices/address/hooks/useAddressCountersQuery.ts @@ -1,13 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; -import type { AddressCounters } from 'types/api/address'; +import type { AddressCounters } from 'client/slices/address/types/api'; import type { ClusterChainConfig } from 'types/multichain'; -import type { ResourceError } from 'lib/api/resources'; -import useApiQuery from 'lib/api/useApiQuery'; -import { publicClient } from 'lib/web3/client'; -import { ADDRESS_COUNTERS } from 'stubs/address'; +import useApiQuery from 'client/api/hooks/useApiQuery'; +import type { ResourceError } from 'client/api/resources'; + +import { ADDRESS_COUNTERS } from 'client/slices/address/stubs/address'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; + import { GET_TRANSACTIONS_COUNT } from 'stubs/RPC'; type RpcResponseType = [ diff --git a/ui/address/utils/useAddressQuery.ts b/client/slices/address/hooks/useAddressQuery.ts similarity index 89% rename from ui/address/utils/useAddressQuery.ts rename to client/slices/address/hooks/useAddressQuery.ts index 29f43bf395..e5e7aa35b2 100644 --- a/ui/address/utils/useAddressQuery.ts +++ b/client/slices/address/hooks/useAddressQuery.ts @@ -1,14 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import type { Address } from 'types/api/address'; +import type { Address } from 'client/slices/address/types/api'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; +import { retry } from 'client/api/hooks/useQueryClientConfig'; +import type { ResourceError } from 'client/api/resources'; + +import { ADDRESS_INFO } from 'client/slices/address/stubs/address'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; -import type { ResourceError } from 'lib/api/resources'; -import useApiQuery from 'lib/api/useApiQuery'; -import { retry } from 'lib/api/useQueryClientConfig'; -import { publicClient } from 'lib/web3/client'; -import { ADDRESS_INFO } from 'stubs/address'; import { GET_BALANCE } from 'stubs/RPC'; import { SECOND } from 'toolkit/utils/consts'; diff --git a/ui/address/utils/useCheckAddressFormat.ts b/client/slices/address/hooks/useCheckAddressFormat.ts similarity index 84% rename from ui/address/utils/useCheckAddressFormat.ts rename to client/slices/address/hooks/useCheckAddressFormat.ts index f4b3315f09..4e9c92a271 100644 --- a/ui/address/utils/useCheckAddressFormat.ts +++ b/client/slices/address/hooks/useCheckAddressFormat.ts @@ -1,8 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import React from 'react'; +import { fromBech32Address, isBech32Address } from 'client/slices/address/utils/bech32'; + import config from 'configs/app'; -import { fromBech32Address, isBech32Address } from 'lib/address/bech32'; export default function useCheckAddressFormat(hash: string) { const router = useRouter(); diff --git a/client/slices/address/mocks/address.ts b/client/slices/address/mocks/address.ts new file mode 100644 index 0000000000..96d09b79b4 --- /dev/null +++ b/client/slices/address/mocks/address.ts @@ -0,0 +1,174 @@ +import type { Address, AddressParam } from 'client/slices/address/types/api'; + +import { tokenInfo } from 'client/slices/token/mocks/info'; + +import { publicTag, privateTag, watchlistName } from 'client/features/account/mocks/address-tags'; + +export const hash = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; + +export const withName: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: null, +}; + +export const withEns: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', +}; + +export const withNameTag: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', + metadata: { + reputation: null, + tags: [ + { tagType: 'name', name: 'Mrs. Duckie', slug: 'mrs-duckie', ordinal: 0, meta: null }, + ], + }, +}; + +export const withoutName: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: null, +}; + +export const delegated: AddressParam = { + ...withoutName, + is_verified: true, + proxy_type: 'eip7702', +}; + +export const token: Address = { + hash: hash, + implementations: null, + is_contract: true, + is_verified: false, + name: null, + private_tags: [], + watchlist_names: [], + watchlist_address_id: null, + public_tags: [], + token: tokenInfo, + block_number_balance_updated_at: 8201413, + coin_balance: '1', + creation_transaction_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98', + creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72', + creation_status: 'success', + exchange_rate: '0.04311', + has_logs: false, + has_token_transfers: true, + has_tokens: true, + has_validated_blocks: false, + ens_domain_name: null, +}; + +export const eoa: Address = { + block_number_balance_updated_at: 30811263, + coin_balance: '2782650189688719421432220500', + creation_transaction_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', + creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', + creation_status: null, + exchange_rate: '0.04311', + has_logs: true, + has_token_transfers: false, + has_tokens: true, + has_validated_blocks: false, + hash: hash, + implementations: [], + is_contract: false, + is_verified: false, + name: null, + private_tags: [ publicTag ], + public_tags: [ privateTag ], + token: null, + watchlist_names: [ watchlistName ], + watchlist_address_id: 42, + ens_domain_name: null, +}; + +export const contract: Address = { + block_number_balance_updated_at: 30811263, + coin_balance: '27826501896887194214322205', + creation_transaction_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', + creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', + creation_status: 'success', + exchange_rate: '0.04311', + has_logs: true, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: false, + hash: hash, + implementations: [ + { address_hash: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' }, + ], + is_contract: true, + is_verified: true, + name: 'EternalStorageProxy', + private_tags: [ publicTag ], + public_tags: [ privateTag ], + token: null, + watchlist_names: [ watchlistName ], + watchlist_address_id: 42, + ens_domain_name: null, +}; + +export const validator: Address = { + block_number_balance_updated_at: 30811932, + coin_balance: '22910462800601256910890', + creation_transaction_hash: null, + creator_address_hash: null, + creation_status: null, + exchange_rate: '0.00432018', + has_logs: false, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: true, + hash: hash, + implementations: [], + is_contract: false, + is_verified: false, + name: 'Kiryl Ihnatsyeu', + private_tags: [], + public_tags: [], + token: null, + watchlist_names: [], + watchlist_address_id: null, + ens_domain_name: null, +}; + +export const filecoin = { + ...validator, + filecoin: { + actor_type: 'evm' as const, + id: 'f02977693', + robust: 'f410fuiwj6a3yxajbohrl5vu6ns6o2e2jriul52lvzci', + }, +}; diff --git a/client/slices/address/mocks/coin-balance-history.ts b/client/slices/address/mocks/coin-balance-history.ts new file mode 100644 index 0000000000..3f7c73e257 --- /dev/null +++ b/client/slices/address/mocks/coin-balance-history.ts @@ -0,0 +1,70 @@ +import type { AddressCoinBalanceHistoryItem, AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryChart } from 'client/slices/address/types/api'; + +export const base: AddressCoinBalanceHistoryItem = { + block_number: 30367643, + block_timestamp: '2022-12-11T17:55:20Z', + delta: '-5568096000000000', + transaction_hash: null, + value: '107014805905725000000', +}; + +export const baseResponse: AddressCoinBalanceHistoryResponse = { + items: [ + { + block_number: 30367643, + block_timestamp: '2022-10-11T17:55:20Z', + delta: '-2105682233848856', + transaction_hash: null, + value: '10102109526582662088', + }, + { + block_number: 30367234, + block_timestamp: '2022-10-01T17:55:20Z', + delta: '1933020674364000', + transaction_hash: null, + value: '10143933697708939226', + }, + { + block_number: 30363402, + block_timestamp: '2022-09-03T17:55:20Z', + delta: '-1448410607186694', + transaction_hash: null, + value: '10142485287101752532', + }, + ], + next_page_params: null, +}; + +export const chartResponse: AddressCoinBalanceHistoryChart = { + items: [ + { + date: '2022-11-02', + value: '128238612887883515', + }, + { + date: '2022-11-03', + value: '199807583157570922', + }, + { + date: '2022-11-04', + value: '114487912907005778', + }, + { + date: '2022-11-05', + value: '219533112907005778', + }, + { + date: '2022-11-06', + value: '116487912907005778', + }, + { + date: '2022-11-07', + value: '199807583157570922', + }, + { + date: '2022-11-08', + value: '216488112907005778', + }, + ], + days: 10, +}; diff --git a/client/slices/address/mocks/counters.ts b/client/slices/address/mocks/counters.ts new file mode 100644 index 0000000000..e2170ad927 --- /dev/null +++ b/client/slices/address/mocks/counters.ts @@ -0,0 +1,29 @@ +import type { AddressCounters } from 'client/slices/address/types/api'; + +export const forAddress: AddressCounters = { + gas_usage_count: '319340525', + token_transfers_count: '420', + transactions_count: '5462', + validations_count: '0', +}; + +export const forContract: AddressCounters = { + gas_usage_count: '319340525', + token_transfers_count: '0', + transactions_count: '5462', + validations_count: '0', +}; + +export const forToken: AddressCounters = { + gas_usage_count: '247479698', + token_transfers_count: '1', + transactions_count: '8474', + validations_count: '0', +}; + +export const forValidator: AddressCounters = { + gas_usage_count: '91675762951', + token_transfers_count: '0', + transactions_count: '820802', + validations_count: '1726416', +}; diff --git a/mocks/address/implementations.ts b/client/slices/address/mocks/implementations.ts similarity index 100% rename from mocks/address/implementations.ts rename to client/slices/address/mocks/implementations.ts diff --git a/client/slices/address/mocks/tab-counters.ts b/client/slices/address/mocks/tab-counters.ts new file mode 100644 index 0000000000..c3fab20653 --- /dev/null +++ b/client/slices/address/mocks/tab-counters.ts @@ -0,0 +1,12 @@ +import type { AddressTabsCounters } from 'client/slices/address/types/api'; + +export const base: AddressTabsCounters = { + internal_transactions_count: 13, + logs_count: 51, + token_balances_count: 3, + token_transfers_count: 3, + transactions_count: 51, + validations_count: 42, + withdrawals_count: 11, + beacon_deposits_count: 10, +}; diff --git a/ui/pages/Address.pw.tsx b/client/slices/address/pages/details/Address.pw.tsx similarity index 91% rename from ui/pages/Address.pw.tsx rename to client/slices/address/pages/details/Address.pw.tsx index 528e067a8d..5192e00a96 100644 --- a/ui/pages/Address.pw.tsx +++ b/client/slices/address/pages/details/Address.pw.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { numberToHex } from 'viem'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as addressCountersMock from 'client/slices/address/mocks/counters'; +import * as addressTabCountersMock from 'client/slices/address/mocks/tab-counters'; + import config from 'configs/app'; -import * as addressMock from 'mocks/address/address'; -import * as addressCountersMock from 'mocks/address/counters'; -import * as addressTabCountersMock from 'mocks/address/tabCounters'; import * as socketServer from 'playwright/fixtures/socketServer'; import { test, expect } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; diff --git a/ui/pages/Address.tsx b/client/slices/address/pages/details/Address.tsx similarity index 81% rename from ui/pages/Address.tsx rename to client/slices/address/pages/details/Address.tsx index 6f07f7e1ab..7778173870 100644 --- a/ui/pages/Address.tsx +++ b/client/slices/address/pages/details/Address.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, HStack } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; @@ -5,56 +7,60 @@ import React from 'react'; import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types'; import type { EntityTag } from 'ui/shared/EntityTags/types'; +import useApiQuery from 'client/api/hooks/useApiQuery'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import useAddressCountersQuery from 'client/slices/address/hooks/useAddressCountersQuery'; +import useAddressQuery from 'client/slices/address/hooks/useAddressQuery'; +import useCheckAddressFormat from 'client/slices/address/hooks/useCheckAddressFormat'; +import AddressBlocksValidated from 'client/slices/address/pages/details/blocks-validated/AddressBlocksValidated'; +import AddressCoinBalance from 'client/slices/address/pages/details/coin-balance/AddressCoinBalance'; +import AddressAlerts from 'client/slices/address/pages/details/info/AddressAlerts'; +import AddressDetails from 'client/slices/address/pages/details/info/AddressDetails'; +import AddressQrCode from 'client/slices/address/pages/details/info/AddressQrCode'; +import AddressInternalTxs from 'client/slices/address/pages/details/internal-txs/AddressInternalTxs'; +import AddressLogs from 'client/slices/address/pages/details/logs/AddressLogs'; +import AddressTokenTransfers, { ADDRESS_TOKEN_TRANSFERS_TAB_IDS } from 'client/slices/address/pages/details/token-transfers/AddressTokenTransfers'; +import AddressTokens from 'client/slices/address/pages/details/tokens/AddressTokens'; +import AddressTxs, { ADDRESS_TXS_TAB_IDS } from 'client/slices/address/pages/details/txs/AddressTxs'; +import AddressWithdrawals from 'client/slices/address/pages/details/withdrawals/AddressWithdrawals'; +import { ADDRESS_TABS_COUNTERS } from 'client/slices/address/stubs/address'; +import getCheckedSummedAddress from 'client/slices/address/utils/get-checked-summed-address'; +import Contract from 'client/slices/contract/pages/details/Contract'; +import { CONTRACT_TAB_IDS } from 'client/slices/contract/utils/tabs'; + +import AddressFavoriteButton from 'client/features/account/pages/address/AddressFavoriteButton'; +import Address3rdPartyWidgets from 'client/features/address-3rd-party-widgets/pages/address/Address3rdPartyWidgets'; +import useAddress3rdPartyWidgets from 'client/features/address-3rd-party-widgets/pages/address/useAddress3rdPartyWidgets'; +import useAddressMetadataInfoQuery from 'client/features/address-metadata/hooks/useAddressMetadataInfoQuery'; +import useAddressMetadataInitUpdate from 'client/features/address-metadata/hooks/useAddressMetadataInitUpdate'; +import useAddressProfileApiQuery from 'client/features/address-profile-api/hooks/useAddressProfileApiQuery'; +import AddressDeposits from 'client/features/chain-variants/beacon-chain/pages/address/AddressDeposits'; +import AddressEpochRewards from 'client/features/chain-variants/celo/pages/address/AddressEpochRewards'; +import AddressMud from 'client/features/chain-variants/mud/pages/address/AddressMud'; +import AddressMultichainInfoButton from 'client/features/multichain-button/pages/address/AddressMultichainInfoButton'; +import AddressClusters from 'client/features/name-services/clusters/pages/address/AddressClusters'; +import useCheckDomainNameParam from 'client/features/name-services/domains/hooks/useCheckDomainNameParam'; +import AddressEnsDomains from 'client/features/name-services/domains/pages/address/AddressEnsDomains'; +import SolidityscanReport from 'client/features/solidity-scan/components/SolidityscanReport'; +import AddressAccountHistory from 'client/features/tx-interpretation/noves/pages/address/AddressAccountHistory'; +import AddressUserOps from 'client/features/user-ops/pages/address/AddressUserOps'; +import { USER_OPS_ACCOUNT } from 'client/features/user-ops/stubs'; +import TokenAddToWallet from 'client/features/web3-wallet/components/TokenAddToWallet'; + +import getChainValidationActionText from 'client/shared/chain/get-chain-validation-action-text'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; +import useEtherscanRedirects from 'client/shared/router/useEtherscanRedirects'; + import config from 'configs/app'; -import getCheckedSummedAddress from 'lib/address/getCheckedSummedAddress'; -import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery'; -import useAddressMetadataInitUpdate from 'lib/address/useAddressMetadataInitUpdate'; -import useApiQuery from 'lib/api/useApiQuery'; import { useAddressClusters } from 'lib/clusters/useAddressClusters'; -import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery'; import useIsSafeAddress from 'lib/hooks/useIsSafeAddress'; -import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import useEtherscanRedirects from 'lib/router/useEtherscanRedirects'; -import useSocketChannel from 'lib/socket/useSocketChannel'; -import useSocketMessage from 'lib/socket/useSocketMessage'; import useFetchXStarScore from 'lib/xStarScore/useFetchXStarScore'; -import { ADDRESS_TABS_COUNTERS } from 'stubs/address'; -import { USER_OPS_ACCOUNT } from 'stubs/userOps'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; -import Address3rdPartyWidgets from 'ui/address/Address3rdPartyWidgets'; -import useAddress3rdPartyWidgets from 'ui/address/address3rdPartyWidgets/useAddress3rdPartyWidgets'; -import AddressAccountHistory from 'ui/address/AddressAccountHistory'; -import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; -import AddressCoinBalance from 'ui/address/AddressCoinBalance'; -import AddressContract from 'ui/address/AddressContract'; -import AddressDeposits from 'ui/address/AddressDeposits'; -import AddressDetails from 'ui/address/AddressDetails'; -import AddressEpochRewards from 'ui/address/AddressEpochRewards'; -import AddressInternalTxs from 'ui/address/AddressInternalTxs'; -import AddressLogs from 'ui/address/AddressLogs'; -import AddressMud from 'ui/address/AddressMud'; -import AddressMultichainInfoButton from 'ui/address/AddressMultichainInfoButton'; -import AddressTokens from 'ui/address/AddressTokens'; -import AddressTokenTransfers, { ADDRESS_TOKEN_TRANSFERS_TAB_IDS } from 'ui/address/AddressTokenTransfers'; -import AddressTxs, { ADDRESS_TXS_TAB_IDS } from 'ui/address/AddressTxs'; -import AddressUserOps from 'ui/address/AddressUserOps'; -import AddressWithdrawals from 'ui/address/AddressWithdrawals'; -import AddressClusters from 'ui/address/clusters/AddressClusters'; -import { CONTRACT_TAB_IDS } from 'ui/address/contract/utils'; -import AddressAlerts from 'ui/address/details/AddressAlerts'; -import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; -import AddressQrCode from 'ui/address/details/AddressQrCode'; -import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains'; -import SolidityscanReport from 'ui/address/SolidityscanReport'; -import useAddressCountersQuery from 'ui/address/utils/useAddressCountersQuery'; -import useAddressQuery from 'ui/address/utils/useAddressQuery'; -import useCheckAddressFormat from 'ui/address/utils/useCheckAddressFormat'; -import useCheckDomainNameParam from 'ui/address/utils/useCheckDomainNameParam'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TextAd from 'ui/shared/ad/TextAd'; -import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import EntityTags from 'ui/shared/EntityTags/EntityTags'; import formatUserTags from 'ui/shared/EntityTags/formatUserTags'; @@ -199,7 +205,7 @@ const AddressPageContent = () => { return tabName; }, component: ( - 0) } @@ -265,12 +271,12 @@ const AddressPageContent = () => { component: , subTabs: TOKEN_TABS, }, - { + config.UI.views.internalTx.isEnabled ? { id: 'internal_txns', title: 'Internal txns', count: addressTabsCountersQuery.data?.internal_transactions_count, component: , - }, + } : undefined, addressTabsCountersQuery.data?.celo_election_rewards_count ? { id: 'epoch_rewards', title: 'Epoch rewards', @@ -285,7 +291,7 @@ const AddressPageContent = () => { addressTabsCountersQuery.data?.validations_count ? { id: 'blocks_validated', - title: `Blocks ${ getNetworkValidationActionText() }`, + title: `Blocks ${ getChainValidationActionText() }`, count: addressTabsCountersQuery.data?.validations_count, component: , } : @@ -459,7 +465,7 @@ const AddressPageContent = () => { icon={{ color: isSafeAddress ? { _light: 'black', _dark: 'white' } : undefined }} /> { !isLoading && addressQuery.data?.is_contract && addressQuery.data.token && - } + } { !isLoading && !addressQuery.data?.is_contract && config.features.account.isEnabled && ( ) } diff --git a/client/slices/address/pages/details/AddressPageMock.tsx b/client/slices/address/pages/details/AddressPageMock.tsx new file mode 100644 index 0000000000..f7be22444c --- /dev/null +++ b/client/slices/address/pages/details/AddressPageMock.tsx @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useRouter } from 'next/router'; +import React from 'react'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +const MockAddressPage = ({ children }: { children: React.JSX.Element }): React.JSX.Element => { + const router = useRouter(); + + const { data } = useApiQuery('general:address', { + pathParams: { hash: router.query.hash?.toString() }, + queryOptions: { enabled: Boolean(router.query.hash) }, + }); + + if (!data) { + return
; + } + + return children; +}; + +export default MockAddressPage; diff --git a/client/slices/address/pages/details/__screenshots__/Address.pw.tsx_default_degradation-view-1.png b/client/slices/address/pages/details/__screenshots__/Address.pw.tsx_default_degradation-view-1.png new file mode 100644 index 0000000000..d48e795674 Binary files /dev/null and b/client/slices/address/pages/details/__screenshots__/Address.pw.tsx_default_degradation-view-1.png differ diff --git a/ui/address/AddressBlocksValidated.tsx b/client/slices/address/pages/details/blocks-validated/AddressBlocksValidated.tsx similarity index 88% rename from ui/address/AddressBlocksValidated.tsx rename to client/slices/address/pages/details/blocks-validated/AddressBlocksValidated.tsx index cebe99b3c5..02c2c45f1b 100644 --- a/ui/address/AddressBlocksValidated.tsx +++ b/client/slices/address/pages/details/blocks-validated/AddressBlocksValidated.tsx @@ -1,18 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import React from 'react'; -import type { SocketMessage } from 'lib/socket/types'; -import type { AddressBlocksValidatedResponse } from 'types/api/address'; +import type { SocketMessage } from 'client/api/socket/types'; +import type { AddressBlocksValidatedResponse } from 'client/slices/address/types/api'; + +import { getResourceKey } from 'client/api/hooks/useApiQuery'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import { BLOCK } from 'client/slices/block/stubs/block'; + +import { currencyUnits } from 'client/shared/chain/units'; +import useIsMounted from 'client/shared/hooks/useIsMounted'; import config from 'configs/app'; -import { getResourceKey } from 'lib/api/useApiQuery'; -import useIsMounted from 'lib/hooks/useIsMounted'; -import useSocketChannel from 'lib/socket/useSocketChannel'; -import useSocketMessage from 'lib/socket/useSocketMessage'; -import { currencyUnits } from 'lib/units'; -import { BLOCK } from 'stubs/block'; import { generateListStub } from 'stubs/utils'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; @@ -22,8 +27,8 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle'; -import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem'; -import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem'; +import AddressBlocksValidatedListItem from './AddressBlocksValidatedListItem'; +import AddressBlocksValidatedTableItem from './AddressBlocksValidatedTableItem'; const OVERLOAD_COUNT = 75; diff --git a/ui/address/blocksValidated/AddressBlocksValidatedListItem.tsx b/client/slices/address/pages/details/blocks-validated/AddressBlocksValidatedListItem.tsx similarity index 85% rename from ui/address/blocksValidated/AddressBlocksValidatedListItem.tsx rename to client/slices/address/pages/details/blocks-validated/AddressBlocksValidatedListItem.tsx index 3a247c9069..abf5e53d46 100644 --- a/ui/address/blocksValidated/AddressBlocksValidatedListItem.tsx +++ b/client/slices/address/pages/details/blocks-validated/AddressBlocksValidatedListItem.tsx @@ -1,15 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Text, Flex } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { Block } from 'types/api/block'; +import type { Block } from 'client/slices/block/types/api'; + +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import getBlockTotalReward from 'client/slices/block/utils/get-block-total-reward'; +import GasUsed from 'client/slices/gas/components/GasUsed'; + +import { currencyUnits } from 'client/shared/chain/units'; import config from 'configs/app'; -import getBlockTotalReward from 'lib/block/getBlockTotalReward'; -import { currencyUnits } from 'lib/units'; import { Skeleton } from 'toolkit/chakra/skeleton'; -import BlockGasUsed from 'ui/shared/block/BlockGasUsed'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; import SimpleValue from 'ui/shared/value/SimpleValue'; @@ -50,7 +54,7 @@ const AddressBlocksValidatedListItem = (props: Props) => { { BigNumber(props.gas_used || 0).toFormat() } - { { BigNumber(props.gas_used || 0).toFormat() } - { }, [ chartsConfig, data ]); return ( - ); }; diff --git a/ui/address/coinBalance/AddressCoinBalanceHistory.tsx b/client/slices/address/pages/details/coin-balance/AddressCoinBalanceHistory.tsx similarity index 92% rename from ui/address/coinBalance/AddressCoinBalanceHistory.tsx rename to client/slices/address/pages/details/coin-balance/AddressCoinBalanceHistory.tsx index a74e32ffc7..c0c67924bb 100644 --- a/ui/address/coinBalance/AddressCoinBalanceHistory.tsx +++ b/client/slices/address/pages/details/coin-balance/AddressCoinBalanceHistory.tsx @@ -1,13 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import type { UseQueryResult } from '@tanstack/react-query'; import React from 'react'; -import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; +import type { AddressCoinBalanceHistoryResponse } from 'client/slices/address/types/api'; import type { PaginationParams } from 'ui/shared/pagination/types'; -import type { ResourceError } from 'lib/api/resources'; +import type { ResourceError } from 'client/api/resources'; + +import { currencyUnits } from 'client/shared/chain/units'; + import { useMultichainContext } from 'lib/contexts/multichain'; -import { currencyUnits } from 'lib/units'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; diff --git a/ui/address/coinBalance/AddressCoinBalanceListItem.tsx b/client/slices/address/pages/details/coin-balance/AddressCoinBalanceListItem.tsx similarity index 90% rename from ui/address/coinBalance/AddressCoinBalanceListItem.tsx rename to client/slices/address/pages/details/coin-balance/AddressCoinBalanceListItem.tsx index e1e6f1cb77..262666713f 100644 --- a/ui/address/coinBalance/AddressCoinBalanceListItem.tsx +++ b/client/slices/address/pages/details/coin-balance/AddressCoinBalanceListItem.tsx @@ -1,14 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Stat, Flex } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; +import type { AddressCoinBalanceHistoryItem } from 'client/slices/address/types/api'; import type { ClusterChainConfig } from 'types/multichain'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + import { Skeleton } from 'toolkit/chakra/skeleton'; import { ZERO } from 'toolkit/utils/consts'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; diff --git a/ui/address/coinBalance/AddressCoinBalanceTableItem.tsx b/client/slices/address/pages/details/coin-balance/AddressCoinBalanceTableItem.tsx similarity index 90% rename from ui/address/coinBalance/AddressCoinBalanceTableItem.tsx rename to client/slices/address/pages/details/coin-balance/AddressCoinBalanceTableItem.tsx index 795b5d9dad..e166e8686e 100644 --- a/ui/address/coinBalance/AddressCoinBalanceTableItem.tsx +++ b/client/slices/address/pages/details/coin-balance/AddressCoinBalanceTableItem.tsx @@ -1,15 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Stat } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; +import type { AddressCoinBalanceHistoryItem } from 'client/slices/address/types/api'; import type { ClusterChainConfig } from 'types/multichain'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + import { Skeleton } from 'toolkit/chakra/skeleton'; import { TableCell, TableRow } from 'toolkit/chakra/table'; import { ZERO } from 'toolkit/utils/consts'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; import ChainIcon from 'ui/shared/externalChains/ChainIcon'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; diff --git a/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..53da5096ba Binary files /dev/null and b/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-1.png b/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..c89f861552 Binary files /dev/null and b/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_default_mobile-base-view-1.png b/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..3f5fe63da2 Binary files /dev/null and b/client/slices/address/pages/details/coin-balance/__screenshots__/AddressCoinBalance.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/address/details/AddressAlerts.pw.tsx b/client/slices/address/pages/details/info/AddressAlerts.pw.tsx similarity index 100% rename from ui/address/details/AddressAlerts.pw.tsx rename to client/slices/address/pages/details/info/AddressAlerts.pw.tsx diff --git a/ui/address/details/AddressAlerts.tsx b/client/slices/address/pages/details/info/AddressAlerts.tsx similarity index 91% rename from ui/address/details/AddressAlerts.tsx rename to client/slices/address/pages/details/info/AddressAlerts.tsx index 92bbd04603..1d9888f8ff 100644 --- a/ui/address/details/AddressAlerts.tsx +++ b/client/slices/address/pages/details/info/AddressAlerts.tsx @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; -import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata'; +import type { AddressMetadataTagFormatted } from 'client/features/address-metadata/types/view'; import type { AlertProps } from 'toolkit/chakra/alert'; import { Alert } from 'toolkit/chakra/alert'; diff --git a/ui/address/details/AddressAlternativeFormat.tsx b/client/slices/address/pages/details/info/AddressAlternativeFormat.tsx similarity index 85% rename from ui/address/details/AddressAlternativeFormat.tsx rename to client/slices/address/pages/details/info/AddressAlternativeFormat.tsx index 023ed2c20e..8116164c0d 100644 --- a/ui/address/details/AddressAlternativeFormat.tsx +++ b/client/slices/address/pages/details/info/AddressAlternativeFormat.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import { BECH_32_SEPARATOR, toBech32Address } from 'client/slices/address/utils/bech32'; + import config from 'configs/app'; -import { BECH_32_SEPARATOR, toBech32Address } from 'lib/address/bech32'; import { useSettingsContext } from 'lib/contexts/settings'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; interface Props { isLoading?: boolean; diff --git a/ui/address/details/AddressBalance.tsx b/client/slices/address/pages/details/info/AddressBalance.tsx similarity index 83% rename from ui/address/details/AddressBalance.tsx rename to client/slices/address/pages/details/info/AddressBalance.tsx index dd26ef049d..c4395def4c 100644 --- a/ui/address/details/AddressBalance.tsx +++ b/client/slices/address/pages/details/info/AddressBalance.tsx @@ -1,15 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import type { SocketMessage } from 'lib/socket/types'; -import type { Address } from 'types/api/address'; +import type { SocketMessage } from 'client/api/socket/types'; +import type { Address } from 'client/slices/address/types/api'; + +import { getResourceKey } from 'client/api/hooks/useApiQuery'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import NativeTokenIcon from 'client/slices/token/components/icon/TokenIconNative'; + +import { currencyUnits } from 'client/shared/chain/units'; -import { getResourceKey } from 'lib/api/useApiQuery'; -import useSocketChannel from 'lib/socket/useSocketChannel'; -import useSocketMessage from 'lib/socket/useSocketMessage'; -import { currencyUnits } from 'lib/units'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; -import NativeTokenIcon from 'ui/shared/NativeTokenIcon'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; interface Props { diff --git a/ui/address/details/AddressCounterItem.tsx b/client/slices/address/pages/details/info/AddressCounterItem.tsx similarity index 90% rename from ui/address/details/AddressCounterItem.tsx rename to client/slices/address/pages/details/info/AddressCounterItem.tsx index 9e6a0d6fb0..217fd7cd56 100644 --- a/ui/address/details/AddressCounterItem.tsx +++ b/client/slices/address/pages/details/info/AddressCounterItem.tsx @@ -1,12 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { UseQueryResult } from '@tanstack/react-query'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { AddressCounters } from 'types/api/address'; +import type { AddressCounters } from 'client/slices/address/types/api'; import { route } from 'nextjs/routes'; -import type { ResourceError } from 'lib/api/resources'; +import type { ResourceError } from 'client/api/resources'; + import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; diff --git a/ui/address/AddressDetails.pw.tsx b/client/slices/address/pages/details/info/AddressDetails.pw.tsx similarity index 93% rename from ui/address/AddressDetails.pw.tsx rename to client/slices/address/pages/details/info/AddressDetails.pw.tsx index b7110df1bc..000d73a75e 100644 --- a/ui/address/AddressDetails.pw.tsx +++ b/client/slices/address/pages/details/info/AddressDetails.pw.tsx @@ -1,17 +1,19 @@ import React from 'react'; -import * as addressMock from 'mocks/address/address'; -import * as countersMock from 'mocks/address/counters'; -import * as tokensMock from 'mocks/address/tokens'; -import * as widgetsMock from 'mocks/address/widgets'; +import type { AddressCountersQuery } from 'client/slices/address/hooks/useAddressCountersQuery'; +import type { AddressQuery } from 'client/slices/address/hooks/useAddressQuery'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as countersMock from 'client/slices/address/mocks/counters'; +import * as tokensMock from 'client/slices/token/mocks/address-tokens'; + +import * as widgetsMock from 'client/features/address-3rd-party-widgets/mocks'; + import type { TestFnArgs } from 'playwright/lib'; import { test, expect, devices } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; +import MockAddressPage from '../AddressPageMock'; import AddressDetails from './AddressDetails'; -import MockAddressPage from './testUtils/MockAddressPage'; -import type { AddressCountersQuery } from './utils/useAddressCountersQuery'; -import type { AddressQuery } from './utils/useAddressQuery'; const WIDGETS_CONFIG_URL = 'http://localhost:4000/address-3rd-party-widgets-config.json'; const ADDRESS_HASH = addressMock.hash; diff --git a/ui/address/AddressDetails.tsx b/client/slices/address/pages/details/info/AddressDetails.tsx similarity index 84% rename from ui/address/AddressDetails.tsx rename to client/slices/address/pages/details/info/AddressDetails.tsx index 3858ff4709..ae22a078d4 100644 --- a/ui/address/AddressDetails.tsx +++ b/client/slices/address/pages/details/info/AddressDetails.tsx @@ -1,36 +1,41 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Text } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import type { AddressCountersQuery } from 'client/slices/address/hooks/useAddressCountersQuery'; +import type { AddressQuery } from 'client/slices/address/hooks/useAddressQuery'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import ContractCreationStatus from 'client/slices/contract/components/ContractCreationStatus'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + +import Address3rdPartyWidgets from 'client/features/address-3rd-party-widgets/pages/address/Address3rdPartyWidgets'; +import useAddress3rdPartyWidgets from 'client/features/address-3rd-party-widgets/pages/address/useAddress3rdPartyWidgets'; +import AddressCeloAccount from 'client/features/chain-variants/celo/pages/address/AddressCeloAccount'; +import FilecoinActorTag from 'client/features/chain-variants/filecoin/pages/address/FilecoinActorTag'; + +import getChainValidationActionText from 'client/shared/chain/get-chain-validation-action-text'; +import getChainValidatorTitle from 'client/shared/chain/get-chain-validator-title'; +import throwOnResourceLoadError from 'client/shared/errors/throw-on-resource-load-error'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import config from 'configs/app'; -import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; -import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText'; -import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import DetailedInfoSponsoredItem from 'ui/shared/DetailedInfo/DetailedInfoSponsoredItem'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; -import ContractCreationStatus from 'ui/shared/statusTag/ContractCreationStatus'; - -import Address3rdPartyWidgets from './Address3rdPartyWidgets'; -import useAddress3rdPartyWidgets from './address3rdPartyWidgets/useAddress3rdPartyWidgets'; -import AddressAlternativeFormat from './details/AddressAlternativeFormat'; -import AddressBalance from './details/AddressBalance'; -import AddressCeloAccount from './details/AddressCeloAccount'; -import AddressImplementations from './details/AddressImplementations'; -import AddressNameInfo from './details/AddressNameInfo'; -import AddressNetWorth from './details/AddressNetWorth'; -import FilecoinActorTag from './filecoin/FilecoinActorTag'; -import TokenSelect from './tokenSelect/TokenSelect'; -import type { AddressCountersQuery } from './utils/useAddressCountersQuery'; -import type { AddressQuery } from './utils/useAddressQuery'; + +import AddressAlternativeFormat from './AddressAlternativeFormat'; +import AddressBalance from './AddressBalance'; +import AddressCounterItem from './AddressCounterItem'; +import AddressImplementations from './AddressImplementations'; +import AddressNameInfo from './AddressNameInfo'; +import AddressNetWorth from './AddressNetWorth'; +import TokenSelect from './token-select/TokenSelect'; interface Props { addressQuery: AddressQuery; @@ -269,10 +274,10 @@ const AddressDetails = ({ addressQuery, countersQuery, isLoading }: Props) => { { data.has_validated_blocks && ( <> - { `Blocks ${ getNetworkValidationActionText() }` } + { `Blocks ${ getChainValidationActionText() }` } { addressQuery.data ? ( diff --git a/ui/address/details/AddressImplementations.tsx b/client/slices/address/pages/details/info/AddressImplementations.tsx similarity index 84% rename from ui/address/details/AddressImplementations.tsx rename to client/slices/address/pages/details/info/AddressImplementations.tsx index f726e5557c..ffeb71a8d6 100644 --- a/ui/address/details/AddressImplementations.tsx +++ b/client/slices/address/pages/details/info/AddressImplementations.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { AddressImplementation } from 'types/api/addressParams'; -import type { SmartContractProxyType } from 'types/api/contract'; +import type { AddressImplementation } from 'client/slices/address/types/api'; +import type { SmartContractProxyType } from 'client/slices/contract/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; interface Props { data: Array; diff --git a/ui/address/details/AddressNameInfo.tsx b/client/slices/address/pages/details/info/AddressNameInfo.tsx similarity index 89% rename from ui/address/details/AddressNameInfo.tsx rename to client/slices/address/pages/details/info/AddressNameInfo.tsx index 8e08fe5e6a..4f83a6c5d1 100644 --- a/ui/address/details/AddressNameInfo.tsx +++ b/client/slices/address/pages/details/info/AddressNameInfo.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { Address } from 'types/api/address'; +import type { Address } from 'client/slices/address/types/api'; + +import TokenEntity from 'client/slices/token/components/entity/TokenEntity'; import { Skeleton } from 'toolkit/chakra/skeleton'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; -import TokenEntity from 'ui/shared/entities/token/TokenEntity'; interface Props { data: Pick; diff --git a/ui/address/details/AddressNetWorth.pw.tsx b/client/slices/address/pages/details/info/AddressNetWorth.pw.tsx similarity index 96% rename from ui/address/details/AddressNetWorth.pw.tsx rename to client/slices/address/pages/details/info/AddressNetWorth.pw.tsx index 65505cf379..6ceb7780df 100644 --- a/ui/address/details/AddressNetWorth.pw.tsx +++ b/client/slices/address/pages/details/info/AddressNetWorth.pw.tsx @@ -1,8 +1,9 @@ import { Box } from '@chakra-ui/react'; import React from 'react'; -import * as addressMock from 'mocks/address/address'; -import * as tokensMock from 'mocks/address/tokens'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as tokensMock from 'client/slices/token/mocks/address-tokens'; + import { test, expect } from 'playwright/lib'; import AddressNetWorth from './AddressNetWorth'; diff --git a/ui/address/details/AddressNetWorth.tsx b/client/slices/address/pages/details/info/AddressNetWorth.tsx similarity index 84% rename from ui/address/details/AddressNetWorth.tsx rename to client/slices/address/pages/details/info/AddressNetWorth.tsx index 973c91fc8f..40f977657f 100644 --- a/ui/address/details/AddressNetWorth.tsx +++ b/client/slices/address/pages/details/info/AddressNetWorth.tsx @@ -1,20 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Text, HStack } from '@chakra-ui/react'; import React from 'react'; -import type { Address } from 'types/api/address'; +import type { Address } from 'client/slices/address/types/api'; + +import useFetchTokens from 'client/slices/token/pages/address/useFetchTokens'; +import { getTokensTotalInfo } from 'client/slices/token/pages/address/utils'; + +import AddressMultichainButton from 'client/features/multichain-button/pages/address/AddressMultichainButton'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; import config from 'configs/app'; -import * as mixpanel from 'lib/mixpanel/index'; import { Skeleton } from 'toolkit/chakra/skeleton'; import TextSeparator from 'ui/shared/TextSeparator'; import calculateUsdValue from 'ui/shared/value/calculateUsdValue'; import SimpleValue from 'ui/shared/value/SimpleValue'; import { DEFAULT_ACCURACY_USD } from 'ui/shared/value/utils'; -import { getTokensTotalInfo } from '../utils/tokenUtils'; -import useFetchTokens from '../utils/useFetchTokens'; -import AddressMultichainButton from './AddressMultichainButton'; - const multichainFeature = config.features.multichainButton; type Props = { diff --git a/ui/address/details/AddressQrCode.pw.tsx b/client/slices/address/pages/details/info/AddressQrCode.pw.tsx similarity index 84% rename from ui/address/details/AddressQrCode.pw.tsx rename to client/slices/address/pages/details/info/AddressQrCode.pw.tsx index 5d88a498b2..f9e1cff6c5 100644 --- a/ui/address/details/AddressQrCode.pw.tsx +++ b/client/slices/address/pages/details/info/AddressQrCode.pw.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import * as addressMock from 'mocks/address/address'; +import * as addressMock from 'client/slices/address/mocks/address'; + import { test, expect } from 'playwright/lib'; import AddressQrCode from './AddressQrCode'; diff --git a/ui/address/details/AddressQrCode.tsx b/client/slices/address/pages/details/info/AddressQrCode.tsx similarity index 89% rename from ui/address/details/AddressQrCode.tsx rename to client/slices/address/pages/details/info/AddressQrCode.tsx index 5771112245..e52d4711f3 100644 --- a/ui/address/details/AddressQrCode.tsx +++ b/client/slices/address/pages/details/info/AddressQrCode.tsx @@ -1,18 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import QRCode from 'qrcode'; import React from 'react'; -import getPageType from 'lib/mixpanel/getPageType'; -import * as mixpanel from 'lib/mixpanel/index'; -import { useRollbar } from 'lib/rollbar'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; +import { useRollbar } from 'client/shared/monitoring/rollbar'; + import { Alert } from 'toolkit/chakra/alert'; import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog'; import { IconButton } from 'toolkit/chakra/icon-button'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; import { useDisclosure } from 'toolkit/hooks/useDisclosure'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import IconSvg from 'ui/shared/IconSvg'; const SVG_OPTIONS = { @@ -34,7 +37,7 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => { const [ qr, setQr ] = React.useState(''); const [ error, setError ] = React.useState(''); - const pageType = getPageType(router.pathname); + const pageType = mixpanel.getPageType(router.pathname); React.useEffect(() => { if (open) { diff --git a/ui/address/details/__screenshots__/AddressAlerts.pw.tsx_default_base-view-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressAlerts.pw.tsx_default_base-view-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressAlerts.pw.tsx_default_base-view-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressAlerts.pw.tsx_default_base-view-1.png diff --git a/ui/address/details/__screenshots__/AddressAlerts.pw.tsx_default_with-scam-token-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressAlerts.pw.tsx_default_with-scam-token-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressAlerts.pw.tsx_default_with-scam-token-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressAlerts.pw.tsx_default_with-scam-token-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_contract-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_contract-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_contract-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_contract-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_filecoin-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_filecoin-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_filecoin-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_filecoin-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-contract-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-contract-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-contract-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-contract-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-filecoin-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-filecoin-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-filecoin-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-filecoin-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-validator-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-validator-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-validator-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-validator-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-with-widgets-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-with-widgets-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-with-widgets-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_mobile-with-widgets-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_validator-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_validator-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_validator-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_validator-1.png diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_with-widgets-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_with-widgets-1.png similarity index 100% rename from ui/address/__screenshots__/AddressDetails.pw.tsx_default_with-widgets-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressDetails.pw.tsx_default_with-widgets-1.png diff --git a/ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_dark-color-mode_with-single-multichain-button-internal-dark-mode-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_dark-color-mode_with-single-multichain-button-internal-dark-mode-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_dark-color-mode_with-single-multichain-button-internal-dark-mode-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_dark-color-mode_with-single-multichain-button-internal-dark-mode-1.png diff --git a/ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_base-view-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_base-view-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_base-view-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_base-view-1.png diff --git a/ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-multichain-button-internal-small-screen-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-multichain-button-internal-small-screen-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-multichain-button-internal-small-screen-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-multichain-button-internal-small-screen-1.png diff --git a/ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-external-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-external-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-external-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-external-1.png diff --git a/ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-internal-dark-mode-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-internal-dark-mode-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-internal-dark-mode-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-single-multichain-button-internal-dark-mode-1.png diff --git a/ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-two-multichain-button-and-promo-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-two-multichain-button-and-promo-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressNetWorth.pw.tsx_default_with-two-multichain-button-and-promo-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressNetWorth.pw.tsx_default_with-two-multichain-button-and-promo-1.png diff --git a/client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_dark-color-mode_default-view-mobile-dark-mode-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_dark-color-mode_default-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..b39bc32132 Binary files /dev/null and b/client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_dark-color-mode_default-view-mobile-dark-mode-1.png differ diff --git a/client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_default_default-view-mobile-dark-mode-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_default_default-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..45458f63dd Binary files /dev/null and b/client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_default_default-view-mobile-dark-mode-1.png differ diff --git a/ui/address/details/__screenshots__/AddressQrCode.pw.tsx_mobile_default-view-mobile-dark-mode-1.png b/client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_mobile_default-view-mobile-dark-mode-1.png similarity index 100% rename from ui/address/details/__screenshots__/AddressQrCode.pw.tsx_mobile_default-view-mobile-dark-mode-1.png rename to client/slices/address/pages/details/info/__screenshots__/AddressQrCode.pw.tsx_mobile_default-view-mobile-dark-mode-1.png diff --git a/ui/address/tokenSelect/TokenSelect.pw.tsx b/client/slices/address/pages/details/info/token-select/TokenSelect.pw.tsx similarity index 93% rename from ui/address/tokenSelect/TokenSelect.pw.tsx rename to client/slices/address/pages/details/info/token-select/TokenSelect.pw.tsx index feeba1fbe5..79bec4d460 100644 --- a/ui/address/tokenSelect/TokenSelect.pw.tsx +++ b/client/slices/address/pages/details/info/token-select/TokenSelect.pw.tsx @@ -1,11 +1,12 @@ import { Flex } from '@chakra-ui/react'; import React from 'react'; -import * as addressMock from 'mocks/address/address'; -import * as tokensMock from 'mocks/address/tokens'; -import { tokenInfoERC20c, tokenInfoERC20a } from 'mocks/tokens/tokenInfo'; +import * as addressMock from 'client/slices/address/mocks/address'; +import MockAddressPage from 'client/slices/address/pages/details/AddressPageMock'; +import * as tokensMock from 'client/slices/token/mocks/address-tokens'; +import { tokenInfoERC20c, tokenInfoERC20a } from 'client/slices/token/mocks/info'; + import { test, expect, devices } from 'playwright/lib'; -import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; import TokenSelect from './TokenSelect'; diff --git a/ui/address/tokenSelect/TokenSelect.tsx b/client/slices/address/pages/details/info/token-select/TokenSelect.tsx similarity index 86% rename from ui/address/tokenSelect/TokenSelect.tsx rename to client/slices/address/pages/details/info/token-select/TokenSelect.tsx index a0e318b885..8f1db1f172 100644 --- a/ui/address/tokenSelect/TokenSelect.tsx +++ b/client/slices/address/pages/details/info/token-select/TokenSelect.tsx @@ -1,25 +1,30 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import { useQueryClient, useIsFetching } from '@tanstack/react-query'; import { sumBy } from 'es-toolkit'; import { useRouter } from 'next/router'; import React from 'react'; -import type { Address } from 'types/api/address'; +import type { Address } from 'client/slices/address/types/api'; import { route } from 'nextjs/routes'; -import { getResourceKey } from 'lib/api/useApiQuery'; +import { getResourceKey } from 'client/api/hooks/useApiQuery'; + +import useFetchTokens from 'client/slices/token/pages/address/useFetchTokens'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import { useMultichainContext } from 'lib/contexts/multichain'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import * as mixpanel from 'lib/mixpanel/index'; -import getQueryParamString from 'lib/router/getQueryParamString'; import { IconButton } from 'toolkit/chakra/icon-button'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; import IconSvg from 'ui/shared/IconSvg'; -import useFetchTokens from '../utils/useFetchTokens'; import TokenSelectDesktop from './TokenSelectDesktop'; import TokenSelectMobile from './TokenSelectMobile'; diff --git a/ui/address/tokenSelect/TokenSelectButton.tsx b/client/slices/address/pages/details/info/token-select/TokenSelectButton.tsx similarity index 90% rename from ui/address/tokenSelect/TokenSelectButton.tsx rename to client/slices/address/pages/details/info/token-select/TokenSelectButton.tsx index 7effe26b92..42ebd0cc03 100644 --- a/ui/address/tokenSelect/TokenSelectButton.tsx +++ b/client/slices/address/pages/details/info/token-select/TokenSelectButton.tsx @@ -1,15 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, chakra } from '@chakra-ui/react'; import React from 'react'; import type { FormattedData } from './types'; -import * as mixpanel from 'lib/mixpanel/index'; +import { getTokensTotalInfo } from 'client/slices/token/pages/address/utils'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; + import { Button } from 'toolkit/chakra/button'; import { space, thinsp } from 'toolkit/utils/htmlEntities'; import IconSvg from 'ui/shared/IconSvg'; -import { getTokensTotalInfo } from '../utils/tokenUtils'; - interface Props { isOpen: boolean; isLoading?: boolean; diff --git a/ui/address/tokenSelect/TokenSelectDesktop.tsx b/client/slices/address/pages/details/info/token-select/TokenSelectDesktop.tsx similarity index 95% rename from ui/address/tokenSelect/TokenSelectDesktop.tsx rename to client/slices/address/pages/details/info/token-select/TokenSelectDesktop.tsx index bf5ee7f68e..886223a1e7 100644 --- a/ui/address/tokenSelect/TokenSelectDesktop.tsx +++ b/client/slices/address/pages/details/info/token-select/TokenSelectDesktop.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormattedData } from './types'; diff --git a/ui/address/tokenSelect/TokenSelectItem.tsx b/client/slices/address/pages/details/info/token-select/TokenSelectItem.tsx similarity index 85% rename from ui/address/tokenSelect/TokenSelectItem.tsx rename to client/slices/address/pages/details/info/token-select/TokenSelectItem.tsx index 15cb93f202..524368c431 100644 --- a/ui/address/tokenSelect/TokenSelectItem.tsx +++ b/client/slices/address/pages/details/info/token-select/TokenSelectItem.tsx @@ -1,20 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Flex } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; +import { isConfidentialTokenType, isFungibleTokenType } from 'client/slices/token/utils/token-types'; + import { route } from 'nextjs/routes'; +import TokenEntity from 'client/slices/token/components/entity/TokenEntity'; +import type { TokenEnhancedData } from 'client/slices/token/pages/address/utils'; + import config from 'configs/app'; import multichainConfig from 'configs/multichain'; -import { isConfidentialTokenType, isFungibleTokenType } from 'lib/token/tokenTypes'; import { Link } from 'toolkit/chakra/link'; import { TruncatedText } from 'toolkit/components/truncation/TruncatedText'; import NativeTokenTag from 'ui/shared/celo/NativeTokenTag'; -import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import calculateUsdValue from 'ui/shared/value/calculateUsdValue'; -import type { TokenEnhancedData } from '../utils/tokenUtils'; - interface Props { data: TokenEnhancedData; } @@ -46,11 +49,11 @@ const TokenSelectItem = ({ data }: Props) => { ); } - const isFungibleToken = isFungibleTokenType(data.token.type); + const isFungibleToken = isFungibleTokenType(data.token.type, chain?.app_config); if (isFungibleToken) { const tokenDecimals = Number(data.token.decimals ?? 18); - const text = `${ BigNumber(data.value).dividedBy(10 ** tokenDecimals).toFormat() } ${ data.token.symbol || '' }`; + const text = `${ BigNumber(data.value ?? '0').dividedBy(10 ** tokenDecimals).toFormat() } ${ data.token.symbol || '' }`; return ( <> @@ -62,7 +65,7 @@ const TokenSelectItem = ({ data }: Props) => { switch (data.token.type) { case 'ERC-721': { - const text = `${ BigNumber(data.value).toFormat() } ${ data.token.symbol || '' }`; + const text = `${ BigNumber(data.value ?? '0').toFormat() } ${ data.token.symbol || '' }`; return ; } case 'ERC-1155': { @@ -72,7 +75,7 @@ const TokenSelectItem = ({ data }: Props) => { #{ data.token_id || 0 } - { BigNumber(data.value).toFormat() } + { BigNumber(data.value ?? '0').toFormat() } ); diff --git a/ui/address/tokenSelect/TokenSelectMenu.tsx b/client/slices/address/pages/details/info/token-select/TokenSelectMenu.tsx similarity index 91% rename from ui/address/tokenSelect/TokenSelectMenu.tsx rename to client/slices/address/pages/details/info/token-select/TokenSelectMenu.tsx index 87092467ed..6c6d41bde1 100644 --- a/ui/address/tokenSelect/TokenSelectMenu.tsx +++ b/client/slices/address/pages/details/info/token-select/TokenSelectMenu.tsx @@ -1,17 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Text, Box, Flex } from '@chakra-ui/react'; import { sumBy } from 'es-toolkit'; import React from 'react'; import type { FormattedData } from './types'; +import { getTokenTypeName } from 'client/slices/token/utils/token-types'; + +import type { Sort } from 'client/slices/token/pages/address/utils'; +import { getSortingFn, sortTokenGroups } from 'client/slices/token/pages/address/utils'; -import { getTokenTypeName } from 'lib/token/tokenTypes'; import { Link } from 'toolkit/chakra/link'; import { FilterInput } from 'toolkit/components/filters/FilterInput'; import { thinsp } from 'toolkit/utils/htmlEntities'; import IconSvg from 'ui/shared/IconSvg'; -import type { Sort } from '../utils/tokenUtils'; -import { getSortingFn, sortTokenGroups } from '../utils/tokenUtils'; import TokenSelectItem from './TokenSelectItem'; interface Props { diff --git a/ui/address/tokenSelect/TokenSelectMobile.tsx b/client/slices/address/pages/details/info/token-select/TokenSelectMobile.tsx similarity index 95% rename from ui/address/tokenSelect/TokenSelectMobile.tsx rename to client/slices/address/pages/details/info/token-select/TokenSelectMobile.tsx index 58f563681d..4d5528b6a4 100644 --- a/ui/address/tokenSelect/TokenSelectMobile.tsx +++ b/client/slices/address/pages/details/info/token-select/TokenSelectMobile.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormattedData } from './types'; diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..ba1c4855d4 Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-2.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-2.png new file mode 100644 index 0000000000..a5f0b6d84f Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-2.png differ diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-1.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..494dba3ea5 Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-2.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-2.png new file mode 100644 index 0000000000..32655955d6 Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-2.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png similarity index 100% rename from ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png rename to client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_long-values-1.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_long-values-1.png new file mode 100644 index 0000000000..53b8417b7c Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_long-values-1.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png similarity index 100% rename from ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png rename to client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_native-token-1.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_native-token-1.png new file mode 100644 index 0000000000..e70b4c79ac Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_native-token-1.png differ diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_sort-1.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_sort-1.png new file mode 100644 index 0000000000..2cb69b1927 Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_sort-1.png differ diff --git a/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_sort-2.png b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_sort-2.png new file mode 100644 index 0000000000..43e65854b3 Binary files /dev/null and b/client/slices/address/pages/details/info/token-select/__screenshots__/TokenSelect.pw.tsx_default_sort-2.png differ diff --git a/client/slices/address/pages/details/info/token-select/types.ts b/client/slices/address/pages/details/info/token-select/types.ts new file mode 100644 index 0000000000..bf9a03469d --- /dev/null +++ b/client/slices/address/pages/details/info/token-select/types.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { TokenEnhancedData } from 'client/slices/token/pages/address/utils'; + +export type FormattedData = Record; + +export interface FormattedDataItem { + items: Array; + isOverflow: boolean; +} diff --git a/ui/address/tokenSelect/useTokenSelect.ts b/client/slices/address/pages/details/info/token-select/useTokenSelect.ts similarity index 91% rename from ui/address/tokenSelect/useTokenSelect.ts rename to client/slices/address/pages/details/info/token-select/useTokenSelect.ts index 1d8f0846be..55ca2b59f4 100644 --- a/ui/address/tokenSelect/useTokenSelect.ts +++ b/client/slices/address/pages/details/info/token-select/useTokenSelect.ts @@ -1,10 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { mapValues } from 'es-toolkit'; import React from 'react'; import type { FormattedData } from './types'; -import type { Sort } from '../utils/tokenUtils'; -import { filterTokens } from '../utils/tokenUtils'; +import type { Sort } from 'client/slices/token/pages/address/utils'; +import { filterTokens } from 'client/slices/token/pages/address/utils'; export default function useTokenSelect(data: FormattedData) { const [ searchTerm, setSearchTerm ] = React.useState(''); diff --git a/ui/address/AddressInternalTxs.pw.tsx b/client/slices/address/pages/details/internal-txs/AddressInternalTxs.pw.tsx similarity index 91% rename from ui/address/AddressInternalTxs.pw.tsx rename to client/slices/address/pages/details/internal-txs/AddressInternalTxs.pw.tsx index 94bdde8c5e..018bd9c6f6 100644 --- a/ui/address/AddressInternalTxs.pw.tsx +++ b/client/slices/address/pages/details/internal-txs/AddressInternalTxs.pw.tsx @@ -1,7 +1,8 @@ import { Box } from '@chakra-ui/react'; import React from 'react'; -import * as internalTxsMock from 'mocks/txs/internalTxs'; +import * as internalTxsMock from 'client/slices/internal-tx/mocks'; + import { test, expect } from 'playwright/lib'; import AddressInternalTxs from './AddressInternalTxs'; diff --git a/client/slices/address/pages/details/internal-txs/AddressInternalTxs.tsx b/client/slices/address/pages/details/internal-txs/AddressInternalTxs.tsx new file mode 100644 index 0000000000..24cf4f25f2 --- /dev/null +++ b/client/slices/address/pages/details/internal-txs/AddressInternalTxs.tsx @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import InternalTxsList from 'client/slices/internal-tx/components/InternalTxsList'; +import InternalTxsTable from 'client/slices/internal-tx/components/InternalTxsTable'; + +import CsvExport from 'client/features/csv-export/components/CsvExport'; + +import useIsMounted from 'client/shared/hooks/useIsMounted'; + +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; + +import AddressTxsFilter from '../txs/AddressTxsFilter'; +import useAddressInternalTxsQuery from './useAddressInternalTxsQuery'; + +type Props = { + shouldRender?: boolean; + isQueryEnabled?: boolean; +}; +const AddressInternalTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => { + const isMounted = useIsMounted(); + + const { hash, query, filterValue, onFilterChange } = useAddressInternalTxsQuery({ enabled: isQueryEnabled }); + const { data, isPlaceholderData, isError, pagination } = query; + + if (!isMounted || !shouldRender) { + return null; + } + + const content = data?.items ? ( + <> + + + + + + + + ) : null ; + + const actionBar = ( + + + + + + ); + + return ( + + { content } + + ); +}; + +export default AddressInternalTxs; diff --git a/client/slices/address/pages/details/internal-txs/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png b/client/slices/address/pages/details/internal-txs/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..3493ea7796 Binary files /dev/null and b/client/slices/address/pages/details/internal-txs/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/client/slices/address/pages/details/internal-txs/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png b/client/slices/address/pages/details/internal-txs/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..d86b0d5b7d Binary files /dev/null and b/client/slices/address/pages/details/internal-txs/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/useAddressInternalTxsQuery.ts b/client/slices/address/pages/details/internal-txs/useAddressInternalTxsQuery.ts similarity index 83% rename from ui/address/useAddressInternalTxsQuery.ts rename to client/slices/address/pages/details/internal-txs/useAddressInternalTxsQuery.ts index df1fdc5f12..a8b4202d3a 100644 --- a/ui/address/useAddressInternalTxsQuery.ts +++ b/client/slices/address/pages/details/internal-txs/useAddressInternalTxsQuery.ts @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import React from 'react'; -import { AddressFromToFilterValues, type AddressFromToFilter } from 'types/api/address'; +import { AddressFromToFilterValues, type AddressFromToFilter } from 'client/slices/address/types/api'; + +import { INTERNAL_TX } from 'client/slices/internal-tx/stubs'; + +import getFilterValueFromQuery from 'client/shared/router/get-filter-value-from-query'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; -import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import { INTERNAL_TX } from 'stubs/internalTx'; import { generateListStub } from 'stubs/utils'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; diff --git a/client/slices/address/pages/details/logs/AddressLogs.tsx b/client/slices/address/pages/details/logs/AddressLogs.tsx new file mode 100644 index 0000000000..4a2576ddea --- /dev/null +++ b/client/slices/address/pages/details/logs/AddressLogs.tsx @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useRouter } from 'next/router'; +import React from 'react'; + +import useAddressQuery from 'client/slices/address/hooks/useAddressQuery'; +import LogItem from 'client/slices/log/components/LogItem'; +import { LOG } from 'client/slices/log/stubs/log'; + +import CsvExport from 'client/features/csv-export/components/CsvExport'; + +import useIsMounted from 'client/shared/hooks/useIsMounted'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import { generateListStub } from 'stubs/utils'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +type Props = { + shouldRender?: boolean; + isQueryEnabled?: boolean; +}; + +const AddressLogs = ({ shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const isMounted = useIsMounted(); + + const hash = getQueryParamString(router.query.hash); + const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ + resourceName: 'general:address_logs', + pathParams: { hash }, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'general:address_logs'>(LOG, 3, { next_page_params: { + block_number: 9005750, + index: 42, + items_count: 50, + transaction_index: 23, + } }), + }, + }); + + const addressQuery = useAddressQuery({ hash }); + + const actionBar = ( + + + + + ); + + if (!isMounted || !shouldRender) { + return null; + } + + const content = data?.items ? data.items.map((item, index) => ( + + )) : null; + + return ( + + { content } + + ); +}; + +export default AddressLogs; diff --git a/ui/address/AddressTokenTransfers.pw.tsx b/client/slices/address/pages/details/token-transfers/AddressTokenTransfers.pw.tsx similarity index 99% rename from ui/address/AddressTokenTransfers.pw.tsx rename to client/slices/address/pages/details/token-transfers/AddressTokenTransfers.pw.tsx index ea64581e06..7ca44c8641 100644 --- a/ui/address/AddressTokenTransfers.pw.tsx +++ b/client/slices/address/pages/details/token-transfers/AddressTokenTransfers.pw.tsx @@ -1,7 +1,8 @@ import { Box } from '@chakra-ui/react'; import React from 'react'; -import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; +import * as tokenTransferMock from 'client/slices/token-transfer/mocks'; + import * as socketServer from 'playwright/fixtures/socketServer'; import { test, expect, devices } from 'playwright/lib'; diff --git a/ui/address/AddressTokenTransfers.tsx b/client/slices/address/pages/details/token-transfers/AddressTokenTransfers.tsx similarity index 82% rename from ui/address/AddressTokenTransfers.tsx rename to client/slices/address/pages/details/token-transfers/AddressTokenTransfers.tsx index eeb54a78a6..0c5a475d84 100644 --- a/ui/address/AddressTokenTransfers.tsx +++ b/client/slices/address/pages/details/token-transfers/AddressTokenTransfers.tsx @@ -1,11 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { HStack } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; +import TokenTransferFilter from 'client/slices/token-transfer/components/TokenTransferFilter'; + +import AddressAdvancedFilterLink from 'client/features/advanced-filter/components/AddressAdvancedFilterLink'; +import CsvExport from 'client/features/csv-export/components/CsvExport'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import useIsMounted from 'client/shared/hooks/useIsMounted'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import config from 'configs/app'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import useIsMounted from 'lib/hooks/useIsMounted'; -import getQueryParamString from 'lib/router/getQueryParamString'; import { INTERCHAIN_TRANSFER } from 'stubs/interchainIndexer'; import { generateListStub } from 'stubs/utils'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; @@ -13,10 +21,7 @@ import TokenTransfersCrossChainContent from 'ui/crossChain/transfers/TokenTransf import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; -import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; -import AddressAdvancedFilterLink from './AddressAdvancedFilterLink'; -import AddressCsvExportLink from './AddressCsvExportLink'; import AddressTokenTransfersLocal from './AddressTokenTransfersLocal'; import useAddressTokenTransfersQuery from './useAddressTokenTransfersQuery'; @@ -114,9 +119,13 @@ const AddressTokenTransfers = ({ shouldRender = true, overloadCount, isQueryEnab const numActiveFilters = (localQuery.filters.type?.length || 0) + (localQuery.filters.filter ? 1 : 0); + if (!localQuery.query.data?.items.length && !numActiveFilters) { + return null; + } + return ( <> - + - - + - - + ); } diff --git a/ui/address/AddressTokenTransfersLocal.tsx b/client/slices/address/pages/details/token-transfers/AddressTokenTransfersLocal.tsx similarity index 78% rename from ui/address/AddressTokenTransfersLocal.tsx rename to client/slices/address/pages/details/token-transfers/AddressTokenTransfersLocal.tsx index dec9df2414..dd537528ef 100644 --- a/ui/address/AddressTokenTransfersLocal.tsx +++ b/client/slices/address/pages/details/token-transfers/AddressTokenTransfersLocal.tsx @@ -1,21 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, HStack } from '@chakra-ui/react'; import React from 'react'; -import type { TokenType } from 'types/api/token'; +import type { TokenType } from 'client/slices/token/types/api'; + +import TokenTransferList from 'client/slices/token-transfer/components/list/TokenTransferList'; +import TokenTransferTable from 'client/slices/token-transfer/components/list/TokenTransferTable'; +import TokenTransferFilter from 'client/slices/token-transfer/components/TokenTransferFilter'; + +import AddressAdvancedFilterLink from 'client/features/advanced-filter/components/AddressAdvancedFilterLink'; +import CsvExport from 'client/features/csv-export/components/CsvExport'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; import { useMultichainContext } from 'lib/contexts/multichain'; -import useIsMobile from 'lib/hooks/useIsMobile'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import Pagination from 'ui/shared/pagination/Pagination'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; -import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; -import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; -import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; -import AddressAdvancedFilterLink from './AddressAdvancedFilterLink'; -import AddressCsvExportLink from './AddressCsvExportLink'; import type { Filters } from './useAddressTokenTransfersQuery'; import useAddressTokenTransfersSocket from './useAddressTokenTransfersSocket'; @@ -92,17 +97,22 @@ const TokenTransfersLocal = ({ query, filters, addressHash, onTypeFilterChange, isLoading={ query.isPlaceholderData } chainConfig={ multichainContext?.chain?.app_config } /> + - diff --git a/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-pagination-1.png b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-pagination-1.png new file mode 100644 index 0000000000..1a9ed99320 Binary files /dev/null and b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-pagination-1.png differ diff --git a/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-without-pagination-1.png b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-without-pagination-1.png new file mode 100644 index 0000000000..35aaecf01f Binary files /dev/null and b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-without-pagination-1.png differ diff --git a/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-pagination-1.png b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-pagination-1.png new file mode 100644 index 0000000000..55342a214f Binary files /dev/null and b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-pagination-1.png differ diff --git a/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_without-pagination-1.png b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_without-pagination-1.png new file mode 100644 index 0000000000..66683a13af Binary files /dev/null and b/client/slices/address/pages/details/token-transfers/__screenshots__/AddressTokenTransfers.pw.tsx_default_without-pagination-1.png differ diff --git a/ui/address/useAddressTokenTransfersQuery.ts b/client/slices/address/pages/details/token-transfers/useAddressTokenTransfersQuery.ts similarity index 80% rename from ui/address/useAddressTokenTransfersQuery.ts rename to client/slices/address/pages/details/token-transfers/useAddressTokenTransfersQuery.ts index 3e87b14e34..1394716f9b 100644 --- a/ui/address/useAddressTokenTransfersQuery.ts +++ b/client/slices/address/pages/details/token-transfers/useAddressTokenTransfersQuery.ts @@ -1,16 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import React from 'react'; -import type { AddressFromToFilter } from 'types/api/address'; -import { AddressFromToFilterValues } from 'types/api/address'; -import type { TokenType } from 'types/api/token'; +import type { AddressFromToFilter } from 'client/slices/address/types/api'; +import { AddressFromToFilterValues } from 'client/slices/address/types/api'; +import type { TokenType } from 'client/slices/token/types/api'; +import { getTokenTypes } from 'client/slices/token/utils/token-types'; + +import { getTokenTransfersStub } from 'client/slices/token-transfer/stubs'; + +import getFilterValueFromQuery from 'client/shared/router/get-filter-value-from-query'; +import getFilterValuesFromQuery from 'client/shared/router/get-filter-values-from-query'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; import multichainConfig from 'configs/multichain'; -import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; -import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import { getTokenTypes } from 'lib/token/tokenTypes'; -import { getTokenTransfersStub } from 'stubs/token'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; export type Filters = { diff --git a/ui/address/useAddressTokenTransfersSocket.ts b/client/slices/address/pages/details/token-transfers/useAddressTokenTransfersSocket.ts similarity index 87% rename from ui/address/useAddressTokenTransfersSocket.ts rename to client/slices/address/pages/details/token-transfers/useAddressTokenTransfersSocket.ts index 4e4213b367..3619817669 100644 --- a/ui/address/useAddressTokenTransfersSocket.ts +++ b/client/slices/address/pages/details/token-transfers/useAddressTokenTransfersSocket.ts @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import type { SocketMessage } from 'lib/socket/types'; -import type { AddressTokenTransferResponse } from 'types/api/address'; -import type { TokenTransfer } from 'types/api/tokenTransfer'; +import type { SocketMessage } from 'client/api/socket/types'; +import type { AddressTokenTransferResponse } from 'client/slices/address/types/api'; +import type { TokenTransfer } from 'client/slices/token-transfer/types/api'; + +import { getResourceKey } from 'client/api/hooks/useApiQuery'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import * as cookies from 'client/shared/storage/cookies'; import config from 'configs/app'; -import { getResourceKey } from 'lib/api/useApiQuery'; import { useAppContext } from 'lib/contexts/app'; import { useMultichainContext } from 'lib/contexts/multichain'; -import * as cookies from 'lib/cookies'; -import useSocketChannel from 'lib/socket/useSocketChannel'; -import useSocketMessage from 'lib/socket/useSocketMessage'; import type { Filters } from './useAddressTokenTransfersQuery'; diff --git a/ui/address/AddressTokens.pw.tsx b/client/slices/address/pages/details/tokens/AddressTokens.pw.tsx similarity index 97% rename from ui/address/AddressTokens.pw.tsx rename to client/slices/address/pages/details/tokens/AddressTokens.pw.tsx index fe4d882e8a..1f6f656ac8 100644 --- a/ui/address/AddressTokens.pw.tsx +++ b/client/slices/address/pages/details/tokens/AddressTokens.pw.tsx @@ -1,12 +1,13 @@ import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { AddressTokenBalance, AddressTokensResponse } from 'types/api/address'; +import type { AddressTokenBalance, AddressTokensResponse } from 'client/slices/address/types/api'; + +import * as addressMock from 'client/slices/address/mocks/address'; +import * as tokensMock from 'client/slices/token/mocks/address-tokens'; +import * as tokenInfo from 'client/slices/token/mocks/info'; +import * as tokenInstance from 'client/slices/token/mocks/instance'; -import * as addressMock from 'mocks/address/address'; -import * as tokensMock from 'mocks/address/tokens'; -import * as tokenInfo from 'mocks/tokens/tokenInfo'; -import * as tokenInstance from 'mocks/tokens/tokenInstance'; import * as socketServer from 'playwright/fixtures/socketServer'; import { test, expect, devices } from 'playwright/lib'; diff --git a/ui/address/AddressTokens.tsx b/client/slices/address/pages/details/tokens/AddressTokens.tsx similarity index 83% rename from ui/address/AddressTokens.tsx rename to client/slices/address/pages/details/tokens/AddressTokens.tsx index d44c1056fe..feee1ab040 100644 --- a/ui/address/AddressTokens.tsx +++ b/client/slices/address/pages/details/tokens/AddressTokens.tsx @@ -1,27 +1,30 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, HStack } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; import type { PaginationParams } from 'ui/shared/pagination/types'; +import { ADDRESS_TOKEN_BALANCE_ERC_20 } from 'client/slices/address/stubs/address'; +import AddressCollections from 'client/slices/token/pages/address/AddressCollections'; +import AddressNftDisplayTypeRadio from 'client/slices/token/pages/address/AddressNftDisplayTypeRadio'; +import AddressNFTs from 'client/slices/token/pages/address/AddressNFTs'; +import AddressNftTypeFilter from 'client/slices/token/pages/address/AddressNftTypeFilter'; +import ERC20Tokens from 'client/slices/token/pages/address/ERC20Tokens'; +import TokenBalances from 'client/slices/token/pages/address/TokenBalances'; +import useAddressNftQuery from 'client/slices/token/pages/address/useAddressNftQuery'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import useIsMounted from 'client/shared/hooks/useIsMounted'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import config from 'configs/app'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import useIsMounted from 'lib/hooks/useIsMounted'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import { ADDRESS_TOKEN_BALANCE_ERC_20 } from 'stubs/address'; import { generateListStub } from 'stubs/utils'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; -import AddressCollections from './tokens/AddressCollections'; -import AddressNftDisplayTypeRadio from './tokens/AddressNftDisplayTypeRadio'; -import AddressNFTs from './tokens/AddressNFTs'; -import AddressNftTypeFilter from './tokens/AddressNftTypeFilter'; -import ERC20Tokens from './tokens/ERC20Tokens'; -import TokenBalances from './tokens/TokenBalances'; -import useAddressNftQuery from './tokens/useAddressNftQuery'; - const TAB_LIST_PROPS = { mt: 1, mb: { base: 6, lg: 1 }, diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_native-token-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_native-token-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_native-token-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_native-token-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-2.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-2.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-2.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-base-flow-2.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-1.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-1.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-1.png diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-2.png b/client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-2.png similarity index 100% rename from ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-2.png rename to client/slices/address/pages/details/tokens/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-custom-token-ZRC-2-2.png diff --git a/ui/address/AddressTxs.pw.tsx b/client/slices/address/pages/details/txs/AddressTxs.pw.tsx similarity index 99% rename from ui/address/AddressTxs.pw.tsx rename to client/slices/address/pages/details/txs/AddressTxs.pw.tsx index c6c595d769..df26ce01ff 100644 --- a/ui/address/AddressTxs.pw.tsx +++ b/client/slices/address/pages/details/txs/AddressTxs.pw.tsx @@ -2,7 +2,8 @@ import { Box } from '@chakra-ui/react'; import type { Locator } from '@playwright/test'; import React from 'react'; -import * as txMock from 'mocks/txs/tx'; +import * as txMock from 'client/slices/tx/mocks/tx'; + import * as socketServer from 'playwright/fixtures/socketServer'; import { test, expect } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; diff --git a/ui/address/AddressTxs.tsx b/client/slices/address/pages/details/txs/AddressTxs.tsx similarity index 82% rename from ui/address/AddressTxs.tsx rename to client/slices/address/pages/details/txs/AddressTxs.tsx index 09e2e0f13e..d382d024d3 100644 --- a/ui/address/AddressTxs.tsx +++ b/client/slices/address/pages/details/txs/AddressTxs.tsx @@ -1,11 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { HStack } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; +import TxsWithApiSorting from 'client/slices/tx/pages/index/list/TxsWithApiSorting'; + +import CsvExport from 'client/features/csv-export/components/CsvExport'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import useIsMounted from 'client/shared/hooks/useIsMounted'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import config from 'configs/app'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import useIsMounted from 'lib/hooks/useIsMounted'; -import getQueryParamString from 'lib/router/getQueryParamString'; import { INTERCHAIN_MESSAGE } from 'stubs/interchainIndexer'; import { generateListStub } from 'stubs/utils'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; @@ -13,9 +20,7 @@ import AddressTxsCrossChain from 'ui/crossChain/address/AddressTxsCrossChain'; import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; -import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting'; -import AddressCsvExportLink from './AddressCsvExportLink'; import AddressTxsFilter from './AddressTxsFilter'; import useAddressTxsQuery from './useAddressTxsQuery'; @@ -79,7 +84,7 @@ const AddressTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => { id: [ 'txs_local', 'txs' ], title: 'Txns', component: ( - { return null; } + if (!localQuery.query.data?.items.length && !localQuery.filterValue) { + return null; + } + return ( <> - + { txsLocalFilter } + - diff --git a/ui/address/AddressTxsFilter.tsx b/client/slices/address/pages/details/txs/AddressTxsFilter.tsx similarity index 83% rename from ui/address/AddressTxsFilter.tsx rename to client/slices/address/pages/details/txs/AddressTxsFilter.tsx index 0d452770cf..cb183c6581 100644 --- a/ui/address/AddressTxsFilter.tsx +++ b/client/slices/address/pages/details/txs/AddressTxsFilter.tsx @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { createListCollection } from '@chakra-ui/react'; import React from 'react'; -import type { AddressFromToFilter } from 'types/api/address'; +import type { AddressFromToFilter } from 'client/slices/address/types/api'; + +import useIsInitialLoading from 'client/shared/hooks/useIsInitialLoading'; -import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; import PopoverFilterRadio from 'ui/shared/filters/PopoverFilterRadio'; const OPTIONS = [ diff --git a/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-desktop-1.png b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-desktop-1.png new file mode 100644 index 0000000000..37bc8d5f06 Binary files /dev/null and b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-desktop-1.png differ diff --git a/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-desktop-2.png b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-desktop-2.png new file mode 100644 index 0000000000..25d3e31381 Binary files /dev/null and b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-desktop-2.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png similarity index 100% rename from ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png rename to client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png diff --git a/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-base-view-1.png b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-base-view-1.png new file mode 100644 index 0000000000..8e0d9e7550 Binary files /dev/null and b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-base-view-1.png differ diff --git a/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_mobile-base-view-1.png b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..dd93fa2ad8 Binary files /dev/null and b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_mobile-base-view-1.png differ diff --git a/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_mobile-table-view-1.png b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_mobile-table-view-1.png new file mode 100644 index 0000000000..d69687edb6 Binary files /dev/null and b/client/slices/address/pages/details/txs/__screenshots__/AddressTxs.pw.tsx_default_mobile-table-view-1.png differ diff --git a/ui/address/useAddressTxsQuery.ts b/client/slices/address/pages/details/txs/useAddressTxsQuery.ts similarity index 81% rename from ui/address/useAddressTxsQuery.ts rename to client/slices/address/pages/details/txs/useAddressTxsQuery.ts index f9fb19e3c5..2b0537c6dd 100644 --- a/ui/address/useAddressTxsQuery.ts +++ b/client/slices/address/pages/details/txs/useAddressTxsQuery.ts @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import React from 'react'; -import type { AddressFromToFilter } from 'types/api/address'; -import { AddressFromToFilterValues } from 'types/api/address'; -import type { TransactionsSorting, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction'; +import type { AddressFromToFilter } from 'client/slices/address/types/api'; +import { AddressFromToFilterValues } from 'client/slices/address/types/api'; +import type { TransactionsSorting, TransactionsSortingField, TransactionsSortingValue } from 'client/slices/tx/types/api'; + +import { SORT_OPTIONS } from 'client/slices/tx/hooks/useTxsSort'; +import { TX } from 'client/slices/tx/stubs/tx'; + +import getFilterValueFromQuery from 'client/shared/router/get-filter-value-from-query'; -import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; -import { TX } from 'stubs/tx'; import { generateListStub } from 'stubs/utils'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; -import { SORT_OPTIONS } from 'ui/txs/useTxsSort'; const getFilterValue = (getFilterValueFromQuery).bind(null, AddressFromToFilterValues); diff --git a/ui/address/AddressWithdrawals.tsx b/client/slices/address/pages/details/withdrawals/AddressWithdrawals.tsx similarity index 79% rename from ui/address/AddressWithdrawals.tsx rename to client/slices/address/pages/details/withdrawals/AddressWithdrawals.tsx index f8bb6e0524..372f956387 100644 --- a/ui/address/AddressWithdrawals.tsx +++ b/client/slices/address/pages/details/withdrawals/AddressWithdrawals.tsx @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; -import useIsMounted from 'lib/hooks/useIsMounted'; -import getQueryParamString from 'lib/router/getQueryParamString'; +import BeaconChainWithdrawalsListItem from 'client/features/chain-variants/beacon-chain/pages/withdrawals/BeaconChainWithdrawalsListItem'; +import BeaconChainWithdrawalsTable from 'client/features/chain-variants/beacon-chain/pages/withdrawals/BeaconChainWithdrawalsTable'; +import { WITHDRAWAL } from 'client/features/chain-variants/beacon-chain/stubs/withdrawals'; + +import useIsMounted from 'client/shared/hooks/useIsMounted'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import { generateListStub } from 'stubs/utils'; -import { WITHDRAWAL } from 'stubs/withdrawals'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; -import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem'; -import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable'; type Props = { shouldRender?: boolean; diff --git a/ui/pages/Accounts.pw.tsx b/client/slices/address/pages/index/Accounts.pw.tsx similarity index 87% rename from ui/pages/Accounts.pw.tsx rename to client/slices/address/pages/index/Accounts.pw.tsx index 45daa24103..5bcc1f5fa4 100644 --- a/ui/pages/Accounts.pw.tsx +++ b/client/slices/address/pages/index/Accounts.pw.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import type { AddressesResponse } from 'types/api/addresses'; +import type { AddressesResponse } from 'client/slices/address/types/api'; + +import * as addressMocks from 'client/slices/address/mocks/address'; -import * as addressMocks from 'mocks/address/address'; import { test, expect } from 'playwright/lib'; import Accounts from './Accounts'; diff --git a/ui/pages/Accounts.tsx b/client/slices/address/pages/index/Accounts.tsx similarity index 89% rename from ui/pages/Accounts.tsx rename to client/slices/address/pages/index/Accounts.tsx index 04013b5e95..f7050154c9 100644 --- a/ui/pages/Accounts.tsx +++ b/client/slices/address/pages/index/Accounts.tsx @@ -1,18 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; -import getItemIndex from 'lib/getItemIndex'; -import { TOP_ADDRESS } from 'stubs/address'; +import { TOP_ADDRESS } from 'client/slices/address/stubs/address'; + +import getItemIndex from 'client/shared/lists/get-item-index'; + import { generateListStub } from 'stubs/utils'; -import AddressesListItem from 'ui/addresses/AddressesListItem'; -import AddressesTable from 'ui/addresses/AddressesTable'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import PageTitle from 'ui/shared/Page/PageTitle'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import AddressesListItem from './AddressesListItem'; +import AddressesTable from './AddressesTable'; + const Accounts = () => { const { isError, isPlaceholderData, data, pagination } = useQueryWithPages({ resourceName: 'general:addresses', diff --git a/ui/addresses/AddressesListItem.tsx b/client/slices/address/pages/index/AddressesListItem.tsx similarity index 90% rename from ui/addresses/AddressesListItem.tsx rename to client/slices/address/pages/index/AddressesListItem.tsx index feb538893c..faf308b6ea 100644 --- a/ui/addresses/AddressesListItem.tsx +++ b/client/slices/address/pages/index/AddressesListItem.tsx @@ -1,15 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, HStack } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { AddressesItem } from 'types/api/addresses'; +import type { AddressesItem } from 'client/slices/address/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + +import { currencyUnits } from 'client/shared/chain/units'; import config from 'configs/app'; -import { currencyUnits } from 'lib/units'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tag } from 'toolkit/chakra/tag'; import { ZERO } from 'toolkit/utils/consts'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; type Props = { diff --git a/ui/addresses/AddressesTable.tsx b/client/slices/address/pages/index/AddressesTable.tsx similarity index 89% rename from ui/addresses/AddressesTable.tsx rename to client/slices/address/pages/index/AddressesTable.tsx index 020d778e31..5b54e2b694 100644 --- a/ui/addresses/AddressesTable.tsx +++ b/client/slices/address/pages/index/AddressesTable.tsx @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type BigNumber from 'bignumber.js'; import React from 'react'; -import type { AddressesItem } from 'types/api/addresses'; +import type { AddressesItem } from 'client/slices/address/types/api'; + +import { currencyUnits } from 'client/shared/chain/units'; -import { currencyUnits } from 'lib/units'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import { ZERO } from 'toolkit/utils/consts'; diff --git a/ui/addresses/AddressesTableItem.tsx b/client/slices/address/pages/index/AddressesTableItem.tsx similarity index 91% rename from ui/addresses/AddressesTableItem.tsx rename to client/slices/address/pages/index/AddressesTableItem.tsx index 07d65e826e..e8c77db422 100644 --- a/ui/addresses/AddressesTableItem.tsx +++ b/client/slices/address/pages/index/AddressesTableItem.tsx @@ -1,14 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { AddressesItem } from 'types/api/addresses'; +import type { AddressesItem } from 'client/slices/address/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; import config from 'configs/app'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { TableCell, TableRow } from 'toolkit/chakra/table'; import { Tag } from 'toolkit/chakra/tag'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import SimpleValue from 'ui/shared/value/SimpleValue'; type Props = { diff --git a/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..e6d4d0a11d Binary files /dev/null and b/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_default_base-view-mobile-dark-mode-1.png b/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_default_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..e8b45eb2cb Binary files /dev/null and b/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_mobile_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..b1c12ee274 Binary files /dev/null and b/client/slices/address/pages/index/__screenshots__/Accounts.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/client/slices/address/stubs/address-params.ts b/client/slices/address/stubs/address-params.ts new file mode 100644 index 0000000000..d819dc83cf --- /dev/null +++ b/client/slices/address/stubs/address-params.ts @@ -0,0 +1,15 @@ +import type { AddressParam } from 'client/slices/address/types/api'; + +export const ADDRESS_HASH = '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a'; + +export const ADDRESS_PARAMS: AddressParam = { + hash: ADDRESS_HASH, + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, +}; diff --git a/client/slices/address/stubs/address.ts b/client/slices/address/stubs/address.ts new file mode 100644 index 0000000000..a01175a26c --- /dev/null +++ b/client/slices/address/stubs/address.ts @@ -0,0 +1,113 @@ +import type { + Address, + AddressCoinBalanceHistoryItem, + AddressCollection, + AddressCounters, + AddressNFT, + AddressTabsCounters, + AddressTokenBalance, + AddressesItem, +} from 'client/slices/address/types/api'; + +import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from 'client/slices/token/stubs'; +import { TX_HASH } from 'client/slices/tx/stubs/tx'; + +import { ADDRESS_HASH } from './address-params'; + +export const ADDRESS_INFO: Address = { + block_number_balance_updated_at: 8774377, + coin_balance: '810941268802273085757', + creation_transaction_hash: null, + creator_address_hash: ADDRESS_HASH, + creation_status: 'success', + exchange_rate: null, + has_logs: true, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: false, + hash: ADDRESS_HASH, + implementations: [ { address_hash: ADDRESS_HASH, name: 'Transparent Upgradable Proxy' } ], + is_contract: true, + is_verified: true, + name: 'ChainLink Token (goerli)', + token: TOKEN_INFO_ERC_20, + private_tags: [], + public_tags: [], + watchlist_names: [], + watchlist_address_id: null, + ens_domain_name: null, +}; + +export const ADDRESS_COUNTERS: AddressCounters = { + gas_usage_count: '8028907522', + token_transfers_count: '420', + transactions_count: '119020', + validations_count: '0', +}; + +export const ADDRESS_TABS_COUNTERS: AddressTabsCounters = { + internal_transactions_count: 10, + logs_count: 10, + token_balances_count: 10, + token_transfers_count: 10, + transactions_count: 10, + validations_count: 10, + withdrawals_count: 10, + beacon_deposits_count: 10, +}; + +export const TOP_ADDRESS: AddressesItem = { + coin_balance: '11886682377162664596540805', + transactions_count: '1835', + hash: '0x4f7A67464B5976d7547c860109e4432d50AfB38e', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [ ], + watchlist_names: [], + ens_domain_name: null, +}; + +export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = { + block_number: 9004413, + block_timestamp: '2023-05-15T13:16:24Z', + delta: '1000000000000000000', + transaction_hash: TX_HASH, + value: '953427250000000000000000', +}; + +export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = { + token: TOKEN_INFO_ERC_20, + token_id: null, + token_instance: null, + value: '1000000000000000000000000', +}; + +export const ADDRESS_NFT_721: AddressNFT = { + token_type: 'ERC-721', + value: '1', + ...TOKEN_INSTANCE, + token: TOKEN_INFO_ERC_721, +}; + +export const ADDRESS_NFT_1155: AddressNFT = { + token_type: 'ERC-1155', + value: '10', + ...TOKEN_INSTANCE, + token: TOKEN_INFO_ERC_1155, +}; + +export const ADDRESS_NFT_404: AddressNFT = { + token_type: 'ERC-404', + value: '10', + ...TOKEN_INSTANCE, + token: TOKEN_INFO_ERC_404, +}; + +export const ADDRESS_COLLECTION: AddressCollection = { + token: TOKEN_INFO_ERC_1155, + amount: '4', + token_instances: Array(4).fill(TOKEN_INSTANCE), +}; diff --git a/client/slices/address/types/api.ts b/client/slices/address/types/api.ts new file mode 100644 index 0000000000..c4e67f212d --- /dev/null +++ b/client/slices/address/types/api.ts @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { AddressMetadataTagApi } from 'client/features/address-metadata/types/api'; +import type { AddressFilecoinParams } from 'client/features/chain-variants/filecoin/types/api'; +import type { AddressZilliqaParams } from 'client/features/chain-variants/zilliqa/types/api'; +import type { Block } from 'client/slices/block/types/api'; +import type { SmartContractCreationStatus, SmartContractProxyType } from 'client/slices/contract/types/api'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; +import type { TokenTransfer, TokenTransferPagination } from 'client/slices/token-transfer/types/api'; +import type { NFTTokenType, TokenInfo, TokenInstance, TokenReputation, TokenType } from 'client/slices/token/types/api'; +import type { Transaction } from 'client/slices/tx/types/api'; + +export interface AddressImplementation { + address_hash: string; + filecoin_robust_address?: string | null; + name?: string | null; +} + +export interface AddressTag { + label: string; + display_name: string; + address_hash: string; +} + +export interface WatchlistName { + label: string; + display_name: string; +} + +export interface UserTags { + private_tags: Array | null; + watchlist_names: Array | null; + public_tags: Array | null; +} + +export type AddressParamBasic = { + hash: string; + implementations: Array | null; + name: string | null; + is_contract: boolean; + is_verified: boolean | null; + ens_domain_name: string | null; + metadata?: { + reputation: number | null; + tags: Array; + } | null; + filecoin?: AddressFilecoinParams; + proxy_type?: SmartContractProxyType | null; + reputation?: TokenReputation; +}; + +export type AddressParam = UserTags & AddressParamBasic; + +export interface AddressCeloParams { + account: { + locked_celo: string; + metadata_url: string | null; + name: string | null; + nonvoting_locked_celo: string; + type: string; + vote_signer_address: AddressParam | null; + validator_signer_address: AddressParam | null; + attestation_signer_address: AddressParam | null; + } | null; +} + +export interface Address extends UserTags { + block_number_balance_updated_at: number | null; + coin_balance: string | null; + creator_address_hash: string | null; + creator_filecoin_robust_address?: string | null; + creation_transaction_hash: string | null; + creation_status: SmartContractCreationStatus | null; + exchange_rate: string | null; + ens_domain_name: string | null; + filecoin?: AddressFilecoinParams; + zilliqa?: AddressZilliqaParams; + celo?: AddressCeloParams; + // TODO: if we are happy with tabs-counters method, should we delete has_something fields? + has_beacon_chain_withdrawals?: boolean; + has_logs: boolean; + has_token_transfers: boolean; + has_tokens: boolean; + has_validated_blocks: boolean; + hash: string; + implementations: Array | null; + is_contract: boolean; + is_verified: boolean; + name: string | null; + token: TokenInfo | null; + watchlist_address_id: number | null; + proxy_type?: SmartContractProxyType | null; +} + +export interface AddressCounters { + transactions_count: string; + token_transfers_count: string; + gas_usage_count: string | null; + validations_count: string | null; +} + +export interface AddressTokenBalance { + token: TokenInfo; + token_id: string | null; + value: string | null; + token_instance: TokenInstance | null; +} +export type AddressTokenBalancesResponse = Array; + +export type AddressNFT = TokenInstance & { + token: TokenInfo; + token_type: Omit; + value: string; +}; + +export type AddressCollection = { + token: TokenInfo; + amount: string; + token_instances: Array>; +}; + +export interface AddressTokensResponse { + items: Array; + next_page_params: { + items_count: number; + token_name: string | null; + token_type: TokenType; + value: number; + fiat_value: string | null; + } | null; +} + +export interface AddressNFTsResponse { + items: Array; + next_page_params: { + items_count: number; + token_id: string; + token_type: TokenType; + token_contract_address_hash: string; + } | null; +} + +export interface AddressCollectionsResponse { + items: Array; + next_page_params: { + token_contract_address_hash: string; + token_type: TokenType; + } | null; +} + +export interface AddressTokensBalancesSocketMessage { + overflow: boolean; + token_balances: Array; +} + +export interface AddressTransactionsResponse { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + } | null; +} + +export const AddressFromToFilterValues = [ 'from', 'to' ] as const; + +export type AddressFromToFilter = typeof AddressFromToFilterValues[number] | undefined; + +export type AddressTxsFilters = { + filter: AddressFromToFilter; +}; + +export interface AddressTokenTransferResponse { + items: Array; + next_page_params: TokenTransferPagination | null; +} + +export type AddressTokenTransferFilters = { + filter?: AddressFromToFilter; + type?: Array; + token?: string; +}; + +export type AddressTokensFilter = { + type: TokenType | Array; +}; + +export type AddressNFTTokensFilter = { + type: Array | undefined; +}; + +export interface AddressCoinBalanceHistoryItem { + block_number: number; + block_timestamp: string; + delta: string; + transaction_hash: string | null; + value: string; +} + +export interface AddressCoinBalanceHistoryResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + } | null; +} + +export type AddressCoinBalanceHistoryChart = { + items: Array<{ + date: string; + value: string; + }>; + days: number; +}; + +export interface AddressBlocksValidatedResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + }; +} +export interface AddressInternalTxsResponse { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + transaction_index: number; + } | null; +} + +export type AddressWithdrawalsResponse = { + items: Array; + next_page_params: { + index: number; + items_count: number; + }; +}; + +export type AddressWithdrawalsItem = { + amount: string; + block_number: number; + index: number; + timestamp: string; + validator_index: number; +}; + +export type AddressTabsCounters = { + internal_transactions_count: number | null; + logs_count: number | null; + token_balances_count: number | null; + token_transfers_count: number | null; + transactions_count: number | null; + validations_count: number | null; + withdrawals_count: number | null; + beacon_deposits_count: number | null; + celo_election_rewards_count?: number | null; +}; + +export type AddressXStarResponse = { + data: { + level: string | null; + }; +}; + +export type AddressesItem = AddressParam & { transactions_count: string; coin_balance: string | null }; + +export type AddressesResponse = { + items: Array; + next_page_params: { + fetched_coin_balance: string; + hash: string; + items_count: number; + } | null; + total_supply: string; +}; diff --git a/client/slices/address/types/config.ts b/client/slices/address/types/config.ts new file mode 100644 index 0000000000..6931af00c6 --- /dev/null +++ b/client/slices/address/types/config.ts @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export const IDENTICON_TYPES = [ + 'github', + 'jazzicon', + 'gradient_avatar', + 'blockie', + 'nouns', +] as const; + +export type IdenticonType = typeof IDENTICON_TYPES[number]; + +export const ADDRESS_VIEWS_IDS = [ + 'top_accounts', +] as const; + +export type AddressViewId = typeof ADDRESS_VIEWS_IDS[number]; + +export const ADDRESS_FORMATS = [ 'base16', 'bech32' ] as const; +export type AddressFormat = typeof ADDRESS_FORMATS[ number ]; diff --git a/lib/address/bech32.ts b/client/slices/address/utils/bech32.ts similarity index 88% rename from lib/address/bech32.ts rename to client/slices/address/utils/bech32.ts index 85fcfda3ed..156cdc717f 100644 --- a/lib/address/bech32.ts +++ b/client/slices/address/utils/bech32.ts @@ -1,8 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { bech32 } from '@scure/base'; +import bytesToHex from 'client/shared/transformers/bytes-to-hex'; +import hexToBytes from 'client/shared/transformers/hex-to-bytes'; + import config from 'configs/app'; -import bytesToHex from 'lib/bytesToHex'; -import hexToBytes from 'lib/hexToBytes'; export const DATA_PART_REGEXP = /^[\da-z]{38}$/; export const BECH_32_SEPARATOR = '1'; // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 diff --git a/client/slices/address/utils/consts.ts b/client/slices/address/utils/consts.ts new file mode 100644 index 0000000000..53bb4b8697 --- /dev/null +++ b/client/slices/address/utils/consts.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { AddressParam } from 'client/slices/address/types/api'; + +export const unknownAddress: Omit = { + is_contract: false, + is_verified: false, + implementations: null, + name: '', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, +}; diff --git a/client/slices/address/utils/get-checked-summed-address.ts b/client/slices/address/utils/get-checked-summed-address.ts new file mode 100644 index 0000000000..736ef6f7e3 --- /dev/null +++ b/client/slices/address/utils/get-checked-summed-address.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { getAddress } from 'viem'; + +import config from 'configs/app'; + +const ERC1191_CHAIN_IDS = [ + '30', // RSK Mainnet + '31', // RSK Testnet +]; + +export default function getCheckedSummedAddress(address: string): string { + try { + return getAddress( + address, + // We need to pass chainId to getAddress to make it work correctly for chains that support ERC-1191 + // https://eips.ethereum.org/EIPS/eip-1191#usage--table + ERC1191_CHAIN_IDS.includes(config.chain.id ?? '') ? Number(config.chain.id) : undefined, + ); + } catch (error) { + return address; + } +} diff --git a/client/slices/address/utils/is-evm-address.spec.ts b/client/slices/address/utils/is-evm-address.spec.ts new file mode 100644 index 0000000000..04b6de1410 --- /dev/null +++ b/client/slices/address/utils/is-evm-address.spec.ts @@ -0,0 +1,27 @@ +import { it, expect } from 'vitest'; + +import { isEvmAddress } from './is-evm-address'; + +it('should return true for valid EVM address', () => { + expect(isEvmAddress('0x1234567890123456789012345678901234567890')).toBe(true); + expect(isEvmAddress('0xabcdef1234567890123456789012345678901234')).toBe(true); + expect(isEvmAddress('0xABCDEF1234567890123456789012345678901234')).toBe(true); +}); + +it('should return false for invalid EVM address', () => { + expect(isEvmAddress('0x123')).toBe(false); + expect(isEvmAddress('123456789012345678901234567890123456789')).toBe(false); + expect(isEvmAddress('0xGGGGGG1234567890123456789012345678901234')).toBe(false); + expect(isEvmAddress('0x12345678901234567890123456789012345678901')).toBe(false); +}); + +it('should return false for empty or null input', () => { + expect(isEvmAddress('')).toBe(false); + expect(isEvmAddress(null as unknown as string)).toBe(false); + expect(isEvmAddress(undefined as unknown as string)).toBe(false); +}); + +it('should handle addresses with extra whitespace', () => { + expect(isEvmAddress(' 0x1234567890123456789012345678901234567890 ')).toBe(true); + expect(isEvmAddress(' 0x123 ')).toBe(false); +}); diff --git a/client/slices/address/utils/is-evm-address.ts b/client/slices/address/utils/is-evm-address.ts new file mode 100644 index 0000000000..13401402fa --- /dev/null +++ b/client/slices/address/utils/is-evm-address.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { ADDRESS_REGEXP } from 'toolkit/utils/regexp'; + +export function isEvmAddress(address: string): boolean { + if (!address) return false; + return ADDRESS_REGEXP.test(address.trim()); +} diff --git a/client/slices/address/utils/tx.ts b/client/slices/address/utils/tx.ts new file mode 100644 index 0000000000..232a6477f3 --- /dev/null +++ b/client/slices/address/utils/tx.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export type TxCourseType = 'in' | 'out' | 'self' | 'unspecified'; + +export function getTxCourseType(from: string, to: string | undefined, current?: string): TxCourseType { + if (current === undefined) { + return 'unspecified'; + } + + const fromLower = from.toLowerCase(); + const toLower = to?.toLowerCase(); + const currentLower = current.toLowerCase(); + + if (toLower && fromLower === toLower && fromLower === currentLower) { + return 'self'; + } + + if (fromLower === currentLower) { + return 'out'; + } + + if (toLower && toLower === currentLower) { + return 'in'; + } + + return 'unspecified'; +} diff --git a/ui/shared/block/BlockPendingUpdateAlert.tsx b/client/slices/block/components/BlockPendingUpdateAlert.tsx similarity index 93% rename from ui/shared/block/BlockPendingUpdateAlert.tsx rename to client/slices/block/components/BlockPendingUpdateAlert.tsx index 18c9fbff2a..35b01d0962 100644 --- a/ui/shared/block/BlockPendingUpdateAlert.tsx +++ b/client/slices/block/components/BlockPendingUpdateAlert.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import config from 'configs/app'; diff --git a/ui/shared/block/BlockPendingUpdateHint.tsx b/client/slices/block/components/BlockPendingUpdateHint.tsx similarity index 94% rename from ui/shared/block/BlockPendingUpdateHint.tsx rename to client/slices/block/components/BlockPendingUpdateHint.tsx index 3aa21e35b1..69b605ada7 100644 --- a/ui/shared/block/BlockPendingUpdateHint.tsx +++ b/client/slices/block/components/BlockPendingUpdateHint.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { BoxProps } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/entities/block/BlockEntity.pw.tsx b/client/slices/block/components/entity/BlockEntity.pw.tsx similarity index 100% rename from ui/shared/entities/block/BlockEntity.pw.tsx rename to client/slices/block/components/entity/BlockEntity.pw.tsx diff --git a/ui/shared/entities/block/BlockEntity.tsx b/client/slices/block/components/entity/BlockEntity.tsx similarity index 95% rename from ui/shared/entities/block/BlockEntity.tsx rename to client/slices/block/components/entity/BlockEntity.tsx index a7021c9135..f6c08c7e25 100644 --- a/ui/shared/entities/block/BlockEntity.tsx +++ b/client/slices/block/components/entity/BlockEntity.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; @@ -6,10 +8,9 @@ import { route } from 'nextjs/routes'; import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; import * as EntityBase from 'ui/shared/entities/base/components'; +import { distributeEntityProps } from 'ui/shared/entities/base/utils'; import getChainTooltipText from 'ui/shared/externalChains/getChainTooltipText'; -import { distributeEntityProps } from '../base/utils'; - type LinkProps = EntityBase.LinkBaseProps & Partial>; const Link = chakra((props: LinkProps) => { diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png b/client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png similarity index 100% rename from ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png rename to client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_dark-color-mode_external-link-dark-mode-1.png diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_customization-1.png b/client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_customization-1.png similarity index 100% rename from ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_customization-1.png rename to client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_customization-1.png diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png b/client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png similarity index 100% rename from ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png rename to client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_external-link-dark-mode-1.png diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-content-1.png b/client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-content-1.png similarity index 100% rename from ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-content-1.png rename to client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-content-1.png diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-subheading-1.png b/client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-subheading-1.png similarity index 100% rename from ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-subheading-1.png rename to client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_icon-sizes-subheading-1.png diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_loading-1.png b/client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_loading-1.png similarity index 100% rename from ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_loading-1.png rename to client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_loading-1.png diff --git a/ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_long-number-1.png b/client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_long-number-1.png similarity index 100% rename from ui/shared/entities/block/__screenshots__/BlockEntity.pw.tsx_default_long-number-1.png rename to client/slices/block/components/entity/__screenshots__/BlockEntity.pw.tsx_default_long-number-1.png diff --git a/client/slices/block/hooks/useBlockInternalTxsQuery.ts b/client/slices/block/hooks/useBlockInternalTxsQuery.ts new file mode 100644 index 0000000000..1992c33079 --- /dev/null +++ b/client/slices/block/hooks/useBlockInternalTxsQuery.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { INTERNAL_TX } from 'client/slices/internal-tx/stubs'; + +import type config from 'configs/app'; +import { generateListStub } from 'stubs/utils'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +import type { BlockQuery } from './useBlockQuery'; + +interface Params { + heightOrHash: string; + blockQuery: BlockQuery; + tab: string; + chainConfig: typeof config; +} + +export default function useBlockInternalTxsQuery({ heightOrHash, blockQuery, tab, chainConfig }: Params) { + const apiQuery = useQueryWithPages({ + resourceName: 'general:block_internal_txs', + pathParams: { height_or_hash: heightOrHash }, + options: { + enabled: Boolean(tab === 'internal_txs' && !blockQuery.isPlaceholderData && chainConfig.UI.views.internalTx.isEnabled), + placeholderData: generateListStub<'general:block_internal_txs'>(INTERNAL_TX, 10, { next_page_params: null }), + refetchOnMount: false, + }, + }); + + return apiQuery; +} diff --git a/client/slices/block/hooks/useBlockQuery.ts b/client/slices/block/hooks/useBlockQuery.ts new file mode 100644 index 0000000000..500700d1dc --- /dev/null +++ b/client/slices/block/hooks/useBlockQuery.ts @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; +import type { Chain, GetBlockReturnType } from 'viem'; + +import type { Block } from 'client/slices/block/types/api'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; +import { retry } from 'client/api/hooks/useQueryClientConfig'; +import type { ResourceError } from 'client/api/resources'; + +import { BLOCK } from 'client/slices/block/stubs/block'; +import formatRpcData from 'client/slices/block/utils/format-rpc-data'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; + +import { GET_BLOCK } from 'stubs/RPC'; +import { SECOND } from 'toolkit/utils/consts'; + +type RpcResponseType = GetBlockReturnType | null; + +export type BlockQuery = UseQueryResult> & { + isDegradedData: boolean; + isFutureBlock: boolean; +}; + +interface Params { + heightOrHash: string; +} + +export default function useBlockQuery({ heightOrHash }: Params): BlockQuery { + const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false); + + const apiQuery = useApiQuery<'general:block', { status: number }>('general:block', { + pathParams: { height_or_hash: heightOrHash }, + queryOptions: { + enabled: Boolean(heightOrHash), + placeholderData: BLOCK, + refetchOnMount: false, + retry: (failureCount, error) => { + if (isRefetchEnabled) { + return false; + } + + return retry(failureCount, error); + }, + refetchInterval: (): number | false => { + return isRefetchEnabled ? 15 * SECOND : false; + }, + }, + }); + + const latestBlockQuery = useQuery({ + queryKey: [ 'RPC', 'block', 'latest' ], + queryFn: async() => { + if (!publicClient) { + return null; + } + return publicClient.getBlock({ blockTag: 'latest' }); + }, + enabled: publicClient !== undefined && (apiQuery.isError || apiQuery.errorUpdateCount > 0), + }); + + const rpcQuery = useQuery({ + queryKey: [ 'RPC', 'block', { heightOrHash } ], + queryFn: async() => { + if (!publicClient) { + return null; + } + + const blockParams = heightOrHash.startsWith('0x') ? { blockHash: heightOrHash as `0x${ string }` } : { blockNumber: BigInt(heightOrHash) }; + return publicClient.getBlock(blockParams).catch(() => null); + }, + select: (block) => { + return formatRpcData(block); + }, + placeholderData: GET_BLOCK, + enabled: !latestBlockQuery.isPending, + retry: false, + refetchOnMount: false, + }); + + React.useEffect(() => { + if (apiQuery.isPlaceholderData || !publicClient) { + return; + } + + if (apiQuery.isError && apiQuery.errorUpdateCount === 1) { + setRefetchEnabled(true); + } else if (!apiQuery.isError) { + setRefetchEnabled(false); + } + }, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]); + + React.useEffect(() => { + if (!rpcQuery.isPlaceholderData && !rpcQuery.data) { + setRefetchEnabled(false); + } + }, [ rpcQuery.data, rpcQuery.isPlaceholderData ]); + + const isRpcQuery = Boolean(publicClient && (apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0 && rpcQuery.data); + const query = isRpcQuery ? rpcQuery as UseQueryResult> : apiQuery; + + return { + ...query, + isDegradedData: isRpcQuery, + isFutureBlock: Boolean( + !heightOrHash.startsWith('0x') && + latestBlockQuery.data && Number(latestBlockQuery.data.number) < Number(heightOrHash), + ), + }; +} diff --git a/client/slices/block/hooks/useBlockTxsQuery.ts b/client/slices/block/hooks/useBlockTxsQuery.ts new file mode 100644 index 0000000000..36ef6b914c --- /dev/null +++ b/client/slices/block/hooks/useBlockTxsQuery.ts @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; +import type { Chain, GetBlockReturnType } from 'viem'; + +import type { BlockTransactionsResponse } from 'client/slices/block/types/api'; + +import { retry } from 'client/api/hooks/useQueryClientConfig'; +import type { ResourceError } from 'client/api/resources'; + +import { unknownAddress } from 'client/slices/address/utils/consts'; +import { TX } from 'client/slices/tx/stubs/tx'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; + +import hexToDecimal from 'client/shared/transformers/hex-to-decimal'; + +import dayjs from 'lib/date/dayjs'; +import { GET_BLOCK_WITH_TRANSACTIONS } from 'stubs/RPC'; +import { generateListStub } from 'stubs/utils'; +import { SECOND } from 'toolkit/utils/consts'; +import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import { emptyPagination } from 'ui/shared/pagination/utils'; + +import type { BlockQuery } from './useBlockQuery'; + +type RpcResponseType = GetBlockReturnType | null; + +export type BlockTxsQuery = QueryWithPagesResult<'general:block_txs'> & { + isDegradedData: boolean; +}; + +interface Params { + heightOrHash: string; + blockQuery: BlockQuery; + tab: string; +} + +export default function useBlockTxsQuery({ heightOrHash, blockQuery, tab }: Params): BlockTxsQuery { + const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false); + + const apiQuery = useQueryWithPages({ + resourceName: 'general:block_txs', + pathParams: { height_or_hash: heightOrHash }, + options: { + enabled: Boolean(tab === 'txs' && !blockQuery.isPlaceholderData && !blockQuery.isDegradedData), + placeholderData: generateListStub<'general:block_txs'>(TX, 50, { next_page_params: { + block_number: 9004925, + index: 49, + items_count: 50, + } }), + refetchOnMount: false, + retry: (failureCount, error) => { + if (isRefetchEnabled) { + return false; + } + + return retry(failureCount, error); + }, + refetchInterval: (): number | false => { + return isRefetchEnabled ? 15 * SECOND : false; + }, + }, + }); + + const rpcQuery = useQuery({ + queryKey: [ 'RPC', 'block_txs', { heightOrHash } ], + queryFn: async() => { + if (!publicClient) { + return null; + } + + const blockParams = heightOrHash.startsWith('0x') ? + { blockHash: heightOrHash as `0x${ string }`, includeTransactions: true } : + { blockNumber: BigInt(heightOrHash), includeTransactions: true }; + return publicClient.getBlock(blockParams).catch(() => null); + }, + select: (block) => { + if (!block) { + return null; + } + + return { + items: block.transactions + .map((tx) => { + if (typeof tx === 'string') { + return; + } + + return { + from: { ...unknownAddress, hash: tx.from as string }, + to: tx.to ? { ...unknownAddress, hash: tx.to as string } : null, + hash: tx.hash as string, + timestamp: block?.timestamp ? dayjs.unix(Number(block.timestamp)).format() : null, + confirmation_duration: null, + status: undefined, + block_number: Number(block.number), + value: tx.value.toString(), + gas_price: tx.gasPrice?.toString() ?? null, + base_fee_per_gas: block?.baseFeePerGas?.toString() ?? null, + max_fee_per_gas: tx.maxFeePerGas?.toString() ?? null, + max_priority_fee_per_gas: tx.maxPriorityFeePerGas?.toString() ?? null, + nonce: tx.nonce, + position: tx.transactionIndex, + type: tx.typeHex ? hexToDecimal(tx.typeHex) : null, + raw_input: tx.input, + gas_used: null, + gas_limit: tx.gas.toString(), + confirmations: 0, + fee: { + value: null, + type: 'actual', + }, + created_contract: null, + result: '', + priority_fee: null, + transaction_burnt_fee: null, + revert_reason: null, + decoded_input: null, + has_error_in_internal_transactions: null, + token_transfers: null, + token_transfers_overflow: false, + exchange_rate: null, + historic_exchange_rate: null, + method: null, + transaction_types: [], + transaction_tag: null, + actions: [], + }; + }) + .filter(Boolean), + next_page_params: null, + }; + }, + placeholderData: GET_BLOCK_WITH_TRANSACTIONS, + enabled: publicClient !== undefined && tab === 'txs' && (blockQuery.isDegradedData || apiQuery.isError || apiQuery.errorUpdateCount > 0), + retry: false, + refetchOnMount: false, + }); + + React.useEffect(() => { + if (apiQuery.isPlaceholderData || !publicClient) { + return; + } + + if (apiQuery.isError && apiQuery.errorUpdateCount === 1) { + setRefetchEnabled(true); + } else if (!apiQuery.isError) { + setRefetchEnabled(false); + } + }, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]); + + React.useEffect(() => { + if (!rpcQuery.isPlaceholderData && !rpcQuery.data) { + setRefetchEnabled(false); + } + }, [ rpcQuery.data, rpcQuery.isPlaceholderData ]); + + const isRpcQuery = Boolean(( + blockQuery.isDegradedData || + ((apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0) + ) && rpcQuery.data && publicClient); + + const rpcQueryWithPages: QueryWithPagesResult<'general:block_txs'> = { + ...rpcQuery as UseQueryResult, + pagination: emptyPagination, + onFilterChange: () => {}, + onSortingChange: () => {}, + chainValue: undefined, + onChainValueChange: () => {}, + }; + + const query = isRpcQuery ? rpcQueryWithPages : apiQuery; + + return { + ...query, + isDegradedData: isRpcQuery, + }; +} diff --git a/client/slices/block/mocks/block.ts b/client/slices/block/mocks/block.ts new file mode 100644 index 0000000000..5837fb873f --- /dev/null +++ b/client/slices/block/mocks/block.ts @@ -0,0 +1,340 @@ +/* eslint-disable max-len */ +import type { RpcBlock } from 'viem'; + +import type { ZilliqaBlockData } from 'client/features/chain-variants/zilliqa/types/api'; +import type { Block, BlocksResponse } from 'client/slices/block/types/api'; + +import * as addressMock from 'client/slices/address/mocks/address'; +import * as tokenMock from 'client/slices/token/mocks/info'; + +import { ZERO_ADDRESS } from 'toolkit/utils/consts'; + +export const base: Block = { + base_fee_per_gas: '10000000000', + burnt_fees: '5449200000000000', + burnt_fees_percentage: 20.292245650793845, + difficulty: '340282366920938463463374607431768211454', + extra_data: 'TODO', + gas_limit: '12500000', + gas_target_percentage: -91.28128, + gas_used: '544920', + gas_used_percentage: 4.35936, + hash: '0xccc75136de485434d578b73df66537c06b34c3c9b12d085daf95890c914fc2bc', + height: 30146364, + miner: { + hash: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41', + implementations: null, + is_contract: false, + is_verified: null, + name: 'Alex Emelyanov', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + nonce: '0x0000000000000000', + parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f', + priority_fee: '23211757500000000', + rewards: [ + { + reward: '500000000000000000', + type: 'POA Mania Reward', + }, + { + reward: '1026853607510000000', + type: 'Validator Reward', + }, + { + reward: '500000000000000000', + type: 'Emission Reward', + }, + ], + size: 2448, + state_root: 'TODO', + timestamp: '2022-11-11T11:59:35Z', + total_difficulty: '10258276095980170141167591583995189665817672619', + transactions_count: 5, + internal_transactions_count: 12, + transaction_fees: '26853607500000000', + type: 'block', + uncles_hashes: [], +}; + +export const genesis: Block = { + base_fee_per_gas: null, + burnt_fees: null, + burnt_fees_percentage: null, + difficulty: '131072', + extra_data: 'TODO', + gas_limit: '6700000', + gas_target_percentage: -100, + gas_used: '0', + gas_used_percentage: 0, + hash: '0x39f02c003dde5b073b3f6e1700fc0b84b4877f6839bb23edadd3d2d82a488634', + height: 0, + miner: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + nonce: '0x0000000000000000', + parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000', + priority_fee: null, + rewards: [], + size: 533, + state_root: 'TODO', + timestamp: '2017-12-16T00:13:24.000000Z', + total_difficulty: '131072', + transactions_count: 0, + internal_transactions_count: 0, + transaction_fees: '0', + type: 'block', + uncles_hashes: [], +}; + +export const base2: Block = { + ...base, + height: base.height - 1, + size: 592, + miner: { + hash: '0xDfE10D55d9248B2ED66f1647df0b0A46dEb25165', + implementations: null, + is_contract: false, + is_verified: null, + name: 'Kiryl Ihnatsyeu', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + timestamp: '2022-11-11T11:46:05Z', + transactions_count: 253, + gas_target_percentage: 23.6433, + gas_used: '6333342', + gas_used_percentage: 87.859504, + burnt_fees: '232438000000000000', + burnt_fees_percentage: 65.3333333333334, + rewards: [ + { + reward: '500000000000000000', + type: 'Chore Reward', + }, + { + reward: '1017432850000000000', + type: 'Miner Reward', + }, + { + reward: '500000000000000000', + type: 'Emission Reward', + }, + ], + is_pending_update: true, +}; + +export const rootstock: Block = { + ...base, + bitcoin_merged_mining_coinbase_transaction: '0x0000000000000080a1219cea298d65d545b56abafe7c5421edfaf084cf9e374bb23ea985ebd86b206088ac0000000000000000266a24aa21a9edb2ac3022ad2a5327449f029b6aa3d2e55605061b5d8171b30abf5b330d1959c900000000000000002a6a52534b424c4f434b3a481d071e57c6c47cb8eb716295a7079b15859962abf35e32f107b21f003f0bb900000000', + bitcoin_merged_mining_header: '0x000000204a7e42cadf8b5b0a094755c5a13298e596d61f361c6d31171a00000000000000970e51977cd6f82bab9ed62e678c8d8ca664af9d5c3b5cea39d5d4337c7abedae334c9649fc63e1982a84aaa', + bitcoin_merged_mining_merkle_proof: '0x09f386e5e6feb20706a1b5d0817eae96f0ebb0d713eeefe6d5625afc6fd87fcdfe8cc9118bb49e32db87f8e928dcb13dd327b526ced76fb9de0115a5dca8d2a9657c929360ad07418fc7e1a3120da27e0002470d0c98c9b8b5b2835e64e379421d2469204533307bf0c5a087d93fd1dfb3aaea3ee83099928860f6cca891cf59d73c4e3c6053ea4b385dce39067e87c28805ddd89c4ff10500401bec7c248f749ad6f0933e6ad270e447d01711aca1cc26d7989ee59e1431fd2fd5d058edca6d', + hash_for_merged_mining: '0x481d071e57c6c47cb8eb716295a7079b15859962abf35e32f107b21f003f0bb9', + minimum_gas_price: '59240000', +}; + +export const celo: Block = { + ...base, + celo: { + base_fee: { + token: tokenMock.tokenInfoERC20a, + amount: '445690000000000', + breakdown: [ + { + address: addressMock.withName, + amount: '356552000000000.0000000000000', + percentage: 80, + }, + { + address: { + ...addressMock.withoutName, + hash: ZERO_ADDRESS, + }, + amount: '89138000000000.0000000000000', + percentage: 20, + }, + ], + recipient: addressMock.contract, + }, + epoch_number: 1486, + l1_era_finalized_epoch_number: 1485, + }, +}; + +export const zilliqaWithAggregateQuorumCertificate: Block = { + ...base, + zilliqa: { + view: 1137735, + aggregate_quorum_certificate: { + signature: '0x82d29e8f06adc890f6574c3d0ae0c811de1db695b05ed2755ef384fe21bc44f6505b99e201f6000a65f38ff6a13e286306d0e380ef1b43a273eb9947b3f11f852e14b93c258c32b516f89696fcb1190b147364b789572ebdf85d79c4cf3cbbbb', + view: 1137735, + signers: [ 1, 2, 3, 8 ], + nested_quorum_certificates: [ + { + signature: '0xaeb3567577f9db68565c6f97c158b17522620a9684c6f6beaa78920951ad4cae0f287b630bdd034c4a4f89ada42e3dbe012985e976a6f64057d735a4531a26b4e46c182414eabbe625e5b15e6645be5b6522bdec113df408874f6d1e0d894dca', + view: 1137732, + proposed_by_validator_index: 1, + signers: [ 3, 8 ], + }, + { + signature: '0xaeb3567577f9db68565c6f97c158b17522620a9684c6f6beaa78920951ad4cae0f287b630bdd034c4a4f89ada42e3dbe012985e976a6f64057d735a4531a26b4e46c182414eabbe625e5b15e6645be5b6522bdec113df408874f6d1e0d894dca', + view: 1137732, + proposed_by_validator_index: 2, + signers: [ 0, 2 ], + }, + ], + }, + quorum_certificate: { + signature: '0xaeb3567577f9db68565c6f97c158b17522620a9684c6f6beaa78920951ad4cae0f287b630bdd034c4a4f89ada42e3dbe012985e976a6f64057d735a4531a26b4e46c182414eabbe625e5b15e6645be5b6522bdec113df408874f6d1e0d894dca', + view: 1137732, + signers: [ 0, 2, 3, 8 ], + }, + }, +}; + +export const zilliqaWithoutAggregateQuorumCertificate: Block = { + ...base, + zilliqa: { + ...zilliqaWithAggregateQuorumCertificate.zilliqa, + aggregate_quorum_certificate: null, + } as ZilliqaBlockData, +}; + +export const withBlobTxs: Block = { + ...base, + blob_gas_price: '21518435987', + blob_gas_used: '393216', + burnt_blob_fees: '8461393325064192', + excess_blob_gas: '79429632', + blob_transactions_count: 1, +}; + +export const withWithdrawals: Block = { + ...base, + withdrawals_count: 2, +}; + +export const baseListResponse: BlocksResponse = { + items: [ + base, + base2, + ], + next_page_params: null, +}; + +export const rpcBlockBase: RpcBlock = { + difficulty: '0x37fcc04bef8', + extraData: '0x476574682f76312e302e312d38326566323666362f6c696e75782f676f312e34', + gasLimit: '0x2fefd8', + gasUsed: '0x0', + hash: '0xfbafb4b7b6f6789338d15ff046f40dc608a42b1a33b093e109c6d7a36cd76f61', + logsBloom: '0x0', + miner: '0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca', + mixHash: '0x038956b9df89d0c1f980fd656d045e912beafa515cff7d7fd3c5f34ffdcb9e4b', + nonce: '0xd8d3392f340bbb22', + number: '0x1869f', + parentHash: '0x576fd45e598c9f86835f50fe2c6e6d11df2d4c4b01f19e4241b7e793d852f9e4', + receiptsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', + size: '0x225', + stateRoot: '0x32356228651d64cc5e6e7be87a556ecdbf40e876251dc867ba9e4bb82a0124a3', + timestamp: '0x55d19741', + totalDifficulty: '0x259e89748daae17', + transactions: [ + '0x0e70849f10e22fe2e53fe6755f86a572aa6bb2fc472f0b87d9e561efa1fc2e1f', + '0xae5624c77f06d0164301380afa7780ebe49debe77eb3d5167004d69bd188a09f', + ], + transactionsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + uncles: [], + baseFeePerGas: null, + blobGasUsed: `0x0`, + excessBlobGas: `0x0`, + sealFields: [], + withdrawals: [ + { address: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amount: '0x12128cd', index: '0x3216bbb', validatorIndex: '0x4dca3' }, + { address: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amount: '0x12027dd', index: '0x3216bbc', validatorIndex: '0x4dca4' }, + ], +}; + +export const rpcBlockWithTxsInfo: RpcBlock = { + ...rpcBlockBase, + transactions: [ + { + accessList: [ + { + address: '0x7af661a6463993e05a171f45d774cf37e761c83f', + storageKeys: [ + '0x0000000000000000000000000000000000000000000000000000000000000007', + '0x000000000000000000000000000000000000000000000000000000000000000c', + '0x0000000000000000000000000000000000000000000000000000000000000008', + '0x0000000000000000000000000000000000000000000000000000000000000006', + '0x0000000000000000000000000000000000000000000000000000000000000009', + '0x000000000000000000000000000000000000000000000000000000000000000a', + ], + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + storageKeys: [ + '0x0d726f311404f8052d44e7004a6ffb747709a6d3666a62ce3f5aad13374680ab', + '0x1a824a6850dcbd9223afea4418727593881e2911ed2e734272a263153159fe26', + '0xfae3a383c82daf853bbd8bbcd21280410599b135c274c01354ea7d3a5e09f43c', + ], + }, + ], + blockHash: '0xeb37ebc94e31773e5c5703073fd3911b2ab596f099d00d18b55ae3ac8203c1d5', + blockNumber: '0x136058d', + chainId: '0x1', + from: '0x111527f1386c6725a2f5986230f3060bdcac041f', + gas: '0xf4240', + gasPrice: '0x1780b2ff9', + hash: '0x0e70849f10e22fe2e53fe6755f86a572aa6bb2fc472f0b87d9e561efa1fc2e1f', + input: '0x258d7af661a6463993e05a171f45d774cf37e761c83f402ab3277301b3574863a151d042dc870fb1b3f0c72cbbdd53a85898f62415fe124406f6608d8802269d1283cdb2a5a329649e5cb4cdcee91ab6', + // maxFeePerGas: '0x3ac1bf7ee', + // maxPriorityFeePerGas: '0x0', + nonce: '0x127b2', + r: '0x3c47223f880a3fb7b1eca368d9d7320d2278f0b679109d9ed0af4080ee386f23', + s: '0x587a441f9472b312ff302d7132547aa250ea06c6203c76831d56a46ec188e664', + to: '0x000000d40b595b94918a28b27d1e2c66f43a51d3', + transactionIndex: '0x0', + type: '0x1', + v: '0x1', + value: '0x31', + yParity: '0x1', + }, + { + accessList: [], + blockHash: '0xeb37ebc94e31773e5c5703073fd3911b2ab596f099d00d18b55ae3ac8203c1d5', + blockNumber: '0x136058d', + chainId: '0x1', + from: '0xe25d2cb47b606bb6fd9272125457a7230e26f956', + gas: '0x47bb0', + gasPrice: '0x1ba875cb6', + hash: '0xae5624c77f06d0164301380afa7780ebe49debe77eb3d5167004d69bd188a09f', + input: '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006696237b00000000000000000000000000000000000000000000000000000000000000040b080604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000b1a2bc2ec500000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000000006d1aaedfab0f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d422990000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d42299000000000000000000000000000000fee13a103a10d593b9ae06b3e05f2e7e1c00000000000000000000000000000000000000000000000000000000000000190000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d42299000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000006cd4db3c8c8d', + // maxFeePerGas: '0x23493c9cd', + // maxPriorityFeePerGas: '0x427c2cbd', + nonce: '0x32b', + r: '0x6566181b3cfd01702b24a2124ea7698b8cc815c7f37d1ea55800f176ca7a94cf', + s: '0x34f8dd837f37746ccd18f4fa71e05de98a2212f1c931f740598e491518616bb3', + to: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', + transactionIndex: '0x1', + type: '0x1', + v: '0x1', + value: '0xb1a2bc2ec50000', + yParity: '0x1', + }, + ], +}; diff --git a/ui/pages/BlockCountdown.pw.tsx b/client/slices/block/pages/countdown-details/BlockCountdown.pw.tsx similarity index 100% rename from ui/pages/BlockCountdown.pw.tsx rename to client/slices/block/pages/countdown-details/BlockCountdown.pw.tsx diff --git a/ui/pages/BlockCountdown.tsx b/client/slices/block/pages/countdown-details/BlockCountdown.tsx similarity index 91% rename from ui/pages/BlockCountdown.tsx rename to client/slices/block/pages/countdown-details/BlockCountdown.tsx index 406b1741b7..246de226a8 100644 --- a/ui/pages/BlockCountdown.tsx +++ b/client/slices/block/pages/countdown-details/BlockCountdown.tsx @@ -1,14 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Center, Flex, Grid } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; import { route } from 'nextjs/routes'; -import useApiQuery from 'lib/api/useApiQuery'; +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import throwOnResourceLoadError from 'client/shared/errors/throw-on-resource-load-error'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import { useMultichainContext } from 'lib/contexts/multichain'; import dayjs from 'lib/date/dayjs'; -import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; -import getQueryParamString from 'lib/router/getQueryParamString'; import { Button } from 'toolkit/chakra/button'; import { Heading } from 'toolkit/chakra/heading'; import { Image } from 'toolkit/chakra/image'; @@ -16,15 +20,15 @@ import { Link } from 'toolkit/chakra/link'; import { ContentLoader } from 'toolkit/components/loaders/ContentLoader'; import { TruncatedText } from 'toolkit/components/truncation/TruncatedText'; import { downloadBlob } from 'toolkit/utils/file'; -import BlockCountdownTimer from 'ui/blockCountdown/BlockCountdownTimer'; -import createGoogleCalendarLink from 'ui/blockCountdown/createGoogleCalendarLink'; -import createIcsFileBlob from 'ui/blockCountdown/createIcsFileBlob'; +import CapybaraRunner from 'ui/games/CapybaraRunner'; import ChainIcon from 'ui/shared/externalChains/ChainIcon'; import IconSvg from 'ui/shared/IconSvg'; import StatsWidget from 'ui/shared/stats/StatsWidget'; import Time from 'ui/shared/time/Time'; -import CapybaraRunner from '../games/CapybaraRunner'; +import BlockCountdownTimer from './BlockCountdownTimer'; +import createGoogleCalendarLink from './create-google-calendar-link'; +import createIcsFileBlob from './create-ics-file-blob'; type Props = { hideCapybaraRunner?: boolean; diff --git a/ui/blockCountdown/BlockCountdownTimer.tsx b/client/slices/block/pages/countdown-details/BlockCountdownTimer.tsx similarity index 92% rename from ui/blockCountdown/BlockCountdownTimer.tsx rename to client/slices/block/pages/countdown-details/BlockCountdownTimer.tsx index 9bead9f672..8984c143e9 100644 --- a/ui/blockCountdown/BlockCountdownTimer.tsx +++ b/client/slices/block/pages/countdown-details/BlockCountdownTimer.tsx @@ -1,10 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { HStack, StackSeparator } from '@chakra-ui/react'; import React from 'react'; import { SECOND } from 'toolkit/utils/consts'; import BlockCountdownTimerItem from './BlockCountdownTimerItem'; -import splitSecondsInPeriods from './splitSecondsInPeriods'; +import splitSecondsInPeriods from './split-seconds-in-periods'; interface Props { value: number; diff --git a/ui/blockCountdown/BlockCountdownTimerItem.tsx b/client/slices/block/pages/countdown-details/BlockCountdownTimerItem.tsx similarity index 93% rename from ui/blockCountdown/BlockCountdownTimerItem.tsx rename to client/slices/block/pages/countdown-details/BlockCountdownTimerItem.tsx index 91bbb892d4..d929139c59 100644 --- a/ui/blockCountdown/BlockCountdownTimerItem.tsx +++ b/client/slices/block/pages/countdown-details/BlockCountdownTimerItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-desktop-1.png b/client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-desktop-1.png similarity index 100% rename from ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-desktop-1.png rename to client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-desktop-1.png diff --git a/ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-mobile-base-view-1.png b/client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-mobile-base-view-1.png similarity index 100% rename from ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-mobile-base-view-1.png rename to client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_long-period-until-the-block-mobile-base-view-1.png diff --git a/ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-desktop-1.png b/client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-desktop-1.png similarity index 100% rename from ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-desktop-1.png rename to client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-desktop-1.png diff --git a/ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-mobile-base-view-1.png b/client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-mobile-base-view-1.png similarity index 100% rename from ui/pages/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-mobile-base-view-1.png rename to client/slices/block/pages/countdown-details/__screenshots__/BlockCountdown.pw.tsx_default_short-period-until-the-block-mobile-base-view-1.png diff --git a/client/slices/block/pages/countdown-details/create-google-calendar-link.ts b/client/slices/block/pages/countdown-details/create-google-calendar-link.ts new file mode 100644 index 0000000000..2f28bf2771 --- /dev/null +++ b/client/slices/block/pages/countdown-details/create-google-calendar-link.ts @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { route } from 'nextjs/routes'; + +import config from 'configs/app'; +import type { TMultichainContext } from 'lib/contexts/multichain'; +import dayjs from 'lib/date/dayjs'; + +interface Params { + timeFromNow: number; + blockHeight: string; + multichainContext?: TMultichainContext | null; +} + +const DATE_FORMAT = 'YYYYMMDDTHHmm'; + +export default function createGoogleCalendarLink({ timeFromNow, blockHeight, multichainContext }: Params): string { + + const chainConfig = multichainContext?.chain.app_config || config; + + const date = dayjs().add(timeFromNow, 's'); + const name = `Block #${ blockHeight } reminder | ${ chainConfig.chain.name }`; + const description = `#${ blockHeight } block creation time on ${ chainConfig.chain.name } blockchain.`; + const startTime = date.format(DATE_FORMAT); + const endTime = date.add(15, 'minutes').format(DATE_FORMAT); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const blockUrl = config.app.baseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: blockHeight } }, multichainContext); + + const url = new URL('calendar/render', 'https://www.google.com/'); + url.searchParams.append('action', 'TEMPLATE'); + url.searchParams.append('text', name); + url.searchParams.append('details', description + '\n' + blockUrl); + url.searchParams.append('dates', `${ startTime }/${ endTime }`); + url.searchParams.append('ctz', timeZone); + + return url.toString(); +} diff --git a/client/slices/block/pages/countdown-details/create-ics-file-blob.ts b/client/slices/block/pages/countdown-details/create-ics-file-blob.ts new file mode 100644 index 0000000000..54190841ce --- /dev/null +++ b/client/slices/block/pages/countdown-details/create-ics-file-blob.ts @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { route } from 'nextjs/routes'; + +import config from 'configs/app'; +import type { TMultichainContext } from 'lib/contexts/multichain'; +import type dayjs from 'lib/date/dayjs'; + +interface Params { + date: dayjs.Dayjs; + blockHeight: string; + multichainContext?: TMultichainContext | null; +} + +const DATE_FORMAT = 'YYYYMMDDTHHmmss'; + +export default function createIcsFileBlob({ date, blockHeight, multichainContext }: Params): Blob { + const chainConfig = multichainContext?.chain.app_config || config; + + const name = `Block #${ blockHeight } reminder | ${ chainConfig.chain.name }`; + const description = `#${ blockHeight } block creation time on ${ chainConfig.chain.name } blockchain.`; + const startTime = date.format(DATE_FORMAT); + const endTime = date.add(15, 'minutes').format(DATE_FORMAT); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const blockUrl = config.app.baseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: blockHeight } }, multichainContext); + + const icsContent = `BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SUMMARY:${ name } +DESCRIPTION:${ description } +DTSTART;TZID=${ timeZone }:${ startTime } +DTEND;TZID=${ timeZone }:${ endTime } +URL:${ blockUrl } +END:VEVENT +END:VCALENDAR`; + + const blob = new Blob([ icsContent ], { type: 'text/calendar' }); + + return blob; +} diff --git a/client/slices/block/pages/countdown-details/split-seconds-in-periods.ts b/client/slices/block/pages/countdown-details/split-seconds-in-periods.ts new file mode 100644 index 0000000000..8c06c82346 --- /dev/null +++ b/client/slices/block/pages/countdown-details/split-seconds-in-periods.ts @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { padStart } from 'es-toolkit/compat'; + +export default function splitSecondsInPeriods(value: number) { + const seconds = value % 60; + const minutes = (value - seconds) / 60 % 60; + const hours = (value - seconds - minutes * 60) / (60 * 60) % 24; + const days = (value - seconds - minutes * 60 - hours * 60 * 60) / (60 * 60 * 24); + + return { + seconds: padStart(String(seconds), 2, '0'), + minutes: padStart(String(minutes), 2, '0'), + hours: padStart(String(hours), 2, '0'), + days: padStart(String(days), 2, '0'), + }; +} diff --git a/ui/pages/BlockCountdownIndex.pw.tsx b/client/slices/block/pages/countdown-index/BlockCountdownIndex.pw.tsx similarity index 100% rename from ui/pages/BlockCountdownIndex.pw.tsx rename to client/slices/block/pages/countdown-index/BlockCountdownIndex.pw.tsx diff --git a/ui/pages/BlockCountdownIndex.tsx b/client/slices/block/pages/countdown-index/BlockCountdownIndex.tsx similarity index 97% rename from ui/pages/BlockCountdownIndex.tsx rename to client/slices/block/pages/countdown-index/BlockCountdownIndex.tsx index 65ccdf3936..a9965be1b7 100644 --- a/ui/pages/BlockCountdownIndex.tsx +++ b/client/slices/block/pages/countdown-index/BlockCountdownIndex.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Box, Center } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; diff --git a/ui/pages/__screenshots__/BlockCountdownIndex.pw.tsx_default_base-view-mobile-1.png b/client/slices/block/pages/countdown-index/__screenshots__/BlockCountdownIndex.pw.tsx_default_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/BlockCountdownIndex.pw.tsx_default_base-view-mobile-1.png rename to client/slices/block/pages/countdown-index/__screenshots__/BlockCountdownIndex.pw.tsx_default_base-view-mobile-1.png diff --git a/ui/pages/__screenshots__/BlockCountdownIndex.pw.tsx_mobile_base-view-mobile-1.png b/client/slices/block/pages/countdown-index/__screenshots__/BlockCountdownIndex.pw.tsx_mobile_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/BlockCountdownIndex.pw.tsx_mobile_base-view-mobile-1.png rename to client/slices/block/pages/countdown-index/__screenshots__/BlockCountdownIndex.pw.tsx_mobile_base-view-mobile-1.png diff --git a/ui/pages/Block.pw.tsx b/client/slices/block/pages/details/Block.pw.tsx similarity index 98% rename from ui/pages/Block.pw.tsx rename to client/slices/block/pages/details/Block.pw.tsx index 38a8ff5831..501a94aff4 100644 --- a/ui/pages/Block.pw.tsx +++ b/client/slices/block/pages/details/Block.pw.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { numberToHex } from 'viem'; +import * as blockMock from 'client/slices/block/mocks/block'; + import config from 'configs/app'; -import * as blockMock from 'mocks/blocks/block'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect } from 'playwright/lib'; diff --git a/client/slices/block/pages/details/Block.tsx b/client/slices/block/pages/details/Block.tsx new file mode 100644 index 0000000000..0cd7e572e0 --- /dev/null +++ b/client/slices/block/pages/details/Block.tsx @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { chakra, Flex } from '@chakra-ui/react'; +import { capitalize } from 'es-toolkit'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types'; +import type { PaginationParams } from 'ui/shared/pagination/types'; + +import { routeParams } from 'nextjs/routes'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import BlockPendingUpdateAlert from 'client/slices/block/components/BlockPendingUpdateAlert'; +import * as BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import useBlockInternalTxsQuery from 'client/slices/block/hooks/useBlockInternalTxsQuery'; +import useBlockQuery from 'client/slices/block/hooks/useBlockQuery'; +import useBlockTxsQuery from 'client/slices/block/hooks/useBlockTxsQuery'; +import BlockDetails from 'client/slices/block/pages/details/BlockDetails'; +import BlockInternalTxs from 'client/slices/block/pages/details/BlockInternalTxs'; +import TxsWithFrontendSorting from 'client/slices/tx/pages/index/list/TxsWithFrontendSorting'; + +import BlockDeposits from 'client/features/chain-variants/beacon-chain/pages/block/BlockDeposits'; +import BlockWithdrawals from 'client/features/chain-variants/beacon-chain/pages/block/BlockWithdrawals'; +import useBlockDepositsQuery from 'client/features/chain-variants/beacon-chain/pages/block/useBlockDepositsQuery'; +import useBlockWithdrawalsQuery from 'client/features/chain-variants/beacon-chain/pages/block/useBlockWithdrawalsQuery'; +import BlockCeloEpochTag from 'client/features/chain-variants/celo/pages/block/BlockCeloEpochTag'; +import useBlockBlobTxsQuery from 'client/features/data-availability/hooks/useBlockBlobTxsQuery'; + +import getChainValidatorTitle from 'client/shared/chain/get-chain-validator-title'; +import throwOnAbsentParamError from 'client/shared/errors/throw-on-absent-param-error'; +import throwOnResourceLoadError from 'client/shared/errors/throw-on-resource-load-error'; +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import config from 'configs/app'; +import { useMultichainContext } from 'lib/contexts/multichain'; +import { Skeleton } from 'toolkit/chakra/skeleton'; +import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; +import TextAd from 'ui/shared/ad/TextAd'; +import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; +import NetworkExplorers from 'ui/shared/NetworkExplorers'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; + +const TAB_LIST_PROPS = { + marginBottom: 0, + pt: 6, + pb: 6, + marginTop: -5, +}; +const TABS_HEIGHT = 88; + +const BlockPageContent = () => { + const router = useRouter(); + const isMobile = useIsMobile(); + const heightOrHash = getQueryParamString(router.query.height_or_hash); + const tab = getQueryParamString(router.query.tab); + const multichainContext = useMultichainContext(); + + const chainConfig = multichainContext?.chain.app_config ?? config; + const beaconChainFeature = chainConfig.features.beaconChain; + + const blockQuery = useBlockQuery({ heightOrHash }); + const blockTxsQuery = useBlockTxsQuery({ heightOrHash, blockQuery, tab }); + const blockWithdrawalsQuery = useBlockWithdrawalsQuery({ heightOrHash, blockQuery, tab }); + const blockDepositsQuery = useBlockDepositsQuery({ heightOrHash, blockQuery, tab }); + const blockBlobTxsQuery = useBlockBlobTxsQuery({ heightOrHash, blockQuery, tab }); + const blockInternalTxsQuery = useBlockInternalTxsQuery({ heightOrHash, blockQuery, tab, chainConfig }); + + const hasPagination = !isMobile && ( + (tab === 'txs' && blockTxsQuery.pagination.isVisible) || + (tab === 'withdrawals' && blockWithdrawalsQuery.pagination.isVisible) || + (tab === 'deposits' && blockDepositsQuery.pagination.isVisible) || + (tab === 'internal_txs' && blockInternalTxsQuery.pagination.isVisible) + ); + + const tabs: Array = React.useMemo(() => ([ + { + id: 'index', + title: 'Details', + component: ( + <> + + { blockQuery.isDegradedData && } + { blockQuery.data?.is_pending_update && } + + + + ), + }, + { + id: 'txs', + title: 'Transactions', + component: ( + <> + { blockTxsQuery.isDegradedData && } + + + ), + }, + chainConfig.UI.views.internalTx.isEnabled ? { + id: 'internal_txs', + title: 'Internal txns', + component: ( + <> + { blockTxsQuery.isDegradedData && } + + + ), + } : null, + chainConfig.features.dataAvailability.isEnabled && blockQuery.data?.blob_transactions_count ? + { + id: 'blob_txs', + title: 'Blob txns', + component: ( + + ), + } : null, + beaconChainFeature.isEnabled && !beaconChainFeature.withdrawalsOnly && Boolean(blockQuery.data?.beacon_deposits_count) ? + { + id: 'deposits', + title: 'Deposits', + component: ( + <> + { blockDepositsQuery.isDegradedData && } + + + ), + } : null, + beaconChainFeature.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ? + { + id: 'withdrawals', + title: 'Withdrawals', + component: ( + <> + { blockWithdrawalsQuery.isDegradedData && + } + + + ), + } : null, + ].filter(Boolean)), [ + beaconChainFeature, + blockBlobTxsQuery, + blockDepositsQuery, + blockInternalTxsQuery, + blockQuery, + blockTxsQuery, + blockWithdrawalsQuery, + chainConfig.UI.views.internalTx.isEnabled, + chainConfig.features.dataAvailability.isEnabled, + hasPagination, + ]); + + let pagination; + if (tab === 'txs') { + pagination = blockTxsQuery.pagination; + } else if (tab === 'withdrawals') { + pagination = blockWithdrawalsQuery.pagination; + } else if (tab === 'deposits') { + pagination = blockDepositsQuery.pagination; + } else if (tab === 'internal_txs') { + pagination = blockInternalTxsQuery.pagination; + } + + throwOnAbsentParamError(heightOrHash); + + if (blockQuery.isError) { + if (!blockQuery.isDegradedData && blockQuery.error.status === 404 && !heightOrHash.startsWith('0x') && blockQuery.isFutureBlock) { + const url = routeParams({ pathname: '/block/countdown/[height]', query: { height: heightOrHash } }, multichainContext); + router.push(url, undefined, { shallow: true }); + return null; + } else { + throwOnResourceLoadError(blockQuery); + } + } + + const title = (() => { + switch (blockQuery.data?.type) { + case 'reorg': + return `Reorged block #${ blockQuery.data?.height }`; + + case 'uncle': + return `Uncle block #${ blockQuery.data?.height }`; + + default: + return `Block #${ blockQuery.data?.height }`; + } + })(); + + const beforeTitleElement = multichainContext?.chain ? ( + + ) : null; + + const titleSecondRow = ( + <> + { !chainConfig.UI.views.block.hiddenFields?.miner && blockQuery.data?.miner && ( + + + { capitalize(getChainValidatorTitle()) } + + + + ) } + + + ); + + return ( + <> + + } + secondRow={ titleSecondRow } + isLoading={ blockQuery.isPlaceholderData } + /> + : null } + stickyEnabled={ hasPagination } + /> + + ); +}; + +export default BlockPageContent; diff --git a/ui/block/BlockDetails.pw.tsx b/client/slices/block/pages/details/BlockDetails.pw.tsx similarity index 92% rename from ui/block/BlockDetails.pw.tsx rename to client/slices/block/pages/details/BlockDetails.pw.tsx index a10a06ef5a..6cd47c6717 100644 --- a/ui/block/BlockDetails.pw.tsx +++ b/client/slices/block/pages/details/BlockDetails.pw.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import * as blockMock from 'mocks/blocks/block'; +import type { BlockQuery } from 'client/slices/block/hooks/useBlockQuery'; +import * as blockMock from 'client/slices/block/mocks/block'; + import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect } from 'playwright/lib'; import BlockDetails from './BlockDetails'; -import type { BlockQuery } from './useBlockQuery'; const hooksConfig = { router: { diff --git a/ui/block/BlockDetails.tsx b/client/slices/block/pages/details/BlockDetails.tsx similarity index 93% rename from ui/block/BlockDetails.tsx rename to client/slices/block/pages/details/BlockDetails.tsx index 2d90281af5..4d0583b3d5 100644 --- a/ui/block/BlockDetails.tsx +++ b/client/slices/block/pages/details/BlockDetails.tsx @@ -1,35 +1,46 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { GridItem, Text, Box } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import { capitalize } from 'es-toolkit'; import { useRouter } from 'next/router'; import React from 'react'; -import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2'; +import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'client/features/rollup/zk-sync/types/api'; import { route, routeParams } from 'nextjs/routes'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import type { BlockQuery } from 'client/slices/block/hooks/useBlockQuery'; +import getBlockReward from 'client/slices/block/utils/get-block-reward'; +import GasUsed from 'client/slices/gas/components/GasUsed'; + +import BlockDetailsBaseFeeCelo from 'client/features/chain-variants/celo/pages/block/BlockDetailsBaseFeeCelo'; +import BlockDetailsZilliqaQuorumCertificate from 'client/features/chain-variants/zilliqa/pages/block/BlockDetailsZilliqaQuorumCertificate'; +import BlockDetailsBlobInfo from 'client/features/data-availability/pages/block/BlockDetailsBlobInfo'; +import * as arbitrum from 'client/features/rollup/arbitrum/utils/batch-verification'; +import BatchEntityL2 from 'client/features/rollup/common/components/BatchEntityL2'; +import BlockEntityL1 from 'client/features/rollup/common/components/BlockEntityL1'; +import TxEntityL1 from 'client/features/rollup/common/components/TxEntityL1'; +import { layerLabels } from 'client/features/rollup/common/utils/layer'; +import OptimisticL2TxnBatchDA from 'client/features/rollup/optimism/components/TxnBatchDA'; +import ZkSyncL2TxnBatchHashesInfo from 'client/features/rollup/zk-sync/pages/batch-details/ZkSyncL2TxnBatchHashesInfo'; +import { formatZkSyncL2TxnBatchStatus } from 'client/features/rollup/zk-sync/utils/format-txn-batch-status'; + +import getChainValidatorTitle from 'client/shared/chain/get-chain-validator-title'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + import config from 'configs/app'; -import getBlockReward from 'lib/block/getBlockReward'; import { useMultichainContext } from 'lib/contexts/multichain'; -import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; -import * as arbitrum from 'lib/rollups/arbitrum'; -import { formatZkSyncL2TxnBatchStatus, layerLabels } from 'lib/rollups/utils'; -import getQueryParamString from 'lib/router/getQueryParamString'; import { CollapsibleDetails } from 'toolkit/chakra/collapsible'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; import { ZERO } from 'toolkit/utils/consts'; import { space } from 'toolkit/utils/htmlEntities'; -import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA'; -import BlockGasUsed from 'ui/shared/block/BlockGasUsed'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2'; -import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1'; -import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import IconSvg from 'ui/shared/IconSvg'; import PrevNext from 'ui/shared/PrevNext'; @@ -40,12 +51,6 @@ import GasPriceValue from 'ui/shared/value/GasPriceValue'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; import { WEI } from 'ui/shared/value/utils'; import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; -import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo'; - -import BlockDetailsBaseFeeCelo from './details/BlockDetailsBaseFeeCelo'; -import BlockDetailsBlobInfo from './details/BlockDetailsBlobInfo'; -import BlockDetailsZilliqaQuorumCertificate from './details/BlockDetailsZilliqaQuorumCertificate'; -import type { BlockQuery } from './useBlockQuery'; const zkSyncVerificationSteps = ZKSYNC_L2_TX_BATCH_STATUSES.map(formatZkSyncL2TxnBatchStatus); @@ -79,7 +84,7 @@ const BlockDetails = ({ query }: Props) => { const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data); - const validatorTitle = getNetworkValidatorTitle(); + const validatorTitle = getChainValidatorTitle(); const rewardBreakDown = (() => { if (rollupFeature.isEnabled || totalReward.isEqualTo(ZERO) || txFees.isEqualTo(ZERO) || burntFees.isEqualTo(ZERO)) { @@ -384,7 +389,7 @@ const BlockDetails = ({ query }: Props) => { <> { { BigNumber(data.gas_used || 0).toFormat() } - { <> ; diff --git a/ui/blocks/BlocksListItem.tsx b/client/slices/block/pages/index/BlocksListItem.tsx similarity index 89% rename from ui/blocks/BlocksListItem.tsx rename to client/slices/block/pages/index/BlocksListItem.tsx index 6cdfc80a1f..4fe6224aea 100644 --- a/ui/blocks/BlocksListItem.tsx +++ b/client/slices/block/pages/index/BlocksListItem.tsx @@ -1,23 +1,27 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Text, Box } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import { capitalize } from 'es-toolkit'; import React from 'react'; -import type { Block } from 'types/api/block'; +import type { Block } from 'client/slices/block/types/api'; import type { ClusterChainConfig } from 'types/multichain'; import { route } from 'nextjs-routes'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import getBlockTotalReward from 'client/slices/block/utils/get-block-total-reward'; +import GasUsed from 'client/slices/gas/components/GasUsed'; + +import getChainValidatorTitle from 'client/shared/chain/get-chain-validator-title'; +import { currencyUnits } from 'client/shared/chain/units'; + import config from 'configs/app'; -import getBlockTotalReward from 'lib/block/getBlockTotalReward'; -import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; -import { currencyUnits } from 'lib/units'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; -import BlockGasUsed from 'ui/shared/block/BlockGasUsed'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import IconSvg from 'ui/shared/IconSvg'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; @@ -77,7 +81,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation, chain ) } { !config.UI.views.block.hiddenFields?.miner && ( - { capitalize(getNetworkValidatorTitle()) } + { capitalize(getChainValidatorTitle()) } { BigNumber(data.gas_used || 0).toFormat() } - { }, }); - const networkUtilization = getNetworkUtilizationParams(statsQuery.data?.network_utilization_percentage ?? 0); + const networkUtilization = getChainUtilizationParams(statsQuery.data?.network_utilization_percentage ?? 0); return ( diff --git a/ui/blocks/BlocksTable.tsx b/client/slices/block/pages/index/BlocksTable.tsx similarity index 86% rename from ui/blocks/BlocksTable.tsx rename to client/slices/block/pages/index/BlocksTable.tsx index 3588513e8a..48bf1ddff6 100644 --- a/ui/blocks/BlocksTable.tsx +++ b/client/slices/block/pages/index/BlocksTable.tsx @@ -1,16 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { capitalize } from 'es-toolkit'; import React from 'react'; -import type { Block } from 'types/api/block'; +import type { Block } from 'client/slices/block/types/api'; import type { ClusterChainConfig } from 'types/multichain'; +import { AddressHighlightProvider } from 'client/slices/address/contexts/address-highlight'; +import BlocksTableItem from 'client/slices/block/pages/index/BlocksTableItem'; + +import getChainValidatorTitle from 'client/shared/chain/get-chain-validator-title'; +import { currencyUnits } from 'client/shared/chain/units'; +import useInitialList from 'client/shared/lists/useInitialList'; + import config from 'configs/app'; -import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; -import useInitialList from 'lib/hooks/useInitialList'; -import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; -import { currencyUnits } from 'lib/units'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; -import BlocksTableItem from 'ui/blocks/BlocksTableItem'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle'; @@ -58,7 +62,7 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum Size, bytes { !config.UI.views.block.hiddenFields?.miner && ( - { capitalize(getNetworkValidatorTitle()) } + { capitalize(getChainValidatorTitle()) } ) } Txn diff --git a/ui/blocks/BlocksTableItem.tsx b/client/slices/block/pages/index/BlocksTableItem.tsx similarity index 90% rename from ui/blocks/BlocksTableItem.tsx rename to client/slices/block/pages/index/BlocksTableItem.tsx index c61ac5c884..2c4009cc7b 100644 --- a/ui/blocks/BlocksTableItem.tsx +++ b/client/slices/block/pages/index/BlocksTableItem.tsx @@ -1,22 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; -import type { Block } from 'types/api/block'; +import type { Block } from 'client/slices/block/types/api'; import type { ClusterChainConfig } from 'types/multichain'; import { route } from 'nextjs-routes'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import BlockPendingUpdateHint from 'client/slices/block/components/BlockPendingUpdateHint'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import getBlockTotalReward from 'client/slices/block/utils/get-block-total-reward'; +import GasUsed from 'client/slices/gas/components/GasUsed'; + import config from 'configs/app'; -import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { TableCell, TableRow } from 'toolkit/chakra/table'; import { Tooltip } from 'toolkit/chakra/tooltip'; -import BlockGasUsed from 'ui/shared/block/BlockGasUsed'; -import BlockPendingUpdateHint from 'ui/shared/block/BlockPendingUpdateHint'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import ChainIcon from 'ui/shared/externalChains/ChainIcon'; import IconSvg from 'ui/shared/IconSvg'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; @@ -104,7 +107,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement, animation, chai { BigNumber(data.gas_used || 0).toFormat() } - ; + gas_target_percentage: number | null; + gas_used_percentage: number | null; + burnt_fees_percentage: number | null; + type: BlockType; + transaction_fees: string | null; + uncles_hashes: Array; + withdrawals_count?: number; + beacon_deposits_count?: number; + is_pending_update?: boolean; +} + +export interface BlocksResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + } | null; +} + +export interface BlockTransactionsResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + index: number; + } | null; +} + +export interface BlockInternalTransactionsResponse { + items: Array; + next_page_params: { + block_index: number; + items_count: number; + } | null; +} + +export interface NewBlockSocketResponse { + average_block_time: string; + block: Block; +} + +export interface BlockFilters { + type?: BlockType; +} + +export interface BlockCountdownResponse { + result: { + CountdownBlock: string; + CurrentBlock: string; + EstimateTimeInSec: string; + RemainingBlock: string; + } | null; +} + +export type { BlockWithdrawalsResponse, BlockWithdrawalsItem } from 'client/features/chain-variants/beacon-chain/types/api'; diff --git a/client/slices/block/utils/format-rpc-data.ts b/client/slices/block/utils/format-rpc-data.ts new file mode 100644 index 0000000000..b522674d56 --- /dev/null +++ b/client/slices/block/utils/format-rpc-data.ts @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { Chain, GetBlockReturnType } from 'viem'; + +import type { Block } from 'client/slices/block/types/api'; + +import { unknownAddress } from 'client/slices/address/utils/consts'; + +import dayjs from 'lib/date/dayjs'; + +export default function formatRpcData(block: GetBlockReturnType | null): Block | null { + if (!block) { + return null; + } + + return { + height: Number(block.number), + timestamp: dayjs.unix(Number(block.timestamp)).format(), + transactions_count: block.transactions.length, + internal_transactions_count: 0, + miner: { ...unknownAddress, hash: block.miner }, + size: Number(block.size), + hash: block.hash, + parent_hash: block.parentHash, + difficulty: block.difficulty?.toString() ?? null, + total_difficulty: block.totalDifficulty?.toString() ?? null, + gas_used: block.gasUsed.toString(), + gas_limit: block.gasLimit.toString(), + nonce: block.nonce, + base_fee_per_gas: block.baseFeePerGas?.toString() ?? null, + burnt_fees: null, + priority_fee: null, + extra_data: block.extraData, + state_root: block.stateRoot, + gas_target_percentage: null, + gas_used_percentage: null, + burnt_fees_percentage: null, + type: 'block', // we can't get this type from RPC, so it will always be a regular block + transaction_fees: null, + uncles_hashes: block.uncles, + withdrawals_count: block.withdrawals?.length, + }; +} diff --git a/client/slices/block/utils/get-block-reward.ts b/client/slices/block/utils/get-block-reward.ts new file mode 100644 index 0000000000..b585d89a34 --- /dev/null +++ b/client/slices/block/utils/get-block-reward.ts @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import BigNumber from 'bignumber.js'; + +import type { Block } from 'client/slices/block/types/api'; + +export default function getBlockReward(block: Block) { + const txFees = BigNumber(block.transaction_fees || 0); + const burntFees = BigNumber(block.burnt_fees || 0); + const minerReward = block.rewards?.find(({ type }) => type === 'Miner Reward' || type === 'Validator Reward')?.reward; + const totalReward = BigNumber(minerReward || 0); + const staticReward = totalReward.minus(txFees).plus(burntFees); + + return { + totalReward, + staticReward, + txFees, + burntFees, + }; +} diff --git a/client/slices/block/utils/get-block-total-reward.ts b/client/slices/block/utils/get-block-total-reward.ts new file mode 100644 index 0000000000..e32a741f48 --- /dev/null +++ b/client/slices/block/utils/get-block-total-reward.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import BigNumber from 'bignumber.js'; + +import type { Block } from 'client/slices/block/types/api'; + +import { ZERO } from 'toolkit/utils/consts'; +import { WEI } from 'ui/shared/value/utils'; + +export default function getBlockTotalReward(block: Block) { + const totalReward = block.rewards + ?.map(({ reward }) => BigNumber(reward)) + .reduce((result, item) => result.plus(item), ZERO) || ZERO; + + return totalReward.div(WEI); +} diff --git a/ui/shared/ContractCertifiedLabel.tsx b/client/slices/contract/components/ContractCertifiedLabel.tsx similarity index 86% rename from ui/shared/ContractCertifiedLabel.tsx rename to client/slices/contract/components/ContractCertifiedLabel.tsx index 0948537d59..91df9319d0 100644 --- a/ui/shared/ContractCertifiedLabel.tsx +++ b/client/slices/contract/components/ContractCertifiedLabel.tsx @@ -1,9 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; import { Tooltip } from 'toolkit/chakra/tooltip'; - -import IconSvg from './IconSvg'; +import IconSvg from 'ui/shared/IconSvg'; type Props = { iconSize: number; diff --git a/ui/shared/statusTag/ContractCreationStatus.tsx b/client/slices/contract/components/ContractCreationStatus.tsx similarity index 84% rename from ui/shared/statusTag/ContractCreationStatus.tsx rename to client/slices/contract/components/ContractCreationStatus.tsx index 62ac35d166..c759d7c772 100644 --- a/ui/shared/statusTag/ContractCreationStatus.tsx +++ b/client/slices/contract/components/ContractCreationStatus.tsx @@ -1,12 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { SmartContractCreationStatus } from 'types/api/contract'; +import type { SmartContractCreationStatus } from 'client/slices/contract/types/api'; import type { BadgeProps } from 'toolkit/chakra/badge'; import { Badge } from 'toolkit/chakra/badge'; import { Tooltip } from 'toolkit/chakra/tooltip'; - -import StatusTag from './StatusTag'; +import StatusTag from 'ui/shared/statusTag/StatusTag'; interface Props extends BadgeProps { status: SmartContractCreationStatus; diff --git a/ui/verifiedContracts/useVerifiedContractsQuery.ts b/client/slices/contract/hooks/useVerifiedContractsQuery.ts similarity index 84% rename from ui/verifiedContracts/useVerifiedContractsQuery.ts rename to client/slices/contract/hooks/useVerifiedContractsQuery.ts index 0a091eea3c..c49ad2c2d4 100644 --- a/ui/verifiedContracts/useVerifiedContractsQuery.ts +++ b/client/slices/contract/hooks/useVerifiedContractsQuery.ts @@ -1,19 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useRouter } from 'next/router'; import React from 'react'; -import type { VerifiedContractsFilters } from 'types/api/contracts'; -import type { VerifiedContractsSorting, VerifiedContractsSortingField, VerifiedContractsSortingValue } from 'types/api/verifiedContracts'; +import type { + VerifiedContractsFilters, + VerifiedContractsSorting, + VerifiedContractsSortingField, + VerifiedContractsSortingValue, +} from 'client/slices/contract/types/api'; + +import { SORT_OPTIONS } from 'client/slices/contract/pages/index/sort'; +import { VERIFIED_CONTRACT_INFO } from 'client/slices/contract/stubs'; + +import useDebounce from 'client/shared/hooks/useDebounce'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; -import useDebounce from 'lib/hooks/useDebounce'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import { VERIFIED_CONTRACT_INFO } from 'stubs/contract'; import { generateListStub } from 'stubs/utils'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; -import { SORT_OPTIONS } from './utils'; - interface Props { isMultichain?: boolean; } diff --git a/client/slices/contract/mocks/counters.ts b/client/slices/contract/mocks/counters.ts new file mode 100644 index 0000000000..e02d5ad9b7 --- /dev/null +++ b/client/slices/contract/mocks/counters.ts @@ -0,0 +1,8 @@ +import type { VerifiedContractsCounters } from '../types/api'; + +export const verifiedContractsCountersMock: VerifiedContractsCounters = { + smart_contracts: '123456789', + new_smart_contracts_24h: '12345', + verified_smart_contracts: '654321', + new_verified_smart_contracts_24h: '0', +}; diff --git a/client/slices/contract/mocks/info.ts b/client/slices/contract/mocks/info.ts new file mode 100644 index 0000000000..ff3d0991a8 --- /dev/null +++ b/client/slices/contract/mocks/info.ts @@ -0,0 +1,151 @@ +/* eslint-disable max-len */ +import type { SmartContract } from 'client/slices/contract/types/api'; + +export const verified: SmartContract = { + abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ], + can_be_visualized_via_sol2uml: true, + compiler_version: 'v0.5.16+commit.9c3226ce', + constructor_args: 'constructor_args', + creation_bytecode: 'creation_bytecode', + creation_status: 'success', + deployed_bytecode: 'deployed_bytecode', + compiler_settings: { + evmVersion: 'london', + remappings: [ + '@openzeppelin/=node_modules/@openzeppelin/', + ], + }, + evm_version: 'default', + is_verified: true, + is_blueprint: false, + name: 'WPOA', + optimization_enabled: true, + optimization_runs: 1500, + source_code: 'source_code', + verified_at: '2021-08-03T10:40:41.679421Z', + decoded_constructor_args: [ + [ '0xc59615da2da226613b1c78f0c6676cac497910bc', { internalType: 'address', name: '_token', type: 'address' } ], + [ [ 1800, 3600, 7200 ], { internalType: 'uint256[]', name: '_durations', type: 'uint256[]' } ], + [ '900000000', { internalType: 'uint256', name: '_totalSupply', type: 'uint256' } ], + ], + external_libraries: [ + { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'Sol' }, + { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' }, + ], + language: 'solidity', + license_type: 'gnu_gpl_v3', + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + file_path: '', + additional_sources: [], + verified_twin_address_hash: null, + conflicting_implementations: null, +}; + +export const certified: SmartContract = { + ...verified, + certified: true, +}; + +export const withMultiplePaths: SmartContract = { + ...verified, + file_path: './simple_storage.sol', + additional_sources: [ + { + file_path: '/contracts/protocol/libraries/logic/GenericLogic.sol', + source_code: '// SPDX-License-Identifier: GPL-3.0 \n pragma solidity >=0.7.0 <0.9.0; \n contract Storage {\n //2112313123; \nuint256 number; \n function store(uint256 num) public {\nnumber = num;\n}\n function retrieve() public view returns (uint256)\n {\nreturn number;\n}\n}', + }, + ], +}; + +export const verifiedViaSourcify: SmartContract = { + ...verified, + is_verified_via_sourcify: true, + is_fully_verified: false, + is_partially_verified: true, + sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/', +}; + +export const verifiedViaEthBytecodeDb: SmartContract = { + ...verified, + is_verified_via_eth_bytecode_db: true, +}; + +export const withTwinAddress: SmartContract = { + ...verified, + is_verified: false, + verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', +}; + +export const withProxyAddress: SmartContract = { + ...verified, + is_verified: false, + verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', +}; + +export const selfDestructed: SmartContract = { + ...verified, + creation_status: 'selfdestructed', +}; + +export const withChangedByteCode: SmartContract = { + ...verified, + is_changed_bytecode: true, + is_blueprint: true, +}; + +export const zkSync: SmartContract = { + ...verified, + zk_compiler_version: 'v1.2.5', + optimization_enabled: true, + optimization_runs: 's', +}; + +export const stylusRust: SmartContract = { + ...verified, + language: 'stylus_rust', + github_repository_metadata: { + commit: 'af5029f822815e32def0015bf8e591e769c62f34', + path_prefix: 'examples/erc20', + repository_url: 'https://github.com/blockscout/cargo-stylus-test-examples', + }, + compiler_version: 'v0.5.6', + package_name: 'erc20', + evm_version: null, +}; + +export const nonVerified: SmartContract = { + is_verified: false, + is_blueprint: false, + creation_bytecode: 'creation_bytecode', + deployed_bytecode: 'deployed_bytecode', + creation_status: 'success', + abi: null, + compiler_version: null, + evm_version: null, + optimization_enabled: null, + optimization_runs: null, + name: null, + verified_at: null, + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + source_code: null, + constructor_args: null, + decoded_constructor_args: null, + can_be_visualized_via_sol2uml: null, + file_path: '', + additional_sources: [], + external_libraries: null, + verified_twin_address_hash: null, + conflicting_implementations: null, + language: null, + license_type: null, +}; diff --git a/client/slices/contract/mocks/list.ts b/client/slices/contract/mocks/list.ts new file mode 100644 index 0000000000..eed2704231 --- /dev/null +++ b/client/slices/contract/mocks/list.ts @@ -0,0 +1,85 @@ +import type { VerifiedContract, VerifiedContractsResponse } from '../types/api'; + +export const contract1: VerifiedContract = { + address: { + hash: '0xef490030ac0d53B70E304b6Bc5bF657dc6780bEB', + implementations: null, + is_contract: true, + is_verified: null, + name: 'MockERC20', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + coin_balance: '2346534676900000008', + compiler_version: 'v0.8.17+commit.8df45f5f', + has_constructor_args: false, + language: 'solidity', + market_cap: null, + optimization_enabled: false, + transactions_count: 7334224, + verified_at: '2022-09-16T18:49:29.605179Z', + license_type: 'mit', +}; + +export const contract2: VerifiedContract = { + address: { + hash: '0xB2218bdEbe8e90f80D04286772B0968ead666942', + implementations: null, + is_contract: true, + is_verified: null, + name: 'EternalStorageProxyWithSomeExternalLibrariesAndEvenMore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + certified: true, + coin_balance: '9078234570352343999', + compiler_version: 'v0.3.1+commit.0463ea4c', + has_constructor_args: true, + language: 'vyper', + market_cap: null, + optimization_enabled: true, + transactions_count: 440, + verified_at: '2021-09-07T20:01:56.076979Z', + license_type: 'bsd_3_clause', +}; + +export const contract3: VerifiedContract = { + address: { + ens_domain_name: null, + hash: '0xf145e3A26c6706F64d95Dc8d9d45022D8b3D676B', + implementations: [], + is_contract: true, + is_verified: true, + metadata: null, + name: 'StylusTestToken', + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + certified: false, + coin_balance: '0', + compiler_version: 'v0.5.6', + has_constructor_args: false, + language: 'stylus_rust', + license_type: 'none', + market_cap: null, + optimization_enabled: false, + transactions_count: 0, + verified_at: '2024-12-03T14:05:42.796224Z', +}; + +export const baseResponse: VerifiedContractsResponse = { + items: [ + contract1, + contract2, + contract3, + ], + next_page_params: { + items_count: '50', + smart_contract_id: '172', + }, +}; diff --git a/mocks/contract/methods.ts b/client/slices/contract/mocks/methods.ts similarity index 98% rename from mocks/contract/methods.ts rename to client/slices/contract/mocks/methods.ts index 38fbc6f9f9..ca6866202e 100644 --- a/mocks/contract/methods.ts +++ b/client/slices/contract/mocks/methods.ts @@ -1,4 +1,4 @@ -import type { SmartContractMethodRead, SmartContractMethodWrite } from 'ui/address/contract/methods/types'; +import type { SmartContractMethodRead, SmartContractMethodWrite } from 'client/slices/contract/pages/details/methods/types'; export const read: Array = [ { diff --git a/client/slices/contract/pages/contract-verification/ContractVerification.tsx b/client/slices/contract/pages/contract-verification/ContractVerification.tsx new file mode 100644 index 0000000000..11afcffdea --- /dev/null +++ b/client/slices/contract/pages/contract-verification/ContractVerification.tsx @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +import ContractVerificationForm from 'client/slices/contract/pages/contract-verification/ContractVerificationForm'; +import useFormConfigQuery from 'client/slices/contract/pages/contract-verification/useFormConfigQuery'; + +import { ContentLoader } from 'toolkit/components/loaders/ContentLoader'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import PageTitle from 'ui/shared/Page/PageTitle'; + +const ContractVerification = () => { + const configQuery = useFormConfigQuery(true); + + const content = (() => { + if (configQuery.isError) { + return ; + } + + if (configQuery.isPending) { + return ; + } + + return ( + + ); + })(); + + return ( + <> + + { content } + + ); +}; + +export default ContractVerification; diff --git a/client/slices/contract/pages/contract-verification/ContractVerificationForAddress.tsx b/client/slices/contract/pages/contract-verification/ContractVerificationForAddress.tsx new file mode 100644 index 0000000000..d21b869075 --- /dev/null +++ b/client/slices/contract/pages/contract-verification/ContractVerificationForAddress.tsx @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { SmartContractVerificationMethodApi } from 'client/slices/contract/types/api'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import ContractVerificationForm from 'client/slices/contract/pages/contract-verification/ContractVerificationForm'; +import useFormConfigQuery from 'client/slices/contract/pages/contract-verification/useFormConfigQuery'; +import type { SmartContractVerificationMethod } from 'client/slices/contract/pages/contract-verification/utils'; + +import throwOnResourceLoadError from 'client/shared/errors/throw-on-resource-load-error'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import { ContentLoader } from 'toolkit/components/loaders/ContentLoader'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import PageTitle from 'ui/shared/Page/PageTitle'; + +const ContractVerificationForAddress = () => { + const router = useRouter(); + + const hash = getQueryParamString(router.query.hash); + const method = getQueryParamString(router.query.method) as SmartContractVerificationMethod; + + const contractQuery = useApiQuery('general:contract', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash), + }, + }); + + throwOnResourceLoadError(contractQuery); + + const configQuery = useFormConfigQuery(Boolean(hash)); + + React.useEffect(() => { + if (method && hash) { + router.replace({ pathname: '/address/[hash]/contract-verification', query: { hash } }, undefined, { scroll: false, shallow: true }); + } + // onMount only + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ ]); + + const isVerifiedContract = contractQuery.data?.is_verified && !contractQuery.data.is_partially_verified; + + React.useEffect(() => { + if (isVerifiedContract) { + router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { scroll: false, shallow: true }); + } + }, [ hash, isVerifiedContract, router ]); + + const content = (() => { + if (configQuery.isError || !hash || contractQuery.isError) { + return ; + } + + if (configQuery.isPending || contractQuery.isPending || isVerifiedContract) { + return ; + } + + return ( + + ); + })(); + + return ( + <> + + + { content } + + ); +}; + +export default ContractVerificationForAddress; diff --git a/ui/contractVerification/ContractVerificationForm.pw.tsx b/client/slices/contract/pages/contract-verification/ContractVerificationForm.pw.tsx similarity index 98% rename from ui/contractVerification/ContractVerificationForm.pw.tsx rename to client/slices/contract/pages/contract-verification/ContractVerificationForm.pw.tsx index 3e95382b1c..24f328fcde 100644 --- a/ui/contractVerification/ContractVerificationForm.pw.tsx +++ b/client/slices/contract/pages/contract-verification/ContractVerificationForm.pw.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect } from 'playwright/lib'; diff --git a/ui/contractVerification/ContractVerificationForm.tsx b/client/slices/contract/pages/contract-verification/ContractVerificationForm.tsx similarity index 92% rename from ui/contractVerification/ContractVerificationForm.tsx rename to client/slices/contract/pages/contract-verification/ContractVerificationForm.tsx index 6dbf8cea19..62ce3c6e08 100644 --- a/ui/contractVerification/ContractVerificationForm.tsx +++ b/client/slices/contract/pages/contract-verification/ContractVerificationForm.tsx @@ -1,23 +1,28 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Grid, Text, chakra } from '@chakra-ui/react'; import React from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form'; import type { FormFields } from './types'; -import type { SocketMessage } from 'lib/socket/types'; -import type { SmartContract, SmartContractVerificationMethodApi } from 'types/api/contract'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SocketMessage } from 'client/api/socket/types'; +import type { SmartContract, SmartContractVerificationMethodApi } from 'client/slices/contract/types/api'; import { route } from 'nextjs-routes'; -import useApiFetch from 'lib/api/useApiFetch'; -import capitalizeFirstLetter from 'lib/capitalizeFirstLetter'; -import delay from 'lib/delay'; -import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; +import useApiFetch from 'client/api/hooks/useApiFetch'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; +import getErrorObjStatusCode from 'client/shared/errors/get-error-obj-status-code'; +import capitalizeFirstLetter from 'client/shared/text/capitalize-first-letter'; +import delay from 'client/shared/utils/delay'; + import useRewardsActivity from 'lib/hooks/useRewardsActivity'; -import * as mixpanel from 'lib/mixpanel/index'; -import useSocketChannel from 'lib/socket/useSocketChannel'; -import useSocketMessage from 'lib/socket/useSocketMessage'; import { Button } from 'toolkit/chakra/button'; import { toaster } from 'toolkit/chakra/toaster'; import { useUpdateEffect } from 'toolkit/hooks/useUpdateEffect'; diff --git a/ui/contractVerification/ContractVerificationFormCodeSnippet.tsx b/client/slices/contract/pages/contract-verification/ContractVerificationFormCodeSnippet.tsx similarity index 87% rename from ui/contractVerification/ContractVerificationFormCodeSnippet.tsx rename to client/slices/contract/pages/contract-verification/ContractVerificationFormCodeSnippet.tsx index d234eb9447..c34719b8e0 100644 --- a/ui/contractVerification/ContractVerificationFormCodeSnippet.tsx +++ b/client/slices/contract/pages/contract-verification/ContractVerificationFormCodeSnippet.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Code } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/contractVerification/ContractVerificationFormRow.tsx b/client/slices/contract/pages/contract-verification/ContractVerificationFormRow.tsx similarity index 88% rename from ui/contractVerification/ContractVerificationFormRow.tsx rename to client/slices/contract/pages/contract-verification/ContractVerificationFormRow.tsx index db676b9731..b4bfa25797 100644 --- a/ui/contractVerification/ContractVerificationFormRow.tsx +++ b/client/slices/contract/pages/contract-verification/ContractVerificationFormRow.tsx @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, GridItem } from '@chakra-ui/react'; import React from 'react'; -import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMobile from 'client/shared/hooks/useIsMobile'; interface Props { children: [React.JSX.Element, React.JSX.Element | null] | (React.JSX.Element | null); diff --git a/ui/contractVerification/ContractVerificationMethod.tsx b/client/slices/contract/pages/contract-verification/ContractVerificationMethod.tsx similarity index 94% rename from ui/contractVerification/ContractVerificationMethod.tsx rename to client/slices/contract/pages/contract-verification/ContractVerificationMethod.tsx index 2704243b38..113ff501f2 100644 --- a/ui/contractVerification/ContractVerificationMethod.tsx +++ b/client/slices/contract/pages/contract-verification/ContractVerificationMethod.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Grid } from '@chakra-ui/react'; import React from 'react'; diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_dark-color-mode_flatten-source-code-method-dark-mode-mobile-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_dark-color-mode_flatten-source-code-method-dark-mode-mobile-1.png new file mode 100644 index 0000000000..04d49edae5 Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_dark-color-mode_flatten-source-code-method-dark-mode-mobile-1.png differ diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_flatten-source-code-method-dark-mode-mobile-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_flatten-source-code-method-dark-mode-mobile-1.png new file mode 100644 index 0000000000..78f2b4f92c Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_flatten-source-code-method-dark-mode-mobile-1.png differ diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_multi-part-files-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_multi-part-files-method-1.png new file mode 100644 index 0000000000..27295171ba Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_multi-part-files-method-1.png differ diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_solidity-foundry-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_solidity-foundry-method-1.png new file mode 100644 index 0000000000..d016610740 Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_solidity-foundry-method-1.png differ diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_solidity-hardhat-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_solidity-hardhat-method-1.png new file mode 100644 index 0000000000..6f374e5336 Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_solidity-hardhat-method-1.png differ diff --git a/ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-method-1.png similarity index 100% rename from ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-method-1.png rename to client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-method-1.png diff --git a/ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-1.png similarity index 100% rename from ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-1.png rename to client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-1.png diff --git a/ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-2.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-2.png similarity index 100% rename from ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-2.png rename to client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_sourcify-with-multiple-contracts-2.png diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_standard-input-json-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_standard-input-json-method-1.png new file mode 100644 index 0000000000..c764623544 Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_standard-input-json-method-1.png differ diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_verification-of-stylus-rust-contract-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_verification-of-stylus-rust-contract-1.png new file mode 100644 index 0000000000..a0b58be284 Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_verification-of-stylus-rust-contract-1.png differ diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_verification-of-zkSync-contract-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_verification-of-zkSync-contract-1.png new file mode 100644 index 0000000000..67c41a111b Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_verification-of-zkSync-contract-1.png differ diff --git a/ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-contract-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-contract-method-1.png similarity index 100% rename from ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-contract-method-1.png rename to client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-contract-method-1.png diff --git a/ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-multi-part-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-multi-part-method-1.png similarity index 100% rename from ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-multi-part-method-1.png rename to client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-multi-part-method-1.png diff --git a/ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-vyper-standard-input-method-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-vyper-standard-input-method-1.png similarity index 100% rename from ui/contractVerification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-vyper-standard-input-method-1.png rename to client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_default_vyper-vyper-standard-input-method-1.png diff --git a/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_mobile_flatten-source-code-method-dark-mode-mobile-1.png b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_mobile_flatten-source-code-method-dark-mode-mobile-1.png new file mode 100644 index 0000000000..10da6c6d25 Binary files /dev/null and b/client/slices/contract/pages/contract-verification/__screenshots__/ContractVerificationForm.pw.tsx_mobile_flatten-source-code-method-dark-mode-mobile-1.png differ diff --git a/ui/contractVerification/fields/ContractVerificationFieldAddress.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldAddress.tsx similarity index 94% rename from ui/contractVerification/fields/ContractVerificationFieldAddress.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldAddress.tsx index 638f2445ed..39e68c669c 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldAddress.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldAddress.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormFields } from '../types'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldAutodetectArgs.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldAutodetectArgs.tsx similarity index 95% rename from ui/contractVerification/fields/ContractVerificationFieldAutodetectArgs.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldAutodetectArgs.tsx index 3bcbb06b85..91b92cab98 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldAutodetectArgs.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldAutodetectArgs.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { useFormContext } from 'react-hook-form'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldCode.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCode.tsx similarity index 93% rename from ui/contractVerification/fields/ContractVerificationFieldCode.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCode.tsx index ac0c78e614..eb0ddcdd82 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldCode.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCode.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormFields } from '../types'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldCommit.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCommit.tsx similarity index 95% rename from ui/contractVerification/fields/ContractVerificationFieldCommit.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCommit.tsx index 4a91ac4f11..2fe73572b4 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldCommit.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCommit.tsx @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Code } from '@chakra-ui/react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import type { FormFields } from '../types'; -import delay from 'lib/delay'; -import useFetch from 'lib/hooks/useFetch'; +import useFetch from 'client/api/hooks/useFetch'; + +import delay from 'client/shared/utils/delay'; + import { Link } from 'toolkit/chakra/link'; import { FormFieldText } from 'toolkit/components/forms/fields/FormFieldText'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldCompiler.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCompiler.tsx similarity index 95% rename from ui/contractVerification/fields/ContractVerificationFieldCompiler.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCompiler.tsx index c761ebe3de..230e13987b 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldCompiler.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldCompiler.tsx @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Code, createListCollection } from '@chakra-ui/react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import type { FormFields } from '../types'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; + +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import { Checkbox } from 'toolkit/chakra/checkbox'; import { FormFieldSelectAsync } from 'toolkit/components/forms/fields/FormFieldSelectAsync'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldConstructorArgs.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldConstructorArgs.tsx similarity index 95% rename from ui/contractVerification/fields/ContractVerificationFieldConstructorArgs.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldConstructorArgs.tsx index 3358e3cba0..1ad1a31e57 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldConstructorArgs.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldConstructorArgs.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormFields } from '../types'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldEvmVersion.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldEvmVersion.tsx similarity index 90% rename from ui/contractVerification/fields/ContractVerificationFieldEvmVersion.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldEvmVersion.tsx index 02ec9bb9a9..b2ce68a39e 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldEvmVersion.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldEvmVersion.tsx @@ -1,8 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { createListCollection } from '@chakra-ui/react'; import React from 'react'; import type { FormFields } from '../types'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; + +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import { Link } from 'toolkit/chakra/link'; import { FormFieldSelect } from 'toolkit/components/forms/fields/FormFieldSelect'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldGitHubRepo.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldGitHubRepo.tsx similarity index 94% rename from ui/contractVerification/fields/ContractVerificationFieldGitHubRepo.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldGitHubRepo.tsx index 3873f95622..e2adc64e7d 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldGitHubRepo.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldGitHubRepo.tsx @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { get } from 'es-toolkit/compat'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import type { FormFields } from '../types'; -import delay from 'lib/delay'; -import useFetch from 'lib/hooks/useFetch'; +import useFetch from 'client/api/hooks/useFetch'; + +import delay from 'client/shared/utils/delay'; + import { FormFieldUrl } from 'toolkit/components/forms/fields/FormFieldUrl'; import ContractVerificationFormRow from '../ContractVerificationFormRow'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldIsYul.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldIsYul.tsx similarity index 91% rename from ui/contractVerification/fields/ContractVerificationFieldIsYul.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldIsYul.tsx index b30140a996..99118d1f76 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldIsYul.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldIsYul.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormFields } from '../types'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldLibraries.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLibraries.tsx similarity index 97% rename from ui/contractVerification/fields/ContractVerificationFieldLibraries.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLibraries.tsx index 6c5618ce92..20c822cfe1 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldLibraries.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLibraries.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldLibraryItem.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLibraryItem.tsx similarity index 98% rename from ui/contractVerification/fields/ContractVerificationFieldLibraryItem.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLibraryItem.tsx index 567b017ff2..2f92551576 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldLibraryItem.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLibraryItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Text } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldLicenseType.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLicenseType.tsx similarity index 89% rename from ui/contractVerification/fields/ContractVerificationFieldLicenseType.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLicenseType.tsx index 7897878d40..2c53d56d5c 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldLicenseType.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldLicenseType.tsx @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { createListCollection } from '@chakra-ui/react'; import React from 'react'; import type { FormFields } from '../types'; -import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; +import { CONTRACT_LICENSES } from 'client/slices/contract/utils/licenses'; + import type { SelectOption } from 'toolkit/chakra/select'; import { FormFieldSelect } from 'toolkit/components/forms/fields/FormFieldSelect'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldMethod.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldMethod.tsx similarity index 96% rename from ui/contractVerification/fields/ContractVerificationFieldMethod.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldMethod.tsx index 426114333c..c03aa6b398 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldMethod.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldMethod.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { List, Box, @@ -6,7 +8,8 @@ import { import React from 'react'; import type { FormFields } from '../types'; -import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract'; + +import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import { Heading } from 'toolkit/chakra/heading'; import { Link } from 'toolkit/chakra/link'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldName.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldName.tsx similarity index 95% rename from ui/contractVerification/fields/ContractVerificationFieldName.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldName.tsx index e04d65888f..5fcd97dc90 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldName.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldName.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Code } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldOptimization.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldOptimization.tsx similarity index 96% rename from ui/contractVerification/fields/ContractVerificationFieldOptimization.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldOptimization.tsx index e418484505..106306acc3 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldOptimization.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldOptimization.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldSources.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldSources.tsx similarity index 99% rename from ui/contractVerification/fields/ContractVerificationFieldSources.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldSources.tsx index ba8e7fb2c5..b0b90f9069 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldSources.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldSources.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Text, Box, Flex, VStack } from '@chakra-ui/react'; import React from 'react'; import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form'; diff --git a/ui/contractVerification/fields/ContractVerificationFieldZkCompiler.tsx b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldZkCompiler.tsx similarity index 91% rename from ui/contractVerification/fields/ContractVerificationFieldZkCompiler.tsx rename to client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldZkCompiler.tsx index 375c20dc19..aa14477ed5 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldZkCompiler.tsx +++ b/client/slices/contract/pages/contract-verification/fields/ContractVerificationFieldZkCompiler.tsx @@ -1,8 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, createListCollection } from '@chakra-ui/react'; import React from 'react'; import type { FormFields } from '../types'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; + +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import { Link } from 'toolkit/chakra/link'; import { FormFieldSelectAsync } from 'toolkit/components/forms/fields/FormFieldSelectAsync'; diff --git a/ui/contractVerification/methods/ContractVerificationFlattenSourceCode.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationFlattenSourceCode.tsx similarity index 91% rename from ui/contractVerification/methods/ContractVerificationFlattenSourceCode.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationFlattenSourceCode.tsx index fcd8f7301f..80e486d4de 100644 --- a/ui/contractVerification/methods/ContractVerificationFlattenSourceCode.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationFlattenSourceCode.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs'; diff --git a/ui/contractVerification/methods/ContractVerificationMultiPartFile.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationMultiPartFile.tsx similarity index 89% rename from ui/contractVerification/methods/ContractVerificationMultiPartFile.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationMultiPartFile.tsx index 73067b01e0..c77b71752b 100644 --- a/ui/contractVerification/methods/ContractVerificationMultiPartFile.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationMultiPartFile.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; diff --git a/ui/contractVerification/methods/ContractVerificationSolidityFoundry.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationSolidityFoundry.tsx similarity index 97% rename from ui/contractVerification/methods/ContractVerificationSolidityFoundry.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationSolidityFoundry.tsx index d2f91d2fe5..34e63881a6 100644 --- a/ui/contractVerification/methods/ContractVerificationSolidityFoundry.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationSolidityFoundry.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; diff --git a/ui/contractVerification/methods/ContractVerificationSolidityHardhat.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationSolidityHardhat.tsx similarity index 94% rename from ui/contractVerification/methods/ContractVerificationSolidityHardhat.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationSolidityHardhat.tsx index 0a75170b4a..da2d59bcb7 100644 --- a/ui/contractVerification/methods/ContractVerificationSolidityHardhat.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationSolidityHardhat.tsx @@ -1,9 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import type { FormFields } from '../types'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; + +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import config from 'configs/app'; import { Link } from 'toolkit/chakra/link'; diff --git a/ui/contractVerification/methods/ContractVerificationSourcify.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationSourcify.tsx similarity index 95% rename from ui/contractVerification/methods/ContractVerificationSourcify.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationSourcify.tsx index 53ec7747a3..85c18c8e22 100644 --- a/ui/contractVerification/methods/ContractVerificationSourcify.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationSourcify.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { useFormContext } from 'react-hook-form'; diff --git a/ui/contractVerification/methods/ContractVerificationStandardInput.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationStandardInput.tsx similarity index 91% rename from ui/contractVerification/methods/ContractVerificationStandardInput.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationStandardInput.tsx index 53a5036077..fdadbb1a99 100644 --- a/ui/contractVerification/methods/ContractVerificationStandardInput.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationStandardInput.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import config from 'configs/app'; diff --git a/ui/contractVerification/methods/ContractVerificationStylusGitHubRepo.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationStylusGitHubRepo.tsx similarity index 90% rename from ui/contractVerification/methods/ContractVerificationStylusGitHubRepo.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationStylusGitHubRepo.tsx index a3fd325df7..355214133e 100644 --- a/ui/contractVerification/methods/ContractVerificationStylusGitHubRepo.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationStylusGitHubRepo.tsx @@ -1,7 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormFields } from '../types'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; + +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import { FormFieldText } from 'toolkit/components/forms/fields/FormFieldText'; diff --git a/ui/contractVerification/methods/ContractVerificationVyperContract.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperContract.tsx similarity index 89% rename from ui/contractVerification/methods/ContractVerificationVyperContract.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperContract.tsx index 80f5a99175..192be82f39 100644 --- a/ui/contractVerification/methods/ContractVerificationVyperContract.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperContract.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; diff --git a/ui/contractVerification/methods/ContractVerificationVyperMultiPartFile.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperMultiPartFile.tsx similarity index 91% rename from ui/contractVerification/methods/ContractVerificationVyperMultiPartFile.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperMultiPartFile.tsx index 8a3372c5df..802dae4df3 100644 --- a/ui/contractVerification/methods/ContractVerificationVyperMultiPartFile.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperMultiPartFile.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import { Link } from 'toolkit/chakra/link'; diff --git a/ui/contractVerification/methods/ContractVerificationVyperStandardInput.tsx b/client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperStandardInput.tsx similarity index 85% rename from ui/contractVerification/methods/ContractVerificationVyperStandardInput.tsx rename to client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperStandardInput.tsx index ace75cfbf4..d99b02133e 100644 --- a/ui/contractVerification/methods/ContractVerificationVyperStandardInput.tsx +++ b/client/slices/contract/pages/contract-verification/methods/ContractVerificationVyperStandardInput.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { SmartContractVerificationConfig } from 'types/client/contract'; +import type { SmartContractVerificationConfig } from 'client/slices/contract/pages/contract-verification/utils'; import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; diff --git a/client/slices/contract/pages/contract-verification/types.ts b/client/slices/contract/pages/contract-verification/types.ts new file mode 100644 index 0000000000..2cf4bd482f --- /dev/null +++ b/client/slices/contract/pages/contract-verification/types.ts @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { SmartContractLicenseType } from 'client/slices/contract/types/api'; + +import type { SmartContractVerificationMethod } from 'client/slices/contract/pages/contract-verification/utils'; + +import type { SelectOption } from 'toolkit/chakra/select'; + +export interface ContractLibrary { + name: string; + address: string; +} + +export interface LicenseOption { + label: string; + value: SmartContractLicenseType; +} + +interface FormFieldsBase { + address: string; + method: Array; + license_type: Array; +} + +export interface FormFieldsFlattenSourceCode extends FormFieldsBase { + is_yul: boolean; + name: string | undefined; + compiler: Array; + evm_version: Array; + is_optimization_enabled: boolean; + optimization_runs: string; + code: string; + autodetect_constructor_args: boolean; + constructor_args: string; + libraries: Array; +} + +export interface FormFieldsStandardInput extends FormFieldsBase { + name: string; + compiler: Array; + sources: Array; + autodetect_constructor_args: boolean; + constructor_args: string; +} + +export interface FormFieldsStandardInputZk extends FormFieldsBase { + name: string; + compiler: Array; + zk_compiler: Array; + sources: Array; + autodetect_constructor_args: boolean; + constructor_args: string; +} + +export interface FormFieldsSourcify extends FormFieldsBase { + sources: Array; + contract_index?: SelectOption; +} + +export interface FormFieldsMultiPartFile extends FormFieldsBase { + compiler: Array; + evm_version: Array; + is_optimization_enabled: boolean; + optimization_runs: string; + sources: Array; + libraries: Array; +} + +export interface FormFieldsVyperContract extends FormFieldsBase { + name: string; + evm_version: Array; + compiler: Array; + code: string; + constructor_args: string | undefined; +} + +export interface FormFieldsVyperMultiPartFile extends FormFieldsBase { + compiler: Array; + evm_version: Array; + sources: Array; + interfaces: Array; +} + +export interface FormFieldsVyperStandardInput extends FormFieldsBase { + compiler: Array; + sources: Array; +} + +export interface FormFieldsStylusGitHubRepo extends FormFieldsBase { + compiler: Array; + repository_url: string; + commit_hash: string; + path_prefix: string; +} + +export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsStandardInputZk | FormFieldsSourcify | +FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile | FormFieldsVyperStandardInput | FormFieldsStylusGitHubRepo; diff --git a/client/slices/contract/pages/contract-verification/useFormConfigQuery.ts b/client/slices/contract/pages/contract-verification/useFormConfigQuery.ts new file mode 100644 index 0000000000..aa18535a1b --- /dev/null +++ b/client/slices/contract/pages/contract-verification/useFormConfigQuery.ts @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import config from 'configs/app'; + +import { isValidVerificationMethod, sortVerificationMethods } from './utils'; + +export default function useFormConfigQuery(enabled: boolean) { + return useApiQuery('general:contract_verification_config', { + queryOptions: { + select: (data) => { + return { + ...data, + verification_options: [ + ...data.verification_options, + ...config.UI.views.address.extraVerificationMethods, + ].filter(isValidVerificationMethod).sort(sortVerificationMethods), + }; + }, + enabled, + }, + }); +} diff --git a/client/slices/contract/pages/contract-verification/utils.ts b/client/slices/contract/pages/contract-verification/utils.ts new file mode 100644 index 0000000000..4a53946ddd --- /dev/null +++ b/client/slices/contract/pages/contract-verification/utils.ts @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { FieldPath, ErrorOption } from 'react-hook-form'; + +import type { SmartContractVerificationMethodExtra } from '../../types/config'; +import type { + ContractLibrary, + FormFields, + FormFieldsFlattenSourceCode, + FormFieldsMultiPartFile, + FormFieldsStandardInput, + FormFieldsStandardInputZk, + FormFieldsStylusGitHubRepo, + FormFieldsVyperContract, + FormFieldsVyperMultiPartFile, + FormFieldsVyperStandardInput, +} from './types'; +import type { + SmartContractVerificationError, + SmartContractLicenseType, + SmartContractVerificationMethodApi, + SmartContractVerificationConfigRaw, +} from 'client/slices/contract/types/api'; + +import type { Params as FetchParams } from 'client/api/hooks/useFetch'; + +import { stripLeadingSlash } from 'toolkit/utils/url'; + +export const SUPPORTED_VERIFICATION_METHODS: Array = [ + 'flattened-code', + 'standard-input', + 'sourcify', + 'multi-part', + 'solidity-hardhat', + 'solidity-foundry', + 'vyper-code', + 'vyper-multi-part', + 'vyper-standard-input', + 'stylus-github-repository', +]; + +export type SmartContractVerificationMethod = SmartContractVerificationMethodApi | SmartContractVerificationMethodExtra; + +export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw { + verification_options: Array; +} + +export const METHOD_LABELS: Record = { + 'flattened-code': 'Solidity (Single file)', + 'standard-input': 'Solidity (Standard JSON input)', + sourcify: 'Sourcify (Solidity or Vyper)', + 'multi-part': 'Solidity (Multi-part files)', + 'vyper-code': 'Vyper (Contract)', + 'vyper-multi-part': 'Vyper (Multi-part files)', + 'vyper-standard-input': 'Vyper (Standard JSON input)', + 'solidity-hardhat': 'Solidity (Hardhat)', + 'solidity-foundry': 'Solidity (Foundry)', + 'stylus-github-repository': 'Stylus (GitHub repository)', +}; + +export const DEFAULT_VALUES: Record = { + 'flattened-code': { + address: '', + method: [ 'flattened-code' ], + is_yul: false, + name: '', + compiler: [], + evm_version: [], + is_optimization_enabled: true, + optimization_runs: '200', + code: '', + autodetect_constructor_args: true, + constructor_args: '', + libraries: [], + license_type: [], + }, + 'standard-input': { + address: '', + method: [ 'standard-input' ], + name: '', + compiler: [], + sources: [], + autodetect_constructor_args: true, + constructor_args: '', + license_type: [], + }, + sourcify: { + address: '', + method: [ 'sourcify' ], + sources: [], + license_type: [], + }, + 'multi-part': { + address: '', + method: [ 'multi-part' ], + compiler: [], + evm_version: [], + is_optimization_enabled: true, + optimization_runs: '200', + sources: [], + libraries: [], + license_type: [], + }, + 'vyper-code': { + address: '', + method: [ 'vyper-code' ], + name: '', + compiler: [], + evm_version: [], + code: '', + constructor_args: '', + license_type: [], + }, + 'vyper-multi-part': { + address: '', + method: [ 'vyper-multi-part' ], + compiler: [], + evm_version: [], + sources: [], + license_type: [], + }, + 'vyper-standard-input': { + address: '', + method: [ 'vyper-standard-input' ], + compiler: [], + sources: [], + license_type: [], + }, + 'solidity-hardhat': { + address: '', + method: [ 'solidity-hardhat' ], + compiler: [], + sources: [], + license_type: [], + }, + 'solidity-foundry': { + address: '', + method: [ 'solidity-foundry' ], + compiler: [], + sources: [], + license_type: [], + }, + 'stylus-github-repository': { + address: '', + method: [ 'stylus-github-repository' ], + compiler: [], + repository_url: '', + commit_hash: '', + path_prefix: '', + license_type: [], + }, +}; + +export function getDefaultValues( + methodParam: SmartContractVerificationMethod | undefined, + config: SmartContractVerificationConfig, + hash: string | undefined, + licenseType: FormFields['license_type'], +) { + const singleMethod = config.verification_options.length === 1 ? config.verification_options[0] : undefined; + const method = singleMethod || methodParam; + + if (!method) { + return { address: hash || '' }; + } + + const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType }; + + if ('evm_version' in defaultValues) { + if (method === 'flattened-code' || method === 'multi-part') { + defaultValues.evm_version = config.solidity_evm_versions.find((value) => value === 'default') ? [ 'default' ] : []; + } + + if (method === 'vyper-multi-part') { + defaultValues.evm_version = config.vyper_evm_versions.find((value) => value === 'default') ? [ 'default' ] : []; + } + } + + if (config.is_rust_verifier_microservice_enabled) { + if (method === 'flattened-code' || method === 'standard-input') { + 'name' in defaultValues && (defaultValues.name = undefined); + 'autodetect_constructor_args' in defaultValues && (defaultValues.autodetect_constructor_args = false); + } + } + + if (singleMethod) { + defaultValues.method = config.verification_options; + } + + return defaultValues; +} + +export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod { + return method && SUPPORTED_VERIFICATION_METHODS.includes(method) ? true : false; +} + +export function sortVerificationMethods(methodA: SmartContractVerificationMethod, methodB: SmartContractVerificationMethod) { + const indexA = SUPPORTED_VERIFICATION_METHODS.indexOf(methodA); + const indexB = SUPPORTED_VERIFICATION_METHODS.indexOf(methodB); + + if (indexA > indexB) { + return 1; + } + + if (indexA < indexB) { + return -1; + } + + return 0; +} + +export function prepareRequestBody(data: FormFields): FetchParams['body'] { + const defaultLicenseType: SmartContractLicenseType = 'none'; + + switch (data.method[0]) { + case 'flattened-code': { + const _data = data as FormFieldsFlattenSourceCode; + return { + compiler_version: _data.compiler?.[0], + source_code: _data.code, + is_optimization_enabled: _data.is_optimization_enabled, + is_yul_contract: _data.is_yul, + optimization_runs: _data.optimization_runs, + contract_name: _data.name || undefined, + libraries: reduceLibrariesArray(_data.libraries), + evm_version: _data.evm_version?.[0], + autodetect_constructor_args: _data.autodetect_constructor_args, + constructor_args: _data.constructor_args, + license_type: _data.license_type?.[0] ?? defaultLicenseType, + }; + } + + case 'standard-input': { + const _data = data as (FormFieldsStandardInput | FormFieldsStandardInputZk); + + const body = new FormData(); + _data.compiler && body.set('compiler_version', _data.compiler?.[0]); + body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType); + body.set('contract_name', _data.name); + body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args))); + body.set('constructor_args', _data.constructor_args); + addFilesToFormData(body, _data.sources, 'files'); + + // zkSync fields + 'zk_compiler' in _data && _data.zk_compiler && body.set('zk_compiler_version', _data.zk_compiler?.[0]); + + return body; + } + + case 'multi-part': { + const _data = data as FormFieldsMultiPartFile; + + const body = new FormData(); + _data.compiler && body.set('compiler_version', _data.compiler?.[0]); + _data.evm_version && body.set('evm_version', _data.evm_version?.[0]); + body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType); + body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled))); + _data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs); + + const libraries = reduceLibrariesArray(_data.libraries); + libraries && body.set('libraries', JSON.stringify(libraries)); + addFilesToFormData(body, _data.sources, 'files'); + + return body; + } + + case 'vyper-code': { + const _data = data as FormFieldsVyperContract; + + return { + compiler_version: _data.compiler?.[0], + evm_version: _data.evm_version?.[0], + source_code: _data.code, + contract_name: _data.name, + constructor_args: _data.constructor_args, + license_type: _data.license_type?.[0] ?? defaultLicenseType, + }; + } + + case 'vyper-multi-part': { + const _data = data as FormFieldsVyperMultiPartFile; + + const body = new FormData(); + _data.compiler && body.set('compiler_version', _data.compiler?.[0]); + _data.evm_version && body.set('evm_version', _data.evm_version?.[0]); + body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType); + addFilesToFormData(body, _data.sources, 'files'); + addFilesToFormData(body, _data.interfaces, 'interfaces'); + + return body; + } + + case 'vyper-standard-input': { + const _data = data as FormFieldsVyperStandardInput; + + const body = new FormData(); + _data.compiler && body.set('compiler_version', _data.compiler?.[0]); + body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType); + addFilesToFormData(body, _data.sources, 'files'); + + return body; + } + + case 'stylus-github-repository': { + const _data = data as FormFieldsStylusGitHubRepo; + + return { + cargo_stylus_version: _data.compiler?.[0], + repository_url: _data.repository_url, + commit: _data.commit_hash, + path_prefix: _data.path_prefix, + license_type: _data.license_type?.[0] ?? defaultLicenseType, + }; + } + + default: { + return {}; + } + } +} + +function reduceLibrariesArray(libraries: Array | undefined) { + if (!libraries || libraries.length === 0) { + return; + } + + if (libraries.every((item) => item.name === '' && item.address === '')) { + return; + } + + return libraries.reduce>((result, item) => { + result[item.name] = item.address; + return result; + }, {}); +} + +function addFilesToFormData(body: FormData, files: Array | undefined, fieldName: 'files' | 'interfaces') { + if (!files) { + return; + } + + for (let index = 0; index < files.length; index++) { + const file = files[index]; + body.set(`${ fieldName }[${ index }]`, file, file.name); + } +} + +const API_ERROR_TO_FORM_FIELD: Record> = { + contract_source_code: 'code', + files: 'sources', + interfaces: 'interfaces', + compiler_version: 'compiler', + constructor_arguments: 'constructor_args', + name: 'name', +}; + +export function formatSocketErrors(errors: SmartContractVerificationError): Array<[FieldPath, ErrorOption] | undefined> { + return Object.entries(errors).map(([ key, value ]) => { + const _key = key as keyof SmartContractVerificationError; + if (!API_ERROR_TO_FORM_FIELD[_key]) { + return; + } + + return [ API_ERROR_TO_FORM_FIELD[_key], { message: value.join(',') } ]; + }); +} + +export function getGitHubOwnerAndRepo(repositoryUrl: string) { + try { + const urlObj = new URL(repositoryUrl); + if (urlObj.hostname !== 'github.com') { + throw new Error(); + } + const [ owner, repo, ...rest ] = stripLeadingSlash(urlObj.pathname).split('/'); + return { owner, repo, rest, url: urlObj }; + } catch (error) { + return; + } +} diff --git a/client/slices/contract/pages/details/Contract.pw.tsx b/client/slices/contract/pages/details/Contract.pw.tsx new file mode 100644 index 0000000000..7ca3ad897e --- /dev/null +++ b/client/slices/contract/pages/details/Contract.pw.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import type { Abi } from 'viem'; + +import * as addressMock from 'client/slices/address/mocks/address'; +import * as contractInfoMock from 'client/slices/contract/mocks/info'; +import * as contractMethodsMock from 'client/slices/contract/mocks/methods'; + +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; + +import Contract from './Contract'; + +const hash = addressMock.contract.hash; + +test.describe('ABI functionality', () => { + test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash } }); + await mockApiResponse( + 'general:contract', + { ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] as Abi }, + { pathParams: { hash } }, + ); + }); + + test('read', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'read_contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeVisible(); + await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click(); + await expect(component.getByLabel('FLASHLOAN_PREMIUM_TOTAL').getByRole('button', { name: 'Read' })).toBeVisible(); + }); + + test('read, no wallet client', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'read_contract' }, + }, + }; + await mockEnvs(ENVS_MAP.noWalletClient); + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeHidden(); + await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click(); + await expect(component.getByLabel('FLASHLOAN_PREMIUM_TOTAL').getByRole('button', { name: 'Read' })).toBeVisible(); + }); + + test('write', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'write_contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeVisible(); + await component.getByText('setReserveInterestRateStrategyAddress').click(); + await expect(component.getByLabel('9.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('9.').getByRole('button', { name: 'Write' })).toBeEnabled(); + + await component.getByText('pause').click(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeEnabled(); + }); + + test('write, no wallet client', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'write_contract' }, + }, + }; + await mockEnvs(ENVS_MAP.noWalletClient); + + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeHidden(); + await component.getByText('setReserveInterestRateStrategyAddress').click(); + await expect(component.getByLabel('9.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('9.').getByRole('button', { name: 'Write' })).toBeDisabled(); + + await component.getByText('pause').click(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeDisabled(); + }); +}); + +test.describe('auto verification status', () => { + const addressData = { ...addressMock.contract, is_verified: false, implementations: [] }; + let contractApiUrl: string; + + test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('general:address', addressData, { pathParams: { hash } }); + contractApiUrl = await mockApiResponse('general:contract', contractInfoMock.nonVerified, { pathParams: { hash } }); + }); + + test('base flow', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressData.hash.toLowerCase()); + + socketServer.sendMessage(socket, channel, 'eth_bytecode_db_lookup_started', { }); + const tabs = component.getByRole('tablist').first(); + await expect(tabs).toHaveScreenshot(); + }); + + test('after verification will refetch contract data', async({ page, render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'contract' }, + }, + }; + await render(, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressData.hash.toLowerCase()); + + socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', { }); + + const contractRequest = await page.waitForRequest(contractApiUrl); + expect(contractRequest).toBeTruthy(); + }); + + test('with one tab', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'contract' }, + }, + }; + await mockEnvs([ + [ 'NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', 'false' ], + ]); + const component = await render(, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressData.hash.toLowerCase()); + + socketServer.sendMessage(socket, channel, 'smart_contract_was_not_verified', { }); + const tabs = component.getByRole('tablist').first(); + await expect(tabs).toHaveScreenshot(); + }); +}); diff --git a/client/slices/contract/pages/details/Contract.tsx b/client/slices/contract/pages/details/Contract.tsx new file mode 100644 index 0000000000..a7227634df --- /dev/null +++ b/client/slices/contract/pages/details/Contract.tsx @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { SocketMessage } from 'client/api/socket/types'; +import type { Address } from 'client/slices/address/types/api'; + +import { getResourceKey } from 'client/api/hooks/useApiQuery'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import type { TContractAutoVerificationStatus } from 'client/slices/contract/pages/details/code/ContractAutoVerificationStatus'; +import ContractAutoVerificationStatus from 'client/slices/contract/pages/details/code/ContractAutoVerificationStatus'; +import useContractTabs from 'client/slices/contract/pages/details/useContractTabs'; +import { CONTRACT_TAB_IDS } from 'client/slices/contract/utils/tabs'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; +import delay from 'client/shared/utils/delay'; + +import type { Props as RoutedTabsProps } from 'toolkit/components/AdaptiveTabs/AdaptiveTabs'; +import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; +import { SECOND } from 'toolkit/utils/consts'; + +interface Props extends Pick { + addressData: Address | undefined; + isLoading?: boolean; + hasMudTab?: boolean; +} + +const AddressContract = ({ addressData, isLoading = false, hasMudTab, ...rest }: Props) => { + const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); + const [ autoVerificationStatus, setAutoVerificationStatus ] = React.useState(null); + + const router = useRouter(); + const queryClient = useQueryClient(); + const isMobile = useIsMobile(); + const handleChannelJoin = React.useCallback(() => { + setIsQueryEnabled(true); + }, []); + const handleChannelError = React.useCallback(() => { + setIsQueryEnabled(true); + }, []); + + const tab = getQueryParamString(router.query.tab); + const isSocketEnabled = Boolean(addressData?.hash) && addressData?.is_contract && !isLoading && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab); + + const channel = useSocketChannel({ + topic: `addresses:${ addressData?.hash?.toLowerCase() }`, + isDisabled: !isSocketEnabled, + onJoin: handleChannelJoin, + onSocketError: handleChannelError, + }); + + const contractTabs = useContractTabs({ + addressData, + isEnabled: isQueryEnabled && !isLoading, + hasMudTab, + channel, + }); + + const handleLookupStartedMessage: SocketMessage.EthBytecodeDbLookupStarted['handler'] = React.useCallback(() => { + setAutoVerificationStatus('pending'); + }, []); + + const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(async() => { + setAutoVerificationStatus('success'); + await queryClient.refetchQueries({ + queryKey: getResourceKey('general:address', { pathParams: { hash: addressData?.hash } }), + }); + await queryClient.refetchQueries({ + queryKey: getResourceKey('general:contract', { pathParams: { hash: addressData?.hash } }), + }); + setAutoVerificationStatus(null); + }, [ addressData?.hash, queryClient ]); + + const handleContractWasNotVerifiedMessage: SocketMessage.SmartContractWasNotVerified['handler'] = React.useCallback(async() => { + setAutoVerificationStatus('failed'); + await delay(10 * SECOND); + setAutoVerificationStatus(null); + }, []); + + useSocketMessage({ channel, event: 'eth_bytecode_db_lookup_started', handler: handleLookupStartedMessage }); + useSocketMessage({ channel, event: 'smart_contract_was_verified', handler: handleContractWasVerifiedMessage }); + useSocketMessage({ channel, event: 'smart_contract_was_not_verified', handler: handleContractWasNotVerifiedMessage }); + + const rightSlot = autoVerificationStatus && !contractTabs.isPartiallyVerified ? + 1 ? 'tooltip' : 'inline' }/> : + null; + + return ( + 1 ? { base: 'auto', md: 6 } : 0 }} + { ...rest } + /> + ); +}; + +export default React.memo(AddressContract); diff --git a/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-base-flow-1.png b/client/slices/contract/pages/details/__screenshots__/Contract.pw.tsx_default_auto-verification-status-base-flow-1.png similarity index 100% rename from ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-base-flow-1.png rename to client/slices/contract/pages/details/__screenshots__/Contract.pw.tsx_default_auto-verification-status-base-flow-1.png diff --git a/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-with-one-tab-1.png b/client/slices/contract/pages/details/__screenshots__/Contract.pw.tsx_default_auto-verification-status-with-one-tab-1.png similarity index 100% rename from ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-with-one-tab-1.png rename to client/slices/contract/pages/details/__screenshots__/Contract.pw.tsx_default_auto-verification-status-with-one-tab-1.png diff --git a/ui/address/contract/ContractAutoVerificationStatus.tsx b/client/slices/contract/pages/details/code/ContractAutoVerificationStatus.tsx similarity index 96% rename from ui/address/contract/ContractAutoVerificationStatus.tsx rename to client/slices/contract/pages/details/code/ContractAutoVerificationStatus.tsx index 7c3b60ee14..810a72c807 100644 --- a/ui/address/contract/ContractAutoVerificationStatus.tsx +++ b/client/slices/contract/pages/details/code/ContractAutoVerificationStatus.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, HStack, Spinner } from '@chakra-ui/react'; import React from 'react'; diff --git a/client/slices/contract/pages/details/code/ContractCode.pw.tsx b/client/slices/contract/pages/details/code/ContractCode.pw.tsx new file mode 100644 index 0000000000..384b64b8b0 --- /dev/null +++ b/client/slices/contract/pages/details/code/ContractCode.pw.tsx @@ -0,0 +1,144 @@ +import React from 'react'; + +import * as addressMock from 'client/slices/address/mocks/address'; +import * as contractMock from 'client/slices/contract/mocks/info'; + +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; +import * as pwConfig from 'playwright/utils/config'; + +import ContractDetails from './ContractCode.pwstory'; + +const hooksConfig = { + router: { + query: { hash: addressMock.contract.hash, tab: 'contract_code' }, + }, +}; + +// FIXME +// test cases which use socket cannot run in parallel since the socket server always run on the same port +test.describe.configure({ mode: 'serial' }); + +test.beforeEach(async({ mockApiResponse, page }) => { + await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/**', (route) => { + route.abort(); + }); + await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } }); +}); + +test.describe('full view', () => { + test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('general:contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } }); + await mockApiResponse( + 'general:contract', + contractMock.withChangedByteCode, + { pathParams: { hash: addressMock.contract.implementations?.[0].address_hash as string } }, + ); + }); + + test('source code +@dark-mode', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash: addressMock.contract.hash, tab: 'contract_source_code' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + await expect(component).toHaveScreenshot(); + }); + + test('compiler', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash: addressMock.contract.hash, tab: 'contract_compiler' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + await expect(component).toHaveScreenshot(); + }); + + test('abi', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash: addressMock.contract.hash, tab: 'contract_abi' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + await expect(component).toHaveScreenshot(); + }); + + test('bytecode', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash: addressMock.contract.hash, tab: 'contract_bytecode' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + await expect(component).toHaveScreenshot(); + }); +}); + +test.describe('mobile view', () => { + test.use({ viewport: pwConfig.viewport.mobile }); + + test('source code', async({ render, createSocket, mockApiResponse }) => { + await mockApiResponse('general:contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } }); + await mockApiResponse( + 'general:contract', + contractMock.withChangedByteCode, + { pathParams: { hash: addressMock.contract.implementations?.[0].address_hash as string } }, + ); + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + await expect(component).toHaveScreenshot(); + }); +}); + +test('verified with multiple sources', async({ render, page, mockApiResponse, createSocket }) => { + await mockApiResponse('general:contract', contractMock.withMultiplePaths, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + + const section = page.locator('section', { hasText: 'Contract source code' }); + await expect(section).toHaveScreenshot(); + + await page.getByRole('button', { name: 'View external libraries' }).click(); + await expect(section).toHaveScreenshot(); + + await page.getByRole('button', { name: 'Open source code in IDE' }).click(); + await expect(section).toHaveScreenshot(); +}); + +test('self destructed', async({ render, mockApiResponse, page, createSocket }) => { + const hooksConfig = { + router: { + query: { hash: addressMock.contract.hash, tab: 'contract_bytecode' }, + }, + }; + await mockApiResponse('general:contract', contractMock.selfDestructed, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + + const section = page.locator('section', { hasText: 'Contract creation code' }); + await expect(section).toHaveScreenshot(); +}); + +test('non verified', async({ render, mockApiResponse, createSocket }) => { + await mockApiResponse('general:address', { ...addressMock.contract, name: null }, { pathParams: { hash: addressMock.contract.hash } }); + await mockApiResponse('general:contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } }); + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); + + await expect(component).toHaveScreenshot(); +}); diff --git a/client/slices/contract/pages/details/code/ContractCode.pwstory.tsx b/client/slices/contract/pages/details/code/ContractCode.pwstory.tsx new file mode 100644 index 0000000000..bbca35215a --- /dev/null +++ b/client/slices/contract/pages/details/code/ContractCode.pwstory.tsx @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useRouter } from 'next/router'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; + +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import useContractTabs from '../useContractTabs'; + +const ContractDetails = () => { + const router = useRouter(); + const hash = getQueryParamString(router.query.hash); + const addressQuery = useApiQuery('general:address', { pathParams: { hash } }); + const channel = useSocketChannel({ + topic: `addresses:${ hash?.toLowerCase() }`, + isDisabled: !addressQuery.data, + }); + + const { tabs } = useContractTabs({ + addressData: addressQuery.data, + isEnabled: true, + channel, + }); + const content = tabs.find(({ id }) => id === 'contract_code')?.component; + return content ?? null; +}; + +export default ContractDetails; diff --git a/client/slices/contract/pages/details/code/ContractCode.tsx b/client/slices/contract/pages/details/code/ContractCode.tsx new file mode 100644 index 0000000000..3e46dc275f --- /dev/null +++ b/client/slices/contract/pages/details/code/ContractCode.tsx @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Grid } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import type { Channel } from 'phoenix'; +import React from 'react'; + +import type { Address, AddressImplementation } from 'client/slices/address/types/api'; +import type { SmartContract } from 'client/slices/contract/types/api'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; +import type { ResourceError } from 'client/api/resources'; + +import * as stubs from 'client/slices/contract/stubs'; + +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import { useMultichainContext } from 'lib/contexts/multichain'; +import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; + +import ContractDetailsAlerts from './alerts/ContractDetailsAlerts'; +import ContractSourceAddressSelector from './ContractSourceAddressSelector'; +import ContractDetailsInfo from './info/ContractDetailsInfo'; +import ContractDetailsInfoCreator from './info/ContractDetailsInfoCreator'; +import ContractDetailsInfoImplementations from './info/ContractDetailsInfoImplementations'; +import useContractCodeTabs from './useContractCodeTabs'; + +const TAB_LIST_PROPS = { flexWrap: 'wrap', rowGap: 2 }; +const LEFT_SLOT_PROPS = { w: { base: '100%', lg: 'auto' } }; + +type Props = { + addressData: Address; + channel: Channel | undefined; + mainContractQuery: UseQueryResult; +}; + +const ContractDetails = ({ addressData, channel, mainContractQuery }: Props) => { + const router = useRouter(); + const sourceAddress = getQueryParamString(router.query.source_address); + const multichainContext = useMultichainContext(); + + const sourceItems: Array = React.useMemo(() => { + const currentAddressDefaultName = addressData?.proxy_type === 'eip7702' ? 'Current address' : 'Current contract'; + const currentAddressItem = { address_hash: addressData.hash, name: addressData?.name || currentAddressDefaultName }; + if (!addressData || !addressData.implementations || addressData.implementations.length === 0) { + return [ currentAddressItem ]; + } + + return [ + currentAddressItem, + ...(addressData?.implementations.filter((item) => item.address_hash !== addressData.hash && item.name) || []), + ]; + }, [ addressData ]); + + const [ selectedItem, setSelectedItem ] = React.useState(undefined); + + React.useEffect(() => { + if (!mainContractQuery.isPlaceholderData) { + setSelectedItem(sourceItems.find((item) => item.address_hash === sourceAddress) || sourceItems[0]); + } + }, [ mainContractQuery.isPlaceholderData, sourceAddress, sourceItems ]); + + const contractQuery = useApiQuery('general:contract', { + pathParams: { hash: selectedItem?.address_hash }, + queryOptions: { + enabled: Boolean(selectedItem?.address_hash && !mainContractQuery.isPlaceholderData && selectedItem.address_hash !== addressData.hash), + refetchOnMount: false, + placeholderData: addressData?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED, + }, + }); + const { data, isPlaceholderData, isError } = selectedItem?.address_hash !== addressData.hash ? contractQuery : mainContractQuery; + + const tabs = useContractCodeTabs({ data, isLoading: isPlaceholderData, addressData, sourceAddress: selectedItem?.address_hash }); + + if (isError) { + return ; + } + + const addressSelector = sourceItems.length > 1 && selectedItem ? ( + + ) : null; + + return ( + <> + + { mainContractQuery.data?.is_verified && ( + + ) } + { !mainContractQuery.data?.is_verified && multichainContext && ( + + { addressData.creator_address_hash && addressData.creation_transaction_hash && ( + + ) } + { addressData.implementations && addressData.implementations.length > 0 && !mainContractQuery.isPlaceholderData && ( + + ) } + + ) } + + + ); +}; + +export default ContractDetails; diff --git a/ui/address/contract/ContractCodeIdes.tsx b/client/slices/contract/pages/details/code/ContractCodeIdes.tsx similarity index 98% rename from ui/address/contract/ContractCodeIdes.tsx rename to client/slices/contract/pages/details/code/ContractCodeIdes.tsx index 8498359a73..7ab1ebc7b9 100644 --- a/ui/address/contract/ContractCodeIdes.tsx +++ b/client/slices/contract/pages/details/code/ContractCodeIdes.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/address/contract/ContractDetailsByteCode.tsx b/client/slices/contract/pages/details/code/ContractDetailsByteCode.tsx similarity index 92% rename from ui/address/contract/ContractDetailsByteCode.tsx rename to client/slices/contract/pages/details/code/ContractDetailsByteCode.tsx index 72ccebec1e..5979f3ab52 100644 --- a/ui/address/contract/ContractDetailsByteCode.tsx +++ b/client/slices/contract/pages/details/code/ContractDetailsByteCode.tsx @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; -import type { Address } from 'types/api/address'; -import type { SmartContract } from 'types/api/contract'; +import type { Address } from 'client/slices/address/types/api'; +import type { SmartContract } from 'client/slices/contract/types/api'; import { Alert } from 'toolkit/chakra/alert'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; diff --git a/ui/address/contract/ContractDetailsConstructorArgs.tsx b/client/slices/contract/pages/details/code/ContractDetailsConstructorArgs.tsx similarity index 90% rename from ui/address/contract/ContractDetailsConstructorArgs.tsx rename to client/slices/contract/pages/details/code/ContractDetailsConstructorArgs.tsx index 20764a5cc7..97c236c7e9 100644 --- a/ui/address/contract/ContractDetailsConstructorArgs.tsx +++ b/client/slices/contract/pages/details/code/ContractDetailsConstructorArgs.tsx @@ -1,13 +1,16 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { SmartContract } from 'types/api/contract'; +import type { SmartContract } from 'client/slices/contract/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import type { Truncation } from 'ui/shared/entities/base/components'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; -import { matchArray } from './methods/form/utils'; +import { matchArray } from '../methods/form/utils'; interface DecodedItemProps { value: unknown; diff --git a/ui/address/contract/ContractDetailsDeployedByteCode.pw.tsx b/client/slices/contract/pages/details/code/ContractDetailsDeployedByteCode.pw.tsx similarity index 92% rename from ui/address/contract/ContractDetailsDeployedByteCode.pw.tsx rename to client/slices/contract/pages/details/code/ContractDetailsDeployedByteCode.pw.tsx index 5571c8ffad..f442d3e5d7 100644 --- a/ui/address/contract/ContractDetailsDeployedByteCode.pw.tsx +++ b/client/slices/contract/pages/details/code/ContractDetailsDeployedByteCode.pw.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import * as addressMock from 'mocks/address/address'; +import * as addressMock from 'client/slices/address/mocks/address'; + import { test, expect } from 'playwright/lib'; import ContractDetailsDeployedByteCode from './ContractDetailsDeployedByteCode'; diff --git a/ui/address/contract/ContractDetailsDeployedByteCode.tsx b/client/slices/contract/pages/details/code/ContractDetailsDeployedByteCode.tsx similarity index 95% rename from ui/address/contract/ContractDetailsDeployedByteCode.tsx rename to client/slices/contract/pages/details/code/ContractDetailsDeployedByteCode.tsx index 819a6787bb..978d600b93 100644 --- a/ui/address/contract/ContractDetailsDeployedByteCode.tsx +++ b/client/slices/contract/pages/details/code/ContractDetailsDeployedByteCode.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, createListCollection } from '@chakra-ui/react'; import React from 'react'; -import type { Address } from 'types/api/address'; +import type { Address } from 'client/slices/address/types/api'; + +import hexToUtf8 from 'client/shared/transformers/hex-to-utf8'; import config from 'configs/app'; -import hexToUtf8 from 'lib/hexToUtf8'; import type { SelectOption } from 'toolkit/chakra/select'; import { Select } from 'toolkit/chakra/select'; import { Skeleton } from 'toolkit/chakra/skeleton'; diff --git a/ui/address/contract/ContractDetailsVerificationButton.tsx b/client/slices/contract/pages/details/code/ContractDetailsVerificationButton.tsx similarity index 96% rename from ui/address/contract/ContractDetailsVerificationButton.tsx rename to client/slices/contract/pages/details/code/ContractDetailsVerificationButton.tsx index 21f76d85f4..f20f064bdf 100644 --- a/ui/address/contract/ContractDetailsVerificationButton.tsx +++ b/client/slices/contract/pages/details/code/ContractDetailsVerificationButton.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { route } from 'nextjs-routes'; diff --git a/ui/address/contract/ContractExternalLibraries.tsx b/client/slices/contract/pages/details/code/ContractExternalLibraries.tsx similarity index 92% rename from ui/address/contract/ContractExternalLibraries.tsx rename to client/slices/contract/pages/details/code/ContractExternalLibraries.tsx index 8d74edb3d3..ed21463e26 100644 --- a/ui/address/contract/ContractExternalLibraries.tsx +++ b/client/slices/contract/pages/details/code/ContractExternalLibraries.tsx @@ -1,9 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, Separator, VStack } from '@chakra-ui/react'; import React from 'react'; -import type { SmartContractExternalLibrary } from 'types/api/contract'; +import type { SmartContractExternalLibrary } from 'client/slices/contract/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; -import useIsMobile from 'lib/hooks/useIsMobile'; import { Alert } from 'toolkit/chakra/alert'; import { Button } from 'toolkit/chakra/button'; import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog'; @@ -12,7 +17,6 @@ import { PopoverRoot, PopoverBody, PopoverContent, PopoverTrigger } from 'toolki import { Skeleton } from 'toolkit/chakra/skeleton'; import { useDisclosure } from 'toolkit/hooks/useDisclosure'; import { apos } from 'toolkit/utils/htmlEntities'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import IconSvg from 'ui/shared/IconSvg'; interface Props { diff --git a/ui/address/contract/ContractSourceAddressSelector.tsx b/client/slices/contract/pages/details/code/ContractSourceAddressSelector.tsx similarity index 95% rename from ui/address/contract/ContractSourceAddressSelector.tsx rename to client/slices/contract/pages/details/code/ContractSourceAddressSelector.tsx index 3b20b6f45c..085f88f911 100644 --- a/ui/address/contract/ContractSourceAddressSelector.tsx +++ b/client/slices/contract/pages/details/code/ContractSourceAddressSelector.tsx @@ -1,12 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, createListCollection, Flex } from '@chakra-ui/react'; import React from 'react'; import { route } from 'nextjs-routes'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + import { Select } from 'toolkit/chakra/select'; import { Skeleton } from 'toolkit/chakra/skeleton'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import LinkNewTab from 'ui/shared/links/LinkNewTab'; export interface Item { diff --git a/ui/address/contract/ContractSourceCode.tsx b/client/slices/contract/pages/details/code/ContractSourceCode.tsx similarity index 95% rename from ui/address/contract/ContractSourceCode.tsx rename to client/slices/contract/pages/details/code/ContractSourceCode.tsx index d3d033c1a6..bbf1038a7e 100644 --- a/ui/address/contract/ContractSourceCode.tsx +++ b/client/slices/contract/pages/details/code/ContractSourceCode.tsx @@ -1,12 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Text } from '@chakra-ui/react'; import React from 'react'; -import type { SmartContract } from 'types/api/contract'; +import type { SmartContract } from 'client/slices/contract/types/api'; import { route } from 'nextjs/routes'; +import { formatLanguageName } from 'client/slices/contract/utils/language'; + import { useMultichainContext } from 'lib/contexts/multichain'; -import formatLanguageName from 'lib/contracts/formatLanguageName'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_dark-color-mode_full-view-source-code-dark-mode-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-source-code-dark-mode-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_dark-color-mode_full-view-source-code-dark-mode-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-source-code-dark-mode-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-abi-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-abi-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-abi-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-abi-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-bytecode-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-bytecode-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-bytecode-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-bytecode-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-compiler-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-compiler-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-compiler-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-compiler-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-source-code-dark-mode-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-source-code-dark-mode-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_full-view-source-code-dark-mode-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_full-view-source-code-dark-mode-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_mobile-view-source-code-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_mobile-view-source-code-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_mobile-view-source-code-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_mobile-view-source-code-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_non-verified-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_non-verified-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_non-verified-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_non-verified-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_self-destructed-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_self-destructed-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_self-destructed-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_self-destructed-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-1.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-2.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-2.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-2.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-2.png diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-3.png b/client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-3.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-3.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-3.png diff --git a/ui/address/contract/__screenshots__/ContractDetailsDeployedByteCode.pw.tsx_default_scilla-decoded-bytecode-1.png b/client/slices/contract/pages/details/code/__screenshots__/ContractDetailsDeployedByteCode.pw.tsx_default_scilla-decoded-bytecode-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractDetailsDeployedByteCode.pw.tsx_default_scilla-decoded-bytecode-1.png rename to client/slices/contract/pages/details/code/__screenshots__/ContractDetailsDeployedByteCode.pw.tsx_default_scilla-decoded-bytecode-1.png diff --git a/ui/address/contract/alerts/ConflictingImplementationsModal.tsx b/client/slices/contract/pages/details/code/alerts/ConflictingImplementationsModal.tsx similarity index 92% rename from ui/address/contract/alerts/ConflictingImplementationsModal.tsx rename to client/slices/contract/pages/details/code/alerts/ConflictingImplementationsModal.tsx index 4bb58b34b5..8e27703bb1 100644 --- a/ui/address/contract/alerts/ConflictingImplementationsModal.tsx +++ b/client/slices/contract/pages/details/code/alerts/ConflictingImplementationsModal.tsx @@ -1,12 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Grid, GridItem, HStack, Text, VStack } from '@chakra-ui/react'; import React from 'react'; -import type { SmartContractConflictingImplementation } from 'types/api/contract'; +import type { SmartContractConflictingImplementation } from 'client/slices/contract/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; import { Button } from 'toolkit/chakra/button'; import { DialogActionTrigger, DialogBody, DialogContent, DialogHeader, DialogRoot, DialogTrigger } from 'toolkit/chakra/dialog'; import { Link } from 'toolkit/chakra/link'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import { PROXY_TYPES } from './utils'; diff --git a/ui/address/contract/alerts/ContractDetailsAlertProxyPattern.pw.tsx b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlertProxyPattern.pw.tsx similarity index 100% rename from ui/address/contract/alerts/ContractDetailsAlertProxyPattern.pw.tsx rename to client/slices/contract/pages/details/code/alerts/ContractDetailsAlertProxyPattern.pw.tsx diff --git a/ui/address/contract/alerts/ContractDetailsAlertProxyPattern.tsx b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlertProxyPattern.tsx similarity index 94% rename from ui/address/contract/alerts/ContractDetailsAlertProxyPattern.tsx rename to client/slices/contract/pages/details/code/alerts/ContractDetailsAlertProxyPattern.tsx index af1e68de0b..b4048bc58b 100644 --- a/ui/address/contract/alerts/ContractDetailsAlertProxyPattern.tsx +++ b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlertProxyPattern.tsx @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'types/api/contract'; +import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'client/slices/contract/types/api'; import { Alert } from 'toolkit/chakra/alert'; import { Link } from 'toolkit/chakra/link'; diff --git a/ui/address/contract/alerts/ContractDetailsAlertVerificationStatus.tsx b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlertVerificationStatus.tsx similarity index 92% rename from ui/address/contract/alerts/ContractDetailsAlertVerificationStatus.tsx rename to client/slices/contract/pages/details/code/alerts/ContractDetailsAlertVerificationStatus.tsx index ba16ec13b2..1a76f5a834 100644 --- a/ui/address/contract/alerts/ContractDetailsAlertVerificationStatus.tsx +++ b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlertVerificationStatus.tsx @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { Address } from 'types/api/address'; -import type { SmartContract } from 'types/api/contract'; +import type { Address } from 'client/slices/address/types/api'; +import type { SmartContract } from 'client/slices/contract/types/api'; import { Alert } from 'toolkit/chakra/alert'; import { Link } from 'toolkit/chakra/link'; diff --git a/ui/address/contract/alerts/ContractDetailsAlerts.pw.tsx b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.pw.tsx similarity index 93% rename from ui/address/contract/alerts/ContractDetailsAlerts.pw.tsx rename to client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.pw.tsx index 5ad385e3bc..2f67e1e6b6 100644 --- a/ui/address/contract/alerts/ContractDetailsAlerts.pw.tsx +++ b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.pw.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import * as addressMock from 'mocks/address/address'; -import * as contractMock from 'mocks/contract/info'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as contractMock from 'client/slices/contract/mocks/info'; + import * as socketServer from 'playwright/fixtures/socketServer'; import { test, expect } from 'playwright/lib'; diff --git a/ui/address/contract/alerts/ContractDetailsAlerts.pwstory.tsx b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.pwstory.tsx similarity index 79% rename from ui/address/contract/alerts/ContractDetailsAlerts.pwstory.tsx rename to client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.pwstory.tsx index 4b5301cb59..18765e726f 100644 --- a/ui/address/contract/alerts/ContractDetailsAlerts.pwstory.tsx +++ b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.pwstory.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; import type { Props } from './ContractDetailsAlerts'; import ContractDetailsAlerts from './ContractDetailsAlerts'; diff --git a/ui/address/contract/alerts/ContractDetailsAlerts.tsx b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.tsx similarity index 88% rename from ui/address/contract/alerts/ContractDetailsAlerts.tsx rename to client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.tsx index ee22533d73..323756dbc1 100644 --- a/ui/address/contract/alerts/ContractDetailsAlerts.tsx +++ b/client/slices/contract/pages/details/code/alerts/ContractDetailsAlerts.tsx @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Box, Flex } from '@chakra-ui/react'; import type { Channel } from 'phoenix'; import React from 'react'; -import type { SocketMessage } from 'lib/socket/types'; -import type { Address } from 'types/api/address'; -import type { SmartContract } from 'types/api/contract'; +import type { SocketMessage } from 'client/api/socket/types'; +import type { Address } from 'client/slices/address/types/api'; +import type { SmartContract } from 'client/slices/contract/types/api'; import { route } from 'nextjs-routes'; -import useSocketMessage from 'lib/socket/useSocketMessage'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + import { Alert } from 'toolkit/chakra/alert'; import { Link } from 'toolkit/chakra/link'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import ContractDetailsAlertProxyPattern from './ContractDetailsAlertProxyPattern'; import ContractDetailsAlertVerificationStatus from './ContractDetailsAlertVerificationStatus'; diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-2.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-2.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-2.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-conflicting-implementations-2.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-but-without-description-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-but-without-description-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-but-without-description-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-but-without-description-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-mobile-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-mobile-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-mobile-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-with-link-mobile-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-without-link-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-without-link-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-without-link-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_default_proxy-type-without-link-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_mobile_proxy-type-with-link-mobile-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_mobile_proxy-type-with-link-mobile-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_mobile_proxy-type-with-link-mobile-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlertProxyPattern.pw.tsx_mobile_proxy-type-with-link-mobile-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-eth-bytecode-db-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-eth-bytecode-db-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-eth-bytecode-db-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-eth-bytecode-db-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-sourcify-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-sourcify-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-sourcify-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-via-sourcify-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-with-changed-byte-code-socket-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-with-changed-byte-code-socket-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-with-changed-byte-code-socket-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_verified-with-changed-byte-code-socket-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_with-twin-address-alert-mobile-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_with-twin-address-alert-mobile-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_with-twin-address-alert-mobile-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_default_with-twin-address-alert-mobile-1.png diff --git a/ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_mobile_with-twin-address-alert-mobile-1.png b/client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_mobile_with-twin-address-alert-mobile-1.png similarity index 100% rename from ui/address/contract/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_mobile_with-twin-address-alert-mobile-1.png rename to client/slices/contract/pages/details/code/alerts/__screenshots__/ContractDetailsAlerts.pw.tsx_mobile_with-twin-address-alert-mobile-1.png diff --git a/client/slices/contract/pages/details/code/alerts/utils.ts b/client/slices/contract/pages/details/code/alerts/utils.ts new file mode 100644 index 0000000000..f5481ca451 --- /dev/null +++ b/client/slices/contract/pages/details/code/alerts/utils.ts @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { SmartContractProxyType } from 'client/slices/contract/types/api'; + +export const PROXY_TYPES: Partial, { + name: string; + link?: string; + description?: string; +}>> = { + eip1167: { + name: 'EIP-1167', + link: 'https://eips.ethereum.org/EIPS/eip-1167', + description: 'Minimal Proxy', + }, + eip1967: { + name: 'EIP-1967', + link: 'https://eips.ethereum.org/EIPS/eip-1967', + description: 'Transparent Proxy', + }, + eip1967_oz: { + name: 'EIP-1967 (Legacy)', + link: 'https://eips.ethereum.org/EIPS/eip-1967', + description: 'Legacy OpenZeppelin Transparent Proxy', + }, + eip1967_beacon: { + name: 'EIP-1967 (Beacon)', + link: 'https://eips.ethereum.org/EIPS/eip-1967', + description: 'Beacon Proxy', + }, + eip1822: { + name: 'EIP-1822', + link: 'https://eips.ethereum.org/EIPS/eip-1822', + description: 'Universal Upgradeable Proxy Standard (UUPS)', + }, + eip2535: { + name: 'EIP-2535', + link: 'https://eips.ethereum.org/EIPS/eip-2535', + description: 'Diamond Proxy', + }, + erc7760: { + name: 'ERC-7760', + link: 'https://eips.ethereum.org/EIPS/eip-7760', + description: 'Minimal Upgradeable Proxy', + }, + resolved_delegate_proxy: { + name: 'ResolvedDelegateProxy', + // eslint-disable-next-line max-len + link: 'https://github.com/ethereum-optimism/optimism/blob/9580179013a04b15e6213ae8aa8d43c3f559ed9a/packages/contracts-bedrock/src/legacy/ResolvedDelegateProxy.sol', + description: 'OP Stack Proxy', + }, + clone_with_immutable_arguments: { + name: 'Clones with immutable arguments', + link: 'https://github.com/wighawag/clones-with-immutable-args', + }, + master_copy: { + name: 'Safe Proxy', + link: 'https://github.com/safe-global/safe-smart-account', + }, + comptroller: { + name: 'Compound Protocol Proxy', + link: 'https://github.com/compound-finance/compound-protocol', + }, + basic_implementation: { + name: 'Generic Proxy', + description: 'implementation() getter', + }, + basic_get_implementation: { + name: 'Generic Proxy', + description: 'getImplementation() getter', + }, + unknown: { + name: 'Unknown proxy pattern', + }, +}; diff --git a/ui/address/contract/info/ContractDetailsInfo.pw.tsx b/client/slices/contract/pages/details/code/info/ContractDetailsInfo.pw.tsx similarity index 91% rename from ui/address/contract/info/ContractDetailsInfo.pw.tsx rename to client/slices/contract/pages/details/code/info/ContractDetailsInfo.pw.tsx index 9a8a2872d5..ba563c0593 100644 --- a/ui/address/contract/info/ContractDetailsInfo.pw.tsx +++ b/client/slices/contract/pages/details/code/info/ContractDetailsInfo.pw.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import * as addressMock from 'mocks/address/address'; -import { contractAudits } from 'mocks/contract/audits'; -import * as contractMock from 'mocks/contract/info'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as contractMock from 'client/slices/contract/mocks/info'; + +import { contractAudits } from 'client/features/contract-audit-reports/mocks'; + import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect } from 'playwright/lib'; diff --git a/ui/address/contract/info/ContractDetailsInfo.tsx b/client/slices/contract/pages/details/code/info/ContractDetailsInfo.tsx similarity index 91% rename from ui/address/contract/info/ContractDetailsInfo.tsx rename to client/slices/contract/pages/details/code/info/ContractDetailsInfo.tsx index c7676e9a52..88f5ab4916 100644 --- a/ui/address/contract/info/ContractDetailsInfo.tsx +++ b/client/slices/contract/pages/details/code/info/ContractDetailsInfo.tsx @@ -1,18 +1,22 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Grid } from '@chakra-ui/react'; import React from 'react'; -import type { Address } from 'types/api/address'; -import type { SmartContract } from 'types/api/contract'; +import type { Address } from 'client/slices/address/types/api'; +import type { SmartContract } from 'client/slices/contract/types/api'; + +import ContractCertifiedLabel from 'client/slices/contract/components/ContractCertifiedLabel'; +import { getGitHubOwnerAndRepo } from 'client/slices/contract/pages/contract-verification/utils'; +import { CONTRACT_LICENSES } from 'client/slices/contract/utils/licenses'; + +import ContractSecurityAudits from 'client/features/contract-audit-reports/components/ContractSecurityAudits'; import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; -import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { Link } from 'toolkit/chakra/link'; -import { getGitHubOwnerAndRepo } from 'ui/contractVerification/utils'; -import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import Time from 'ui/shared/time/Time'; -import ContractSecurityAudits from '../audits/ContractSecurityAudits'; import ContractDetailsInfoCreator from './ContractDetailsInfoCreator'; import ContractDetailsInfoImplementations from './ContractDetailsInfoImplementations'; import ContractDetailsInfoItem from './ContractDetailsInfoItem'; diff --git a/client/slices/contract/pages/details/code/info/ContractDetailsInfoCreator.tsx b/client/slices/contract/pages/details/code/info/ContractDetailsInfoCreator.tsx new file mode 100644 index 0000000000..0d55e95455 --- /dev/null +++ b/client/slices/contract/pages/details/code/info/ContractDetailsInfoCreator.tsx @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Text, Flex } from '@chakra-ui/react'; +import React from 'react'; + +import type { SmartContractCreationStatus } from 'client/slices/contract/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import ContractCreationStatus from 'client/slices/contract/components/ContractCreationStatus'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + +import ContractDetailsInfoItem from './ContractDetailsInfoItem'; + +interface Props { + addressHash: string; + txHash: string; + creationStatus: SmartContractCreationStatus | null; + isLoading: boolean; +} + +const ContractDetailsInfoCreator = ({ addressHash, txHash, creationStatus, isLoading }: Props) => { + return ( + + + + at txn + + { creationStatus && } + + + ); +}; + +export default React.memo(ContractDetailsInfoCreator); diff --git a/ui/address/contract/info/ContractDetailsInfoImplementations.tsx b/client/slices/contract/pages/details/code/info/ContractDetailsInfoImplementations.tsx similarity index 80% rename from ui/address/contract/info/ContractDetailsInfoImplementations.tsx rename to client/slices/contract/pages/details/code/info/ContractDetailsInfoImplementations.tsx index aef64a7475..322b20ab74 100644 --- a/ui/address/contract/info/ContractDetailsInfoImplementations.tsx +++ b/client/slices/contract/pages/details/code/info/ContractDetailsInfoImplementations.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { AddressImplementation } from 'types/api/addressParams'; -import type { SmartContractProxyType } from 'types/api/contract'; +import type { AddressImplementation } from 'client/slices/address/types/api'; +import type { SmartContractProxyType } from 'client/slices/contract/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import ContractDetailsInfoItem from './ContractDetailsInfoItem'; diff --git a/ui/address/contract/info/ContractDetailsInfoItem.tsx b/client/slices/contract/pages/details/code/info/ContractDetailsInfoItem.tsx similarity index 94% rename from ui/address/contract/info/ContractDetailsInfoItem.tsx rename to client/slices/contract/pages/details/code/info/ContractDetailsInfoItem.tsx index f35e8b5de1..61c841d0fc 100644 --- a/ui/address/contract/info/ContractDetailsInfoItem.tsx +++ b/client/slices/contract/pages/details/code/info/ContractDetailsInfoItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { BoxProps } from '@chakra-ui/react'; import { chakra, Flex } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_stylus-rust-contract-1.png b/client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_stylus-rust-contract-1.png similarity index 100% rename from ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_stylus-rust-contract-1.png rename to client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_stylus-rust-contract-1.png diff --git a/ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-has-audits-1.png b/client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-has-audits-1.png similarity index 100% rename from ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-has-audits-1.png rename to client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-has-audits-1.png diff --git a/ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-no-audits-1.png b/client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-no-audits-1.png similarity index 100% rename from ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-no-audits-1.png rename to client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-audits-feature-no-audits-1.png diff --git a/ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-certified-icon-1.png b/client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-certified-icon-1.png similarity index 100% rename from ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-certified-icon-1.png rename to client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_with-certified-icon-1.png diff --git a/ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_zkSync-contract-1.png b/client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_zkSync-contract-1.png similarity index 100% rename from ui/address/contract/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_zkSync-contract-1.png rename to client/slices/contract/pages/details/code/info/__screenshots__/ContractDetailsInfo.pw.tsx_default_zkSync-contract-1.png diff --git a/client/slices/contract/pages/details/code/useContractCodeTabs.tsx b/client/slices/contract/pages/details/code/useContractCodeTabs.tsx new file mode 100644 index 0000000000..9b2854f9ab --- /dev/null +++ b/client/slices/contract/pages/details/code/useContractCodeTabs.tsx @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Flex } from '@chakra-ui/react'; +import React from 'react'; + +import type { Address } from 'client/slices/address/types/api'; +import type { SmartContract } from 'client/slices/contract/types/api'; + +import CodeViewSnippet from 'ui/shared/CodeViewSnippet'; + +import type { CONTRACT_DETAILS_TAB_IDS } from '../../../utils/tabs'; +import ContractDetailsByteCode from './ContractDetailsByteCode'; +import ContractDetailsConstructorArgs from './ContractDetailsConstructorArgs'; +import ContractSourceCode from './ContractSourceCode'; + +interface Tab { + id: typeof CONTRACT_DETAILS_TAB_IDS[number]; + title: string; + component: React.ReactNode; +} + +interface Props { + data: SmartContract | undefined; + isLoading: boolean; + addressData: Address; + sourceAddress: string | undefined; +} + +export default function useContractCodeTabs({ data, isLoading, addressData, sourceAddress }: Props): Array { + + return React.useMemo(() => { + + if (!sourceAddress) { + return []; + } + + return [ + (data?.constructor_args || data?.source_code) ? { + id: 'contract_source_code' as const, + title: 'Code', + component: ( + + + { data?.source_code && ( + + ) } + + ), + } : undefined, + + data?.compiler_settings ? { + id: 'contract_compiler' as const, + title: 'Compiler', + component: ( + + ), + } : undefined, + + data?.abi ? { + id: 'contract_abi' as const, + title: 'ABI', + component: ( + + ), + } : undefined, + + (data?.creation_bytecode || data?.deployed_bytecode) ? { + id: 'contract_bytecode' as const, + title: 'Bytecode', + component: , + } : undefined, + ].filter(Boolean); + }, [ isLoading, addressData, data, sourceAddress ]); +} diff --git a/ui/address/contract/methods/ContractAbi.tsx b/client/slices/contract/pages/details/methods/ContractAbi.tsx similarity index 98% rename from ui/address/contract/methods/ContractAbi.tsx rename to client/slices/contract/pages/details/methods/ContractAbi.tsx index 34f3dbead5..d920578204 100644 --- a/ui/address/contract/methods/ContractAbi.tsx +++ b/client/slices/contract/pages/details/methods/ContractAbi.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import { range } from 'es-toolkit'; import React from 'react'; diff --git a/ui/address/contract/methods/ContractAbiItem.tsx b/client/slices/contract/pages/details/methods/ContractAbiItem.tsx similarity index 98% rename from ui/address/contract/methods/ContractAbiItem.tsx rename to client/slices/contract/pages/details/methods/ContractAbiItem.tsx index b47fabbe46..50c5edb852 100644 --- a/ui/address/contract/methods/ContractAbiItem.tsx +++ b/client/slices/contract/pages/details/methods/ContractAbiItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; import { Element } from 'react-scroll'; diff --git a/ui/address/contract/methods/ContractMethodsAlerts.tsx b/client/slices/contract/pages/details/methods/ContractMethodsAlerts.tsx similarity index 83% rename from ui/address/contract/methods/ContractMethodsAlerts.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsAlerts.tsx index 6003c8a25f..8840711323 100644 --- a/ui/address/contract/methods/ContractMethodsAlerts.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsAlerts.tsx @@ -1,9 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { VStack } from '@chakra-ui/react'; import React from 'react'; -import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'types/api/contract'; +import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'client/slices/contract/types/api'; -import ContractCodeProxyPattern from '../alerts/ContractDetailsAlertProxyPattern'; +import ContractCodeProxyPattern from '../code/alerts/ContractDetailsAlertProxyPattern'; import ConnectWalletAlert from './alerts/ConnectWalletAlert'; import ContractCustomAbiAlert from './alerts/ContractCustomAbiAlert'; diff --git a/ui/address/contract/methods/ContractMethodsContainer.tsx b/client/slices/contract/pages/details/methods/ContractMethodsContainer.tsx similarity index 93% rename from ui/address/contract/methods/ContractMethodsContainer.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsContainer.tsx index bb387144ec..b2d224e27d 100644 --- a/ui/address/contract/methods/ContractMethodsContainer.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsContainer.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { MethodType } from './types'; diff --git a/ui/address/contract/methods/ContractMethodsCustom.pw.tsx b/client/slices/contract/pages/details/methods/ContractMethodsCustom.pw.tsx similarity index 90% rename from ui/address/contract/methods/ContractMethodsCustom.pw.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsCustom.pw.tsx index 87928ee2f5..b5ca4a883d 100644 --- a/ui/address/contract/methods/ContractMethodsCustom.pw.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsCustom.pw.tsx @@ -2,8 +2,9 @@ import type { BrowserContext } from '@playwright/test'; import React from 'react'; import type { Abi } from 'viem'; -import * as addressMock from 'mocks/address/address'; -import * as methodsMock from 'mocks/contract/methods'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as methodsMock from 'client/slices/contract/mocks/methods'; + import { contextWithAuth } from 'playwright/fixtures/auth'; import { test, expect } from 'playwright/lib'; diff --git a/ui/address/contract/methods/ContractMethodsCustom.tsx b/client/slices/contract/pages/details/methods/ContractMethodsCustom.tsx similarity index 88% rename from ui/address/contract/methods/ContractMethodsCustom.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsCustom.tsx index 91415d5d42..5490623dc1 100644 --- a/ui/address/contract/methods/ContractMethodsCustom.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsCustom.tsx @@ -1,19 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import React from 'react'; -import type { SmartContract } from 'types/api/contract'; +import type { SmartContract } from 'client/slices/contract/types/api'; + +import useApiQuery, { getResourceKey } from 'client/api/hooks/useApiQuery'; + +import AuthGuard from 'client/features/account/components/auth-modal/guard/AuthGuard'; +import useIsAuth from 'client/features/account/hooks/useIsAuth'; +import CustomAbiModal from 'client/features/account/pages/custom-abi/CustomAbiModal/CustomAbiModal'; + +import getQueryParamString from 'client/shared/router/get-query-param-string'; -import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; -import getQueryParamString from 'lib/router/getQueryParamString'; import { Button } from 'toolkit/chakra/button'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { useDisclosure } from 'toolkit/hooks/useDisclosure'; -import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; -import AuthGuard from 'ui/snippets/auth/guard/AuthGuard'; -import useIsAuth from 'ui/snippets/auth/useIsAuth'; import ContractAbi from './ContractAbi'; import ContractMethodsAlerts from './ContractMethodsAlerts'; diff --git a/ui/address/contract/methods/ContractMethodsFilters.tsx b/client/slices/contract/pages/details/methods/ContractMethodsFilters.tsx similarity index 97% rename from ui/address/contract/methods/ContractMethodsFilters.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsFilters.tsx index b9735737bc..bc45497e1d 100644 --- a/ui/address/contract/methods/ContractMethodsFilters.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsFilters.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/address/contract/methods/ContractMethodsProxy.pw.tsx b/client/slices/contract/pages/details/methods/ContractMethodsProxy.pw.tsx similarity index 90% rename from ui/address/contract/methods/ContractMethodsProxy.pw.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsProxy.pw.tsx index f98c820b1e..0e4e303a93 100644 --- a/ui/address/contract/methods/ContractMethodsProxy.pw.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsProxy.pw.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import * as addressMock from 'mocks/address/address'; -import * as contractMock from 'mocks/contract/info'; -import * as methodsMock from 'mocks/contract/methods'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as contractMock from 'client/slices/contract/mocks/info'; +import * as methodsMock from 'client/slices/contract/mocks/methods'; + import { test, expect } from 'playwright/lib'; import ContractMethodsProxy from './ContractMethodsProxy'; diff --git a/ui/address/contract/methods/ContractMethodsProxy.tsx b/client/slices/contract/pages/details/methods/ContractMethodsProxy.tsx similarity index 87% rename from ui/address/contract/methods/ContractMethodsProxy.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsProxy.tsx index c453f8d8ea..58aa1fd0ff 100644 --- a/ui/address/contract/methods/ContractMethodsProxy.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsProxy.tsx @@ -1,14 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; -import type { AddressImplementation } from 'types/api/addressParams'; -import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'types/api/contract'; +import type { AddressImplementation } from 'client/slices/address/types/api'; +import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'client/slices/contract/types/api'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; -import useApiQuery from 'lib/api/useApiQuery'; -import getQueryParamString from 'lib/router/getQueryParamString'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; -import ContractSourceAddressSelector from '../ContractSourceAddressSelector'; +import ContractSourceAddressSelector from '../code/ContractSourceAddressSelector'; import ContractAbi from './ContractAbi'; import ContractMethodsAlerts from './ContractMethodsAlerts'; import ContractMethodsContainer from './ContractMethodsContainer'; diff --git a/ui/address/contract/methods/ContractMethodsRegular.pw.tsx b/client/slices/contract/pages/details/methods/ContractMethodsRegular.pw.tsx similarity index 95% rename from ui/address/contract/methods/ContractMethodsRegular.pw.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsRegular.pw.tsx index 75bce559bb..1ab9eaf2b7 100644 --- a/ui/address/contract/methods/ContractMethodsRegular.pw.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsRegular.pw.tsx @@ -1,8 +1,9 @@ import React from 'react'; import type { Abi, AbiFunction } from 'viem'; -import * as addressMock from 'mocks/address/address'; -import * as methodsMock from 'mocks/contract/methods'; +import * as addressMock from 'client/slices/address/mocks/address'; +import * as methodsMock from 'client/slices/contract/mocks/methods'; + import { test, expect } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; diff --git a/ui/address/contract/methods/ContractMethodsRegular.tsx b/client/slices/contract/pages/details/methods/ContractMethodsRegular.tsx similarity index 92% rename from ui/address/contract/methods/ContractMethodsRegular.tsx rename to client/slices/contract/pages/details/methods/ContractMethodsRegular.tsx index 97cb9500f0..442e3ea816 100644 --- a/ui/address/contract/methods/ContractMethodsRegular.tsx +++ b/client/slices/contract/pages/details/methods/ContractMethodsRegular.tsx @@ -1,9 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; import type { Abi } from 'viem'; -import getQueryParamString from 'lib/router/getQueryParamString'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; import ContractAbi from './ContractAbi'; import ContractMethodsAlerts from './ContractMethodsAlerts'; diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_with-data-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_with-data-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_with-data-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_with-data-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_without-data-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_without-data-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_without-data-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsCustom.pw.tsx_default_without-data-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_dark-color-mode_all-methods-dark-mode-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_dark-color-mode_all-methods-dark-mode-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_dark-color-mode_all-methods-dark-mode-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_dark-color-mode_all-methods-dark-mode-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-dark-mode-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-dark-mode-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-dark-mode-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-dark-mode-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-mobile-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-mobile-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-mobile-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_all-methods-mobile-1.png diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_can-simulate-method-1.png b/client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_can-simulate-method-1.png similarity index 100% rename from ui/address/contract/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_can-simulate-method-1.png rename to client/slices/contract/pages/details/methods/__screenshots__/ContractMethodsRegular.pw.tsx_default_can-simulate-method-1.png diff --git a/ui/address/contract/methods/alerts/ConnectWalletAlert.tsx b/client/slices/contract/pages/details/methods/alerts/ConnectWalletAlert.tsx similarity index 88% rename from ui/address/contract/methods/alerts/ConnectWalletAlert.tsx rename to client/slices/contract/pages/details/methods/alerts/ConnectWalletAlert.tsx index 482807d097..eae01570f5 100644 --- a/ui/address/contract/methods/alerts/ConnectWalletAlert.tsx +++ b/client/slices/contract/pages/details/methods/alerts/ConnectWalletAlert.tsx @@ -1,14 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Spinner } from '@chakra-ui/react'; import React from 'react'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + +import useWeb3Wallet from 'client/features/connect-wallet/hooks/useWallet'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import config from 'configs/app'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import useWeb3Wallet from 'lib/web3/useWallet'; import { Alert } from 'toolkit/chakra/alert'; import { Button } from 'toolkit/chakra/button'; import { IconButton } from 'toolkit/chakra/icon-button'; import { Skeleton } from 'toolkit/chakra/skeleton'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import IconSvg from 'ui/shared/IconSvg'; interface Props { diff --git a/ui/address/contract/methods/alerts/ContractCustomAbiAlert.tsx b/client/slices/contract/pages/details/methods/alerts/ContractCustomAbiAlert.tsx similarity index 92% rename from ui/address/contract/methods/alerts/ContractCustomAbiAlert.tsx rename to client/slices/contract/pages/details/methods/alerts/ContractCustomAbiAlert.tsx index 97ff8227ee..444fd31fba 100644 --- a/ui/address/contract/methods/alerts/ContractCustomAbiAlert.tsx +++ b/client/slices/contract/pages/details/methods/alerts/ContractCustomAbiAlert.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { Alert } from 'toolkit/chakra/alert'; diff --git a/ui/address/contract/methods/form/ContractMethodAddressButton.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodAddressButton.tsx similarity index 87% rename from ui/address/contract/methods/form/ContractMethodAddressButton.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodAddressButton.tsx index 14533d9d8e..4940241f83 100644 --- a/ui/address/contract/methods/form/ContractMethodAddressButton.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodAddressButton.tsx @@ -1,6 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import useAccount from 'lib/web3/useAccount'; +import useAccount from 'client/features/connect-wallet/hooks/useAccount'; + import { Button } from 'toolkit/chakra/button'; import { Tooltip } from 'toolkit/chakra/tooltip'; diff --git a/ui/address/contract/methods/form/ContractMethodFieldAccordion.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodFieldAccordion.tsx similarity index 97% rename from ui/address/contract/methods/form/ContractMethodFieldAccordion.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodFieldAccordion.tsx index fd1171e654..553f1220cb 100644 --- a/ui/address/contract/methods/form/ContractMethodFieldAccordion.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodFieldAccordion.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/address/contract/methods/form/ContractMethodFieldInput.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodFieldInput.tsx similarity index 99% rename from ui/address/contract/methods/form/ContractMethodFieldInput.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodFieldInput.tsx index e118313a04..f4e942b234 100644 --- a/ui/address/contract/methods/form/ContractMethodFieldInput.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodFieldInput.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; import { useController, useFormContext } from 'react-hook-form'; diff --git a/ui/address/contract/methods/form/ContractMethodFieldInputArray.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodFieldInputArray.tsx similarity index 99% rename from ui/address/contract/methods/form/ContractMethodFieldInputArray.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodFieldInputArray.tsx index 80a80b5606..f9181ecca9 100644 --- a/ui/address/contract/methods/form/ContractMethodFieldInputArray.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodFieldInputArray.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; diff --git a/ui/address/contract/methods/form/ContractMethodFieldInputTuple.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodFieldInputTuple.tsx similarity index 97% rename from ui/address/contract/methods/form/ContractMethodFieldInputTuple.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodFieldInputTuple.tsx index 6f820a0dd8..8e0e32e98a 100644 --- a/ui/address/contract/methods/form/ContractMethodFieldInputTuple.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodFieldInputTuple.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { useFormContext } from 'react-hook-form'; diff --git a/ui/address/contract/methods/form/ContractMethodFieldLabel.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodFieldLabel.tsx similarity index 94% rename from ui/address/contract/methods/form/ContractMethodFieldLabel.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodFieldLabel.tsx index 37ba41e4be..86ae7a409d 100644 --- a/ui/address/contract/methods/form/ContractMethodFieldLabel.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodFieldLabel.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/address/contract/methods/form/ContractMethodForm.pw.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodForm.pw.tsx similarity index 100% rename from ui/address/contract/methods/form/ContractMethodForm.pw.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodForm.pw.tsx diff --git a/ui/address/contract/methods/form/ContractMethodForm.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodForm.tsx similarity index 99% rename from ui/address/contract/methods/form/ContractMethodForm.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodForm.tsx index 3063bc1b2a..4bfd6e6345 100644 --- a/ui/address/contract/methods/form/ContractMethodForm.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodForm.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, chakra } from '@chakra-ui/react'; import React from 'react'; import type { SubmitHandler } from 'react-hook-form'; @@ -6,8 +8,9 @@ import { encodeFunctionData, type AbiFunction } from 'viem'; import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, SmartContractMethod } from '../types'; +import * as mixpanel from 'client/shared/analytics/mixpanel'; + import config from 'configs/app'; -import * as mixpanel from 'lib/mixpanel/index'; import { Button } from 'toolkit/chakra/button'; import { Tooltip } from 'toolkit/chakra/tooltip'; import { useDisclosure } from 'toolkit/hooks/useDisclosure'; diff --git a/ui/address/contract/methods/form/ContractMethodMultiplyButton.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodMultiplyButton.tsx similarity index 98% rename from ui/address/contract/methods/form/ContractMethodMultiplyButton.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodMultiplyButton.tsx index 9c204dd9c3..2611da85b8 100644 --- a/ui/address/contract/methods/form/ContractMethodMultiplyButton.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodMultiplyButton.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, List, Input, ListItem } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/address/contract/methods/form/ContractMethodOutput.pw.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodOutput.pw.tsx similarity index 100% rename from ui/address/contract/methods/form/ContractMethodOutput.pw.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodOutput.pw.tsx diff --git a/ui/address/contract/methods/form/ContractMethodOutput.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodOutput.tsx similarity index 96% rename from ui/address/contract/methods/form/ContractMethodOutput.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodOutput.tsx index 901767aaf1..9d74681701 100644 --- a/ui/address/contract/methods/form/ContractMethodOutput.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodOutput.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { AbiFunction } from 'viem'; diff --git a/ui/address/contract/methods/form/ContractMethodResultPublicClient.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodResultPublicClient.tsx similarity index 94% rename from ui/address/contract/methods/form/ContractMethodResultPublicClient.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodResultPublicClient.tsx index 946d359f6b..3b8d393845 100644 --- a/ui/address/contract/methods/form/ContractMethodResultPublicClient.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodResultPublicClient.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { Alert } from 'toolkit/chakra/alert'; diff --git a/ui/address/contract/methods/form/ContractMethodResultWalletClient.pw.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodResultWalletClient.pw.tsx similarity index 100% rename from ui/address/contract/methods/form/ContractMethodResultWalletClient.pw.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodResultWalletClient.pw.tsx diff --git a/ui/address/contract/methods/form/ContractMethodResultWalletClient.tsx b/client/slices/contract/pages/details/methods/form/ContractMethodResultWalletClient.tsx similarity index 98% rename from ui/address/contract/methods/form/ContractMethodResultWalletClient.tsx rename to client/slices/contract/pages/details/methods/form/ContractMethodResultWalletClient.tsx index d118be256f..3dc48ce184 100644 --- a/ui/address/contract/methods/form/ContractMethodResultWalletClient.tsx +++ b/client/slices/contract/pages/details/methods/form/ContractMethodResultWalletClient.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Spinner, Box } from '@chakra-ui/react'; import React from 'react'; import type { UseWaitForTransactionReceiptReturnType } from 'wagmi'; diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png diff --git a/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..b1f5114f04 Binary files /dev/null and b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_preview-mode-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_preview-mode-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_preview-mode-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_preview-mode-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_result-mode-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_result-mode-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_result-mode-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_result-mode-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_single-output-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_single-output-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_single-output-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodOutput.pw.tsx_default_single-output-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png diff --git a/ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png b/client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png similarity index 100% rename from ui/address/contract/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png rename to client/slices/contract/pages/details/methods/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png diff --git a/ui/address/contract/methods/form/resultPublicClient/Item.tsx b/client/slices/contract/pages/details/methods/form/resultPublicClient/Item.tsx similarity index 96% rename from ui/address/contract/methods/form/resultPublicClient/Item.tsx rename to client/slices/contract/pages/details/methods/form/resultPublicClient/Item.tsx index 09f095ed0d..d374f9c029 100644 --- a/ui/address/contract/methods/form/resultPublicClient/Item.tsx +++ b/client/slices/contract/pages/details/methods/form/resultPublicClient/Item.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { AbiParameter } from 'viem'; diff --git a/ui/address/contract/methods/form/resultPublicClient/ItemArray.tsx b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemArray.tsx similarity index 97% rename from ui/address/contract/methods/form/resultPublicClient/ItemArray.tsx rename to client/slices/contract/pages/details/methods/form/resultPublicClient/ItemArray.tsx index f4bc9aaa89..897fb15e96 100644 --- a/ui/address/contract/methods/form/resultPublicClient/ItemArray.tsx +++ b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemArray.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; import type { AbiParameter } from 'viem'; diff --git a/ui/address/contract/methods/form/resultPublicClient/ItemLabel.tsx b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemLabel.tsx similarity index 89% rename from ui/address/contract/methods/form/resultPublicClient/ItemLabel.tsx rename to client/slices/contract/pages/details/methods/form/resultPublicClient/ItemLabel.tsx index 1b0d3f9a8d..284467a02a 100644 --- a/ui/address/contract/methods/form/resultPublicClient/ItemLabel.tsx +++ b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemLabel.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; import type { AbiParameter } from 'viem'; diff --git a/ui/address/contract/methods/form/resultPublicClient/ItemPrimitive.tsx b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemPrimitive.tsx similarity index 96% rename from ui/address/contract/methods/form/resultPublicClient/ItemPrimitive.tsx rename to client/slices/contract/pages/details/methods/form/resultPublicClient/ItemPrimitive.tsx index 91fe5d3c75..81e99dfd0e 100644 --- a/ui/address/contract/methods/form/resultPublicClient/ItemPrimitive.tsx +++ b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemPrimitive.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; import type { AbiParameter } from 'viem'; diff --git a/ui/address/contract/methods/form/resultPublicClient/ItemTuple.tsx b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemTuple.tsx similarity index 96% rename from ui/address/contract/methods/form/resultPublicClient/ItemTuple.tsx rename to client/slices/contract/pages/details/methods/form/resultPublicClient/ItemTuple.tsx index 79d5574d03..d7f5375281 100644 --- a/ui/address/contract/methods/form/resultPublicClient/ItemTuple.tsx +++ b/client/slices/contract/pages/details/methods/form/resultPublicClient/ItemTuple.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; import type { AbiParameter } from 'viem'; diff --git a/client/slices/contract/pages/details/methods/form/resultPublicClient/utils.ts b/client/slices/contract/pages/details/methods/form/resultPublicClient/utils.ts new file mode 100644 index 0000000000..e405146d34 --- /dev/null +++ b/client/slices/contract/pages/details/methods/form/resultPublicClient/utils.ts @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +const TAB_SIZE = 2; +export const printRowOffset = (level: number) => ' '.repeat(level * TAB_SIZE); diff --git a/ui/address/contract/methods/form/useFormatFieldValue.tsx b/client/slices/contract/pages/details/methods/form/useFormatFieldValue.tsx similarity index 95% rename from ui/address/contract/methods/form/useFormatFieldValue.tsx rename to client/slices/contract/pages/details/methods/form/useFormatFieldValue.tsx index 70bd8270fe..cde53e93a7 100644 --- a/ui/address/contract/methods/form/useFormatFieldValue.tsx +++ b/client/slices/contract/pages/details/methods/form/useFormatFieldValue.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { MatchInt } from './utils'; diff --git a/ui/address/contract/methods/form/useValidateField.tsx b/client/slices/contract/pages/details/methods/form/useValidateField.tsx similarity index 98% rename from ui/address/contract/methods/form/useValidateField.tsx rename to client/slices/contract/pages/details/methods/form/useValidateField.tsx index de9d506f31..caba0fcf6a 100644 --- a/ui/address/contract/methods/form/useValidateField.tsx +++ b/client/slices/contract/pages/details/methods/form/useValidateField.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { getAddress, isAddress, isHex } from 'viem'; diff --git a/ui/address/contract/methods/form/utils.spec.ts b/client/slices/contract/pages/details/methods/form/utils.spec.ts similarity index 100% rename from ui/address/contract/methods/form/utils.spec.ts rename to client/slices/contract/pages/details/methods/form/utils.spec.ts diff --git a/client/slices/contract/pages/details/methods/form/utils.ts b/client/slices/contract/pages/details/methods/form/utils.ts new file mode 100644 index 0000000000..743b747cee --- /dev/null +++ b/client/slices/contract/pages/details/methods/form/utils.ts @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { set } from 'es-toolkit/compat'; + +import type { ContractAbiItemInput } from '../types'; + +export type ContractMethodFormFields = Record; + +export const INT_REGEXP = /^(u)?int(\d+)?$/i; + +export const BYTES_REGEXP = /^bytes(\d+)?$/i; + +export const ARRAY_REGEXP = /^(.*)\[(\d*)\]$/; + +export interface MatchArray { + itemType: string; + size: number; + isNested: boolean; +} + +export const matchArray = (argType: string): MatchArray | null => { + const match = argType.match(ARRAY_REGEXP); + if (!match) { + return null; + } + + const [ , itemType, size ] = match; + const isNested = Boolean(matchArray(itemType)); + + return { + itemType, + size: size ? Number(size) : Infinity, + isNested, + }; +}; + +export interface MatchInt { + isUnsigned: boolean; + power: string; + min: bigint; + max: bigint; +} + +export const matchInt = (argType: string): MatchInt | null => { + const match = argType.match(INT_REGEXP); + if (!match) { + return null; + } + + const [ , isUnsigned, power = '256' ] = match; + const [ min, max ] = getIntBoundaries(Number(power), Boolean(isUnsigned)); + + return { isUnsigned: Boolean(isUnsigned), power, min, max }; +}; + +export const transformDataForArrayItem = (data: ContractAbiItemInput, index: number): ContractAbiItemInput => { + const arrayMatchType = matchArray(data.type); + const arrayMatchInternalType = data.internalType ? matchArray(data.internalType) : null; + const childrenInternalType = arrayMatchInternalType?.itemType.replaceAll('struct ', ''); + + const postfix = childrenInternalType ? ' ' + childrenInternalType : ''; + + return { + ...data, + type: arrayMatchType?.itemType || data.type, + internalType: childrenInternalType, + name: `#${ index + 1 }${ postfix }`, + }; +}; + +export const getIntBoundaries = (power: number, isUnsigned: boolean) => { + const maxUnsigned = BigInt(2 ** power); + const max = isUnsigned ? maxUnsigned - BigInt(1) : maxUnsigned / BigInt(2) - BigInt(1); + const min = isUnsigned ? BigInt(0) : -maxUnsigned / BigInt(2); + return [ min, max ]; +}; + +export function transformFormDataToMethodArgs(formData: ContractMethodFormFields) { + const result: Array = []; + + for (const field in formData) { + const value = formData[field]; + const castedValue = castValue(value); + set(result, field.replaceAll(':', '.'), castedValue); + } + + const filteredResult = filterOutEmptyItems(result); + const mappedResult = mapEmptyNestedArrays(filteredResult); + return mappedResult; +} + +function castValue(value: unknown): unknown { + if (typeof value === 'string') { + return value === '""' ? '' : value; + } + + if (Array.isArray(value)) { + return value.map(castValue); + } + + return value; +} + +function filterOutEmptyItems(array: Array): Array { + // The undefined value may occur in two cases: + // 1. When an optional form field is left blank by the user. + // The only optional field is the native coin value, which is safely handled in the form submit handler. + // 2. When the user adds and removes items from a field array. + // In this scenario, empty items need to be filtered out to maintain the correct sequence of arguments. + // We don't use isEmptyField() function here because of the second case otherwise it will not keep the correct order of arguments. + return array + .map((item) => Array.isArray(item) ? filterOutEmptyItems(item) : item) + .filter((item) => item !== undefined); +} + +function isEmptyField(field: unknown): boolean { + // the empty string is meant that the field was touched but left empty + // the undefined is meant that the field was not touched + return field === undefined || field === ''; +} + +function isEmptyNestedArray(array: Array): boolean { + return array.flat(Infinity).filter((item) => !isEmptyField(item)).length === 0; +} + +function mapEmptyNestedArrays(array: Array): Array { + return array.map((item) => Array.isArray(item) && isEmptyNestedArray(item) ? [] : item); +} + +export function getFieldLabel(input: ContractAbiItemInput, isRequired?: boolean) { + const name = input.name || input.internalType || ''; + return `${ name } (${ input.type })${ isRequired ? '*' : '' }`; +} diff --git a/client/slices/contract/pages/details/methods/types.ts b/client/slices/contract/pages/details/methods/types.ts new file mode 100644 index 0000000000..5639546291 --- /dev/null +++ b/client/slices/contract/pages/details/methods/types.ts @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { AbiFunction, AbiFallback as AbiFallbackViem, AbiReceive } from 'abitype'; +import type { AbiParameter, AbiStateMutability } from 'viem'; + +export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' }; + +export type MethodType = 'read' | 'write' | 'all'; +export type MethodCallStrategy = 'read' | 'write' | 'simulate' | 'copy_calldata'; +export type ResultViewMode = 'preview' | 'result'; + +// we manually add inputs and outputs to the fallback method because viem doesn't support it +// but as we discussed with @k1rill-fedoseev, it's a good idea to have them for fallback method of any contract +// also, according to @k1rill-fedoseev, fallback method can act as a read method when it has 'view' state mutability +// but viem doesn't aware of this and thinks that fallback method state mutability can only be 'payable' or 'nonpayable' +// so we have to redefine the stateMutability as well to include "view" option +// see "addInputsToFallback" and "isReadMethod" functions in utils.ts +export interface AbiFallback extends Pick { + inputs: Array; + outputs: Array; + stateMutability: Exclude; +} + +export type SmartContractMethodCustomFields = { method_id: string } | { is_invalid: boolean }; +export type SmartContractMethodRead = AbiFunction & SmartContractMethodCustomFields; +export type SmartContractMethodWrite = AbiFunction & SmartContractMethodCustomFields | AbiFallback | AbiReceive; +export type SmartContractMethod = SmartContractMethodRead | SmartContractMethodWrite; + +export interface FormSubmitResultPublicClient { + source: 'public_client'; + data: unknown | Error; + estimatedGas?: bigint; +} +export interface FormSubmitResultWalletClient { + source: 'wallet_client'; + data: { hash: `0x${ string }` } | Error; +} +export type FormSubmitResult = FormSubmitResultPublicClient | FormSubmitResultWalletClient; + +export type FormSubmitHandler = (item: SmartContractMethod, args: Array, submitType: MethodCallStrategy | undefined) => Promise; diff --git a/ui/address/contract/methods/useCallMethodPublicClient.ts b/client/slices/contract/pages/details/methods/useCallMethodPublicClient.ts similarity index 96% rename from ui/address/contract/methods/useCallMethodPublicClient.ts rename to client/slices/contract/pages/details/methods/useCallMethodPublicClient.ts index 41bfb6b08f..2d605d5cb9 100644 --- a/ui/address/contract/methods/useCallMethodPublicClient.ts +++ b/client/slices/contract/pages/details/methods/useCallMethodPublicClient.ts @@ -1,12 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { encodeFunctionData, getAddress } from 'viem'; import { usePublicClient } from 'wagmi'; import type { FormSubmitResult, MethodCallStrategy, SmartContractMethod } from './types'; +import useAccount from 'client/features/connect-wallet/hooks/useAccount'; + import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; -import useAccount from 'lib/web3/useAccount'; import { getNativeCoinValue } from './utils'; diff --git a/ui/address/contract/methods/useCallMethodWalletClient.ts b/client/slices/contract/pages/details/methods/useCallMethodWalletClient.ts similarity index 97% rename from ui/address/contract/methods/useCallMethodWalletClient.ts rename to client/slices/contract/pages/details/methods/useCallMethodWalletClient.ts index 4dab912eb9..dfdd89efa6 100644 --- a/ui/address/contract/methods/useCallMethodWalletClient.ts +++ b/client/slices/contract/pages/details/methods/useCallMethodWalletClient.ts @@ -1,13 +1,16 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { getAddress, type Abi } from 'viem'; import { useAccount, useSwitchChain, useWriteContract, useSendTransaction } from 'wagmi'; import type { FormSubmitResult, SmartContractMethod } from './types'; +import useWallet from 'client/features/connect-wallet/hooks/useWallet'; + import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; import useRewardsActivity from 'lib/hooks/useRewardsActivity'; -import useWallet from 'lib/web3/useWallet'; import { getNativeCoinValue } from './utils'; diff --git a/ui/address/contract/methods/useFormSubmit.ts b/client/slices/contract/pages/details/methods/useFormSubmit.ts similarity index 96% rename from ui/address/contract/methods/useFormSubmit.ts rename to client/slices/contract/pages/details/methods/useFormSubmit.ts index 7c1f171d8b..d9796fb58a 100644 --- a/ui/address/contract/methods/useFormSubmit.ts +++ b/client/slices/contract/pages/details/methods/useFormSubmit.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { FormSubmitHandler } from './types'; diff --git a/ui/address/contract/methods/useMethodsFilters.ts b/client/slices/contract/pages/details/methods/useMethodsFilters.ts similarity index 94% rename from ui/address/contract/methods/useMethodsFilters.ts rename to client/slices/contract/pages/details/methods/useMethodsFilters.ts index 78e5709713..f04538ad7e 100644 --- a/ui/address/contract/methods/useMethodsFilters.ts +++ b/client/slices/contract/pages/details/methods/useMethodsFilters.ts @@ -1,12 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { pickBy } from 'es-toolkit'; import { useRouter } from 'next/router'; import React from 'react'; import type { MethodType, SmartContractMethod } from './types'; -import getQueryParamString from 'lib/router/getQueryParamString'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; -import type { CONTRACT_MAIN_TAB_IDS } from '../utils'; +import type { CONTRACT_MAIN_TAB_IDS } from '../../../utils/tabs'; import { TYPE_FILTER_OPTIONS, isReadMethod, isWriteMethod } from './utils'; function getInitialMethodType(tab: string) { diff --git a/ui/address/contract/methods/useScrollToMethod.ts b/client/slices/contract/pages/details/methods/useScrollToMethod.ts similarity index 95% rename from ui/address/contract/methods/useScrollToMethod.ts rename to client/slices/contract/pages/details/methods/useScrollToMethod.ts index 5034d1fe91..f89270d18d 100644 --- a/ui/address/contract/methods/useScrollToMethod.ts +++ b/client/slices/contract/pages/details/methods/useScrollToMethod.ts @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { scroller } from 'react-scroll'; diff --git a/client/slices/contract/pages/details/methods/utils.ts b/client/slices/contract/pages/details/methods/utils.ts new file mode 100644 index 0000000000..c3c87d7466 --- /dev/null +++ b/client/slices/contract/pages/details/methods/utils.ts @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { Abi } from 'abitype'; +import { toFunctionSelector } from 'viem'; + +import type { MethodType, SmartContractMethod, SmartContractMethodRead, SmartContractMethodWrite } from './types'; + +export const getNativeCoinValue = (value: unknown) => { + if (typeof value !== 'string') { + return BigInt(0); + } + + return BigInt(value); +}; + +export const isMethod = (method: Abi[number]) => + (method.type === 'function' || method.type === 'fallback' || method.type === 'receive'); + +export const isReadMethod = (method: SmartContractMethod): method is SmartContractMethodRead => + ( + method.type === 'function' && + (method.constant || method.stateMutability === 'view' || method.stateMutability === 'pure') + ) || ( + method.type === 'fallback' && method.stateMutability === 'view' + ); + +export const isWriteMethod = (method: SmartContractMethod): method is SmartContractMethodWrite => + (method.type === 'function' || method.type === 'fallback' || method.type === 'receive') && + !isReadMethod(method); + +export const enrichWithMethodId = (method: SmartContractMethod): SmartContractMethod => { + if (method.type !== 'function') { + return method; + } + + try { + return { + ...method, + method_id: toFunctionSelector(method), + }; + } catch (error) { + return { + ...method, + is_invalid: true, + }; + } +}; + +export const addInputsToFallback = (method: SmartContractMethod): SmartContractMethod => { + if (method.type === 'fallback') { + return { + ...method, + inputs: [ { internalType: 'bytes', name: 'input', type: 'bytes' } ], + outputs: [ { internalType: 'bytes', name: 'output', type: 'bytes' } ], + }; + } + + return method; +}; + +const getNameForSorting = (method: SmartContractMethod) => { + if ('name' in method) { + return method.name; + } + + return method.type === 'fallback' ? 'fallback' : 'receive'; +}; + +export const formatAbi = (abi: Abi) => { + + const methods = abi.filter(isMethod) as Array; + + return methods + .map(enrichWithMethodId) + .map(addInputsToFallback) + .sort((a, b) => { + const aName = getNameForSorting(a); + const bName = getNameForSorting(b); + return aName.localeCompare(bName); + }); +}; + +export const TYPE_FILTER_OPTIONS: Array<{ value: MethodType; title: string }> = [ + { value: 'all', title: 'All' }, + { value: 'read', title: 'Read' }, + { value: 'write', title: 'Write' }, +]; diff --git a/ui/address/contract/useContractTabs.tsx b/client/slices/contract/pages/details/useContractTabs.tsx similarity index 81% rename from ui/address/contract/useContractTabs.tsx rename to client/slices/contract/pages/details/useContractTabs.tsx index 412b601ce2..48b93fbbf7 100644 --- a/ui/address/contract/useContractTabs.tsx +++ b/client/slices/contract/pages/details/useContractTabs.tsx @@ -1,21 +1,27 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { Channel } from 'phoenix'; import React from 'react'; -import type { Address } from 'types/api/address'; +import type { Address } from 'client/slices/address/types/api'; import type { ClusterChainConfig } from 'types/multichain'; +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import ContractDetails from 'client/slices/contract/pages/details/code/ContractCode'; +import ContractMethodsCustom from 'client/slices/contract/pages/details/methods/ContractMethodsCustom'; +import ContractMethodsProxy from 'client/slices/contract/pages/details/methods/ContractMethodsProxy'; +import ContractMethodsRegular from 'client/slices/contract/pages/details/methods/ContractMethodsRegular'; +import * as stubs from 'client/slices/contract/stubs'; + +import ContractMethodsMudSystem from 'client/features/chain-variants/mud/pages/contract/ContractMethodsMudSystem'; +import { MUD_SYSTEMS } from 'client/features/chain-variants/mud/stubs/contract'; + import config from 'configs/app'; -import useApiQuery from 'lib/api/useApiQuery'; -import * as stubs from 'stubs/contract'; import { ContentLoader } from 'toolkit/components/loaders/ContentLoader'; -import ContractDetails from 'ui/address/contract/ContractDetails'; -import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom'; -import ContractMethodsMudSystem from 'ui/address/contract/methods/ContractMethodsMudSystem'; -import ContractMethodsProxy from 'ui/address/contract/methods/ContractMethodsProxy'; -import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular'; -import type { CONTRACT_MAIN_TAB_IDS } from './utils'; -import { CONTRACT_DETAILS_TAB_IDS } from './utils'; +import type { CONTRACT_MAIN_TAB_IDS } from '../../utils/tabs'; +import { CONTRACT_DETAILS_TAB_IDS } from '../../utils/tabs'; interface ContractTab { id: typeof CONTRACT_MAIN_TAB_IDS[number] | Array; @@ -54,7 +60,7 @@ export default function useContractTabs({ addressData, isEnabled, hasMudTab, cha queryOptions: { enabled: Boolean(isEnabled && hasMudTab && addressData?.is_contract), refetchOnMount: false, - placeholderData: stubs.MUD_SYSTEMS, + placeholderData: MUD_SYSTEMS, }, }); diff --git a/ui/pages/VerifiedContracts.pw.tsx b/client/slices/contract/pages/index/VerifiedContracts.pw.tsx similarity index 81% rename from ui/pages/VerifiedContracts.pw.tsx rename to client/slices/contract/pages/index/VerifiedContracts.pw.tsx index 494adc6fb0..4cbfddd0af 100644 --- a/ui/pages/VerifiedContracts.pw.tsx +++ b/client/slices/contract/pages/index/VerifiedContracts.pw.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { verifiedContractsCountersMock } from 'mocks/contracts/counters'; -import * as verifiedContractsMock from 'mocks/contracts/index'; +import { verifiedContractsCountersMock } from 'client/slices/contract/mocks/counters'; +import * as verifiedContractsMock from 'client/slices/contract/mocks/list'; + import { test, expect } from 'playwright/lib'; import VerifiedContracts from './VerifiedContracts'; diff --git a/ui/pages/VerifiedContracts.tsx b/client/slices/contract/pages/index/VerifiedContracts.tsx similarity index 82% rename from ui/pages/VerifiedContracts.tsx rename to client/slices/contract/pages/index/VerifiedContracts.tsx index a55ea115f5..efbcb5ded8 100644 --- a/ui/pages/VerifiedContracts.tsx +++ b/client/slices/contract/pages/index/VerifiedContracts.tsx @@ -1,20 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, createListCollection, HStack } from '@chakra-ui/react'; import React from 'react'; +import useVerifiedContractsQuery from 'client/slices/contract/hooks/useVerifiedContractsQuery'; +import VerifiedContractsCounters from 'client/slices/contract/pages/index/VerifiedContractsCounters'; +import VerifiedContractsFilter from 'client/slices/contract/pages/index/VerifiedContractsFilter'; +import VerifiedContractsList from 'client/slices/contract/pages/index/VerifiedContractsList'; +import VerifiedContractsTable from 'client/slices/contract/pages/index/VerifiedContractsTable'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import config from 'configs/app'; -import useIsMobile from 'lib/hooks/useIsMobile'; import { FilterInput } from 'toolkit/components/filters/FilterInput'; import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import PageTitle from 'ui/shared/Page/PageTitle'; import Pagination from 'ui/shared/pagination/Pagination'; import Sort from 'ui/shared/sort/Sort'; -import useVerifiedContractsQuery from 'ui/verifiedContracts/useVerifiedContractsQuery'; -import { SORT_OPTIONS } from 'ui/verifiedContracts/utils'; -import VerifiedContractsCounters from 'ui/verifiedContracts/VerifiedContractsCounters'; -import VerifiedContractsFilter from 'ui/verifiedContracts/VerifiedContractsFilter'; -import VerifiedContractsList from 'ui/verifiedContracts/VerifiedContractsList'; -import VerifiedContractsTable from 'ui/verifiedContracts/VerifiedContractsTable'; + +import { SORT_OPTIONS } from './sort'; const sortCollection = createListCollection({ items: SORT_OPTIONS, diff --git a/ui/verifiedContracts/VerifiedContractsCounters.tsx b/client/slices/contract/pages/index/VerifiedContractsCounters.tsx similarity index 95% rename from ui/verifiedContracts/VerifiedContractsCounters.tsx rename to client/slices/contract/pages/index/VerifiedContractsCounters.tsx index d360da1db7..60de41612b 100644 --- a/ui/verifiedContracts/VerifiedContractsCounters.tsx +++ b/client/slices/contract/pages/index/VerifiedContractsCounters.tsx @@ -1,10 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import { VERIFIED_CONTRACTS_COUNTERS, VERIFIED_CONTRACTS_COUNTERS_MICROSERVICE } from 'client/slices/contract/stubs'; + import config from 'configs/app'; -import useApiQuery from 'lib/api/useApiQuery'; import { useMultichainContext } from 'lib/contexts/multichain'; -import { VERIFIED_CONTRACTS_COUNTERS, VERIFIED_CONTRACTS_COUNTERS_MICROSERVICE } from 'stubs/contract'; import StatsWidget from 'ui/shared/stats/StatsWidget'; const VerifiedContractsCounters = () => { diff --git a/ui/verifiedContracts/VerifiedContractsFilter.tsx b/client/slices/contract/pages/index/VerifiedContractsFilter.tsx similarity index 85% rename from ui/verifiedContracts/VerifiedContractsFilter.tsx rename to client/slices/contract/pages/index/VerifiedContractsFilter.tsx index 9f5271a596..98e8fb38cb 100644 --- a/ui/verifiedContracts/VerifiedContractsFilter.tsx +++ b/client/slices/contract/pages/index/VerifiedContractsFilter.tsx @@ -1,10 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { createListCollection } from '@chakra-ui/react'; import React from 'react'; -import type { VerifiedContractsFilter as TVerifiedContractsFilter } from 'types/api/contracts'; +import type { VerifiedContractsFilter as TVerifiedContractsFilter } from 'client/slices/contract/types/api'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import { formatLanguageName } from 'client/slices/contract/utils/language'; -import useApiQuery from 'lib/api/useApiQuery'; -import formatLanguageName from 'lib/contracts/formatLanguageName'; import type { SelectOption } from 'toolkit/chakra/select'; import PopoverFilterRadio from 'ui/shared/filters/PopoverFilterRadio'; diff --git a/ui/verifiedContracts/VerifiedContractsList.tsx b/client/slices/contract/pages/index/VerifiedContractsList.tsx similarity index 81% rename from ui/verifiedContracts/VerifiedContractsList.tsx rename to client/slices/contract/pages/index/VerifiedContractsList.tsx index 9c43623a2d..0a21c7bc0a 100644 --- a/ui/verifiedContracts/VerifiedContractsList.tsx +++ b/client/slices/contract/pages/index/VerifiedContractsList.tsx @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { VerifiedContract } from 'types/api/contracts'; +import type { VerifiedContract } from 'client/slices/contract/types/api'; import VerifiedContractsListItem from './VerifiedContractsListItem'; diff --git a/ui/verifiedContracts/VerifiedContractsListItem.tsx b/client/slices/contract/pages/index/VerifiedContractsListItem.tsx similarity index 89% rename from ui/verifiedContracts/VerifiedContractsListItem.tsx rename to client/slices/contract/pages/index/VerifiedContractsListItem.tsx index 356d174877..eaa1e56aa8 100644 --- a/ui/verifiedContracts/VerifiedContractsListItem.tsx +++ b/client/slices/contract/pages/index/VerifiedContractsListItem.tsx @@ -1,14 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import React from 'react'; -import type { VerifiedContract } from 'types/api/contracts'; +import type { VerifiedContract } from 'client/slices/contract/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import ContractCertifiedLabel from 'client/slices/contract/components/ContractCertifiedLabel'; +import { formatLanguageName } from 'client/slices/contract/utils/language'; +import { CONTRACT_LICENSES } from 'client/slices/contract/utils/licenses'; + +import { currencyUnits } from 'client/shared/chain/units'; -import formatLanguageName from 'lib/contracts/formatLanguageName'; -import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; -import { currencyUnits } from 'lib/units'; import { Skeleton } from 'toolkit/chakra/skeleton'; -import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import IconSvg from 'ui/shared/IconSvg'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; diff --git a/ui/verifiedContracts/VerifiedContractsTable.tsx b/client/slices/contract/pages/index/VerifiedContractsTable.tsx similarity index 89% rename from ui/verifiedContracts/VerifiedContractsTable.tsx rename to client/slices/contract/pages/index/VerifiedContractsTable.tsx index ef85771005..c897602fc7 100644 --- a/ui/verifiedContracts/VerifiedContractsTable.tsx +++ b/client/slices/contract/pages/index/VerifiedContractsTable.tsx @@ -1,15 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { VerifiedContract } from 'types/api/contracts'; -import type { VerifiedContractsSortingField, VerifiedContractsSortingValue } from 'types/api/verifiedContracts'; +import type { VerifiedContract, VerifiedContractsSortingField, VerifiedContractsSortingValue } from 'client/slices/contract/types/api'; + +import { SORT_SEQUENCE } from 'client/slices/contract/pages/index/sort'; + +import { currencyUnits } from 'client/shared/chain/units'; import { useMultichainContext } from 'lib/contexts/multichain'; -import { currencyUnits } from 'lib/units'; import { TableBody, TableColumnHeader, TableColumnHeaderSortable, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import getNextSortValue from 'ui/shared/sort/getNextSortValue'; import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle'; -import { SORT_SEQUENCE } from 'ui/verifiedContracts/utils'; import VerifiedContractsTableItem from './VerifiedContractsTableItem'; diff --git a/ui/verifiedContracts/VerifiedContractsTableItem.tsx b/client/slices/contract/pages/index/VerifiedContractsTableItem.tsx similarity index 91% rename from ui/verifiedContracts/VerifiedContractsTableItem.tsx rename to client/slices/contract/pages/index/VerifiedContractsTableItem.tsx index 44910dd488..10fd4b93e4 100644 --- a/ui/verifiedContracts/VerifiedContractsTableItem.tsx +++ b/client/slices/contract/pages/index/VerifiedContractsTableItem.tsx @@ -1,16 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; -import type { VerifiedContract } from 'types/api/contracts'; +import type { VerifiedContract } from 'client/slices/contract/types/api'; import type { ClusterChainConfig } from 'types/multichain'; -import formatLanguageName from 'lib/contracts/formatLanguageName'; -import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import ContractCertifiedLabel from 'client/slices/contract/components/ContractCertifiedLabel'; +import { formatLanguageName } from 'client/slices/contract/utils/language'; +import { CONTRACT_LICENSES } from 'client/slices/contract/utils/licenses'; + import { Skeleton } from 'toolkit/chakra/skeleton'; import { TableCell, TableRow } from 'toolkit/chakra/table'; import { Tooltip } from 'toolkit/chakra/tooltip'; -import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import ChainIcon from 'ui/shared/externalChains/ChainIcon'; import IconSvg from 'ui/shared/IconSvg'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; diff --git a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png b/client/slices/contract/pages/index/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png rename to client/slices/contract/pages/index/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png diff --git a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png b/client/slices/contract/pages/index/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png rename to client/slices/contract/pages/index/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png diff --git a/client/slices/contract/pages/index/sort.ts b/client/slices/contract/pages/index/sort.ts new file mode 100644 index 0000000000..dc4ef20c1a --- /dev/null +++ b/client/slices/contract/pages/index/sort.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { VerifiedContractsSortingValue, VerifiedContractsSortingField } from 'client/slices/contract/types/api'; + +import type { SelectOption } from 'toolkit/chakra/select'; + +export const SORT_OPTIONS: Array> = [ + { label: 'Default', value: 'default' }, + { label: 'Balance descending', value: 'balance-desc' }, + { label: 'Balance ascending', value: 'balance-asc' }, + { label: 'Txs count descending', value: 'transactions_count-desc' }, + { label: 'Txs count ascending', value: 'transactions_count-asc' }, +]; + +export const SORT_SEQUENCE: Record> = { + balance: [ 'balance-desc', 'balance-asc', 'default' ], + transactions_count: [ 'transactions_count-desc', 'transactions_count-asc', 'default' ], +}; diff --git a/client/slices/contract/stubs.ts b/client/slices/contract/stubs.ts new file mode 100644 index 0000000000..1918541765 --- /dev/null +++ b/client/slices/contract/stubs.ts @@ -0,0 +1,89 @@ +import type { SmartContract, VerifiedContract, VerifiedContractsCounters } from './types/api'; +import type * as stats from '@blockscout/stats-types'; + +import { ADDRESS_PARAMS } from 'client/slices/address/stubs/address-params'; + +import { CHAIN_STATS_COUNTER } from 'client/features/chain-stats/stubs/counters'; + +export const CONTRACT_CODE_UNVERIFIED = { + creation_bytecode: '0x60806040526e', + deployed_bytecode: '0x608060405233', + creation_status: 'success', +} as SmartContract; + +export const CONTRACT_CODE_VERIFIED = { + abi: [ + { + inputs: [], + name: 'symbol', + outputs: [ { internalType: 'string', name: '', type: 'string' } ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ { internalType: 'address', name: 'newOwner', type: 'address' } ], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + additional_sources: [], + can_be_visualized_via_sol2uml: true, + compiler_settings: { + compilationTarget: { + 'contracts/StubContract.sol': 'StubContract', + }, + evmVersion: 'london', + libraries: {}, + metadata: { + bytecodeHash: 'ipfs', + }, + optimizer: { + enabled: false, + runs: 200, + }, + remappings: [], + }, + compiler_version: 'v0.8.7+commit.e28d00a7', + constructor_args: '0000000000000000000000005c7bcd6e7de5423a257d81b442095a1a6ced35c5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + creation_bytecode: '0x6080604052348', + deployed_bytecode: '0x60806040', + evm_version: 'london', + external_libraries: [], + file_path: 'contracts/StubContract.sol', + is_verified: true, + name: 'StubContract', + optimization_enabled: false, + optimization_runs: 200, + source_code: 'source_code', + verified_at: '2023-02-21T14:39:16.906760Z', + license_type: 'mit', +} as unknown as SmartContract; + +export const VERIFIED_CONTRACT_INFO: VerifiedContract = { + address: { ...ADDRESS_PARAMS, name: 'StubContract' }, + coin_balance: '30319033612988277', + compiler_version: 'v0.8.17+commit.8df45f5f', + has_constructor_args: true, + language: 'solidity', + market_cap: null, + optimization_enabled: false, + transactions_count: 565058, + verified_at: '2023-04-10T13:16:33.884921Z', + license_type: 'mit', +}; + +export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { + smart_contracts: '123456789', + new_smart_contracts_24h: '12345', + verified_smart_contracts: '654321', + new_verified_smart_contracts_24h: '1234', +}; + +export const VERIFIED_CONTRACTS_COUNTERS_MICROSERVICE: stats.ContractsPageStats = { + total_contracts: CHAIN_STATS_COUNTER, + new_contracts_24h: CHAIN_STATS_COUNTER, + total_verified_contracts: CHAIN_STATS_COUNTER, + new_verified_contracts_24h: CHAIN_STATS_COUNTER, +}; diff --git a/client/slices/contract/types/api.ts b/client/slices/contract/types/api.ts new file mode 100644 index 0000000000..e697e66902 --- /dev/null +++ b/client/slices/contract/types/api.ts @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { Abi, AbiType } from 'abitype'; + +import type { AddressImplementation, AddressParam } from 'client/slices/address/types/api'; + +export type SmartContractMethodArgType = AbiType; +export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable'; + +export type SmartContractCreationStatus = 'success' | 'failed' | 'selfdestructed'; + +export type SmartContractLicenseType = +'none' | +'unlicense' | +'mit' | +'gnu_gpl_v2' | +'gnu_gpl_v3' | +'gnu_lgpl_v2_1' | +'gnu_lgpl_v3' | +'bsd_2_clause' | +'bsd_3_clause' | +'mpl_2_0' | +'osl_3_0' | +'apache_2_0' | +'gnu_agpl_v3' | +'bsl_1_1'; + +export type SmartContractProxyType = + 'eip1167' | + 'eip1967' | + 'eip1967_oz' | + 'eip1967_beacon' | + 'eip1822' | + 'eip930' | + 'eip2535' | + 'eip7702' | + 'erc7760' | + 'master_copy' | + 'basic_implementation' | + 'basic_get_implementation' | + 'comptroller' | + 'clone_with_immutable_arguments' | + 'resolved_delegate_proxy' | + 'unknown' | + null; + +export interface SmartContractConflictingImplementation { + proxy_type: NonNullable; + implementations: Array; +} + +export interface SmartContract { + deployed_bytecode: string | null; + creation_bytecode: string | null; + creation_status: SmartContractCreationStatus | null; + abi: Abi | null; + compiler_version: string | null; + evm_version: string | null; + optimization_enabled: boolean | null; + optimization_runs: number | string | null; + name: string | null; + verified_at: string | null; + is_blueprint: boolean | null; + is_verified: boolean | null; + is_verified_via_eth_bytecode_db: boolean | null; + is_changed_bytecode: boolean | null; + conflicting_implementations: Array | null; + + // sourcify info >>> + is_verified_via_sourcify: boolean | null; + is_fully_verified: boolean | null; + is_partially_verified: boolean | null; + sourcify_repo_url: string | null; + // <<<< + source_code: string | null; + constructor_args: string | null; + decoded_constructor_args: Array | null; + can_be_visualized_via_sol2uml: boolean | null; + file_path: string; + additional_sources: Array<{ file_path: string; source_code: string }>; + external_libraries: Array | null; + compiler_settings?: { + evmVersion?: string; + remappings?: Array; + }; + verified_twin_address_hash: string | null; + verified_twin_filecoin_robust_address?: string | null; + language: string | null; + license_type: SmartContractLicenseType | null; + certified?: boolean; + zk_compiler_version?: string; + github_repository_metadata?: { + commit?: string; + path_prefix?: string; + repository_url?: string; + }; + package_name?: string; +} + +export type SmartContractDecodedConstructorArg = [ + unknown, + { + internalType: SmartContractMethodArgType; + name: string; + type: SmartContractMethodArgType; + }, +]; + +export interface SmartContractExternalLibrary { + address_hash: string; + name: string; +} + +// VERIFICATION + +export type SmartContractVerificationMethodApi = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part' | +'vyper-code' | 'vyper-multi-part' | 'vyper-standard-input' | 'stylus-github-repository'; + +export interface SmartContractVerificationConfigRaw { + solidity_compiler_versions: Array; + solidity_evm_versions: Array; + verification_options: Array; + vyper_compiler_versions: Array; + stylus_compiler_versions?: Array; + vyper_evm_versions: Array; + is_rust_verifier_microservice_enabled: boolean; + license_types: Record; + zk_compiler_versions?: Array; + zk_optimization_modes?: Array; +} + +export type SmartContractVerificationResponse = { + status: 'error'; + errors: SmartContractVerificationError; +} | { + status: 'success'; +}; + +export interface SmartContractVerificationError { + contract_source_code?: Array; + files?: Array; + interfaces?: Array; + compiler_version?: Array; + constructor_arguments?: Array; + name?: Array; +} + +// VERIFIED CONTRACTS + +export type VerifiedContractsLanguage = 'solidity' | 'vyper' | 'yul' | 'scilla' | 'stylus_rust' | 'geas'; + +export interface VerifiedContract { + address: AddressParam; + certified?: boolean; + coin_balance: string; + compiler_version: string | null; + language: VerifiedContractsLanguage; + has_constructor_args: boolean; + optimization_enabled: boolean; + transactions_count: number | null; + verified_at: string; + market_cap: string | null; + license_type: SmartContractLicenseType | null; + zk_compiler_version?: string; +} + +export interface VerifiedContractsResponse { + items: Array; + next_page_params: { + items_count: string; + smart_contract_id: string; + } | null; +} + +export type VerifiedContractsFilter = VerifiedContractsLanguage; + +export interface VerifiedContractsFilters { + q: string | undefined; + filter: VerifiedContractsFilter | undefined; +} + +export type VerifiedContractsCounters = { + new_smart_contracts_24h: string; + new_verified_smart_contracts_24h: string; + smart_contracts: string; + verified_smart_contracts: string; +}; + +export interface VerifiedContractsSorting { + sort: 'balance' | 'transactions_count'; + order: 'asc' | 'desc'; +} + +export type VerifiedContractsSortingField = VerifiedContractsSorting['sort']; + +export type VerifiedContractsSortingValue = `${ VerifiedContractsSortingField }-${ VerifiedContractsSorting['order'] }` | 'default'; diff --git a/client/slices/contract/types/client.ts b/client/slices/contract/types/client.ts new file mode 100644 index 0000000000..7700132cf7 --- /dev/null +++ b/client/slices/contract/types/client.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { SmartContractLicenseType } from './api'; + +export interface ContractLicense { + type: SmartContractLicenseType; + url: string; + label: string; + title: string; +} diff --git a/client/slices/contract/types/config.ts b/client/slices/contract/types/config.ts new file mode 100644 index 0000000000..cca449d7ca --- /dev/null +++ b/client/slices/contract/types/config.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export const SMART_CONTRACT_EXTRA_VERIFICATION_METHODS = [ + 'solidity-hardhat' as const, + 'solidity-foundry' as const, +]; + +export type SmartContractVerificationMethodExtra = (typeof SMART_CONTRACT_EXTRA_VERIFICATION_METHODS)[number]; + +export interface ContractCodeIde { + title: string; + url: string; + icon_url: string; +} diff --git a/client/slices/contract/utils/language.ts b/client/slices/contract/utils/language.ts new file mode 100644 index 0000000000..39ad343b1c --- /dev/null +++ b/client/slices/contract/utils/language.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export function formatLanguageName(language: string) { + return language.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); +} diff --git a/lib/contracts/licenses.ts b/client/slices/contract/utils/licenses.ts similarity index 95% rename from lib/contracts/licenses.ts rename to client/slices/contract/utils/licenses.ts index 123149e294..b2b94364cd 100644 --- a/lib/contracts/licenses.ts +++ b/client/slices/contract/utils/licenses.ts @@ -1,4 +1,6 @@ -import type { ContractLicense } from 'types/client/contract'; +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { ContractLicense } from '../types/client'; export const CONTRACT_LICENSES: Array = [ { diff --git a/client/slices/contract/utils/tabs.ts b/client/slices/contract/utils/tabs.ts new file mode 100644 index 0000000000..128e41c7ca --- /dev/null +++ b/client/slices/contract/utils/tabs.ts @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export const CONTRACT_MAIN_TAB_IDS = [ + 'contract_code', + 'read_contract', + 'write_contract', + 'read_write_contract', + 'read_proxy', + 'write_proxy', + 'read_write_proxy', + 'read_custom_methods', + 'write_custom_methods', + 'read_write_custom_methods', + 'mud_system', +] as const; + +export const CONTRACT_DETAILS_TAB_IDS = [ + 'contract_source_code', + 'contract_compiler', + 'contract_abi', + 'contract_bytecode', +] as const; + +export const CONTRACT_TAB_IDS = (CONTRACT_MAIN_TAB_IDS as unknown as Array).concat(CONTRACT_DETAILS_TAB_IDS as unknown as Array); diff --git a/ui/shared/gas/GasInfoTooltip.pw.tsx b/client/slices/gas/components/GasInfoTooltip.pw.tsx similarity index 95% rename from ui/shared/gas/GasInfoTooltip.pw.tsx rename to client/slices/gas/components/GasInfoTooltip.pw.tsx index ed19047fd2..4946d08a04 100644 --- a/ui/shared/gas/GasInfoTooltip.pw.tsx +++ b/client/slices/gas/components/GasInfoTooltip.pw.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import type { GasPriceInfo } from 'types/api/stats'; +import type { GasPriceInfo } from 'client/slices/gas/types/api'; + +import * as statsMock from 'client/slices/home/mocks/stats'; -import * as statsMock from 'mocks/stats/index'; import { test, expect } from 'playwright/lib'; import GasInfoTooltip from './GasInfoTooltip'; diff --git a/ui/shared/gas/GasInfoTooltip.tsx b/client/slices/gas/components/GasInfoTooltip.tsx similarity index 95% rename from ui/shared/gas/GasInfoTooltip.tsx rename to client/slices/gas/components/GasInfoTooltip.tsx index e0246421f1..866aa5c5e6 100644 --- a/ui/shared/gas/GasInfoTooltip.tsx +++ b/client/slices/gas/components/GasInfoTooltip.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, @@ -5,7 +7,7 @@ import { } from '@chakra-ui/react'; import React from 'react'; -import type { HomeStats } from 'types/api/stats'; +import type { HomeStats } from 'client/slices/home/types/api'; import type { ExcludeUndefined } from 'types/utils'; import { route } from 'nextjs-routes'; diff --git a/ui/shared/gas/GasInfoTooltipRow.tsx b/client/slices/gas/components/GasInfoTooltipRow.tsx similarity index 82% rename from ui/shared/gas/GasInfoTooltipRow.tsx rename to client/slices/gas/components/GasInfoTooltipRow.tsx index 56b15b13c6..ead600ddb1 100644 --- a/ui/shared/gas/GasInfoTooltipRow.tsx +++ b/client/slices/gas/components/GasInfoTooltipRow.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, chakra } from '@chakra-ui/react'; import React from 'react'; -import type { GasPriceInfo } from 'types/api/stats'; +import type { GasPriceInfo } from 'client/slices/gas/types/api'; + +import GasPrice from 'client/slices/gas/components/GasPrice'; import { space } from 'toolkit/utils/htmlEntities'; -import GasPrice from 'ui/shared/gas/GasPrice'; interface Props { name: string; diff --git a/ui/shared/gas/GasInfoUpdateTimer.tsx b/client/slices/gas/components/GasInfoUpdateTimer.tsx similarity index 96% rename from ui/shared/gas/GasInfoUpdateTimer.tsx rename to client/slices/gas/components/GasInfoUpdateTimer.tsx index 8b81918e24..0bda4a43df 100644 --- a/ui/shared/gas/GasInfoUpdateTimer.tsx +++ b/client/slices/gas/components/GasInfoUpdateTimer.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import dayjs from 'lib/date/dayjs'; diff --git a/ui/shared/gas/GasPrice.tsx b/client/slices/gas/components/GasPrice.tsx similarity index 91% rename from ui/shared/gas/GasPrice.tsx rename to client/slices/gas/components/GasPrice.tsx index 5e32b09148..fdd2a45a8d 100644 --- a/ui/shared/gas/GasPrice.tsx +++ b/client/slices/gas/components/GasPrice.tsx @@ -1,13 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra } from '@chakra-ui/react'; import React from 'react'; -import type { GasPriceInfo } from 'types/api/stats'; -import type { GasUnit } from 'types/client/gasTracker'; +import type { GasPriceInfo } from 'client/slices/gas/types/api'; +import type { GasUnit } from 'client/slices/gas/types/config'; import config from 'configs/app'; import { useMultichainContext } from 'lib/contexts/multichain'; -import formatGasValue from './formatGasValue'; +import formatGasValue from '../utils/format-gas-value'; const UNITS_TO_API_FIELD_MAP: Record = { gwei: 'price', diff --git a/client/slices/gas/components/GasUsed.tsx b/client/slices/gas/components/GasUsed.tsx new file mode 100644 index 0000000000..41de6a3ac9 --- /dev/null +++ b/client/slices/gas/components/GasUsed.tsx @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { chakra } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import config from 'configs/app'; +import { Tooltip } from 'toolkit/chakra/tooltip'; +import TextSeparator from 'ui/shared/TextSeparator'; +import Utilization from 'ui/shared/Utilization/Utilization'; + +import GasUsedToTargetRatio from './GasUsedToTargetRatio'; + +const rollupFeature = config.features.rollup; + +interface Props { + className?: string; + gasUsed?: string; + gasLimit: string; + gasTarget?: number; + isLoading?: boolean; +} + +const GasUsed = ({ className, gasUsed, gasLimit, gasTarget, isLoading }: Props) => { + const hasGasUtilization = + gasUsed && gasUsed !== '0' && + (!rollupFeature.isEnabled || rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium'); + + if (!hasGasUtilization) { + return null; + } + + return ( + <> + + + + { gasTarget && ( + <> + + + + ) } + + ); +}; + +export default React.memo(chakra(GasUsed)); diff --git a/ui/shared/GasUsedToTargetRatio.tsx b/client/slices/gas/components/GasUsedToTargetRatio.tsx similarity index 91% rename from ui/shared/GasUsedToTargetRatio.tsx rename to client/slices/gas/components/GasUsedToTargetRatio.tsx index e258414809..885678c5f6 100644 --- a/ui/shared/GasUsedToTargetRatio.tsx +++ b/client/slices/gas/components/GasUsedToTargetRatio.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import { Skeleton } from 'toolkit/chakra/skeleton'; diff --git a/ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_all-data-1.png b/client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_all-data-1.png similarity index 100% rename from ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_all-data-1.png rename to client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_all-data-1.png diff --git a/ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_no-data-1.png b/client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_no-data-1.png similarity index 100% rename from ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_no-data-1.png rename to client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_no-data-1.png diff --git a/ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-with-data-1.png b/client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-with-data-1.png similarity index 100% rename from ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-with-data-1.png rename to client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-with-data-1.png diff --git a/ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-without-data-1.png b/client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-without-data-1.png similarity index 100% rename from ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-without-data-1.png rename to client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_one-unit-without-data-1.png diff --git a/ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_without-primary-unit-price-1.png b/client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_without-primary-unit-price-1.png similarity index 100% rename from ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_without-primary-unit-price-1.png rename to client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_without-primary-unit-price-1.png diff --git a/ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_without-secondary-unit-price-1.png b/client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_without-secondary-unit-price-1.png similarity index 100% rename from ui/shared/gas/__screenshots__/GasInfoTooltip.pw.tsx_default_without-secondary-unit-price-1.png rename to client/slices/gas/components/__screenshots__/GasInfoTooltip.pw.tsx_default_without-secondary-unit-price-1.png diff --git a/client/slices/gas/types/api.ts b/client/slices/gas/types/api.ts new file mode 100644 index 0000000000..7fb179fe79 --- /dev/null +++ b/client/slices/gas/types/api.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export type GasPrices = { + average: GasPriceInfo | null; + fast: GasPriceInfo | null; + slow: GasPriceInfo | null; +}; + +export interface GasPriceInfo { + fiat_price: string | null; + price: number | null; + time: number | null; + base_fee: number | null; + priority_fee: number | null; +} diff --git a/client/slices/gas/types/config.ts b/client/slices/gas/types/config.ts new file mode 100644 index 0000000000..4dff97c4e6 --- /dev/null +++ b/client/slices/gas/types/config.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export const GAS_UNITS = [ + 'usd', + 'gwei', +] as const; + +export type GasUnit = typeof GAS_UNITS[number]; diff --git a/client/slices/gas/utils/format-gas-value.ts b/client/slices/gas/utils/format-gas-value.ts new file mode 100644 index 0000000000..21b8491930 --- /dev/null +++ b/client/slices/gas/utils/format-gas-value.ts @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { GasPriceInfo } from 'client/slices/gas/types/api'; +import type { GasUnit } from 'client/slices/gas/types/config'; + +import { currencyUnits } from 'client/shared/chain/units'; + +export default function formatGasValue(data: GasPriceInfo, unit: GasUnit) { + switch (unit) { + case 'gwei': { + if (!data.price) { + return `N/A ${ currencyUnits.gwei }`; + } + + if (Number(data.price) < 0.1) { + return `< 0.1 ${ currencyUnits.gwei }`; + } + + return `${ Number(data.price).toLocaleString(undefined, { maximumFractionDigits: 1 }) } ${ currencyUnits.gwei }`; + } + + case 'usd': { + if (!data.fiat_price) { + return `$N/A`; + } + + if (Number(data.fiat_price) < 0.01) { + return `< $0.01`; + } + + return `$${ Number(data.fiat_price).toLocaleString(undefined, { maximumFractionDigits: 2 }) }`; + } + } +} diff --git a/client/slices/home/contexts/home-data-context.tsx b/client/slices/home/contexts/home-data-context.tsx new file mode 100644 index 0000000000..422e4f63cb --- /dev/null +++ b/client/slices/home/contexts/home-data-context.tsx @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +import useHomeBlocksData, { type HomeBlocksQueryResult } from 'client/slices/home/hooks/useHomeBlocksData'; + +import useHomeLatestBatchData, { type HomeLatestBatchQueryResult } from 'client/features/rollup/common/hooks/useHomeLatestBatchData'; + +type HomeDataContextValue = { + blocksQuery: HomeBlocksQueryResult | undefined; + latestBatchQuery: HomeLatestBatchQueryResult | undefined; +}; + +const HomeDataContext = React.createContext(null); + +export function HomeDataContextProvider({ children }: { children: React.ReactNode }) { + const blocksQuery = useHomeBlocksData(); + const latestBatchQuery = useHomeLatestBatchData(); + + const value = React.useMemo(() => ({ + blocksQuery, + latestBatchQuery, + }), [ blocksQuery, latestBatchQuery ]); + + return ( + + { children } + + ); +} + +export function useHomeDataContext(): HomeDataContextValue { + const ctx = React.useContext(HomeDataContext); + if (!ctx) { + throw new Error('useHomeDataContext must be used within HomeDataContextProvider'); + } + return ctx; +} diff --git a/client/slices/home/contexts/rpc-data-context.tsx b/client/slices/home/contexts/rpc-data-context.tsx new file mode 100644 index 0000000000..19bb108086 --- /dev/null +++ b/client/slices/home/contexts/rpc-data-context.tsx @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useQueries, useQuery } from '@tanstack/react-query'; +import React from 'react'; + +import type { Block } from 'client/slices/block/types/api'; +import type { Transaction } from 'client/slices/tx/types/api'; + +import formatBlockRpcData from 'client/slices/block/utils/format-rpc-data'; +import formatTxRpcData from 'client/slices/tx/utils/format-rpc-data'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; + +import { SECOND } from 'toolkit/utils/consts'; + +export type SubscriptionId = 'latest-blocks' | 'latest-txs' | 'stats-widgets'; + +interface HomeRpcDataContext { + blocks: Array; + txs: Array; + totalTxs: number; + isError: boolean; + isLoading: boolean; + isEnabled: boolean; + enable: (isEnabled: boolean, id: SubscriptionId) => void; + subscriptions: Array; +} + +export const HomeRpcDataContext = React.createContext(null); + +const ITEMS_LIMIT = 5; + +export function HomeRpcDataContextProvider({ children }: { children: React.ReactNode }) { + const [ blocks, setBlocks ] = React.useState>([]); + const [ txs, setTxs ] = React.useState>([]); + const [ totalTxs, setTotalTxs ] = React.useState(0); + const [ isLoading, setIsLoading ] = React.useState(true); + const [ isError, setIsError ] = React.useState(false); + const [ isEnabled, setIsEnabled ] = React.useState(false); + const [ subscriptions, setSubscriptions ] = React.useState>([]); + + const query = useQuery({ + queryKey: [ 'RPC', 'watch-blocks' ], + queryFn: async() => { + if (!publicClient) { + return null; + } + + return publicClient.watchBlocks({ + onBlock: (block) => { + setTxs((prevTxs) => { + try { + const newTxs = block.transactions.map((tx) => formatTxRpcData(tx, null, null, block)).filter(Boolean); + const nextTxs = prevTxs.length < ITEMS_LIMIT ? [ ...prevTxs, ...newTxs ].slice(0, ITEMS_LIMIT) : prevTxs; + + const totalTxs = prevTxs.length + newTxs.length; + setTotalTxs(totalTxs); + + return nextTxs; + } catch (_) { + setIsError(true); + return prevTxs; + } + }); + setBlocks((prev) => { + try { + return [ + formatBlockRpcData({ + ...block, + transactions: block.transactions.map((tx) => tx.hash), + }), + ...prev, + ].filter(Boolean).slice(0, ITEMS_LIMIT); + } catch (_) { + setIsError(true); + return prev; + } + }); + }, + onError: () => { + setIsError(true); + setIsLoading(false); + }, + pollingInterval: 5 * SECOND, + includeTransactions: true, + }); + }, + enabled: Boolean(publicClient) && isEnabled, + }); + + const receiptQueries = useQueries({ + queries: txs.map((tx) => ({ + queryKey: [ 'RPC', 'tx-receipt', { hash: tx.hash } ], + queryFn: async() => { + if (!publicClient) { + return null; + } + return publicClient.getTransactionReceipt({ hash: tx.hash as `0x${ string }` }); + }, + enabled: txs.length > 0 && !isError && Boolean(publicClient), + staleTime: Infinity, + })), + }); + + const areReceiptsLoading = txs.length === 0 || receiptQueries.some((query) => query.isPending); + + React.useEffect(() => { + if (!areReceiptsLoading) { + setTxs((prev) => { + return prev.map((tx) => { + const receipt = receiptQueries.find((query) => query.data?.transactionHash === tx.hash); + if (!receipt) { + return tx; + } + return { + ...tx, + status: receipt?.status === 'success' ? 'ok' : 'error', + }; + }); + }); + setIsLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ areReceiptsLoading ]); + + const unwatch = query.data; + const isQueryError = query.isError; + + React.useEffect(() => { + return () => { + unwatch?.(); + }; + }, [ unwatch ]); + + const enable = React.useCallback((isEnabled: boolean, id: SubscriptionId) => { + if (!publicClient) { + setIsError(true); + setIsLoading(false); + setIsEnabled(false); + return; + } + + setIsEnabled(isEnabled); + if (isEnabled) { + setIsLoading(true); + setSubscriptions((prev) => [ ...prev, id ]); + } else { + setIsLoading(false); + setSubscriptions((prev) => { + const next = prev.filter((subscription) => subscription !== id); + if (next.length === 0) { + unwatch?.(); + } + return next; + }); + } + }, [ unwatch ]); + + const value = React.useMemo(() => ({ + blocks, + txs, + totalTxs, + isError: isQueryError || isError, + isLoading, + isEnabled, + enable, + subscriptions, + }), [ blocks, txs, totalTxs, isQueryError, isError, isLoading, isEnabled, enable, subscriptions ]); + + return ( + + { children } + + ); +} + +export function useHomeRpcDataContext() { + const context = React.useContext(HomeRpcDataContext); + if (!context) { + throw new Error('useHomeRpcDataContext must be used within a HomeRpcDataContextProvider'); + } + return context; +} diff --git a/client/slices/home/hooks/useChartDataQuery.ts b/client/slices/home/hooks/useChartDataQuery.ts new file mode 100644 index 0000000000..6f790e9ff8 --- /dev/null +++ b/client/slices/home/hooks/useChartDataQuery.ts @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { ChainIndicatorId } from 'client/slices/home/types/config'; +import type { LineChartData } from 'toolkit/components/charts/line/types'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import { getChartData } from 'client/slices/home/utils/chart'; + +import config from 'configs/app'; + +const rollupFeature = config.features.rollup; +const isOptimisticRollup = rollupFeature.isEnabled && rollupFeature.type === 'optimistic'; +const isArbitrumRollup = rollupFeature.isEnabled && rollupFeature.type === 'arbitrum'; + +const isStatsFeatureEnabled = config.features.stats.isEnabled; + +export type UseFetchChartDataResult = { + isError: boolean; + isPending: boolean; + data: LineChartData; +}; + +export default function useChartDataQuery(indicatorId: ChainIndicatorId): UseFetchChartDataResult { + const statsDailyTxsQuery = useApiQuery('stats:pages_main', { + queryOptions: { + refetchOnMount: false, + enabled: isStatsFeatureEnabled && indicatorId === 'daily_txs', + select: (data) => data.daily_new_transactions?.chart.map((item) => ({ date: new Date(item.date), value: Number(item.value) })) || [], + }, + }); + + const statsDailyOperationalTxsQuery = useApiQuery('stats:pages_main', { + queryOptions: { + refetchOnMount: false, + enabled: isStatsFeatureEnabled && indicatorId === 'daily_operational_txs', + select: (data) => { + if (isArbitrumRollup) { + return data.daily_new_operational_transactions?.chart.map((item) => ({ date: new Date(item.date), value: Number(item.value) })) || []; + } else if (isOptimisticRollup) { + return data.op_stack_daily_new_operational_transactions?.chart.map((item) => ({ date: new Date(item.date), value: Number(item.value) })) || []; + } + return []; + }, + }, + }); + + const apiDailyTxsQuery = useApiQuery('general:stats_charts_txs', { + queryOptions: { + refetchOnMount: false, + enabled: !isStatsFeatureEnabled && indicatorId === 'daily_txs', + select: (data) => data.chart_data.map((item) => ({ date: new Date(item.date), value: item.transactions_count })), + }, + }); + + const coinPriceQuery = useApiQuery('general:stats_charts_market', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'coin_price', + select: (data) => data.chart_data.map((item) => ({ date: new Date(item.date), value: item.closing_price })), + }, + }); + + const secondaryCoinPriceQuery = useApiQuery('general:stats_charts_secondary_coin_price', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'secondary_coin_price', + select: (data) => data.chart_data.map((item) => ({ date: new Date(item.date), value: item.closing_price })), + }, + }); + + const marketCapQuery = useApiQuery('general:stats_charts_market', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'market_cap', + select: (data) => data.chart_data.map((item) => ( + { + date: new Date(item.date), + value: (() => { + if (item.market_cap !== undefined) { + return item.market_cap; + } + + if (item.closing_price === null) { + return null; + } + + return Number(item.closing_price) * Number(data.available_supply); + })(), + })), + }, + }); + + const tvlQuery = useApiQuery('general:stats_charts_market', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'tvl', + select: (data) => data.chart_data.map((item) => ( + { + date: new Date(item.date), + value: item.tvl !== undefined ? item.tvl : 0, + })), + }, + }); + + switch (indicatorId) { + case 'daily_txs': { + const query = isStatsFeatureEnabled ? statsDailyTxsQuery : apiDailyTxsQuery; + return { + data: getChartData(indicatorId, query.data || []), + isError: query.isError, + isPending: query.isPending, + }; + } + case 'daily_operational_txs': { + return { + data: getChartData(indicatorId, statsDailyOperationalTxsQuery.data || []), + isError: statsDailyOperationalTxsQuery.isError, + isPending: statsDailyOperationalTxsQuery.isPending, + }; + } + case 'coin_price': { + return { + data: getChartData(indicatorId, coinPriceQuery.data || []), + isError: coinPriceQuery.isError, + isPending: coinPriceQuery.isPending, + }; + } + case 'secondary_coin_price': { + return { + data: getChartData(indicatorId, secondaryCoinPriceQuery.data || []), + isError: secondaryCoinPriceQuery.isError, + isPending: secondaryCoinPriceQuery.isPending, + }; + } + case 'market_cap': { + return { + data: getChartData(indicatorId, marketCapQuery.data || []), + isError: marketCapQuery.isError, + isPending: marketCapQuery.isPending, + }; + } + case 'tvl': { + return { + data: getChartData(indicatorId, tvlQuery.data || []), + isError: tvlQuery.isError, + isPending: tvlQuery.isPending, + }; + } + } +} diff --git a/client/slices/home/hooks/useHomeBlocksData.ts b/client/slices/home/hooks/useHomeBlocksData.ts new file mode 100644 index 0000000000..f153beb4a3 --- /dev/null +++ b/client/slices/home/hooks/useHomeBlocksData.ts @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useQueryClient } from '@tanstack/react-query'; +import type { UseQueryResult } from '@tanstack/react-query'; +import React from 'react'; + +import type { SocketMessage } from 'client/api/socket/types'; +import type { Block } from 'client/slices/block/types/api'; + +import useApiQuery, { getResourceKey } from 'client/api/hooks/useApiQuery'; +import type { ResourceError, ResourcePayload } from 'client/api/resources'; +import useSocketChannel from 'client/api/socket/useSocketChannel'; +import useSocketMessage from 'client/api/socket/useSocketMessage'; + +import { BLOCK } from 'client/slices/block/stubs/block'; + +import config from 'configs/app'; + +/** Max blocks kept in React Query cache for `general:homepage_blocks` (fetch + socket). */ +const HOME_BLOCKS_QUERY_LIMIT = 5; + +const isHomepageBlocksDataEnabled = (() => { + const rollupFeature = config.features.rollup; + const isLatestBlocksReplacedByBatches = rollupFeature.isEnabled && + !rollupFeature.homepage.showLatestBlocks && + [ 'arbitrum' ].includes(rollupFeature.type); + + return !isLatestBlocksReplacedByBatches || config.UI.homepage.stats.includes('total_blocks'); +})(); + +export type HomeBlocksQueryResult = UseQueryResult< + ResourcePayload<'general:homepage_blocks'>, + ResourceError +>; + +export default function useHomeBlocksData(): HomeBlocksQueryResult | undefined { + const queryClient = useQueryClient(); + + const blocksQuery = useApiQuery('general:homepage_blocks', { + queryOptions: { + enabled: isHomepageBlocksDataEnabled, + placeholderData: Array(HOME_BLOCKS_QUERY_LIMIT).fill(BLOCK), + }, + }); + + const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { + queryClient.setQueryData(getResourceKey('general:homepage_blocks'), (prevData: Array | undefined) => { + const newData = prevData ? [ ...prevData ] : []; + + if (newData.some((block) => block.height === payload.block.height)) { + return newData; + } + + return [ payload.block, ...newData ].sort((b1, b2) => b2.height - b1.height).slice(0, HOME_BLOCKS_QUERY_LIMIT); + }); + }, [ queryClient ]); + + const channel = useSocketChannel({ + topic: 'blocks:new_block', + isDisabled: !isHomepageBlocksDataEnabled || blocksQuery.isPlaceholderData || blocksQuery.isError, + }); + useSocketMessage({ + channel, + event: 'new_block', + handler: handleNewBlockMessage, + }); + + return isHomepageBlocksDataEnabled ? blocksQuery : undefined; +} diff --git a/mocks/stats/daily_txs.ts b/client/slices/home/mocks/charts.ts similarity index 100% rename from mocks/stats/daily_txs.ts rename to client/slices/home/mocks/charts.ts diff --git a/client/slices/home/mocks/stats.ts b/client/slices/home/mocks/stats.ts new file mode 100644 index 0000000000..34094f355b --- /dev/null +++ b/client/slices/home/mocks/stats.ts @@ -0,0 +1,84 @@ +import { mapValues } from 'es-toolkit'; + +import type { HomeStats } from 'client/slices/home/types/api'; + +export const base: HomeStats = { + average_block_time: 6212.0, + coin_price: '0.00199678', + coin_price_change_percentage: -7.42, + coin_image: 'http://localhost:3100/utia.jpg', + gas_prices: { + average: { + fiat_price: '1.39', + price: 23.75, + time: 12030.25, + base_fee: 2.22222, + priority_fee: 12.424242, + }, + fast: { + fiat_price: '1.74', + price: 29.72, + time: 8763.25, + base_fee: 4.44444, + priority_fee: 22.242424, + }, + slow: { + fiat_price: '1.35', + price: 23.04, + time: 20100.25, + base_fee: 1.11111, + priority_fee: 7.8909, + }, + }, + gas_price_updated_at: '2022-11-11T11:09:49.051171Z', + gas_prices_update_in: 300000, + gas_used_today: '4108680603', + market_cap: '330809.96443288102524', + network_utilization_percentage: 1.55372064, + static_gas_price: '10', + total_addresses: '19667249', + total_blocks: '30215608', + total_gas_used: '0', + total_transactions: '82258122', + transactions_today: '26815', + tvl: '1767425.102766552', +}; + +export const withBtcLocked: HomeStats = { + ...base, + rootstock_locked_btc: '3337493406696977561374', +}; + +export const withoutFiatPrices: HomeStats = { + ...base, + gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null) : null, +}; + +export const withoutGweiPrices: HomeStats = { + ...base, + gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null) : null, +}; + +export const withoutBothPrices: HomeStats = { + ...base, + gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null) : null, +}; + +export const withoutGasInfo: HomeStats = { + ...base, + gas_prices: null, +}; + +export const withSecondaryCoin: HomeStats = { + ...base, + secondary_coin_price: '3.398', + secondary_coin_image: 'http://localhost:3100/secondary_utia.jpg', +}; + +export const noChartData: HomeStats = { + ...base, + transactions_today: null, + coin_price: null, + market_cap: null, + tvl: null, +}; diff --git a/ui/home/HeroBanner.pw.tsx b/client/slices/home/pages/index/HeroBanner.pw.tsx similarity index 77% rename from ui/home/HeroBanner.pw.tsx rename to client/slices/home/pages/index/HeroBanner.pw.tsx index bb59828ef5..a985c5430a 100644 --- a/ui/home/HeroBanner.pw.tsx +++ b/client/slices/home/pages/index/HeroBanner.pw.tsx @@ -1,9 +1,10 @@ import type { BrowserContext } from '@playwright/test'; import React from 'react'; +import * as profileMock from 'client/features/account/mocks/user-profile'; + import * as rewardsBalanceMock from 'mocks/rewards/balance'; import * as dailyRewardMock from 'mocks/rewards/dailyReward'; -import * as profileMock from 'mocks/user/profile'; import { contextWithAuth } from 'playwright/fixtures/auth'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { contextWithRewards } from 'playwright/fixtures/rewards'; @@ -24,7 +25,7 @@ authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiRes await mockEnvs([ ...ENVS_MAP.rewardsService, // eslint-disable-next-line max-len - [ 'NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG', `{"background":["lightpink","no-repeat center/cover url(${ IMAGE_URL })"],"text_color":["deepskyblue","white"],"border":["3px solid green","3px dashed yellow"],"button":{"_default":{"background":["deeppink"],"text_color":["white"]},"_selected":{"background":["lime"]}}}` ], + [ 'NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG', `{"text": "Duck migration observer","background":["lightpink","no-repeat center/cover url(${ IMAGE_URL })"],"text_color":["deepskyblue","white"],"border":["3px solid green","3px dashed yellow"],"button":{"_default":{"background":["deeppink"],"text_color":["white"]},"_selected":{"background":["lime"]}}}` ], ]); await page.route(IMAGE_URL, (route) => { diff --git a/ui/home/HeroBanner.tsx b/client/slices/home/pages/index/HeroBanner.tsx similarity index 79% rename from ui/home/HeroBanner.tsx rename to client/slices/home/pages/index/HeroBanner.tsx index 58f761eb33..d7c8d1f2e3 100644 --- a/ui/home/HeroBanner.tsx +++ b/client/slices/home/pages/index/HeroBanner.tsx @@ -1,15 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + // we use custom heading size for hero banner // eslint-disable-next-line no-restricted-imports import { Box, Flex, Heading } from '@chakra-ui/react'; import React from 'react'; +import SearchBar from 'client/slices/search/components/search-bar/SearchBarDesktop'; +import SearchBarMobile from 'client/slices/search/components/search-bar/SearchBarMobile'; + +import UserProfileDesktop from 'client/features/account/components/user-profile/UserProfileDesktop'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import config from 'configs/app'; -import useIsMobile from 'lib/hooks/useIsMobile'; import RewardsButton from 'ui/rewards/RewardsButton'; import AdBanner from 'ui/shared/ad/AdBanner'; -import SearchBar from 'ui/snippets/searchBar/SearchBarDesktop'; -import SearchBarMobile from 'ui/snippets/searchBar/SearchBarMobile'; -import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop'; export const BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)'; @@ -49,6 +54,16 @@ const HeroBanner = () => { config.UI.homepage.heroBanner?.border?.[1] || config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT, }; + const text = (() => { + if (config.UI.homepage.heroBanner?.text) { + return config.UI.homepage.heroBanner.text; + } + + return config.meta.seo.enhancedDataEnabled ? + `${ config.chain.name } blockchain explorer` : + `${ config.chain.name } explorer`; + })(); + return ( { fontWeight={{ base: 500, lg: 700 }} color={ textColor } > - { - config.meta.seo.enhancedDataEnabled ? - `${ config.chain.name } blockchain explorer` : - `${ config.chain.name } explorer` - } + { text } { config.UI.navigation.layout === 'vertical' && ( diff --git a/ui/pages/Home.pw.tsx b/client/slices/home/pages/index/Home.pw.tsx similarity index 94% rename from ui/pages/Home.pw.tsx rename to client/slices/home/pages/index/Home.pw.tsx index a62b6cc675..fe310a456f 100644 --- a/ui/pages/Home.pw.tsx +++ b/client/slices/home/pages/index/Home.pw.tsx @@ -2,12 +2,14 @@ import type { Locator } from '@playwright/test'; import React from 'react'; import type { PublicRpcSchema, RpcTransaction } from 'viem'; +import * as blockMock from 'client/slices/block/mocks/block'; +import * as dailyTxsMock from 'client/slices/home/mocks/charts'; +import * as statsMock from 'client/slices/home/mocks/stats'; +import * as txMock from 'client/slices/tx/mocks/tx'; + +import * as statsMainMock from 'client/features/chain-stats/mocks/home'; + import config from 'configs/app'; -import * as blockMock from 'mocks/blocks/block'; -import * as dailyTxsMock from 'mocks/stats/daily_txs'; -import * as statsMock from 'mocks/stats/index'; -import * as statsMainMock from 'mocks/stats/main'; -import * as txMock from 'mocks/txs/tx'; import { test, expect, devices } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; diff --git a/client/slices/home/pages/index/Home.tsx b/client/slices/home/pages/index/Home.tsx new file mode 100644 index 0000000000..ff5ad02058 --- /dev/null +++ b/client/slices/home/pages/index/Home.tsx @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Box, Flex } from '@chakra-ui/react'; +import React from 'react'; + +import { HomeDataContextProvider } from 'client/slices/home/contexts/home-data-context'; +import { HomeRpcDataContextProvider } from 'client/slices/home/contexts/rpc-data-context'; + +import LatestArbitrumL2Batches from 'client/features/rollup/arbitrum/pages/home/LatestArbitrumL2Batches'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; + +import config from 'configs/app'; +import AdBanner from 'ui/shared/ad/AdBanner'; + +import LatestBlocks from './blocks/LatestBlocks'; +import ChainIndicators from './charts/ChainIndicators'; +import HeroBanner from './HeroBanner'; +import Highlights from './highlights/Highlights'; +import Stats from './stats/Stats'; +import Transactions from './txs/Transactions'; + +const rollupFeature = config.features.rollup; + +const Home = () => { + const isMobile = useIsMobile(); + + const leftWidget = (() => { + if (rollupFeature.isEnabled && !rollupFeature.homepage.showLatestBlocks) { + switch (rollupFeature.type) { + case 'arbitrum': + return ; + } + } + + return ; + })(); + + return ( + + + + + + + + + { !isMobile && config.UI.homepage.highlights && } + { isMobile && } + + { leftWidget } + + + + + + + + ); +}; + +export default Home; diff --git a/client/slices/home/pages/index/__screenshots__/HeroBanner.pw.tsx_dark-color-mode_customization-dark-mode-1.png b/client/slices/home/pages/index/__screenshots__/HeroBanner.pw.tsx_dark-color-mode_customization-dark-mode-1.png new file mode 100644 index 0000000000..15474c9d5a Binary files /dev/null and b/client/slices/home/pages/index/__screenshots__/HeroBanner.pw.tsx_dark-color-mode_customization-dark-mode-1.png differ diff --git a/client/slices/home/pages/index/__screenshots__/HeroBanner.pw.tsx_default_customization-dark-mode-1.png b/client/slices/home/pages/index/__screenshots__/HeroBanner.pw.tsx_default_customization-dark-mode-1.png new file mode 100644 index 0000000000..588d704a7e Binary files /dev/null and b/client/slices/home/pages/index/__screenshots__/HeroBanner.pw.tsx_default_customization-dark-mode-1.png differ diff --git a/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-base-view-1.png b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-base-view-1.png new file mode 100644 index 0000000000..157d760eb9 Binary files /dev/null and b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-base-view-1.png differ diff --git a/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_degradation-view-1.png b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_degradation-view-1.png new file mode 100644 index 0000000000..1752f475c2 Binary files /dev/null and b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_degradation-view-1.png differ diff --git a/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_error-view-1.png b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_error-view-1.png new file mode 100644 index 0000000000..0972193fd1 Binary files /dev/null and b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_error-view-1.png differ diff --git a/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..d477482e18 Binary files /dev/null and b/client/slices/home/pages/index/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png differ diff --git a/client/slices/home/pages/index/blocks/LatestBlocks.pw.tsx b/client/slices/home/pages/index/blocks/LatestBlocks.pw.tsx new file mode 100644 index 0000000000..751e1f9f5a --- /dev/null +++ b/client/slices/home/pages/index/blocks/LatestBlocks.pw.tsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import * as blockMock from 'client/slices/block/mocks/block'; +import { HomeDataContextProvider } from 'client/slices/home/contexts/home-data-context'; +import { HomeRpcDataContextProvider } from 'client/slices/home/contexts/rpc-data-context'; +import * as statsMock from 'client/slices/home/mocks/stats'; + +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; + +import LatestBlocks from './LatestBlocks'; + +test('default view +@mobile +@dark-mode', async({ render, mockApiResponse }) => { + await mockApiResponse('general:stats', statsMock.base); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + const component = await render( + + + + + , + ); + await expect(component).toHaveScreenshot(); +}); + +test('L2 view', async({ render, mockEnvs, mockApiResponse }) => { + await mockEnvs(ENVS_MAP.optimisticRollup); + await mockApiResponse('general:stats', statsMock.base); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + const component = await render( + + + + + , + ); + await expect(component).toHaveScreenshot(); +}); + +test('no reward view', async({ render, mockEnvs, mockApiResponse }) => { + await mockEnvs(ENVS_MAP.blockHiddenFields); + await mockApiResponse('general:stats', statsMock.base); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + const component = await render( + + + + + , + ); + await expect(component).toHaveScreenshot(); +}); + +test('with long block height', async({ render, mockApiResponse }) => { + await mockApiResponse('general:stats', statsMock.base); + await mockApiResponse('general:homepage_blocks', [ { ...blockMock.base, height: 123456789012345 } ]); + const component = await render( + + + + + , + ); + await expect(component).toHaveScreenshot(); +}); + +test.describe('socket', () => { + test.describe.configure({ mode: 'serial' }); + test('new item', async({ render, mockApiResponse, createSocket }) => { + await mockApiResponse('general:stats', statsMock.base); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + const component = await render( + + + + + , + undefined, + { withSocket: true }, + ); + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'blocks:new_block'); + socketServer.sendMessage(socket, channel, 'new_block', { + average_block_time: '6212.0', + block: { + ...blockMock.base, + height: blockMock.base.height + 1, + timestamp: '2022-11-11T11:59:58Z', + }, + }); + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/client/slices/home/pages/index/blocks/LatestBlocks.tsx b/client/slices/home/pages/index/blocks/LatestBlocks.tsx new file mode 100644 index 0000000000..96905e8ebf --- /dev/null +++ b/client/slices/home/pages/index/blocks/LatestBlocks.tsx @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { chakra, Box, Flex, Text, VStack, HStack } from '@chakra-ui/react'; +import { upperFirst } from 'es-toolkit'; +import React from 'react'; + +import type { Block } from 'client/slices/block/types/api'; + +import { route } from 'nextjs-routes'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import { useHomeDataContext } from 'client/slices/home/contexts/home-data-context'; +import { useHomeRpcDataContext } from 'client/slices/home/contexts/rpc-data-context'; +import { HOMEPAGE_STATS } from 'client/slices/home/stubs'; + +import getChainUtilizationParams from 'client/shared/chain/get-chain-utilization-params'; +import useIsMobile from 'client/shared/hooks/useIsMobile'; +import useInitialList from 'client/shared/lists/useInitialList'; + +import config from 'configs/app'; +import { Heading } from 'toolkit/chakra/heading'; +import { Link } from 'toolkit/chakra/link'; +import { Skeleton } from 'toolkit/chakra/skeleton'; +import { Tooltip } from 'toolkit/chakra/tooltip'; +import { nbsp } from 'toolkit/utils/htmlEntities'; +import FallbackRpcIcon from 'ui/shared/fallbacks/FallbackRpcIcon'; + +import LatestBlocksDegraded from './LatestBlocksDegraded'; +import LatestBlocksItem from './LatestBlocksItem'; + +const LatestBlocks = () => { + const isMobile = useIsMobile(); + // const blocksMaxCount = isMobile ? 2 : 3; + let blocksMaxCount: number; + if (config.features.rollup.isEnabled || config.UI.views.block.hiddenFields?.total_reward) { + blocksMaxCount = isMobile ? 4 : 5; + } else { + blocksMaxCount = isMobile ? 2 : 3; + } + const { blocksQuery } = useHomeDataContext(); + const initialList = useInitialList({ + data: blocksQuery?.data ?? [], + idFn: (block) => block.height, + enabled: Boolean(blocksQuery && !blocksQuery.isPlaceholderData), + }); + + const statsQueryResult = useApiQuery('general:stats', { + queryOptions: { + refetchOnMount: false, + placeholderData: HOMEPAGE_STATS, + }, + }); + + const rpcDataContext = useHomeRpcDataContext(); + const isRpcData = rpcDataContext.isEnabled && !rpcDataContext.isLoading && !rpcDataContext.isError && rpcDataContext.subscriptions.includes('latest-blocks'); + + const content = (() => { + if (blocksQuery?.isError) { + return ; + } + if (blocksQuery?.data && blocksQuery.data.length > 0) { + const dataToShow = blocksQuery.data.slice(0, blocksMaxCount); + + return ( + <> + + { dataToShow.map(((block, index) => ( + + ))) } + + + View all blocks + + + ); + } + return No latest blocks found.; + })(); + + const networkUtilization = getChainUtilizationParams(statsQueryResult.data?.network_utilization_percentage ?? 0); + + return ( + + + Latest blocks + { isRpcData && } + + { statsQueryResult.data?.network_utilization_percentage !== undefined && ( + + + Network utilization:{ nbsp } + + + + { statsQueryResult.data?.network_utilization_percentage.toFixed(2) }% + + + + ) } + { statsQueryResult.data?.celo && ( + + Current epoch: + #{ statsQueryResult.data.celo.epoch_number } + + ) } + + { content } + + + ); +}; + +export default LatestBlocks; diff --git a/ui/home/fallbacks/LatestBlocksDegraded.tsx b/client/slices/home/pages/index/blocks/LatestBlocksDegraded.tsx similarity index 77% rename from ui/home/fallbacks/LatestBlocksDegraded.tsx rename to client/slices/home/pages/index/blocks/LatestBlocksDegraded.tsx index 05362a2032..e841fb7377 100644 --- a/ui/home/fallbacks/LatestBlocksDegraded.tsx +++ b/client/slices/home/pages/index/blocks/LatestBlocksDegraded.tsx @@ -1,18 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, VStack } from '@chakra-ui/react'; import React from 'react'; -import type { Block } from 'types/api/block'; +import type { Block } from 'client/slices/block/types/api'; import { route } from 'nextjs-routes'; -import useInitialList from 'lib/hooks/useInitialList'; -import { publicClient } from 'lib/web3/client'; -import { BLOCK } from 'stubs/block'; +import { BLOCK } from 'client/slices/block/stubs/block'; +import { useHomeRpcDataContext } from 'client/slices/home/contexts/rpc-data-context'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; + +import useInitialList from 'client/shared/lists/useInitialList'; + import { Link } from 'toolkit/chakra/link'; -import LatestBlocksItem from '../LatestBlocksItem'; import LatestBlocksFallback from './LatestBlocksFallback'; -import { useHomeRpcDataContext } from './rpcDataContext'; +import LatestBlocksItem from './LatestBlocksItem'; interface Props { maxNum: number; diff --git a/ui/home/fallbacks/LatestBlocksFallback.tsx b/client/slices/home/pages/index/blocks/LatestBlocksFallback.tsx similarity index 95% rename from ui/home/fallbacks/LatestBlocksFallback.tsx rename to client/slices/home/pages/index/blocks/LatestBlocksFallback.tsx index 3e50c6e244..fa474628aa 100644 --- a/ui/home/fallbacks/LatestBlocksFallback.tsx +++ b/client/slices/home/pages/index/blocks/LatestBlocksFallback.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, VStack } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/home/LatestBlocksItem.tsx b/client/slices/home/pages/index/blocks/LatestBlocksItem.tsx similarity index 82% rename from ui/home/LatestBlocksItem.tsx rename to client/slices/home/pages/index/blocks/LatestBlocksItem.tsx index b47c766261..c31d4a9ce7 100644 --- a/ui/home/LatestBlocksItem.tsx +++ b/client/slices/home/pages/index/blocks/LatestBlocksItem.tsx @@ -1,18 +1,22 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, Grid } from '@chakra-ui/react'; import { capitalize } from 'es-toolkit'; import React from 'react'; -import type { Block } from 'types/api/block'; +import type { Block } from 'client/slices/block/types/api'; + +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import getBlockTotalReward from 'client/slices/block/utils/get-block-total-reward'; + +import getChainValidatorTitle from 'client/shared/chain/get-chain-validator-title'; +import { currencyUnits } from 'client/shared/chain/units'; import config from 'configs/app'; -import getBlockTotalReward from 'lib/block/getBlockTotalReward'; -import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; -import { currencyUnits } from 'lib/units'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; import { thinsp } from 'toolkit/utils/htmlEntities'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import IconSvg from 'ui/shared/IconSvg'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; import SimpleValue from 'ui/shared/value/SimpleValue'; @@ -77,7 +81,7 @@ const LatestBlocksItem = ({ block, isLoading, animation }: Props) => { { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.miner && ( <> - { capitalize(getNetworkValidatorTitle()) } + { capitalize(getChainValidatorTitle()) } { }; }, [ ]); - const { rect, ref, axes, innerWidth, innerHeight, chartMargin } = useTimeChartController({ + const { rect, ref, axes, innerWidth, innerHeight, chartMargin } = useLineChartController({ data, margin: CHART_MARGIN, axesConfig, @@ -33,14 +35,14 @@ const ChainIndicatorChartContent = ({ data }: Props) => { return ( - - { strokeWidth={ 3 } animation="left" /> - - + { yScale={ axes.y.scale } data={ data } /> - + ); diff --git a/ui/home/indicators/ChainIndicatorItem.tsx b/client/slices/home/pages/index/charts/ChainIndicatorItem.tsx similarity index 92% rename from ui/home/indicators/ChainIndicatorItem.tsx rename to client/slices/home/pages/index/charts/ChainIndicatorItem.tsx index 48355d1f6b..1f538e5321 100644 --- a/ui/home/indicators/ChainIndicatorItem.tsx +++ b/client/slices/home/pages/index/charts/ChainIndicatorItem.tsx @@ -1,11 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Text, Flex, Box } from '@chakra-ui/react'; import React from 'react'; -import type { TChainIndicator } from './types'; -import type { ChainIndicatorId } from 'types/homepage'; +import type { TChainIndicator } from 'client/slices/home/types/client'; +import type { ChainIndicatorId } from 'client/slices/home/types/config'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { mdash } from 'toolkit/utils/htmlEntities'; + interface Props { indicator: TChainIndicator; isSelected: boolean; diff --git a/ui/home/indicators/ChainIndicators.pw.tsx b/client/slices/home/pages/index/charts/ChainIndicators.pw.tsx similarity index 95% rename from ui/home/indicators/ChainIndicators.pw.tsx rename to client/slices/home/pages/index/charts/ChainIndicators.pw.tsx index f577898217..84b91ce65b 100644 --- a/ui/home/indicators/ChainIndicators.pw.tsx +++ b/client/slices/home/pages/index/charts/ChainIndicators.pw.tsx @@ -1,8 +1,9 @@ import type { Locator } from '@playwright/test'; import React from 'react'; -import * as dailyTxsMock from 'mocks/stats/daily_txs'; -import * as statsMock from 'mocks/stats/index'; +import * as dailyTxsMock from 'client/slices/home/mocks/charts'; +import * as statsMock from 'client/slices/home/mocks/stats'; + import { test, expect } from 'playwright/lib'; import ChainIndicators from './ChainIndicators'; diff --git a/ui/home/indicators/ChainIndicators.tsx b/client/slices/home/pages/index/charts/ChainIndicators.tsx similarity index 93% rename from ui/home/indicators/ChainIndicators.tsx rename to client/slices/home/pages/index/charts/ChainIndicators.tsx index a925986368..645e975ebf 100644 --- a/ui/home/indicators/ChainIndicators.tsx +++ b/client/slices/home/pages/index/charts/ChainIndicators.tsx @@ -1,18 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { TChainIndicator } from './types'; +import type { TChainIndicator } from 'client/slices/home/types/client'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import useChartDataQuery from 'client/slices/home/hooks/useChartDataQuery'; +import { HOMEPAGE_STATS } from 'client/slices/home/stubs'; +import { isIndicatorEnabled, sortIndicators } from 'client/slices/home/utils/indicators'; +import NativeTokenIcon from 'client/slices/token/components/icon/TokenIconNative'; + +import { HOMEPAGE_STATS_MICROSERVICE } from 'client/features/chain-stats/stubs/home'; import config from 'configs/app'; -import useApiQuery from 'lib/api/useApiQuery'; -import { HOMEPAGE_STATS, HOMEPAGE_STATS_MICROSERVICE } from 'stubs/stats'; import IconSvg from 'ui/shared/IconSvg'; -import NativeTokenIcon from 'ui/shared/NativeTokenIcon'; import ChainIndicatorsChart from './ChainIndicatorsChart'; import ChainIndicatorsContainer from './ChainIndicatorsContainer'; import ChainIndicatorsList from './ChainIndicatorsList'; -import useChartDataQuery from './useChartDataQuery'; -import { isIndicatorEnabled, sortIndicators } from './utils/indicators'; const isStatsFeatureEnabled = config.features.stats.isEnabled; const rollupFeature = config.features.rollup; diff --git a/ui/home/indicators/ChainIndicatorsChart.tsx b/client/slices/home/pages/index/charts/ChainIndicatorsChart.tsx similarity index 94% rename from ui/home/indicators/ChainIndicatorsChart.tsx rename to client/slices/home/pages/index/charts/ChainIndicatorsChart.tsx index ecf9cfdd55..0e09e2a3af 100644 --- a/ui/home/indicators/ChainIndicatorsChart.tsx +++ b/client/slices/home/pages/index/charts/ChainIndicatorsChart.tsx @@ -1,6 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Text } from '@chakra-ui/react'; import React from 'react'; +import type { UseFetchChartDataResult } from 'client/slices/home/hooks/useChartDataQuery'; + import { Skeleton } from 'toolkit/chakra/skeleton'; import { Hint } from 'toolkit/components/Hint/Hint'; import { mdash } from 'toolkit/utils/htmlEntities'; @@ -8,7 +12,6 @@ import FallbackChart from 'ui/shared/fallbacks/FallbackChart'; import IconSvg from 'ui/shared/IconSvg'; import ChainIndicatorChartContainer from './ChainIndicatorChartContainer'; -import type { UseFetchChartDataResult } from './useChartDataQuery'; interface Props { isLoading: boolean; diff --git a/ui/home/indicators/ChainIndicatorsContainer.tsx b/client/slices/home/pages/index/charts/ChainIndicatorsContainer.tsx similarity index 91% rename from ui/home/indicators/ChainIndicatorsContainer.tsx rename to client/slices/home/pages/index/charts/ChainIndicatorsContainer.tsx index bbd8dcba9e..9b0706f51e 100644 --- a/ui/home/indicators/ChainIndicatorsContainer.tsx +++ b/client/slices/home/pages/index/charts/ChainIndicatorsContainer.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/home/indicators/ChainIndicatorsList.tsx b/client/slices/home/pages/index/charts/ChainIndicatorsList.tsx similarity index 83% rename from ui/home/indicators/ChainIndicatorsList.tsx rename to client/slices/home/pages/index/charts/ChainIndicatorsList.tsx index 4454c13170..47328b0bc5 100644 --- a/ui/home/indicators/ChainIndicatorsList.tsx +++ b/client/slices/home/pages/index/charts/ChainIndicatorsList.tsx @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; -import type { TChainIndicator } from './types'; -import type { ChainIndicatorId } from 'types/homepage'; +import type { TChainIndicator } from 'client/slices/home/types/client'; +import type { ChainIndicatorId } from 'client/slices/home/types/config'; import ChainIndicatorItem from './ChainIndicatorItem'; diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png b/client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png similarity index 100% rename from ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png rename to client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png b/client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png similarity index 100% rename from ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png rename to client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png b/client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png similarity index 100% rename from ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png rename to client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png b/client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png similarity index 100% rename from ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png rename to client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png b/client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png similarity index 100% rename from ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png rename to client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png b/client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png similarity index 100% rename from ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png rename to client/slices/home/pages/index/charts/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png diff --git a/ui/home/Highlights.pw.tsx b/client/slices/home/pages/index/highlights/Highlights.pw.tsx similarity index 97% rename from ui/home/Highlights.pw.tsx rename to client/slices/home/pages/index/highlights/Highlights.pw.tsx index 3b4f7d9cd4..f87d3c5a41 100644 --- a/ui/home/Highlights.pw.tsx +++ b/client/slices/home/pages/index/highlights/Highlights.pw.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type { HighlightsBannerConfig } from 'types/homepage'; +import type { HighlightsBannerConfig } from 'client/slices/home/types/client'; import { test, expect } from 'playwright/lib'; diff --git a/ui/home/Highlights.tsx b/client/slices/home/pages/index/highlights/Highlights.tsx similarity index 81% rename from ui/home/Highlights.tsx rename to client/slices/home/pages/index/highlights/Highlights.tsx index fcf02d8df2..508210fd37 100644 --- a/ui/home/Highlights.tsx +++ b/client/slices/home/pages/index/highlights/Highlights.tsx @@ -1,16 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { StackProps } from '@chakra-ui/react'; import { HStack } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { shuffle } from 'es-toolkit'; import React from 'react'; -import type { HighlightsBannerConfig } from 'types/homepage'; +import type { HighlightsBannerConfig } from 'client/slices/home/types/client'; + +import useFetch from 'client/api/hooks/useFetch'; + +import { HOMEPAGE_HIGHLIGHTS_BANNER } from 'client/slices/home/stubs'; import config from 'configs/app'; -import useFetch from 'lib/hooks/useFetch'; -import { HOMEPAGE_HIGHLIGHTS_BANNER } from 'stubs/homepage'; -import HighlightsItem from './highlights/HighlightsItem'; +import HighlightsItem from './HighlightsItem'; const HIGHLIGHTS_BANNER_COUNT = 3; diff --git a/ui/home/highlights/HighlightsItem.tsx b/client/slices/home/pages/index/highlights/HighlightsItem.tsx similarity index 95% rename from ui/home/highlights/HighlightsItem.tsx rename to client/slices/home/pages/index/highlights/HighlightsItem.tsx index a6d02d34e1..c00d296d8b 100644 --- a/ui/home/highlights/HighlightsItem.tsx +++ b/client/slices/home/pages/index/highlights/HighlightsItem.tsx @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { JsxStyleProps } from '@chakra-ui/react'; import { Text, VStack, HStack } from '@chakra-ui/react'; import React from 'react'; -import type { HighlightsBannerConfig } from 'types/homepage'; +import type { HighlightsBannerConfig } from 'client/slices/home/types/client'; import config from 'configs/app'; import { useColorModeValue } from 'toolkit/chakra/color-mode'; diff --git a/ui/home/__screenshots__/Highlights.pw.tsx_dark-color-mode_three-banners-dark-mode-1.png b/client/slices/home/pages/index/highlights/__screenshots__/Highlights.pw.tsx_dark-color-mode_three-banners-dark-mode-1.png similarity index 100% rename from ui/home/__screenshots__/Highlights.pw.tsx_dark-color-mode_three-banners-dark-mode-1.png rename to client/slices/home/pages/index/highlights/__screenshots__/Highlights.pw.tsx_dark-color-mode_three-banners-dark-mode-1.png diff --git a/ui/home/__screenshots__/Highlights.pw.tsx_default_three-banners-dark-mode-1.png b/client/slices/home/pages/index/highlights/__screenshots__/Highlights.pw.tsx_default_three-banners-dark-mode-1.png similarity index 100% rename from ui/home/__screenshots__/Highlights.pw.tsx_default_three-banners-dark-mode-1.png rename to client/slices/home/pages/index/highlights/__screenshots__/Highlights.pw.tsx_default_three-banners-dark-mode-1.png diff --git a/ui/home/__screenshots__/Highlights.pw.tsx_default_two-banners-1.png b/client/slices/home/pages/index/highlights/__screenshots__/Highlights.pw.tsx_default_two-banners-1.png similarity index 100% rename from ui/home/__screenshots__/Highlights.pw.tsx_default_two-banners-1.png rename to client/slices/home/pages/index/highlights/__screenshots__/Highlights.pw.tsx_default_two-banners-1.png diff --git a/client/slices/home/pages/index/stats/LatestBatchStatsWidget.tsx b/client/slices/home/pages/index/stats/LatestBatchStatsWidget.tsx new file mode 100644 index 0000000000..cd01677157 --- /dev/null +++ b/client/slices/home/pages/index/stats/LatestBatchStatsWidget.tsx @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { chakra } from '@chakra-ui/react'; +import React from 'react'; + +import { useHomeDataContext } from 'client/slices/home/contexts/home-data-context'; + +import StatsWidget from 'ui/shared/stats/StatsWidget'; + +type Props = { + className?: string; + isLoading: boolean; +}; + +const LatestBatchStatsWidget = ({ className, isLoading }: Props) => { + const { latestBatchQuery } = useHomeDataContext(); + + if (latestBatchQuery?.data === undefined) { + return null; + } + + return ( + + ); +}; + +export default chakra(React.memo(LatestBatchStatsWidget)); diff --git a/client/slices/home/pages/index/stats/LatestBlockStatsWidget.tsx b/client/slices/home/pages/index/stats/LatestBlockStatsWidget.tsx new file mode 100644 index 0000000000..995667480d --- /dev/null +++ b/client/slices/home/pages/index/stats/LatestBlockStatsWidget.tsx @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { chakra } from '@chakra-ui/react'; +import React from 'react'; + +import { useHomeDataContext } from 'client/slices/home/contexts/home-data-context'; + +import StatsWidget from 'ui/shared/stats/StatsWidget'; + +type Props = { + className?: string; + isLoading: boolean; + fallbackValue: number | string | undefined; +}; + +const LatestBlockStatsWidget = ({ className, isLoading, fallbackValue }: Props) => { + const { blocksQuery } = useHomeDataContext(); + + const value = blocksQuery?.data?.[0]?.height ?? fallbackValue; + if (value === undefined) { + return null; + } + + return ( + + ); +}; + +export default chakra(React.memo(LatestBlockStatsWidget)); diff --git a/client/slices/home/pages/index/stats/Stats.pw.tsx b/client/slices/home/pages/index/stats/Stats.pw.tsx new file mode 100644 index 0000000000..94c792c3f7 --- /dev/null +++ b/client/slices/home/pages/index/stats/Stats.pw.tsx @@ -0,0 +1,77 @@ +import type { Locator } from '@playwright/test'; +import React from 'react'; + +import * as blockMock from 'client/slices/block/mocks/block'; +import { HomeDataContextProvider } from 'client/slices/home/contexts/home-data-context'; +import * as statsMock from 'client/slices/home/mocks/stats'; + +import { test, expect } from 'playwright/lib'; + +import Stats from './Stats'; + +test.describe('all items', () => { + let component: Locator; + + test.beforeEach(async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_blocks","average_block_time","total_txs","wallet_addresses","gas_tracker","btc_locked"]' ], + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], + ]); + await mockApiResponse('general:stats', statsMock.withBtcLocked); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + component = await render( + + + , + ); + }); + + test('+@mobile +@dark-mode', async() => { + await expect(component).toHaveScreenshot(); + }); +}); + +test('no gas info', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], + ]); + await mockApiResponse('general:stats', statsMock.withoutGasInfo); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + const component = await render( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","gas_tracker","wallet_addresses","total_blocks"]' ], + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], + ]); + await mockApiResponse('general:stats', statsMock.base); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + const component = await render( + + + , + ); + await expect(component).toHaveScreenshot(); +}); + +test('3 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","wallet_addresses","total_blocks"]' ], + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], + ]); + await mockApiResponse('general:stats', statsMock.base); + await mockApiResponse('general:homepage_blocks', [ blockMock.base, blockMock.base2 ]); + const component = await render( + + + , + ); + await expect(component).toHaveScreenshot(); +}); diff --git a/client/slices/home/pages/index/stats/Stats.tsx b/client/slices/home/pages/index/stats/Stats.tsx new file mode 100644 index 0000000000..7e864be72f --- /dev/null +++ b/client/slices/home/pages/index/stats/Stats.tsx @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Grid } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import GasInfoTooltip from 'client/slices/gas/components/GasInfoTooltip'; +import GasPrice from 'client/slices/gas/components/GasPrice'; +import { useHomeDataContext } from 'client/slices/home/contexts/home-data-context'; +import { HOMEPAGE_STATS } from 'client/slices/home/stubs'; +import type { HomeStatsItem } from 'client/slices/home/utils/stats'; +import { homeStatsWidgetCommonStyles, isHomeStatsItemEnabled, sortHomeStatsItems } from 'client/slices/home/utils/stats'; + +import { HOMEPAGE_STATS_MICROSERVICE } from 'client/features/chain-stats/stubs/home'; +import { layerLabels } from 'client/features/rollup/common/utils/layer'; + +import config from 'configs/app'; +import IconSvg from 'ui/shared/IconSvg'; +import StatsWidget from 'ui/shared/stats/StatsWidget'; +import { WEI } from 'ui/shared/value/utils'; + +import LatestBatchStatsWidget from './LatestBatchStatsWidget'; +import LatestBlockStatsWidget from './LatestBlockStatsWidget'; +import StatsDegraded from './StatsDegraded'; + +const rollupFeature = config.features.rollup; +const isOptimisticRollup = rollupFeature.isEnabled && rollupFeature.type === 'optimistic'; +const isArbitrumRollup = rollupFeature.isEnabled && rollupFeature.type === 'arbitrum'; +const isStatsFeatureEnabled = config.features.stats.isEnabled; + +const Stats = () => { + const [ hasGasTracker, setHasGasTracker ] = React.useState(config.features.gasTracker.isEnabled); + const { blocksQuery, latestBatchQuery } = useHomeDataContext(); + + // data from stats microservice is prioritized over data from stats api + const statsQuery = useApiQuery('stats:pages_main', { + queryOptions: { + refetchOnMount: false, + placeholderData: isStatsFeatureEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined, + enabled: isStatsFeatureEnabled, + }, + }); + + const apiQuery = useApiQuery('general:stats', { + queryOptions: { + refetchOnMount: false, + placeholderData: HOMEPAGE_STATS, + }, + }); + + const isPlaceholderData = statsQuery.isPlaceholderData || apiQuery.isPlaceholderData || blocksQuery?.isPlaceholderData; + + React.useEffect(() => { + if (!isPlaceholderData && !apiQuery.data?.gas_prices?.average) { + setHasGasTracker(false); + } + // should run only after initial fetch + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isPlaceholderData ]); + + const hasStatsError = apiQuery.isError || statsQuery.isError || blocksQuery?.isError || latestBatchQuery?.isError; + + if (hasStatsError) { + return ; + } + + const isLoading = isPlaceholderData || latestBatchQuery?.isPlaceholderData; + + const apiData = apiQuery.data; + const statsData = statsQuery.data; + + const items: Array = (() => { + if (!statsData && !apiData) { + return []; + } + + const gasInfoTooltip = hasGasTracker && apiData?.gas_prices && apiData.gas_prices.average ? ( + + + + ) : null; + + return [ + latestBatchQuery?.data !== undefined && { + id: 'latest_batch' as const, + component: , + }, + (blocksQuery?.data?.[0]?.height ?? statsData?.total_blocks?.value ?? apiData?.total_blocks) && { + id: 'total_blocks' as const, + component: ( + + ), + }, + (statsData?.average_block_time?.value || apiData?.average_block_time) && { + id: 'average_block_time' as const, + icon: 'clock-light' as const, + label: statsData?.average_block_time?.title || 'Average block time', + value: `${ + statsData?.average_block_time?.value ? + Number(statsData.average_block_time.value).toFixed(1) : + (apiData!.average_block_time / 1000).toFixed(1) + }s`, + isLoading, + }, + (statsData?.total_transactions?.value || apiData?.total_transactions) && { + id: 'total_txs' as const, + icon: 'transactions' as const, + label: statsData?.total_transactions?.title || 'Total transactions', + value: Number(statsData?.total_transactions?.value || apiData?.total_transactions).toLocaleString(), + href: { pathname: '/txs' as const }, + isLoading, + }, + (isArbitrumRollup && statsData?.total_operational_transactions?.value) && { + id: 'total_operational_txs' as const, + icon: 'transactions' as const, + label: statsData?.total_operational_transactions?.title || 'Total operational transactions', + value: Number(statsData?.total_operational_transactions?.value).toLocaleString(), + href: { pathname: '/txs' as const }, + isLoading, + }, + (isOptimisticRollup && statsData?.op_stack_total_operational_transactions?.value) && { + id: 'total_operational_txs' as const, + icon: 'transactions' as const, + label: statsData?.op_stack_total_operational_transactions?.title || 'Total operational transactions', + value: Number(statsData?.op_stack_total_operational_transactions?.value).toLocaleString(), + href: { pathname: '/txs' as const }, + isLoading, + }, + apiData?.last_output_root_size && { + id: 'latest_l1_state_batch' as const, + icon: 'txn_batches' as const, + label: `Latest ${ layerLabels.parent } state batch`, + value: apiData?.last_output_root_size, + href: { pathname: '/batches' as const }, + isLoading, + }, + (statsData?.total_addresses?.value || apiData?.total_addresses) && { + id: 'wallet_addresses' as const, + icon: 'wallet' as const, + label: statsData?.total_addresses?.title || 'Wallet addresses', + value: Number(statsData?.total_addresses?.value || apiData?.total_addresses).toLocaleString(), + isLoading, + }, + hasGasTracker && apiData?.gas_prices && { + id: 'gas_tracker' as const, + icon: 'gas' as const, + label: 'Gas tracker', + value: apiData.gas_prices.average ? : 'N/A', + hint: gasInfoTooltip, + isLoading, + }, + apiData?.rootstock_locked_btc && { + id: 'btc_locked' as const, + icon: 'coins/bitcoin' as const, + label: 'BTC Locked in 2WP', + value: `${ BigNumber(apiData.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC`, + isLoading, + }, + apiData?.celo && { + id: 'current_epoch' as const, + icon: 'hourglass' as const, + label: 'Current epoch', + value: `#${ apiData.celo.epoch_number }`, + href: { pathname: '/epochs/[number]' as const, query: { number: String(apiData.celo.epoch_number) } }, + isLoading, + }, + ] + .filter(Boolean) + .filter(isHomeStatsItemEnabled) + .sort(sortHomeStatsItems); + })(); + + if (items.length === 0) { + return null; + } + + return ( + + { items.map((item) => { + if ('component' in item) { + return { item.component }; + } + + return ( + + ); + }) } + + ); +}; + +export default Stats; diff --git a/ui/home/fallbacks/StatsDegraded.tsx b/client/slices/home/pages/index/stats/StatsDegraded.tsx similarity index 87% rename from ui/home/fallbacks/StatsDegraded.tsx rename to client/slices/home/pages/index/stats/StatsDegraded.tsx index 67472bfe18..579f5aa2e6 100644 --- a/ui/home/fallbacks/StatsDegraded.tsx +++ b/client/slices/home/pages/index/stats/StatsDegraded.tsx @@ -1,20 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Grid } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import BigNumber from 'bignumber.js'; import React from 'react'; +import GasPrice from 'client/slices/gas/components/GasPrice'; +import { useHomeRpcDataContext } from 'client/slices/home/contexts/rpc-data-context'; +import type { HomeStatsWidgetItem } from 'client/slices/home/utils/stats'; +import { homeStatsWidgetCommonStyles, isHomeStatsItemEnabled, sortHomeStatsItems } from 'client/slices/home/utils/stats'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; + import dayjs from 'lib/date/dayjs'; -import { publicClient } from 'lib/web3/client'; import { mdash } from 'toolkit/utils/htmlEntities'; import FallbackRpcIcon from 'ui/shared/fallbacks/FallbackRpcIcon'; -import GasPrice from 'ui/shared/gas/GasPrice'; import StatsWidget from 'ui/shared/stats/StatsWidget'; import { GWEI } from 'ui/shared/value/utils'; -import type { HomeStatsItem } from '../utils'; -import { isHomeStatsItemEnabled, sortHomeStatsItems } from '../utils'; -import { useHomeRpcDataContext } from './rpcDataContext'; - const StatsDegraded = () => { const [ averageBlockTime, setAverageBlockTime ] = React.useState(undefined); @@ -72,7 +75,7 @@ const StatsDegraded = () => { } }, [ blocks ]); - const items: Array = (() => { + const items: Array = (() => { return [ { id: 'latest_batch' as const, @@ -84,7 +87,7 @@ const StatsDegraded = () => { { id: 'total_blocks' as const, icon: 'block' as const, - label: 'Total blocks', + label: 'Latest block', value: blocks[0] ? blocks[0].height.toLocaleString() : mdash, isFallback: blocks[0] === undefined, hint: blocks[0] && !isLoading ? : undefined, @@ -165,12 +168,12 @@ const StatsDegraded = () => { flexBasis="50%" flexGrow={ 1 } > - { items.map((item, index) => ( + { items.map((item) => ( + { ...homeStatsWidgetCommonStyles }/> ), ) } diff --git a/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_dark-color-mode_all-items-mobile-dark-mode-1.png b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_dark-color-mode_all-items-mobile-dark-mode-1.png new file mode 100644 index 0000000000..7fdd6bc0c7 Binary files /dev/null and b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_dark-color-mode_all-items-mobile-dark-mode-1.png differ diff --git a/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_default_all-items-mobile-dark-mode-1.png b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_default_all-items-mobile-dark-mode-1.png new file mode 100644 index 0000000000..17a5eaa0e8 Binary files /dev/null and b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_default_all-items-mobile-dark-mode-1.png differ diff --git a/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_default_no-gas-info-1.png b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_default_no-gas-info-1.png new file mode 100644 index 0000000000..7e34ce5977 Binary files /dev/null and b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_default_no-gas-info-1.png differ diff --git a/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_3-items-default-view-mobile---default-1.png b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_3-items-default-view-mobile---default-1.png new file mode 100644 index 0000000000..797e95154d Binary files /dev/null and b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_3-items-default-view-mobile---default-1.png differ diff --git a/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_4-items-default-view-mobile---default-1.png b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_4-items-default-view-mobile---default-1.png new file mode 100644 index 0000000000..65be4e5653 Binary files /dev/null and b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_4-items-default-view-mobile---default-1.png differ diff --git a/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_all-items-mobile-dark-mode-1.png b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_all-items-mobile-dark-mode-1.png new file mode 100644 index 0000000000..a6f0bf72aa Binary files /dev/null and b/client/slices/home/pages/index/stats/__screenshots__/Stats.pw.tsx_mobile_all-items-mobile-dark-mode-1.png differ diff --git a/ui/home/LatestTxs.pw.tsx b/client/slices/home/pages/index/txs/LatestTxs.pw.tsx similarity index 96% rename from ui/home/LatestTxs.pw.tsx rename to client/slices/home/pages/index/txs/LatestTxs.pw.tsx index 18985d2f8f..64a0fca366 100644 --- a/ui/home/LatestTxs.pw.tsx +++ b/client/slices/home/pages/index/txs/LatestTxs.pw.tsx @@ -1,9 +1,10 @@ import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { AddressParam } from 'types/api/addressParams'; +import type { AddressParam } from 'client/slices/address/types/api'; + +import * as txMock from 'client/slices/tx/mocks/tx'; -import * as txMock from 'mocks/txs/tx'; import * as socketServer from 'playwright/fixtures/socketServer'; import { test as base, expect, devices } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; diff --git a/ui/home/LatestTxs.tsx b/client/slices/home/pages/index/txs/LatestTxs.tsx similarity index 83% rename from ui/home/LatestTxs.tsx rename to client/slices/home/pages/index/txs/LatestTxs.tsx index de06da0fe5..5dac696782 100644 --- a/ui/home/LatestTxs.tsx +++ b/client/slices/home/pages/index/txs/LatestTxs.tsx @@ -1,18 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, Text } from '@chakra-ui/react'; import React from 'react'; import { route } from 'nextjs-routes'; +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import { AddressHighlightProvider } from 'client/slices/address/contexts/address-highlight'; +import useNewTxsSocket from 'client/slices/tx/hooks/useTxsSocketTypeAll'; +import { TX } from 'client/slices/tx/stubs/tx'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import config from 'configs/app'; -import useApiQuery from 'lib/api/useApiQuery'; -import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import { TX } from 'stubs/tx'; import { Link } from 'toolkit/chakra/link'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; -import useNewTxsSocket from 'ui/txs/socket/useTxsSocketTypeAll'; -import LatestTxsDegraded from './fallbacks/LatestTxsDegraded'; +import LatestTxsDegraded from './LatestTxsDegraded'; import LatestTxsItem from './LatestTxsItem'; import LatestTxsItemMobile from './LatestTxsItemMobile'; diff --git a/ui/home/fallbacks/LatestTxsDegraded.tsx b/client/slices/home/pages/index/txs/LatestTxsDegraded.tsx similarity index 82% rename from ui/home/fallbacks/LatestTxsDegraded.tsx rename to client/slices/home/pages/index/txs/LatestTxsDegraded.tsx index 4995479a0f..a027b4519e 100644 --- a/ui/home/fallbacks/LatestTxsDegraded.tsx +++ b/client/slices/home/pages/index/txs/LatestTxsDegraded.tsx @@ -1,20 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import { clamp } from 'es-toolkit'; import React from 'react'; import { route } from 'nextjs-routes'; +import { AddressHighlightProvider } from 'client/slices/address/contexts/address-highlight'; +import { useHomeRpcDataContext } from 'client/slices/home/contexts/rpc-data-context'; +import { TX } from 'client/slices/tx/stubs/tx'; + +import { publicClient } from 'client/features/connect-wallet/utils/public-client'; + import config from 'configs/app'; -import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; -import { publicClient } from 'lib/web3/client'; -import { TX } from 'stubs/tx'; import { Link } from 'toolkit/chakra/link'; -import LatestTxsItem from '../LatestTxsItem'; -import LatestTxsItemMobile from '../LatestTxsItemMobile'; import LatestTxsDegradedNewItems from './LatestTxsDegradedNewItems'; import LatestTxsFallback from './LatestTxsFallback'; -import { useHomeRpcDataContext } from './rpcDataContext'; +import LatestTxsItem from './LatestTxsItem'; +import LatestTxsItemMobile from './LatestTxsItemMobile'; const zetachainFeature = config.features.zetachain; diff --git a/ui/home/fallbacks/LatestTxsDegradedNewItems.tsx b/client/slices/home/pages/index/txs/LatestTxsDegradedNewItems.tsx similarity index 83% rename from ui/home/fallbacks/LatestTxsDegradedNewItems.tsx rename to client/slices/home/pages/index/txs/LatestTxsDegradedNewItems.tsx index 395e07071d..4e2442dfc0 100644 --- a/ui/home/fallbacks/LatestTxsDegradedNewItems.tsx +++ b/client/slices/home/pages/index/txs/LatestTxsDegradedNewItems.tsx @@ -1,6 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import useGradualIncrement from 'lib/hooks/useGradualIncrement'; +import useGradualIncrement from 'client/shared/hooks/useGradualIncrement'; + import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; interface Props { diff --git a/ui/home/fallbacks/LatestTxsFallback.tsx b/client/slices/home/pages/index/txs/LatestTxsFallback.tsx similarity index 97% rename from ui/home/fallbacks/LatestTxsFallback.tsx rename to client/slices/home/pages/index/txs/LatestTxsFallback.tsx index 6de0da0011..326df5d1f9 100644 --- a/ui/home/fallbacks/LatestTxsFallback.tsx +++ b/client/slices/home/pages/index/txs/LatestTxsFallback.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { BoxProps } from '@chakra-ui/react'; import { Box, HStack, VStack } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/home/LatestTxsItem.tsx b/client/slices/home/pages/index/txs/LatestTxsItem.tsx similarity index 85% rename from ui/home/LatestTxsItem.tsx rename to client/slices/home/pages/index/txs/LatestTxsItem.tsx index 1bd503765e..3ade28cbeb 100644 --- a/ui/home/LatestTxsItem.tsx +++ b/client/slices/home/pages/index/txs/LatestTxsItem.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, @@ -7,22 +9,24 @@ import { } from '@chakra-ui/react'; import React from 'react'; -import type { Transaction } from 'types/api/transaction'; +import type { Transaction } from 'client/slices/tx/types/api'; + +import AddressFromTo from 'client/slices/address/components/from-to/AddressFromTo'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; +import TxAdditionalInfo from 'client/slices/tx/components/TxAdditionalInfo'; +import TxFee from 'client/slices/tx/components/TxFee'; +import TxStatus from 'client/slices/tx/components/TxStatus'; +import TxType from 'client/slices/tx/components/TxType'; + +import TxWatchListTags from 'client/features/account/components/TxWatchListTags'; import config from 'configs/app'; import { Skeleton } from 'toolkit/chakra/skeleton'; -import AddressFromTo from 'ui/shared/address/AddressFromTo'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; import EntityTag from 'ui/shared/EntityTags/EntityTag'; -import TxStatus from 'ui/shared/statusTag/TxStatus'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; -import TxFee from 'ui/shared/tx/TxFee'; -import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; -import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; -import TxType from 'ui/txs/TxType'; -type Props = { +interface Props { tx: Transaction; isLoading?: boolean; }; diff --git a/ui/home/LatestTxsItemMobile.tsx b/client/slices/home/pages/index/txs/LatestTxsItemMobile.tsx similarity index 83% rename from ui/home/LatestTxsItemMobile.tsx rename to client/slices/home/pages/index/txs/LatestTxsItemMobile.tsx index 6e102f6e0e..aa8ebba3a4 100644 --- a/ui/home/LatestTxsItemMobile.tsx +++ b/client/slices/home/pages/index/txs/LatestTxsItemMobile.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, @@ -7,20 +9,22 @@ import { } from '@chakra-ui/react'; import React from 'react'; -import type { Transaction } from 'types/api/transaction'; +import type { Transaction } from 'client/slices/tx/types/api'; + +import AddressFromTo from 'client/slices/address/components/from-to/AddressFromTo'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; +import TxAdditionalInfo from 'client/slices/tx/components/TxAdditionalInfo'; +import TxFee from 'client/slices/tx/components/TxFee'; +import TxStatus from 'client/slices/tx/components/TxStatus'; +import TxType from 'client/slices/tx/components/TxType'; + +import TxWatchListTags from 'client/features/account/components/TxWatchListTags'; import config from 'configs/app'; import { Skeleton } from 'toolkit/chakra/skeleton'; -import AddressFromTo from 'ui/shared/address/AddressFromTo'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; import EntityTag from 'ui/shared/EntityTags/EntityTag'; -import TxStatus from 'ui/shared/statusTag/TxStatus'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; -import TxFee from 'ui/shared/tx/TxFee'; -import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; -import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; -import TxType from 'ui/txs/TxType'; type Props = { tx: Transaction; diff --git a/client/slices/home/pages/index/txs/Transactions.tsx b/client/slices/home/pages/index/txs/Transactions.tsx new file mode 100644 index 0000000000..ce7112678a --- /dev/null +++ b/client/slices/home/pages/index/txs/Transactions.tsx @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { HStack } from '@chakra-ui/react'; +import React from 'react'; + +import { SocketProvider } from 'client/api/socket/context'; + +import { useHomeRpcDataContext } from 'client/slices/home/contexts/rpc-data-context'; + +import useAuth from 'client/features/account/hooks/useIsAuth'; +import LatestWatchlistTxs from 'client/features/account/pages/home/LatestWatchlistTxs'; +import LatestZetaChainCCTXs from 'client/features/chain-variants/zeta-chain/pages/home/LatestZetaChainCCTXs'; +import LatestCrossChainTxs from 'client/features/cross-chain-txs/pages/home/LatestCrossChainTxs'; +import LatestArbitrumDeposits from 'client/features/rollup/arbitrum/pages/home/LatestArbitrumDeposits'; +import { layerLabels } from 'client/features/rollup/common/utils/layer'; +import LatestOptimisticDeposits from 'client/features/rollup/optimism/pages/home/LatestOptimisticDeposits'; + +import config from 'configs/app'; +import { Heading } from 'toolkit/chakra/heading'; +import AdaptiveTabs from 'toolkit/components/AdaptiveTabs/AdaptiveTabs'; +import FallbackRpcIcon from 'ui/shared/fallbacks/FallbackRpcIcon'; + +import LatestTxs from './LatestTxs'; + +const rollupFeature = config.features.rollup; +const zetachainFeature = config.features.zetachain; +const crossChainTxsFeature = config.features.crossChainTxs; + +const Transactions = () => { + + const isAuth = useAuth(); + const rpcDataContext = useHomeRpcDataContext(); + const isRpcData = rpcDataContext.isEnabled && !rpcDataContext.isLoading && !rpcDataContext.isError && rpcDataContext.subscriptions.includes('latest-txs'); + + if ((rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum')) || isAuth || zetachainFeature.isEnabled) { + const tabs = [ + zetachainFeature.isEnabled && { + id: 'cctx', + title: 'Cross-chain', + component: ( + + + + ), + }, + { id: 'txn', title: zetachainFeature.isEnabled ? 'ZetaChain EVM' : 'Latest txn', component: }, + rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && + { id: 'deposits', title: `Deposits (${ layerLabels.parent }→${ layerLabels.current } txn)`, component: }, + rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && + { id: 'deposits', title: `Deposits (${ layerLabels.parent }→${ layerLabels.current } txn)`, component: }, + isAuth && { id: 'watchlist', title: 'Watch list', component: }, + ].filter(Boolean); + return ( + <> + + Transactions + { isRpcData && } + + + + ); + } + + if (crossChainTxsFeature.isEnabled) { + const tabs = [ + { id: 'txs', title: 'Txns', component: }, + { id: 'cross_chain_txs', title: 'Cross-chain txns', component: }, + ]; + + return ( + <> + + Latest transactions + { isRpcData && } + + + + ); + } + + return ( + <> + + Latest transactions + { isRpcData && } + + + + ); +}; + +export default Transactions; diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png b/client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png similarity index 100% rename from ui/home/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png rename to client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png b/client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png similarity index 100% rename from ui/home/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png rename to client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png b/client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png similarity index 100% rename from ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png rename to client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-one-tag-1.png b/client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-one-tag-1.png similarity index 100% rename from ui/home/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-one-tag-1.png rename to client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-one-tag-1.png diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-two-or-more-tags-1.png b/client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-two-or-more-tags-1.png similarity index 100% rename from ui/home/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-two-or-more-tags-1.png rename to client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_small-desktop-two-or-more-tags-1.png diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png b/client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png similarity index 100% rename from ui/home/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png rename to client/slices/home/pages/index/txs/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png diff --git a/client/slices/home/stubs.ts b/client/slices/home/stubs.ts new file mode 100644 index 0000000000..299c52e9ab --- /dev/null +++ b/client/slices/home/stubs.ts @@ -0,0 +1,47 @@ +import type { HomeStats } from 'client/slices/home/types/api'; + +export const HOMEPAGE_HIGHLIGHTS_BANNER = { + title: 'Duck Deep into Transactions', + description: 'Explore and track all blockchain transactions', +}; + +export const HOMEPAGE_STATS: HomeStats = { + average_block_time: 14346, + coin_price: '1807.68', + coin_price_change_percentage: 42, + gas_prices: { + average: { + fiat_price: '1.01', + price: 20.41, + time: 12283, + base_fee: 2.22222, + priority_fee: 12.424242, + }, + fast: { + fiat_price: '1.26', + price: 25.47, + time: 9321, + base_fee: 4.44444, + priority_fee: 22.242424, + }, + slow: { + fiat_price: '0.97', + price: 19.55, + time: 24543, + base_fee: 1.11111, + priority_fee: 7.8909, + }, + }, + gas_price_updated_at: '2022-11-11T11:09:49.051171Z', + gas_prices_update_in: 300000, + gas_used_today: '0', + market_cap: '0', + network_utilization_percentage: 22.56, + static_gas_price: null, + total_addresses: '28634064', + total_blocks: '8940150', + total_gas_used: '0', + total_transactions: '193823272', + transactions_today: '0', + tvl: '1767425.102766552', +}; diff --git a/client/slices/home/types/api.ts b/client/slices/home/types/api.ts new file mode 100644 index 0000000000..6cec82dacc --- /dev/null +++ b/client/slices/home/types/api.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { GasPrices } from 'client/slices/gas/types/api'; + +export type HomeStats = { + total_blocks: string; + total_addresses: string; + total_transactions: string; + average_block_time: number; + coin_image?: string | null; + coin_price: string | null; + coin_price_change_percentage: number | null; // e.g -6.22 + total_gas_used: string; + transactions_today: string | null; + gas_used_today: string; + gas_prices: GasPrices | null; + gas_price_updated_at: string | null; + gas_prices_update_in: number; + static_gas_price: string | null; + market_cap: string | null; + network_utilization_percentage: number; + tvl: string | null; + rootstock_locked_btc?: string | null; + last_output_root_size?: string | null; + secondary_coin_price?: string | null; + secondary_coin_image?: string | null; + celo?: { + epoch_number: number; + }; +}; diff --git a/client/slices/home/types/client.ts b/client/slices/home/types/client.ts new file mode 100644 index 0000000000..e4d1ee5b9a --- /dev/null +++ b/client/slices/home/types/client.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type React from 'react'; +import type { ReactElement } from 'react'; + +import type { ChainIndicatorId, HomeStatsWidgetId } from 'client/slices/home/types/config'; + +import type { Props as StatsWidgetProps } from 'ui/shared/stats/StatsWidget'; + +export interface HighlightsBannerConfig { + title: string; + description: string; + title_color?: Array; + description_color?: Array; + background?: Array; + side_img_url?: Array; + is_pinned?: boolean; + page_path?: string; + redirect_url?: string; +} + +export interface TChainIndicator { + id: ChainIndicatorId; + title: string; + titleShort?: string; + value: string; + valueDiff?: number; + icon: React.ReactNode; + hint?: string; +} + +export type HomeStatsComponentItem = { id: HomeStatsWidgetId; component: ReactElement }; +export type HomeStatsWidgetItem = StatsWidgetProps & { id: HomeStatsWidgetId; component?: undefined }; + +export type HomeStatsItem = HomeStatsComponentItem | HomeStatsWidgetItem; diff --git a/client/slices/home/types/config.ts b/client/slices/home/types/config.ts new file mode 100644 index 0000000000..33e3a69a20 --- /dev/null +++ b/client/slices/home/types/config.ts @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'daily_operational_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const; +export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number]; + +export const HOME_STATS_WIDGET_IDS = [ + 'latest_batch', + 'total_blocks', + 'average_block_time', + 'total_txs', + 'total_operational_txs', + 'latest_l1_state_batch', + 'wallet_addresses', + 'gas_tracker', + 'btc_locked', + 'current_epoch', +] as const; +export type HomeStatsWidgetId = typeof HOME_STATS_WIDGET_IDS[number]; + +export interface HeroBannerButtonState { + background?: Array; + text_color?: Array; +} + +export interface HeroBannerConfig { + background?: Array; + text_color?: Array; + border?: Array; + button?: { + _default?: HeroBannerButtonState; + _hover?: HeroBannerButtonState; + _selected?: HeroBannerButtonState; + }; + search?: { + border_width?: Array; + }; + text?: string; +} diff --git a/client/slices/home/utils/chart.ts b/client/slices/home/utils/chart.ts new file mode 100644 index 0000000000..1890776120 --- /dev/null +++ b/client/slices/home/utils/chart.ts @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { ChainIndicatorId } from 'client/slices/home/types/config'; + +import config from 'configs/app'; +import type { LineChartData, LineChartDataItem, LineChartItemRaw, LineChartItem } from 'toolkit/components/charts/line'; +import { sortByDateAsc } from 'ui/shared/chart/utils'; + +const CHART_ITEMS: Record> = { + daily_txs: { + name: 'Tx/day', + valueFormatter: (x: number) => x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + }, + daily_operational_txs: { + name: 'Tx/day', + valueFormatter: (x: number) => x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + }, + coin_price: { + name: `${ config.features.multichain.isEnabled ? 'ETH' : config.chain.currency.symbol } price`, + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), + }, + secondary_coin_price: { + name: `${ config.chain.currency.symbol } price`, + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), + }, + market_cap: { + name: 'Market cap', + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2 }), + }, + tvl: { + name: 'TVL', + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + }, +}; + +const nonNullTailReducer = (result: Array, item: LineChartItemRaw) => { + if (item.value === null && result.length === 0) { + return result; + } + result.unshift(item); + return result; +}; + +const mapNullToZero: (item: LineChartItemRaw) => LineChartItem = (item) => ({ ...item, value: Number(item.value) }); + +export function prepareChartItems(items: Array) { + return items + .sort(sortByDateAsc) + .reduceRight(nonNullTailReducer, [] as Array) + .map(mapNullToZero); +} + +export function getChartData(indicatorId: ChainIndicatorId, data: Array): LineChartData { + return [ { + id: indicatorId.replace(' ', '_'), + charts: [], + items: prepareChartItems(data), + name: CHART_ITEMS[indicatorId].name, + valueFormatter: CHART_ITEMS[indicatorId].valueFormatter, + } ]; +} diff --git a/ui/home/indicators/utils/indicators.ts b/client/slices/home/utils/indicators.ts similarity index 78% rename from ui/home/indicators/utils/indicators.ts rename to client/slices/home/utils/indicators.ts index c0726d20e3..793271c7b8 100644 --- a/ui/home/indicators/utils/indicators.ts +++ b/client/slices/home/utils/indicators.ts @@ -1,4 +1,6 @@ -import type { TChainIndicator } from '../types'; +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { TChainIndicator } from 'client/slices/home/types/client'; import config from 'configs/app'; diff --git a/client/slices/home/utils/stats.ts b/client/slices/home/utils/stats.ts new file mode 100644 index 0000000000..9c649d809b --- /dev/null +++ b/client/slices/home/utils/stats.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { ReactElement } from 'react'; + +import type { HomeStatsWidgetId } from 'client/slices/home/types/config'; + +import config from 'configs/app'; +import type { Props as StatsWidgetProps } from 'ui/shared/stats/StatsWidget'; + +export type HomeStatsComponentItem = { id: HomeStatsWidgetId; component: ReactElement }; +export type HomeStatsWidgetItem = StatsWidgetProps & { id: HomeStatsWidgetId; component?: undefined }; + +export type HomeStatsItem = HomeStatsComponentItem | HomeStatsWidgetItem; + +export const homeStatsWidgetCommonStyles = { + _odd: { + _last: { + gridColumn: 'span 2', + }, + }, +} as const; + +export const isHomeStatsItemEnabled = (item: { id: HomeStatsWidgetId }) => config.UI.homepage.stats.includes(item.id); + +export const sortHomeStatsItems = (a: { id: HomeStatsWidgetId }, b: { id: HomeStatsWidgetId }) => { + const indexA = config.UI.homepage.stats.indexOf(a.id); + const indexB = config.UI.homepage.stats.indexOf(b.id); + if (indexA > indexB) { + return 1; + } + if (indexA < indexB) { + return -1; + } + return 0; +}; diff --git a/ui/internalTxs/InternalTxsList.tsx b/client/slices/internal-tx/components/InternalTxsList.tsx similarity index 87% rename from ui/internalTxs/InternalTxsList.tsx rename to client/slices/internal-tx/components/InternalTxsList.tsx index e367a428b9..43aba67cd8 100644 --- a/ui/internalTxs/InternalTxsList.tsx +++ b/client/slices/internal-tx/components/InternalTxsList.tsx @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; import { useMultichainContext } from 'lib/contexts/multichain'; diff --git a/ui/internalTxs/InternalTxsListItem.tsx b/client/slices/internal-tx/components/InternalTxsListItem.tsx similarity index 82% rename from ui/internalTxs/InternalTxsListItem.tsx rename to client/slices/internal-tx/components/InternalTxsListItem.tsx index d1fda5ff57..e932bfe14a 100644 --- a/ui/internalTxs/InternalTxsListItem.tsx +++ b/client/slices/internal-tx/components/InternalTxsListItem.tsx @@ -1,20 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, HStack } from '@chakra-ui/react'; import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; import type { ClusterChainConfig } from 'types/multichain'; -import { currencyUnits } from 'lib/units'; +import AddressFromTo from 'client/slices/address/components/from-to/AddressFromTo'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import { TX_INTERNALS_ITEMS } from 'client/slices/internal-tx/utils/utils'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; +import TxStatus from 'client/slices/tx/components/TxStatus'; + +import { currencyUnits } from 'client/shared/chain/units'; + import { Badge } from 'toolkit/chakra/badge'; import { Skeleton } from 'toolkit/chakra/skeleton'; -import AddressFromTo from 'ui/shared/address/AddressFromTo'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; -import TxStatus from 'ui/shared/statusTag/TxStatus'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; -import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; type Props = InternalTransaction & { currentAddress?: string; isLoading?: boolean; showBlockInfo?: boolean; chainData?: ClusterChainConfig }; diff --git a/ui/internalTxs/InternalTxsTable.tsx b/client/slices/internal-tx/components/InternalTxsTable.tsx similarity index 87% rename from ui/internalTxs/InternalTxsTable.tsx rename to client/slices/internal-tx/components/InternalTxsTable.tsx index 7f17603c77..d6c153c822 100644 --- a/ui/internalTxs/InternalTxsTable.tsx +++ b/client/slices/internal-tx/components/InternalTxsTable.tsx @@ -1,10 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; + +import { AddressHighlightProvider } from 'client/slices/address/contexts/address-highlight'; + +import { currencyUnits } from 'client/shared/chain/units'; -import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; import { useMultichainContext } from 'lib/contexts/multichain'; -import { currencyUnits } from 'lib/units'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle'; diff --git a/ui/internalTxs/InternalTxsTableItem.tsx b/client/slices/internal-tx/components/InternalTxsTableItem.tsx similarity index 84% rename from ui/internalTxs/InternalTxsTableItem.tsx rename to client/slices/internal-tx/components/InternalTxsTableItem.tsx index b5ed3afa52..1802e397cb 100644 --- a/ui/internalTxs/InternalTxsTableItem.tsx +++ b/client/slices/internal-tx/components/InternalTxsTableItem.tsx @@ -1,19 +1,22 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex } from '@chakra-ui/react'; import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; import type { ClusterChainConfig } from 'types/multichain'; +import AddressFromTo from 'client/slices/address/components/from-to/AddressFromTo'; +import BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import { TX_INTERNALS_ITEMS } from 'client/slices/internal-tx/utils/utils'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; +import TxStatus from 'client/slices/tx/components/TxStatus'; + import { Badge } from 'toolkit/chakra/badge'; import { TableCell, TableRow } from 'toolkit/chakra/table'; -import AddressFromTo from 'ui/shared/address/AddressFromTo'; -import BlockEntity from 'ui/shared/entities/block/BlockEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; import ChainIcon from 'ui/shared/externalChains/ChainIcon'; -import TxStatus from 'ui/shared/statusTag/TxStatus'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; -import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; type Props = InternalTransaction & { currentAddress?: string; isLoading?: boolean; showBlockInfo?: boolean; chainData?: ClusterChainConfig }; diff --git a/client/slices/internal-tx/hooks/useInternalTxsQuery.ts b/client/slices/internal-tx/hooks/useInternalTxsQuery.ts new file mode 100644 index 0000000000..efb980cd0c --- /dev/null +++ b/client/slices/internal-tx/hooks/useInternalTxsQuery.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useRouter } from 'next/router'; +import React from 'react'; + +import { INTERNAL_TX } from 'client/slices/internal-tx/stubs'; + +import useDebounce from 'client/shared/hooks/useDebounce'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import { generateListStub } from 'stubs/utils'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +interface Props { + isMultichain?: boolean; +} + +export default function useInternalTxsQuery({ isMultichain }: Props = {}) { + const router = useRouter(); + const [ searchTerm, setSearchTerm ] = React.useState(getQueryParamString(router.query.transaction_hash) || undefined); + const debouncedSearchTerm = useDebounce(searchTerm || '', 300); + + const query = useQueryWithPages({ + resourceName: 'general:internal_txs', + filters: { transaction_hash: debouncedSearchTerm }, + options: { + placeholderData: generateListStub<'general:internal_txs'>( + INTERNAL_TX, + 50, + { + next_page_params: { + items_count: 50, + block_number: 1, + index: 1, + transaction_hash: '0x123', + transaction_index: 1, + }, + }, + ), + }, + isMultichain, + }); + + const onSearchTermChange = React.useCallback((value: string) => { + query.onFilterChange({ transaction_hash: value }); + setSearchTerm(value); + }, [ query ]); + + return React.useMemo(() => ({ + query, + searchTerm, + debouncedSearchTerm, + onSearchTermChange, + }), [ query, searchTerm, debouncedSearchTerm, onSearchTermChange ]); +} diff --git a/client/slices/internal-tx/mocks.ts b/client/slices/internal-tx/mocks.ts new file mode 100644 index 0000000000..af9e4a350d --- /dev/null +++ b/client/slices/internal-tx/mocks.ts @@ -0,0 +1,80 @@ +import type { InternalTransaction, InternalTransactionsResponse } from 'client/slices/internal-tx/types/api'; + +export const base: InternalTransaction = { + block_number: 29611822, + created_contract: null, + error: null, + from: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeStore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + gas_limit: '757586', + index: 1, + success: true, + timestamp: '2022-10-10T14:43:05.000000Z', + to: { + hash: '0x502a9C8af2441a1E276909405119FaE21F3dC421', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeCreditHistory', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61', + type: 'call', + value: '42000000000000000000', +}; + +export const typeStaticCall: InternalTransaction = { + ...base, + type: 'staticcall', + to: { + ...base.to, + name: null, + }, + gas_limit: '63424243', + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff62', +}; + +export const withContractCreated: InternalTransaction = { + ...base, + type: 'delegatecall', + to: null, + from: { + ...base.from, + name: null, + }, + created_contract: { + hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a', + implementations: null, + is_contract: true, + is_verified: null, + name: 'Shavuha token', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + value: '1420000000000000000', + gas_limit: '5433', + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff63', +}; + +export const baseResponse: InternalTransactionsResponse = { + items: [ + base, + typeStaticCall, + withContractCreated, + ], + next_page_params: null, +}; diff --git a/ui/pages/InternalTxs.tsx b/client/slices/internal-tx/pages/index/InternalTxs.tsx similarity index 84% rename from ui/pages/InternalTxs.tsx rename to client/slices/internal-tx/pages/index/InternalTxs.tsx index 406900dec6..1feaf2b3f7 100644 --- a/ui/pages/InternalTxs.tsx +++ b/client/slices/internal-tx/pages/index/InternalTxs.tsx @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; -import useIsMobile from 'lib/hooks/useIsMobile'; +import InternalTxsList from 'client/slices/internal-tx/components/InternalTxsList'; +import InternalTxsTable from 'client/slices/internal-tx/components/InternalTxsTable'; +import useInternalTxsQuery from 'client/slices/internal-tx/hooks/useInternalTxsQuery'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import { FilterInput } from 'toolkit/components/filters/FilterInput'; -import InternalTxsList from 'ui/internalTxs/InternalTxsList'; -import InternalTxsTable from 'ui/internalTxs/InternalTxsTable'; -import useInternalTxsQuery from 'ui/internalTxs/useInternalTxsQuery'; import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import PageTitle from 'ui/shared/Page/PageTitle'; diff --git a/ui/tx/TxInternals.pw.tsx b/client/slices/internal-tx/pages/tx/TxInternals.pw.tsx similarity index 77% rename from ui/tx/TxInternals.pw.tsx rename to client/slices/internal-tx/pages/tx/TxInternals.pw.tsx index 08db79959c..e67beeecff 100644 --- a/ui/tx/TxInternals.pw.tsx +++ b/client/slices/internal-tx/pages/tx/TxInternals.pw.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import * as internalTxsMock from 'mocks/txs/internalTxs'; -import * as txMock from 'mocks/txs/tx'; +import * as internalTxsMock from 'client/slices/internal-tx/mocks'; +import type { TxQuery } from 'client/slices/tx/hooks/useTxQuery'; +import * as txMock from 'client/slices/tx/mocks/tx'; + import { test, expect } from 'playwright/lib'; import TxInternals from './TxInternals'; -import type { TxQuery } from './useTxQuery'; const TX_HASH = txMock.base.hash; const hooksConfig = { diff --git a/ui/tx/TxInternals.tsx b/client/slices/internal-tx/pages/tx/TxInternals.tsx similarity index 88% rename from ui/tx/TxInternals.tsx rename to client/slices/internal-tx/pages/tx/TxInternals.tsx index f830765856..f090e216a6 100644 --- a/ui/tx/TxInternals.tsx +++ b/client/slices/internal-tx/pages/tx/TxInternals.tsx @@ -1,11 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; + +import { INTERNAL_TX } from 'client/slices/internal-tx/stubs'; +import TxPendingAlert from 'client/slices/tx/components/TxPendingAlert'; +import TxSocketAlert from 'client/slices/tx/components/TxSocketAlert'; +import type { TxQuery } from 'client/slices/tx/hooks/useTxQuery'; import compareBns from 'lib/bigint/compareBns'; // import { apos } from 'toolkit/utils/htmlEntities'; -import { INTERNAL_TX } from 'stubs/internalTx'; import { generateListStub } from 'stubs/utils'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; @@ -13,13 +19,10 @@ import DataListDisplay from 'ui/shared/DataListDisplay'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import { default as getNextSortValueShared } from 'ui/shared/sort/getNextSortValue'; -import TxInternalsList from 'ui/tx/internals/TxInternalsList'; -import TxInternalsTable from 'ui/tx/internals/TxInternalsTable'; -import type { Sort, SortField } from 'ui/tx/internals/utils'; -import TxPendingAlert from 'ui/tx/TxPendingAlert'; -import TxSocketAlert from 'ui/tx/TxSocketAlert'; -import type { TxQuery } from './useTxQuery'; +import type { Sort, SortField } from '../../utils/utils'; +import TxInternalsList from './TxInternalsList'; +import TxInternalsTable from './TxInternalsTable'; const SORT_SEQUENCE: Record> = { value: [ 'value-desc', 'value-asc', 'default' ], diff --git a/client/slices/internal-tx/pages/tx/TxInternalsList.tsx b/client/slices/internal-tx/pages/tx/TxInternalsList.tsx new file mode 100644 index 0000000000..4cc3ce3506 --- /dev/null +++ b/client/slices/internal-tx/pages/tx/TxInternalsList.tsx @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; + +import TxInternalsListItem from './TxInternalsListItem'; + +const TxInternalsList = ({ data, isLoading }: { data: Array; isLoading?: boolean }) => { + return ( + + { data.map((item, index) => ) } + + ); +}; + +export default TxInternalsList; diff --git a/ui/tx/internals/TxInternalsListItem.tsx b/client/slices/internal-tx/pages/tx/TxInternalsListItem.tsx similarity index 81% rename from ui/tx/internals/TxInternalsListItem.tsx rename to client/slices/internal-tx/pages/tx/TxInternalsListItem.tsx index 3435a45054..8483817e44 100644 --- a/ui/tx/internals/TxInternalsListItem.tsx +++ b/client/slices/internal-tx/pages/tx/TxInternalsListItem.tsx @@ -1,16 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, HStack } from '@chakra-ui/react'; import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; + +import AddressFromTo from 'client/slices/address/components/from-to/AddressFromTo'; +import TxStatus from 'client/slices/tx/components/TxStatus'; + +import { currencyUnits } from 'client/shared/chain/units'; -import { currencyUnits } from 'lib/units'; import { Badge } from 'toolkit/chakra/badge'; import { Skeleton } from 'toolkit/chakra/skeleton'; -import AddressFromTo from 'ui/shared/address/AddressFromTo'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; -import TxStatus from 'ui/shared/statusTag/TxStatus'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; -import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; + +import { TX_INTERNALS_ITEMS } from '../../utils/utils'; type Props = InternalTransaction & { isLoading?: boolean }; diff --git a/ui/tx/internals/TxInternalsTable.tsx b/client/slices/internal-tx/pages/tx/TxInternalsTable.tsx similarity index 79% rename from ui/tx/internals/TxInternalsTable.tsx rename to client/slices/internal-tx/pages/tx/TxInternalsTable.tsx index 8c0c043cd7..c67f2a8185 100644 --- a/ui/tx/internals/TxInternalsTable.tsx +++ b/client/slices/internal-tx/pages/tx/TxInternalsTable.tsx @@ -1,12 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; + +import { AddressHighlightProvider } from 'client/slices/address/contexts/address-highlight'; + +import { currencyUnits } from 'client/shared/chain/units'; -import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; -import { currencyUnits } from 'lib/units'; import { TableBody, TableColumnHeader, TableColumnHeaderSortable, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; -import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem'; -import type { Sort, SortField } from 'ui/tx/internals/utils'; + +import type { Sort, SortField } from '../../utils/utils'; +import TxInternalsTableItem from './TxInternalsTableItem'; interface Props { data: Array; diff --git a/ui/tx/internals/TxInternalsTableItem.tsx b/client/slices/internal-tx/pages/tx/TxInternalsTableItem.tsx similarity index 83% rename from ui/tx/internals/TxInternalsTableItem.tsx rename to client/slices/internal-tx/pages/tx/TxInternalsTableItem.tsx index 725ee28311..5a72a80476 100644 --- a/ui/tx/internals/TxInternalsTableItem.tsx +++ b/client/slices/internal-tx/pages/tx/TxInternalsTableItem.tsx @@ -1,14 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex } from '@chakra-ui/react'; import React from 'react'; -import type { InternalTransaction } from 'types/api/internalTransaction'; +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; + +import AddressFromTo from 'client/slices/address/components/from-to/AddressFromTo'; +import TxStatus from 'client/slices/tx/components/TxStatus'; import { Badge } from 'toolkit/chakra/badge'; import { TableCell, TableRow } from 'toolkit/chakra/table'; -import AddressFromTo from 'ui/shared/address/AddressFromTo'; -import TxStatus from 'ui/shared/statusTag/TxStatus'; import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; -import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; + +import { TX_INTERNALS_ITEMS } from '../../utils/utils'; type Props = InternalTransaction & { isLoading?: boolean; diff --git a/ui/tx/__screenshots__/TxInternals.pw.tsx_default_base-view-mobile-1.png b/client/slices/internal-tx/pages/tx/__screenshots__/TxInternals.pw.tsx_default_base-view-mobile-1.png similarity index 100% rename from ui/tx/__screenshots__/TxInternals.pw.tsx_default_base-view-mobile-1.png rename to client/slices/internal-tx/pages/tx/__screenshots__/TxInternals.pw.tsx_default_base-view-mobile-1.png diff --git a/ui/tx/__screenshots__/TxInternals.pw.tsx_mobile_base-view-mobile-1.png b/client/slices/internal-tx/pages/tx/__screenshots__/TxInternals.pw.tsx_mobile_base-view-mobile-1.png similarity index 100% rename from ui/tx/__screenshots__/TxInternals.pw.tsx_mobile_base-view-mobile-1.png rename to client/slices/internal-tx/pages/tx/__screenshots__/TxInternals.pw.tsx_mobile_base-view-mobile-1.png diff --git a/client/slices/internal-tx/stubs.ts b/client/slices/internal-tx/stubs.ts new file mode 100644 index 0000000000..a09244f499 --- /dev/null +++ b/client/slices/internal-tx/stubs.ts @@ -0,0 +1,19 @@ +import type { InternalTransaction } from 'client/slices/internal-tx/types/api'; + +import { ADDRESS_PARAMS } from 'client/slices/address/stubs/address-params'; +import { TX_HASH } from 'client/slices/tx/stubs/tx'; + +export const INTERNAL_TX: InternalTransaction = { + block_number: 9006105, + created_contract: null, + error: null, + from: ADDRESS_PARAMS, + gas_limit: '754278', + index: 1, + success: true, + timestamp: '2023-05-15T20:14:00.000000Z', + to: ADDRESS_PARAMS, + transaction_hash: TX_HASH, + type: 'staticcall', + value: '22324344900000000', +}; diff --git a/client/slices/internal-tx/types/api.ts b/client/slices/internal-tx/types/api.ts new file mode 100644 index 0000000000..e3f33a1430 --- /dev/null +++ b/client/slices/internal-tx/types/api.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { AddressParam } from 'client/slices/address/types/api'; + +export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'; + +export type InternalTransaction = ( + { + to: AddressParam; + created_contract: null; + } | + { + to: null; + created_contract: AddressParam; + } +) & { + error: string | null; + success: boolean; + type: TxInternalsType; + transaction_hash: string; + from: AddressParam; + value: string; + index: number; + block_number: number; + timestamp: string; + gas_limit: string; +}; + +export interface InternalTransactionsResponse { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + transaction_hash: string; + transaction_index: number; + } | null; +} + +export interface InternalTransactionFilters { + transaction_hash: string; +} diff --git a/client/slices/internal-tx/utils/utils.ts b/client/slices/internal-tx/utils/utils.ts new file mode 100644 index 0000000000..6f13d0cacb --- /dev/null +++ b/client/slices/internal-tx/utils/utils.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { TxInternalsType } from 'client/slices/internal-tx/types/api'; + +export type Sort = 'value-asc' | 'value-desc' | 'gas-limit-asc' | 'gas-limit-desc' | 'default'; +export type SortField = 'value' | 'gas-limit'; + +interface TxInternalsTypeItem { + title: string; + id: TxInternalsType; +} + +export const TX_INTERNALS_ITEMS: Array = [ + { title: 'Call', id: 'call' }, + { title: 'Delegate call', id: 'delegatecall' }, + { title: 'Static call', id: 'staticcall' }, + { title: 'Create', id: 'create' }, + { title: 'Create2', id: 'create2' }, + { title: 'Self-destruct', id: 'selfdestruct' }, + { title: 'Reward', id: 'reward' }, +]; diff --git a/ui/shared/logs/LogDecodedInputData.pw.tsx b/client/slices/log/components/LogDecodedInputData.pw.tsx similarity index 89% rename from ui/shared/logs/LogDecodedInputData.pw.tsx rename to client/slices/log/components/LogDecodedInputData.pw.tsx index d5cd7b2201..7ac46b9c17 100644 --- a/ui/shared/logs/LogDecodedInputData.pw.tsx +++ b/client/slices/log/components/LogDecodedInputData.pw.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import * as mocks from 'mocks/txs/decodedInputData'; +import * as mocks from 'client/slices/log/mocks/decoded-input'; + import { test, expect } from 'playwright/lib'; import LogDecodedInputData from './LogDecodedInputData'; diff --git a/ui/shared/logs/LogDecodedInputData.tsx b/client/slices/log/components/LogDecodedInputData.tsx similarity index 86% rename from ui/shared/logs/LogDecodedInputData.tsx rename to client/slices/log/components/LogDecodedInputData.tsx index 5921b71061..21d34efee1 100644 --- a/ui/shared/logs/LogDecodedInputData.tsx +++ b/client/slices/log/components/LogDecodedInputData.tsx @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { DecodedInput } from 'types/api/decodedInput'; +import type { DecodedInput } from 'client/slices/log/types/api'; import LogDecodedInputDataHeader from './LogDecodedInputDataHeader'; import LogDecodedInputDataTable from './LogDecodedInputDataTable'; diff --git a/ui/shared/logs/LogDecodedInputDataHeader.tsx b/client/slices/log/components/LogDecodedInputDataHeader.tsx similarity index 97% rename from ui/shared/logs/LogDecodedInputDataHeader.tsx rename to client/slices/log/components/LogDecodedInputDataHeader.tsx index 09dfe7ad3e..114dc7df89 100644 --- a/ui/shared/logs/LogDecodedInputDataHeader.tsx +++ b/client/slices/log/components/LogDecodedInputDataHeader.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { FlexProps } from '@chakra-ui/react'; import { Separator, Flex, VStack } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/logs/LogDecodedInputDataTable.tsx b/client/slices/log/components/LogDecodedInputDataTable.tsx similarity index 94% rename from ui/shared/logs/LogDecodedInputDataTable.tsx rename to client/slices/log/components/LogDecodedInputDataTable.tsx index b4da096050..3a4368ec7a 100644 --- a/ui/shared/logs/LogDecodedInputDataTable.tsx +++ b/client/slices/log/components/LogDecodedInputDataTable.tsx @@ -1,13 +1,16 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Flex, Grid } from '@chakra-ui/react'; import React from 'react'; -import type { DecodedInput } from 'types/api/decodedInput'; +import type { DecodedInput } from 'client/slices/log/types/api'; import type { ArrayElement } from 'types/utils'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + import { Skeleton } from 'toolkit/chakra/skeleton'; import { TruncatedText } from 'toolkit/components/truncation/TruncatedText'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; interface Props { data: DecodedInput['parameters']; diff --git a/ui/shared/logs/LogIndex.tsx b/client/slices/log/components/LogIndex.tsx similarity index 94% rename from ui/shared/logs/LogIndex.tsx rename to client/slices/log/components/LogIndex.tsx index ed793b973f..6920168e23 100644 --- a/ui/shared/logs/LogIndex.tsx +++ b/client/slices/log/components/LogIndex.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { HTMLChakraProps } from '@chakra-ui/react'; import { Center } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/shared/logs/LogItem.pw.tsx b/client/slices/log/components/LogItem.pw.tsx similarity index 92% rename from ui/shared/logs/LogItem.pw.tsx rename to client/slices/log/components/LogItem.pw.tsx index b98794b842..da7a04a488 100644 --- a/ui/shared/logs/LogItem.pw.tsx +++ b/client/slices/log/components/LogItem.pw.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import * as addressMocks from 'mocks/address/address'; -import * as inputDataMocks from 'mocks/txs/decodedInputData'; +import * as addressMocks from 'client/slices/address/mocks/address'; +import * as inputDataMocks from 'client/slices/log/mocks/decoded-input'; + import { test, expect } from 'playwright/lib'; import LogItem from './LogItem'; diff --git a/ui/shared/logs/LogItem.tsx b/client/slices/log/components/LogItem.tsx similarity index 92% rename from ui/shared/logs/LogItem.tsx rename to client/slices/log/components/LogItem.tsx index 0faddf7453..3fd41ba294 100644 --- a/ui/shared/logs/LogItem.tsx +++ b/client/slices/log/components/LogItem.tsx @@ -1,27 +1,30 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Grid, GridItem } from '@chakra-ui/react'; import React from 'react'; -import type { Log } from 'types/api/log'; +import type { TransactionLog } from 'client/slices/log/types/api'; import type { ClusterChainConfig } from 'types/multichain'; import { route } from 'nextjs-routes'; // import searchIcon from 'icons/search.svg'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import TxEntity from 'client/slices/tx/components/entity/TxEntity'; + import { Alert } from 'toolkit/chakra/alert'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { space } from 'toolkit/utils/htmlEntities'; import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; -import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData'; -import LogTopic from 'ui/shared/logs/LogTopic'; import type { DataType } from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData'; +import LogDecodedInputData from './LogDecodedInputData'; import LogIndex from './LogIndex'; +import LogTopic from './LogTopic'; -type Props = Log & { +type Props = TransactionLog & { type: 'address' | 'transaction'; isLoading?: boolean; defaultDataType?: DataType; diff --git a/ui/shared/logs/LogTopic.pw.tsx b/client/slices/log/components/LogTopic.pw.tsx similarity index 100% rename from ui/shared/logs/LogTopic.pw.tsx rename to client/slices/log/components/LogTopic.pw.tsx diff --git a/ui/shared/logs/LogTopic.tsx b/client/slices/log/components/LogTopic.tsx similarity index 91% rename from ui/shared/logs/LogTopic.tsx rename to client/slices/log/components/LogTopic.tsx index 65dfe6cd21..aff3a694a1 100644 --- a/ui/shared/logs/LogTopic.tsx +++ b/client/slices/log/components/LogTopic.tsx @@ -1,13 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { createListCollection, Flex } from '@chakra-ui/react'; import { capitalize } from 'es-toolkit'; import React from 'react'; -import hexToAddress from 'lib/hexToAddress'; -import hexToUtf8 from 'lib/hexToUtf8'; +import AddressEntity from 'client/slices/address/components/entity/AddressEntity'; + +import hexToAddress from 'client/shared/transformers/hex-to-address'; +import hexToUtf8 from 'client/shared/transformers/hex-to-utf8'; + import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select'; import { Skeleton } from 'toolkit/chakra/skeleton'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import LogIndex from './LogIndex'; diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png b/client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png rename to client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png b/client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png rename to client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png b/client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png rename to client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png b/client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png rename to client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png b/client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png rename to client/slices/log/components/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png b/client/slices/log/components/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png rename to client/slices/log/components/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png b/client/slices/log/components/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png rename to client/slices/log/components/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-default-data-type-1.png b/client/slices/log/components/__screenshots__/LogItem.pw.tsx_default_with-default-data-type-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-default-data-type-1.png rename to client/slices/log/components/__screenshots__/LogItem.pw.tsx_default_with-default-data-type-1.png diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_without-decoded-input-data-mobile-1.png b/client/slices/log/components/__screenshots__/LogItem.pw.tsx_default_without-decoded-input-data-mobile-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_without-decoded-input-data-mobile-1.png rename to client/slices/log/components/__screenshots__/LogItem.pw.tsx_default_without-decoded-input-data-mobile-1.png diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png b/client/slices/log/components/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png rename to client/slices/log/components/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_without-decoded-input-data-mobile-1.png b/client/slices/log/components/__screenshots__/LogItem.pw.tsx_mobile_without-decoded-input-data-mobile-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_without-decoded-input-data-mobile-1.png rename to client/slices/log/components/__screenshots__/LogItem.pw.tsx_mobile_without-decoded-input-data-mobile-1.png diff --git a/ui/shared/logs/__screenshots__/LogTopic.pw.tsx_mobile_address-view-mobile---default-1.png b/client/slices/log/components/__screenshots__/LogTopic.pw.tsx_mobile_address-view-mobile---default-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogTopic.pw.tsx_mobile_address-view-mobile---default-1.png rename to client/slices/log/components/__screenshots__/LogTopic.pw.tsx_mobile_address-view-mobile---default-1.png diff --git a/ui/shared/logs/__screenshots__/LogTopic.pw.tsx_mobile_hex-view-mobile---default-1.png b/client/slices/log/components/__screenshots__/LogTopic.pw.tsx_mobile_hex-view-mobile---default-1.png similarity index 100% rename from ui/shared/logs/__screenshots__/LogTopic.pw.tsx_mobile_hex-view-mobile---default-1.png rename to client/slices/log/components/__screenshots__/LogTopic.pw.tsx_mobile_hex-view-mobile---default-1.png diff --git a/client/slices/log/mocks/decoded-input.ts b/client/slices/log/mocks/decoded-input.ts new file mode 100644 index 0000000000..04504b8f90 --- /dev/null +++ b/client/slices/log/mocks/decoded-input.ts @@ -0,0 +1,49 @@ +import type { DecodedInput } from '../types/api'; + +export const withoutIndexedFields: DecodedInput = { + method_call: 'CreditSpended(uint256 _type, uint256 _quantity)', + method_id: '58cdf94a', + parameters: [ + { + name: '_type', + type: 'uint256', + value: '3', + }, + { + name: '_quantity', + type: 'uint256', + value: '1', + }, + ], +}; + +export const withIndexedFields: DecodedInput = { + method_call: 'Transfer(address indexed from, address indexed to, uint256 value)', + method_id: 'ddf252ad', + parameters: [ + { + indexed: true, + name: 'from', + type: 'address', + value: '0xd789a607ceac2f0e14867de4eb15b15c9ffb5859', + }, + { + indexed: true, + name: 'to', + type: 'address', + value: '0x7d20a8d54f955b4483a66ab335635ab66e151c51', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + value: '31567373703130350', + }, + { + indexed: true, + name: 'inputArray', + type: 'uint256[2][2]', + value: [ [ '1', '1' ], [ '1', '1' ] ], + }, + ], +}; diff --git a/client/slices/log/stubs/log.ts b/client/slices/log/stubs/log.ts new file mode 100644 index 0000000000..9cd0c8e701 --- /dev/null +++ b/client/slices/log/stubs/log.ts @@ -0,0 +1,36 @@ +import type { TransactionLog } from '../types/api'; + +import { ADDRESS_PARAMS } from 'client/slices/address/stubs/address-params'; +import { TX_HASH } from 'client/slices/tx/stubs/tx'; + +export const LOG: TransactionLog = { + address: ADDRESS_PARAMS, + data: '0x000000000000000000000000000000000000000000000000000000d75e4be200', + decoded: { + method_call: 'CreditSpended(uint256 indexed _type, uint256 _quantity)', + method_id: '58cdf94a', + parameters: [ + { + indexed: true, + name: '_type', + type: 'uint256', + value: 'placeholder', + }, + { + indexed: false, + name: '_quantity', + type: 'uint256', + value: 'placeholder', + }, + ], + }, + index: 42, + topics: [ + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + '0x000000000000000000000000c52ea157a7fb3e25a069d47df0428ac70cd656b1', + '0x000000000000000000000000302fd86163cb9ad5533b3952dafa3b633a82bc51', + null, + ], + transaction_hash: TX_HASH, + block_timestamp: '2022-02-02T12:00:00Z', +}; diff --git a/client/slices/log/types/api.ts b/client/slices/log/types/api.ts new file mode 100644 index 0000000000..a0251e7e23 --- /dev/null +++ b/client/slices/log/types/api.ts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { AddressParam } from 'client/slices/address/types/api'; + +export interface DecodedInput { + method_call: string; + method_id: string; + parameters: Array; +} + +export interface DecodedInputParams { + name: string; + type: string; + value: string | Array | Record; + indexed?: boolean; +} + +export interface TransactionLog { + address: AddressParam; + topics: Array; + data: string; + index: number; + decoded: DecodedInput | null; + transaction_hash: string | null; + block_timestamp: string | null; +} + +export interface LogsResponseTx { + items: Array; + next_page_params: { + index: number; + items_count: number; + transaction_hash: string; + } | null; +} + +export interface LogsResponseAddress { + items: Array; + next_page_params: { + index: number; + items_count: number; + transaction_index: number; + block_number: number; + } | null; +} diff --git a/ui/snippets/searchBar/SearchBarBackdrop.tsx b/client/slices/search/components/search-bar/SearchBarBackdrop.tsx similarity index 90% rename from ui/snippets/searchBar/SearchBarBackdrop.tsx rename to client/slices/search/components/search-bar/SearchBarBackdrop.tsx index 957bf60572..721c5fcdcd 100644 --- a/ui/snippets/searchBar/SearchBarBackdrop.tsx +++ b/client/slices/search/components/search-bar/SearchBarBackdrop.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import React from 'react'; diff --git a/ui/snippets/searchBar/SearchBarDesktop.pw.tsx b/client/slices/search/components/search-bar/SearchBarDesktop.pw.tsx similarity index 91% rename from ui/snippets/searchBar/SearchBarDesktop.pw.tsx rename to client/slices/search/components/search-bar/SearchBarDesktop.pw.tsx index 97a5b63d1b..92b788f9ee 100644 --- a/ui/snippets/searchBar/SearchBarDesktop.pw.tsx +++ b/client/slices/search/components/search-bar/SearchBarDesktop.pw.tsx @@ -1,7 +1,14 @@ import React from 'react'; +import * as searchMock from 'client/slices/search/mocks'; + +import { metatag1, metatag2, metatag3 } from 'client/features/address-metadata/mocks/search'; +import { tacOperation1 } from 'client/features/chain-variants/tac/mocks/search'; +import { blob1 } from 'client/features/data-availability/mocks/search'; +import { domain1 } from 'client/features/name-services/domains/stubs/search'; +import { userOp1 } from 'client/features/user-ops/mocks/search'; + import { apps as appsMock } from 'mocks/apps/apps'; -import * as searchMock from 'mocks/search/index'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect } from 'playwright/lib'; @@ -78,9 +85,9 @@ test('search by address hash', async({ render, page, mockApiResponse }) => { test('search by meta tag +@dark-mode', async({ render, page, mockApiResponse }) => { const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.metatag1, - searchMock.metatag2, - searchMock.metatag3, + metatag1, + metatag2, + metatag3, ], { queryParams: { q: 'utko' } }); await render(); await page.getByPlaceholder(/search/i).fill('utko'); @@ -126,10 +133,10 @@ test('search by tx hash', async({ render, page, mockApiResponse }) => { test('search by tac operation hash', async({ render, page, mockApiResponse }) => { const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.tacOperation1, - ], { queryParams: { q: searchMock.tacOperation1.tac_operation.operation_id } }); + tacOperation1, + ], { queryParams: { q: tacOperation1.tac_operation.operation_id } }); await render(); - await page.getByPlaceholder(/search/i).fill(searchMock.tacOperation1.tac_operation.operation_id); + await page.getByPlaceholder(/search/i).fill(tacOperation1.tac_operation.operation_id); await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); @@ -138,10 +145,10 @@ test('search by tac operation hash', async({ render, page, mockApiResponse }) => test('search by blob hash', async({ render, page, mockApiResponse, mockEnvs }) => { await mockEnvs(ENVS_MAP.dataAvailability); const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.blob1, - ], { queryParams: { q: searchMock.blob1.blob_hash } }); + blob1, + ], { queryParams: { q: blob1.blob_hash } }); await render(); - await page.getByPlaceholder(/search/i).fill(searchMock.blob1.blob_hash); + await page.getByPlaceholder(/search/i).fill(blob1.blob_hash); await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); @@ -150,11 +157,11 @@ test('search by blob hash', async({ render, page, mockApiResponse, mockEnvs }) = test('search by domain name', async({ render, page, mockApiResponse, mockEnvs }) => { await mockEnvs(ENVS_MAP.nameService); const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.domain1, - ], { queryParams: { q: searchMock.domain1.ens_info.name } }); + domain1, + ], { queryParams: { q: domain1.ens_info.name } }); await render(); - await page.getByPlaceholder(/search/i).fill(searchMock.domain1.ens_info.name); + await page.getByPlaceholder(/search/i).fill(domain1.ens_info.name); await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); @@ -163,7 +170,7 @@ test('search by domain name', async({ render, page, mockApiResponse, mockEnvs }) test('search by user op hash', async({ render, page, mockApiResponse, mockEnvs }) => { await mockEnvs(ENVS_MAP.userOps); const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.userOp1, + userOp1, ], { queryParams: { q: searchMock.tx1.transaction_hash } }); await render(); await page.getByPlaceholder(/search/i).fill(searchMock.tx1.transaction_hash); diff --git a/ui/snippets/searchBar/SearchBarDesktop.tsx b/client/slices/search/components/search-bar/SearchBarDesktop.tsx similarity index 94% rename from ui/snippets/searchBar/SearchBarDesktop.tsx rename to client/slices/search/components/search-bar/SearchBarDesktop.tsx index ca4dac661d..ccd2cf2fbd 100644 --- a/ui/snippets/searchBar/SearchBarDesktop.tsx +++ b/client/slices/search/components/search-bar/SearchBarDesktop.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { useClickAway } from '@uidotdev/usehooks'; import { debounce } from 'es-toolkit'; import { useRouter } from 'next/router'; @@ -7,9 +9,13 @@ import React from 'react'; import type { Route } from 'nextjs-routes'; import { route } from 'nextjs-routes'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import * as mixpanel from 'lib/mixpanel/index'; -import { getRecentSearchKeywords, saveToRecentKeywords } from 'lib/recentSearchKeywords'; +import { getRecentSearchKeywords, saveToRecentKeywords } from 'client/slices/search/utils/recent-search-keywords'; + +import useSearchWithClusters from 'client/features/name-services/clusters/hooks/useSearchWithClusters'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import { Link } from 'toolkit/chakra/link'; import { PopoverBody, PopoverContent, PopoverFooter, PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover'; import { useDisclosure } from 'toolkit/hooks/useDisclosure'; @@ -18,7 +24,6 @@ import SearchBarBackdrop from './SearchBarBackdrop'; import SearchBarInput from './SearchBarInput'; import SearchBarRecentKeywords from './SearchBarRecentKeywords'; import SearchBarSuggest from './SearchBarSuggest/SearchBarSuggest'; -import useSearchWithClusters from './useSearchWithClusters'; type Props = { isHeroBanner?: boolean; diff --git a/ui/snippets/searchBar/SearchBarInput.pw.tsx b/client/slices/search/components/search-bar/SearchBarInput.pw.tsx similarity index 100% rename from ui/snippets/searchBar/SearchBarInput.pw.tsx rename to client/slices/search/components/search-bar/SearchBarInput.pw.tsx diff --git a/ui/snippets/searchBar/SearchBarInput.tsx b/client/slices/search/components/search-bar/SearchBarInput.tsx similarity index 97% rename from ui/snippets/searchBar/SearchBarInput.tsx rename to client/slices/search/components/search-bar/SearchBarInput.tsx index eb606037c9..bee9b435a7 100644 --- a/ui/snippets/searchBar/SearchBarInput.tsx +++ b/client/slices/search/components/search-bar/SearchBarInput.tsx @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import type { HTMLChakraProps } from '@chakra-ui/react'; import { chakra, Center } from '@chakra-ui/react'; import React from 'react'; import type { ChangeEvent, FormEvent, FocusEvent } from 'react'; +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import config from 'configs/app'; -import useIsMobile from 'lib/hooks/useIsMobile'; import { useColorModeValue } from 'toolkit/chakra/color-mode'; import { Input } from 'toolkit/chakra/input'; import { InputGroup } from 'toolkit/chakra/input-group'; diff --git a/ui/snippets/searchBar/SearchBarMobile.pw.tsx b/client/slices/search/components/search-bar/SearchBarMobile.pw.tsx similarity index 92% rename from ui/snippets/searchBar/SearchBarMobile.pw.tsx rename to client/slices/search/components/search-bar/SearchBarMobile.pw.tsx index dbc711f30c..d1640d34ab 100644 --- a/ui/snippets/searchBar/SearchBarMobile.pw.tsx +++ b/client/slices/search/components/search-bar/SearchBarMobile.pw.tsx @@ -1,8 +1,15 @@ import type { Page } from '@playwright/test'; import React from 'react'; +import * as searchMock from 'client/slices/search/mocks'; + +import { metatag1, metatag2, metatag3 } from 'client/features/address-metadata/mocks/search'; +import { tacOperation1 } from 'client/features/chain-variants/tac/mocks/search'; +import { blob1 } from 'client/features/data-availability/mocks/search'; +import { domain1 } from 'client/features/name-services/domains/stubs/search'; +import { userOp1 } from 'client/features/user-ops/mocks/search'; + import { apps as appsMock } from 'mocks/apps/apps'; -import * as searchMock from 'mocks/search/index'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { test, expect, devices } from 'playwright/lib'; @@ -108,9 +115,9 @@ test('search by address hash', async({ render, page, mockApiResponse }) => { test('search by meta tag', async({ render, page, mockApiResponse }) => { const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.metatag1, - searchMock.metatag2, - searchMock.metatag3, + metatag1, + metatag2, + metatag3, ], { queryParams: { q: 'utko' } }); await render(); await openSearchDrawer(page); @@ -160,11 +167,11 @@ test('search by tx hash', async({ render, page, mockApiResponse }) => { test('search by tac operation hash', async({ render, page, mockApiResponse }) => { const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.tacOperation1, - ], { queryParams: { q: searchMock.tacOperation1.tac_operation.operation_id } }); + tacOperation1, + ], { queryParams: { q: tacOperation1.tac_operation.operation_id } }); await render(); await openSearchDrawer(page); - await getSearchInput(page).fill(searchMock.tacOperation1.tac_operation.operation_id); + await getSearchInput(page).fill(tacOperation1.tac_operation.operation_id); await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 600 } }); @@ -173,11 +180,11 @@ test('search by tac operation hash', async({ render, page, mockApiResponse }) => test('search by blob hash', async({ render, page, mockApiResponse, mockEnvs }) => { await mockEnvs(ENVS_MAP.dataAvailability); const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.blob1, - ], { queryParams: { q: searchMock.blob1.blob_hash } }); + blob1, + ], { queryParams: { q: blob1.blob_hash } }); await render(); await openSearchDrawer(page); - await getSearchInput(page).fill(searchMock.blob1.blob_hash); + await getSearchInput(page).fill(blob1.blob_hash); await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 600 } }); @@ -186,12 +193,12 @@ test('search by blob hash', async({ render, page, mockApiResponse, mockEnvs }) = test('search by domain name', async({ render, page, mockApiResponse, mockEnvs }) => { await mockEnvs(ENVS_MAP.nameService); const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.domain1, - ], { queryParams: { q: searchMock.domain1.ens_info.name } }); + domain1, + ], { queryParams: { q: domain1.ens_info.name } }); await render(); await openSearchDrawer(page); - await getSearchInput(page).fill(searchMock.domain1.ens_info.name); + await getSearchInput(page).fill(domain1.ens_info.name); await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 600 } }); @@ -200,7 +207,7 @@ test('search by domain name', async({ render, page, mockApiResponse, mockEnvs }) test('search by user op hash', async({ render, page, mockApiResponse, mockEnvs }) => { await mockEnvs(ENVS_MAP.userOps); const apiUrl = await mockApiResponse('general:quick_search', [ - searchMock.userOp1, + userOp1, ], { queryParams: { q: searchMock.tx1.transaction_hash } }); await render(); await openSearchDrawer(page); diff --git a/ui/snippets/searchBar/SearchBarMobile.tsx b/client/slices/search/components/search-bar/SearchBarMobile.tsx similarity index 94% rename from ui/snippets/searchBar/SearchBarMobile.tsx rename to client/slices/search/components/search-bar/SearchBarMobile.tsx index 5533e9e475..3c7447ce82 100644 --- a/ui/snippets/searchBar/SearchBarMobile.tsx +++ b/client/slices/search/components/search-bar/SearchBarMobile.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import type { FormEvent } from 'react'; @@ -6,8 +8,11 @@ import React from 'react'; import type { Route } from 'nextjs-routes'; import { route } from 'nextjs-routes'; -import * as mixpanel from 'lib/mixpanel/index'; -import { getRecentSearchKeywords, saveToRecentKeywords } from 'lib/recentSearchKeywords'; +import useQuickSearchQuery from 'client/slices/search/hooks/useQuickSearchQuery'; +import { getRecentSearchKeywords, saveToRecentKeywords } from 'client/slices/search/utils/recent-search-keywords'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; + import { Button } from 'toolkit/chakra/button'; import { DrawerRoot, @@ -22,11 +27,10 @@ import { import { Link } from 'toolkit/chakra/link'; import { useDisclosure } from 'toolkit/hooks/useDisclosure'; import IconSvg from 'ui/shared/IconSvg'; -import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput'; +import SearchBarInput from './SearchBarInput'; import SearchBarRecentKeywords from './SearchBarRecentKeywords'; import SearchBarSuggest from './SearchBarSuggest/SearchBarSuggest'; -import useQuickSearchQuery from './useQuickSearchQuery'; type Props = { isHeroBanner?: boolean; diff --git a/ui/snippets/searchBar/SearchBarRecentKeywords.tsx b/client/slices/search/components/search-bar/SearchBarRecentKeywords.tsx similarity index 93% rename from ui/snippets/searchBar/SearchBarRecentKeywords.tsx rename to client/slices/search/components/search-bar/SearchBarRecentKeywords.tsx index 619a4678ed..bb0470314c 100644 --- a/ui/snippets/searchBar/SearchBarRecentKeywords.tsx +++ b/client/slices/search/components/search-bar/SearchBarRecentKeywords.tsx @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, Text } from '@chakra-ui/react'; import React from 'react'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import { clearRecentSearchKeywords, getRecentSearchKeywords, removeRecentSearchKeyword } from 'lib/recentSearchKeywords'; +import { clearRecentSearchKeywords, getRecentSearchKeywords, removeRecentSearchKeyword } from 'client/slices/search/utils/recent-search-keywords'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; + import { Link } from 'toolkit/chakra/link'; import { ClearButton } from 'toolkit/components/buttons/ClearButton'; import TextAd from 'ui/shared/ad/TextAd'; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggest.tsx similarity index 92% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggest.tsx index e3ad7811fa..51fa4d971c 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggest.tsx @@ -1,30 +1,36 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Box, Flex, Text } from '@chakra-ui/react'; import type { UseQueryResult } from '@tanstack/react-query'; import { debounce } from 'es-toolkit'; import React from 'react'; import type { ListCctxsResponse } from '@blockscout/zetachain-cctx-types'; -import type { QuickSearchResultItem } from 'types/client/search'; +import type { QuickSearchResultItem } from 'client/slices/search/types/client'; + +import type { ResourceError } from 'client/api/resources'; + +import type { ApiCategory, Category, ItemsCategoriesMap } from 'client/slices/search/utils/search-categories'; +import { getItemCategory, searchCategories } from 'client/slices/search/utils/search-categories'; + +import ExternalSearchItem from 'client/features/chain-variants/zeta-chain/components/ExternalSearchItem'; +import SearchBarSuggestZetaChainCCTX from 'client/features/chain-variants/zeta-chain/components/SearchBarSuggestZetaChainCCTX'; +import type { ExternalSearchItem as ExternalSearchItemType } from 'client/features/chain-variants/zeta-chain/utils/external-search'; +import SearchBarSuggestApp from 'client/features/marketplace/components/SearchBarSuggestApp'; + +import useIsMobile from 'client/shared/hooks/useIsMobile'; import config from 'configs/app'; import multichainConfig from 'configs/multichain'; -import type { ResourceError } from 'lib/api/resources'; import { useSettingsContext } from 'lib/contexts/settings'; -import useIsMobile from 'lib/hooks/useIsMobile'; -import type { ExternalSearchItem as ExternalSearchItemType } from 'lib/search/externalSearch'; import AdaptiveTabs from 'toolkit/components/AdaptiveTabs/AdaptiveTabs'; import { ContentLoader } from 'toolkit/components/loaders/ContentLoader'; import * as regexp from 'toolkit/utils/regexp'; import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps'; import TextAd from 'ui/shared/ad/TextAd'; -import ExternalSearchItem from 'ui/shared/search/ExternalSearchItem'; -import type { ApiCategory, Category, ItemsCategoriesMap } from 'ui/shared/search/utils'; -import { getItemCategory, searchCategories } from 'ui/shared/search/utils'; -import SearchBarSuggestApp from './SearchBarSuggestApp'; import SearchBarSuggestBlockCountdown from './SearchBarSuggestBlockCountdown'; import SearchBarSuggestItem from './SearchBarSuggestItem'; -import SearchBarSuggestZetaChainCCTX from './SearchBarSuggestZetaChainCCTX'; const TABS_HEIGHT = 72; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestAddress.tsx similarity index 88% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestAddress.tsx index 3dc0839c27..51c1b305b4 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestAddress.tsx @@ -1,18 +1,22 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Box, Text, Flex, Grid } from '@chakra-ui/react'; import React from 'react'; import type { ItemsProps } from './types'; -import type { SearchResultAddressOrContract, SearchResultMetadataTag } from 'types/api/search'; +import type { SearchResultAddressOrContract, SearchResultMetadataTag } from 'client/slices/search/types/api'; import type * as multichain from 'types/client/multichainAggregator'; -import { toBech32Address } from 'lib/address/bech32'; +import * as AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import { toBech32Address } from 'client/slices/address/utils/bech32'; +import ContractCertifiedLabel from 'client/slices/contract/components/ContractCertifiedLabel'; +import SearchResultEntityTag from 'client/slices/search/pages/search-results/SearchResultEntityTag'; + +import highlightText from 'client/shared/text/highlight-text'; + import dayjs from 'lib/date/dayjs'; -import highlightText from 'lib/highlightText'; import * as contract from 'lib/multichain/contract'; import { ADDRESS_REGEXP } from 'toolkit/utils/regexp'; -import SearchResultEntityTag from 'ui/searchResults/SearchResultEntityTag'; -import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; -import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; type Props = ItemsProps; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlock.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestBlock.tsx similarity index 91% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlock.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestBlock.tsx index b122028728..e0ee483924 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlock.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestBlock.tsx @@ -1,13 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Text, Flex, Grid, Box } from '@chakra-ui/react'; import React from 'react'; import type { ItemsProps } from './types'; +import type { SearchResultBlock } from 'client/slices/search/types/client'; import type * as multichain from 'types/client/multichainAggregator'; -import type { SearchResultBlock } from 'types/client/search'; -import highlightText from 'lib/highlightText'; +import * as BlockEntity from 'client/slices/block/components/entity/BlockEntity'; + +import highlightText from 'client/shared/text/highlight-text'; + import { Tag } from 'toolkit/chakra/tag'; -import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import Time from 'ui/shared/time/Time'; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlockCountdown.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestBlockCountdown.tsx similarity index 82% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlockCountdown.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestBlockCountdown.tsx index 0ab7e29ea6..966bad2f9c 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlockCountdown.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestBlockCountdown.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Box } from '@chakra-ui/react'; import React from 'react'; @@ -17,7 +19,7 @@ const SearchBarSuggestBlockCountdown = ({ blockHeight, onClick, className, isMul if (isMultichain) { return ( - This block hasn’t been created yet. View existing blocks. + This block hasn't been created yet. View existing blocks. ); } diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestItem.tsx similarity index 86% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestItem.tsx index 10af1d3c06..7157edf5f3 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestItem.tsx @@ -1,24 +1,28 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { QuickSearchResultItem } from 'types/client/search'; -import type { AddressFormat } from 'types/views/address'; +import type { AddressFormat } from 'client/slices/address/types/config'; +import type { QuickSearchResultItem } from 'client/slices/search/types/client'; import { route } from 'nextjs/routes'; +import { isEvmAddress } from 'client/slices/address/utils/is-evm-address'; + +import SearchBarSuggestTacOperation from 'client/features/chain-variants/tac/components/SearchBarSuggestTacOperation'; +import SearchBarSuggestBlob from 'client/features/data-availability/components/SearchBarSuggestBlob'; +import SearchBarSuggestCluster from 'client/features/name-services/clusters/components/SearchBarSuggestCluster'; +import SearchBarSuggestDomain from 'client/features/name-services/domains/components/SearchBarSuggestDomain'; +import SearchBarSuggestUserOp from 'client/features/user-ops/components/SearchBarSuggestUserOp'; + import multichainConfig from 'configs/multichain'; -import { isEvmAddress } from 'lib/address/isEvmAddress'; import SearchBarSuggestAddress from './SearchBarSuggestAddress'; -import SearchBarSuggestBlob from './SearchBarSuggestBlob'; import SearchBarSuggestBlock from './SearchBarSuggestBlock'; -import SearchBarSuggestCluster from './SearchBarSuggestCluster'; -import SearchBarSuggestDomain from './SearchBarSuggestDomain'; import SearchBarSuggestItemLink from './SearchBarSuggestItemLink'; import SearchBarSuggestLabel from './SearchBarSuggestLabel'; -import SearchBarSuggestTacOperation from './SearchBarSuggestTacOperation'; import SearchBarSuggestToken from './SearchBarSuggestToken'; import SearchBarSuggestTx from './SearchBarSuggestTx'; -import SearchBarSuggestUserOp from './SearchBarSuggestUserOp'; interface Props { data: QuickSearchResultItem; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItemLink.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestItemLink.tsx similarity index 94% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItemLink.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestItemLink.tsx index cfb75dd31c..6f3aaad53c 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItemLink.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestItemLink.tsx @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; import type { LinkProps } from 'toolkit/chakra/link'; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestLabel.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestLabel.tsx similarity index 87% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestLabel.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestLabel.tsx index 04f51855aa..8b505a622b 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestLabel.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestLabel.tsx @@ -1,11 +1,15 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Grid, Text, Flex } from '@chakra-ui/react'; import React from 'react'; import type { ItemsProps } from './types'; -import type { SearchResultLabel } from 'types/api/search'; +import type { SearchResultLabel } from 'client/slices/search/types/api'; + +import { toBech32Address } from 'client/slices/address/utils/bech32'; + +import highlightText from 'client/shared/text/highlight-text'; -import { toBech32Address } from 'lib/address/bech32'; -import highlightText from 'lib/highlightText'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import IconSvg from 'ui/shared/IconSvg'; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestToken.tsx similarity index 90% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestToken.tsx index 0d08d3ff81..5b00be0cc4 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestToken.tsx @@ -1,16 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { Grid, Text, Flex } from '@chakra-ui/react'; import { mapValues } from 'es-toolkit'; import React from 'react'; import type { ItemsProps } from './types'; -import type { SearchResultToken } from 'types/api/search'; +import type { SearchResultToken } from 'client/slices/search/types/api'; import type * as multichain from 'types/client/multichainAggregator'; -import { toBech32Address } from 'lib/address/bech32'; -import highlightText from 'lib/highlightText'; +import { toBech32Address } from 'client/slices/address/utils/bech32'; +import ContractCertifiedLabel from 'client/slices/contract/components/ContractCertifiedLabel'; +import * as TokenEntity from 'client/slices/token/components/entity/TokenEntity'; + +import highlightText from 'client/shared/text/highlight-text'; + import * as contract from 'lib/multichain/contract'; -import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; -import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import IconSvg from 'ui/shared/IconSvg'; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestTx.tsx b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestTx.tsx similarity index 87% rename from ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestTx.tsx rename to client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestTx.tsx index 7725084bd7..15ff973cf4 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestTx.tsx +++ b/client/slices/search/components/search-bar/SearchBarSuggest/SearchBarSuggestTx.tsx @@ -1,11 +1,14 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import { chakra, Text, Flex } from '@chakra-ui/react'; import React from 'react'; import type { ItemsProps } from './types'; -import type { SearchResultTx } from 'types/api/search'; +import type { SearchResultTx } from 'client/slices/search/types/api'; import type * as multichain from 'types/client/multichainAggregator'; -import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; +import * as TxEntity from 'client/slices/tx/components/entity/TxEntity'; + import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import Time from 'ui/shared/time/Time'; diff --git a/client/slices/search/components/search-bar/SearchBarSuggest/types.ts b/client/slices/search/components/search-bar/SearchBarSuggest/types.ts new file mode 100644 index 0000000000..2d4b5a12be --- /dev/null +++ b/client/slices/search/components/search-bar/SearchBarSuggest/types.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import type { AddressFormat } from 'client/slices/address/types/config'; +import type { ClusterChainConfig } from 'types/multichain'; + +export interface ItemsProps { + data: Data; + searchTerm: string; + isMobile?: boolean | undefined; + addressFormat?: AddressFormat; + chainInfo?: ClusterChainConfig; +} diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-contract-name-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-contract-name-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-contract-name-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-contract-name-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-meta-tag-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-meta-tag-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-meta-tag-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-meta-tag-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-tag-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-tag-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-tag-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-tag-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-no-results-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-no-results-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-no-results-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-no-results-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-with-results-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-with-results-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-with-results-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_block-countdown-with-results-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_recent-keywords-suggest-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_recent-keywords-suggest-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_recent-keywords-suggest-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_recent-keywords-suggest-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_scroll-suggest-to-category-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_scroll-suggest-to-category-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_scroll-suggest-to-category-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_scroll-suggest-to-category-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-address-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-address-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-address-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-address-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-blob-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-blob-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-blob-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-blob-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-number-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-number-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-number-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-block-number-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-contract-name-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-contract-name-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-contract-name-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-contract-name-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-domain-name-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-domain-name-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-domain-name-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-domain-name-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-meta-tag-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-meta-tag-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-meta-tag-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-meta-tag-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-name-homepage-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-name-homepage-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-name-homepage-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-name-homepage-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tac-operation-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tac-operation-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tac-operation-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tac-operation-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tag-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tag-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tag-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tag-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-token-name-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-token-name-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-token-name-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-token-name-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tx-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tx-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tx-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-tx-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-user-op-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-user-op-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-user-op-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-by-user-op-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-with-view-all-link-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-with-view-all-link-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-with-view-all-link-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_search-with-view-all-link-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_with-apps-default-view-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_with-apps-default-view-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarDesktop.pw.tsx_default_with-apps-default-view-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarDesktop.pw.tsx_default_with-apps-default-view-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-home-page-mobile-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-home-page-mobile-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-home-page-mobile-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-home-page-mobile-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-regular-page-mobile-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-regular-page-mobile-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-regular-page-mobile-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_dark-color-mode_input-on-regular-page-mobile-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-home-page-mobile-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-home-page-mobile-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-home-page-mobile-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-home-page-mobile-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-regular-page-mobile-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-regular-page-mobile-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-regular-page-mobile-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_default_input-on-regular-page-mobile-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-home-page-mobile-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-home-page-mobile-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-home-page-mobile-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-home-page-mobile-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-regular-page-mobile-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-regular-page-mobile-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-regular-page-mobile-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarInput.pw.tsx_mobile_input-on-regular-page-mobile-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-name-homepage-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_dark-color-mode_search-by-token-name-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-no-results-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-no-results-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-no-results-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-no-results-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-with-results-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-with-results-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-with-results-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_block-countdown-with-results-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_recent-keywords-suggest-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_recent-keywords-suggest-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_recent-keywords-suggest-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_recent-keywords-suggest-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_scroll-suggest-to-category-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_scroll-suggest-to-category-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_scroll-suggest-to-category-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_scroll-suggest-to-category-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-address-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-address-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-address-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-address-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-blob-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-blob-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-blob-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-blob-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-number-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-number-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-number-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-block-number-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-contract-name-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-contract-name-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-contract-name-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-contract-name-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-domain-name-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-domain-name-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-domain-name-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-domain-name-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-meta-tag-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-meta-tag-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-meta-tag-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-meta-tag-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-name-homepage-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-name-homepage-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-name-homepage-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-name-homepage-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tac-operation-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tac-operation-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tac-operation-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tac-operation-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tag-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tag-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tag-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tag-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-token-name-dark-mode-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-token-name-dark-mode-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-token-name-dark-mode-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-token-name-dark-mode-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tx-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tx-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tx-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-tx-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-user-op-hash-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-user-op-hash-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-user-op-hash-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-by-user-op-hash-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-with-view-all-link-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-with-view-all-link-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_search-with-view-all-link-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_search-with-view-all-link-1.png diff --git a/ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_with-apps-default-view-1.png b/client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_with-apps-default-view-1.png similarity index 100% rename from ui/snippets/searchBar/__screenshots__/SearchBarMobile.pw.tsx_default_with-apps-default-view-1.png rename to client/slices/search/components/search-bar/__screenshots__/SearchBarMobile.pw.tsx_default_with-apps-default-view-1.png diff --git a/client/slices/search/hooks/useQuickSearchQuery.ts b/client/slices/search/hooks/useQuickSearchQuery.ts new file mode 100644 index 0000000000..8ff03c5230 --- /dev/null +++ b/client/slices/search/hooks/useQuickSearchQuery.ts @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import { isBech32Address, fromBech32Address } from 'client/slices/address/utils/bech32'; + +import { getExternalSearchItem } from 'client/features/chain-variants/zeta-chain/utils/external-search'; +import useSearchMultichain from 'client/features/multichain/hooks/useSearchMultichain'; + +import useDebounce from 'client/shared/hooks/useDebounce'; + +import config from 'configs/app'; +import multichainConfig from 'configs/multichain'; + +export default function useQuickSearchQuery() { + const [ searchTerm, setSearchTerm ] = React.useState(''); + + const debouncedSearchTerm = useDebounce(searchTerm, 300); + + const isMultichain = React.useMemo(() => { + return Boolean(multichainConfig()); + }, []); + + const mainQuery = useApiQuery('general:quick_search', { + queryParams: { q: isBech32Address(debouncedSearchTerm) ? fromBech32Address(debouncedSearchTerm) : debouncedSearchTerm }, + queryOptions: { + enabled: debouncedSearchTerm.trim().length > 0 && !isMultichain, + }, + }); + + const multichainQuery = useSearchMultichain({ searchTerm: debouncedSearchTerm, enabled: isMultichain }); + + const redirectCheckQuery = useApiQuery('general:search_check_redirect', { + // on pages with regular search bar we check redirect on every search term change + // in order to prepend its result to suggest list since this resource is much faster than regular search + queryParams: { q: debouncedSearchTerm }, + queryOptions: { enabled: Boolean(debouncedSearchTerm) && !isMultichain }, + }); + + const zetaChainCCTXQuery = useApiQuery('zetachain:transactions', { + queryParams: { + hash: debouncedSearchTerm, + limit: 10, + offset: 0, + direction: 'DESC', + }, + queryOptions: { enabled: debouncedSearchTerm.trim().length > 0 && config.features.zetachain.isEnabled }, + }); + + const query = isMultichain ? multichainQuery : mainQuery; + + return React.useMemo(() => ({ + searchTerm, + debouncedSearchTerm, + handleSearchTermChange: setSearchTerm, + query, + redirectCheckQuery, + externalSearchItem: getExternalSearchItem(debouncedSearchTerm), + zetaChainCCTXQuery, + }), [ debouncedSearchTerm, query, redirectCheckQuery, searchTerm, zetaChainCCTXQuery ]); +} diff --git a/client/slices/search/hooks/useSearchQuery.ts b/client/slices/search/hooks/useSearchQuery.ts new file mode 100644 index 0000000000..79566f9d4b --- /dev/null +++ b/client/slices/search/hooks/useSearchQuery.ts @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { useRouter } from 'next/router'; +import React from 'react'; + +import useApiQuery from 'client/api/hooks/useApiQuery'; + +import { fromBech32Address, isBech32Address } from 'client/slices/address/utils/bech32'; +import { SEARCH_RESULT_ITEM, SEARCH_RESULT_NEXT_PAGE_PARAMS } from 'client/slices/search/stubs'; + +import { getExternalSearchItem } from 'client/features/chain-variants/zeta-chain/utils/external-search'; + +import useDebounce from 'client/shared/hooks/useDebounce'; +import useUpdateValueEffect from 'client/shared/hooks/useUpdateValueEffect'; +import getQueryParamString from 'client/shared/router/get-query-param-string'; + +import config from 'configs/app'; +import { generateListStub } from 'stubs/utils'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +export default function useSearchQuery(withRedirectCheck?: boolean) { + const router = useRouter(); + const q = React.useRef(getQueryParamString(router.query.q)); + const initialValue = q.current; + + const [ searchTerm, setSearchTerm ] = React.useState(initialValue); + + const debouncedSearchTerm = useDebounce(searchTerm, 300); + const pathname = router.pathname; + + const query = useQueryWithPages({ + resourceName: 'general:search', + filters: { q: isBech32Address(debouncedSearchTerm) ? fromBech32Address(debouncedSearchTerm) : debouncedSearchTerm }, + options: { + enabled: debouncedSearchTerm.trim().length > 0, + placeholderData: generateListStub<'general:search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }), + }, + }); + + const redirectCheckQuery = useApiQuery('general:search_check_redirect', { + // on search result page we check redirect only once on mount + queryParams: { q: q.current }, + queryOptions: { enabled: Boolean(q.current) && withRedirectCheck }, + }); + + const zetaChainCCTXQuery = useApiQuery('zetachain:transactions', { + queryParams: { + hash: debouncedSearchTerm, + limit: 50, + offset: 0, + direction: 'DESC', + }, + queryOptions: { enabled: config.features.zetachain.isEnabled && debouncedSearchTerm.trim().length > 0 }, + }); + + useUpdateValueEffect(() => { + query.onFilterChange({ q: debouncedSearchTerm }); + }, debouncedSearchTerm); + + return React.useMemo(() => ({ + searchTerm, + debouncedSearchTerm, + handleSearchTermChange: setSearchTerm, + query, + redirectCheckQuery, + pathname, + zetaChainCCTXQuery, + externalSearchItem: getExternalSearchItem(debouncedSearchTerm), + }), [ debouncedSearchTerm, pathname, query, redirectCheckQuery, searchTerm, zetaChainCCTXQuery ]); +} diff --git a/client/slices/search/mocks.ts b/client/slices/search/mocks.ts new file mode 100644 index 0000000000..1b3dad474e --- /dev/null +++ b/client/slices/search/mocks.ts @@ -0,0 +1,148 @@ +import type { + SearchResultToken, + SearchResultBlock, + SearchResultAddressOrContract, + SearchResultTx, + SearchResultLabel, + SearchResult, +} from 'client/slices/search/types/api'; + +import { metatag1 } from 'client/features/address-metadata/mocks/search'; +import { tacOperation1 } from 'client/features/chain-variants/tac/mocks/search'; +import { blob1 } from 'client/features/data-availability/mocks/search'; +import { domain1 } from 'client/features/name-services/domains/stubs/search'; + +export const token1: SearchResultToken = { + address_hash: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + name: 'Toms NFT', + symbol: 'TNT', + token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + type: 'token' as const, + icon_url: 'http://localhost:3000/token-icon.png', + token_type: 'ERC-721', + total_supply: '10000001', + exchange_rate: null, + is_verified_via_admin_panel: true, + is_smart_contract_verified: true, + is_smart_contract_address: true, + reputation: 'ok', +}; + +export const token2: SearchResultToken = { + address_hash: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + name: 'TomToken', + symbol: 'pdE1B', + token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + type: 'token' as const, + icon_url: null, + token_type: 'ERC-20', + total_supply: '10000001', + exchange_rate: '1.11', + is_verified_via_admin_panel: false, + is_smart_contract_verified: false, + is_smart_contract_address: false, + reputation: 'ok', +}; + +export const block1: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', + block_number: 8198536, + type: 'block' as const, + timestamp: '2022-12-11T17:55:20Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', +}; + +export const block2: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', + block_number: 8198536, + block_type: 'reorg', + type: 'block' as const, + timestamp: '2022-12-11T18:55:20Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', +}; + +export const block3: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3', + block_number: 8198536, + block_type: 'uncle', + type: 'block' as const, + timestamp: '2022-12-11T18:11:11Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3', +}; + +export const address1: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: null, + type: 'address' as const, + is_smart_contract_verified: false, + is_smart_contract_address: false, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const address2: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + name: null, + type: 'address' as const, + is_smart_contract_verified: false, + is_smart_contract_address: false, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + ens_info: { + address_hash: '0x1234567890123456789012345678901234567890', + expiry_date: '2022-12-11T17:55:20Z', + name: 'utko.eth', + names_count: 1, + }, +}; + +export const contract1: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'Unknown contract in this network', + type: 'contract' as const, + is_smart_contract_verified: true, + is_smart_contract_address: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const contract2: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'Super utko', + type: 'contract' as const, + is_smart_contract_verified: true, + certified: true, + is_smart_contract_address: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const label1: SearchResultLabel = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'utko', + type: 'label' as const, + is_smart_contract_verified: true, + is_smart_contract_address: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const tx1: SearchResultTx = { + transaction_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', + type: 'transaction' as const, + timestamp: '2022-12-11T17:55:20Z', + url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', +}; + +export const baseResponse: SearchResult = { + items: [ + token1, + token2, + block1, + address1, + contract1, + tx1, + blob1, + domain1, + metatag1, + tacOperation1, + ], + next_page_params: null, +}; diff --git a/ui/searchResults/SearchResultEntityTag.tsx b/client/slices/search/pages/search-results/SearchResultEntityTag.tsx similarity index 79% rename from ui/searchResults/SearchResultEntityTag.tsx rename to client/slices/search/pages/search-results/SearchResultEntityTag.tsx index 906f677f20..8ebb1d7388 100644 --- a/ui/searchResults/SearchResultEntityTag.tsx +++ b/client/slices/search/pages/search-results/SearchResultEntityTag.tsx @@ -1,8 +1,11 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + import React from 'react'; -import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; +import type { AddressMetadataTagApi } from 'client/features/address-metadata/types/api'; + +import highlightText from 'client/shared/text/highlight-text'; -import highlightText from 'lib/highlightText'; import type { TagProps } from 'toolkit/chakra/tag'; import { Tag } from 'toolkit/chakra/tag'; import EntityTagIcon from 'ui/shared/EntityTags/EntityTagIcon'; diff --git a/client/slices/search/pages/search-results/SearchResultListItem.tsx b/client/slices/search/pages/search-results/SearchResultListItem.tsx new file mode 100644 index 0000000000..3a0f046a40 --- /dev/null +++ b/client/slices/search/pages/search-results/SearchResultListItem.tsx @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { chakra, Flex, Grid, Box, Text } from '@chakra-ui/react'; +import React from 'react'; +import xss from 'xss'; + +import type { AddressFormat } from 'client/slices/address/types/config'; +import type { SearchResultItem } from 'client/slices/search/types/client'; + +import { route } from 'nextjs-routes'; + +import * as AddressEntity from 'client/slices/address/components/entity/AddressEntity'; +import { toBech32Address } from 'client/slices/address/utils/bech32'; +import * as BlockEntity from 'client/slices/block/components/entity/BlockEntity'; +import ContractCertifiedLabel from 'client/slices/contract/components/ContractCertifiedLabel'; +import { saveToRecentKeywords } from 'client/slices/search/utils/recent-search-keywords'; +import type { SearchResultAppItem } from 'client/slices/search/utils/search-categories'; +import { getItemCategory, searchItemTitles } from 'client/slices/search/utils/search-categories'; +import * as TokenEntity from 'client/slices/token/components/entity/TokenEntity'; +import * as TxEntity from 'client/slices/tx/components/entity/TxEntity'; + +import * as TacOperationEntity from 'client/features/chain-variants/tac/components/TacOperationEntity'; +import TacOperationStatus from 'client/features/chain-variants/tac/components/TacOperationStatus'; +import * as BlobEntity from 'client/features/data-availability/components/entity/BlobEntity'; +import * as UserOpEntity from 'client/features/user-ops/components/entity/UserOpEntity'; + +import * as mixpanel from 'client/shared/analytics/mixpanel'; +import highlightText from 'client/shared/text/highlight-text'; + +import dayjs from 'lib/date/dayjs'; +import { useColorMode } from 'toolkit/chakra/color-mode'; +import { Image } from 'toolkit/chakra/image'; +import { Link } from 'toolkit/chakra/link'; +import { Skeleton } from 'toolkit/chakra/skeleton'; +import { Tag } from 'toolkit/chakra/tag'; +import { SECOND } from 'toolkit/utils/consts'; +import { ADDRESS_REGEXP } from 'toolkit/utils/regexp'; +import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; +import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; +import IconSvg from 'ui/shared/IconSvg'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import Time from 'ui/shared/time/Time'; + +import SearchResultEntityTag from './SearchResultEntityTag'; + +interface Props { + data: SearchResultItem | SearchResultAppItem; + searchTerm: string; + isLoading?: boolean; + addressFormat?: AddressFormat; +} + +const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Props) => { + + const handleLinkClick = React.useCallback((e: React.MouseEvent) => { + saveToRecentKeywords(searchTerm); + mixpanel.logEvent(mixpanel.EventTypes.SEARCH_QUERY, { + 'Search query': searchTerm, + 'Source page type': 'Search results', + 'Result URL': e.currentTarget.href, + }); + }, [ searchTerm ]); + + const { colorMode } = useColorMode(); + + const firstRow = (() => { + switch (data.type) { + case 'token': { + const name = data.name + (data.symbol ? ` (${ data.symbol })` : ''); + + return ( + + + + + + { data.certified && } + { data.is_verified_via_admin_panel && !data.certified && } + { data.reputation && } + + ); + } + + case 'metadata_tag': + case 'contract': + case 'address': { + const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); + const hash = addressFormat === 'bech32' ? toBech32Address(data.address_hash) : data.address_hash; + + const address = { + hash: data.address_hash, + filecoin: { + robust: data.filecoin_robust_address, + }, + is_contract: data.type === 'contract' || data.is_smart_contract_address, + is_verified: data.is_smart_contract_verified, + name: null, + implementations: null, + ens_domain_name: null, + }; + + return ( + + + + + + + + ); + } + + case 'label': { + return ( + + + + + + + ); + } + + case 'app': { + const title = ; + return ( + + { + + { title } + + + ); + } + + case 'block': { + const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase(); + const isFutureBlock = data.timestamp === undefined; + const href = isFutureBlock ? + route({ pathname: '/block/countdown/[height]', query: { height: String(data.block_number) } }) : + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: data.block_hash ?? String(data.block_number) } }); + + return ( + + + + + + { data.block_type === 'reorg' && !isLoading && Reorg } + { data.block_type === 'uncle' && !isLoading && Uncle } + + ); + } + + case 'transaction': { + return ( + + + + + + + ); + } + + case 'zetaChainCCTX': { + return ( + + + + + + + ); + } + + case 'tac_operation': { + return ( + + + + + + + + ); + } + + case 'blob': { + return ( + + + + + + + ); + } + + case 'user_operation': { + return ( + + + + + + + ); + } + + case 'ens_domain': { + return ( + + + + + + + ); + } + } + })(); + + const secondRow = (() => { + switch (data.type) { + case 'token': { + const templateCols = `1fr + ${ (data.token_type === 'ERC-20' && data.exchange_rate) || (data.token_type !== 'ERC-20' && data.total_supply) ? ' auto' : '' }`; + const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address_hash) : data.address_hash); + + return ( + + + + + + { data.is_smart_contract_verified && } + + + { data.token_type === 'ERC-20' && data.exchange_rate && `$${ Number(data.exchange_rate).toLocaleString() }` } + { data.token_type !== 'ERC-20' && data.total_supply && `Items ${ Number(data.total_supply).toLocaleString() }` } + + + ); + } + case 'block': { + const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase(); + const isFutureBlock = data.timestamp === undefined; + + if (isFutureBlock) { + return Learn estimated time for this block to be created.; + } + + return ( + <> + + + + + + + ); + } + case 'transaction': { + return ( +