Commit 10d5794c authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge branch 'main' into tab-routing

parents 3a5184ea 11eb4feb
__snapshots__/** filter=lfs diff=lfs merge=lfs -text
name: playwright
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.27.0-focal
steps:
- run: apt-get update && apt-get install git-lfs
- uses: actions/checkout@v3
with:
lfs: 'true'
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: yarn
- name: Run your tests
run: HOME=/root yarn test-ct
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: playwright-report
path: playwright-report
\ No newline at end of file
......@@ -28,6 +28,7 @@ yarn-error.log*
# local env files
.env*.local
/configs/envs/.env.secrets
# vercel
.vercel
......@@ -39,3 +40,6 @@ yarn-error.log*
# Sentry
.sentryclirc
/test-results/
/playwright-report/
/playwright/.cache/
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; }
git lfs post-checkout "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; }
git lfs post-commit "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; }
git lfs post-merge "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; }
git lfs pre-push "$@"
{
"recommendations": [
"formulahendry.auto-close-tag",
"formulahendry.auto-rename-tag",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
]
}
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": [],
"label": "dev server",
"detail": "start local dev server",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiGreen",
"id": "server-process"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "typescript",
"label": "tsc build",
"detail": "run ts typechecking",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"icon": {
"color": "terminal.ansiCyan",
"id": "symbol-type-parameter"
},
"presentation": {
"reveal": "never",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"group": "build",
}
]
}
\ No newline at end of file
......@@ -9,6 +9,7 @@ Core technologies what used in the project are
- [Next.js](https://nextjs.org/) as application framework
- [Chakra](https://chakra-ui.com/) as component library; our theme customization could be found in `/theme` folder
- [css-modules](https://github.com/css-modules/css-modules) as lib for styling custom components
- [playwright](https://playwright.dev/) as a tool for components visual testing
And of course our premier language is [Typescript](https://www.typescriptlang.org/)
......@@ -21,6 +22,11 @@ For local development please follow next steps:
- create local env file `.env.local` according to `.env.example` snapshot (see list of used environment variables [below](#environment-variables))
- run `yarn dev` to spin up local dev server and navigate to the host from logs output
## Components visual testing
We use [playwright experimental components testing](https://playwright.dev/docs/test-components) for visual (screenshots) CI check. Test renders a single component in headless browser in docker, generates screenshots and then compares this screenshot with a reference one.
To perform testing locally you need to install docker and run `yarn test-docker`
## Environment variables
### Variables list
The app instance could be customized by passing following variables to NodeJS environment.
......
[
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "easy-staking",
"title": "Easy Staking Revoke.cash",
......@@ -18,7 +20,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "curve",
"title": "Curve Revoke.cash",
......@@ -36,7 +40,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "honwyswap",
"title": "HonwySwap Revoke.cash",
......@@ -54,7 +60,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "sushi",
"title": "Sushi Revoke.cash",
......@@ -72,7 +80,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
100
],
"author": "xDaichain",
"id": "bao-finance",
"title": "Bao Finance",
......@@ -90,7 +100,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "component",
"title": "Component",
......@@ -108,7 +120,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "pooltogether",
"title": "PoolTogether",
......@@ -126,7 +140,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "swapr",
"title": "Swapr",
......@@ -144,7 +160,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "levinswap",
"title": "Levinswap",
......@@ -162,7 +180,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "omen",
"title": "Omen",
......@@ -180,7 +200,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "nifty-ink",
"title": "Nifty Ink",
......@@ -198,7 +220,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "treasure-chess",
"title": "Treasure Chess",
......@@ -216,7 +240,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "unique-one",
"title": "Unique.One",
......@@ -234,7 +260,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "cold-truth-culture",
"title": "Cold Truth Culture",
......@@ -252,7 +280,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "xdai-bridge",
"title": "xDai Bridge",
......@@ -270,7 +300,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "omni-bridge",
"title": "OmniBridge",
......@@ -288,7 +320,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "gnosis-safe",
"title": "Gnosis Safe",
......@@ -306,7 +340,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "multisender",
"title": "Multisender",
......@@ -324,7 +360,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 1,
"chainIds": [
1
],
"author": "xDaichain",
"id": "disperse",
"title": "Disperse",
......@@ -342,7 +380,9 @@
"github": "https://github.com/mikhin"
},
{
"chainId": 99,
"chainIds": [
99
],
"author": "xDaichain",
"id": "symmetric",
"title": "Symmetric",
......
/* eslint-disable max-len */
export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'success' as TxStatus,
status: 'ok' as Transaction['status'],
block_num: 15006918,
confirmation_num: 283,
confirmation_duration: 30,
......@@ -52,4 +52,4 @@ export const tx = {
export type TxType = 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
export type TxStatus = 'success' | 'failed' | 'pending';
import type { Transaction } from 'types/api/transaction';
import type { TxInternalsType } from 'types/api/tx';
export const data = [
{
id: 1,
type: 'call' as TxInternalsType,
status: 'success' as const,
from: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
to: { hash: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e' },
value: 0.25207646303,
gasLimit: 369472,
},
{
id: 2,
type: 'delegate_call' as TxInternalsType,
status: 'success' as const,
from: { hash: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
to: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
value: 0.5633333,
gasLimit: 340022,
},
{
id: 3,
type: 'static_call' as TxInternalsType,
status: 'failed' as const,
from: { hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830' },
to: { hash: '0x35317007D203b8a86CA727ad44E473E40450E378' },
value: 0.421152366,
gasLimit: 509333,
},
];
export const data = [
{
address: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
topics: [
{ hex: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' },
{ hex: '0x000000000000000000000000def171fe48cf0115b1d80b88dc8eab59176fee57' },
{ hex: '0x000000000000000000000000c465c0a16228ef6fe1bf29c04fdb04bb797fd537' },
],
data: '0x000000000000000000000000000000000000000000000000019faae14eb88000',
},
{
address: '0x73968b9a57c6e53d41345fd57a6e6ae27d6cdb2f',
topics: [
{ hex: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' },
{ hex: '0x000000000000000000000000c465c0a16228ef6fe1bf29c04fdb04bb797fd537' },
{ hex: '0x0000000000000000000000008453d9385af5f49edad9905345cd2411b5c5831b' },
],
data: '0x000000000000000000000000000000000000000000000013b6ee62022c95ced4',
},
];
import type { Transaction } from 'types/api/transaction';
import { tx } from './tx';
import type { TxType, TxStatus } from './tx';
import type { TxType } from './tx';
export const txs = [
{
......@@ -10,7 +12,7 @@ export const txs = [
},
{
...tx,
status: 'failed' as TxStatus,
status: 'error' as Transaction['status'],
errorText: 'Error: (Awaiting internal transactions for reason)',
txType: 'contract-call' as TxType,
method: 'CommitHash CommitHash CommitHash CommitHash',
......@@ -25,7 +27,7 @@ export const txs = [
},
{
...tx,
status: 'pending' as TxStatus,
status: null,
txType: 'token-transfer' as TxType,
method: 'Multicall',
address_from: {
......
import BigNumber from 'bignumber.js';
export const WEI = new BigNumber(10 ** 18);
export const GWEI = new BigNumber(10 ** 9);
export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
......@@ -22,7 +22,7 @@ export default function useNavItems() {
const selectedNetwork = useNetwork();
const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainId === selectedNetwork?.chainId),
marketplaceApps.filter(item => item.chainIds.includes(selectedNetwork?.chainId)),
[ selectedNetwork?.chainId ])
.length > 0;
......
export default function getConfirmationString(durations: Array<number>) {
if (durations.length === 0) {
return '';
}
const [ lower, upper ] = durations.map((time) => time / 1_000);
if (!upper) {
return `Confirmed within ${ lower } secs`;
}
if (lower === 0) {
return `Confirmed within <= ${ upper } secs`;
}
return `Confirmed within ${ lower } - ${ upper } secs`;
}
......@@ -13,7 +13,9 @@
"start": "next start",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons"
"format-svg": "./node_modules/.bin/svgo -r ./icons",
"test-ct": "playwright test -c playwright-ct.config.ts",
"test-docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.27.0-focal ./run-tests.sh"
},
"dependencies": {
"@chakra-ui/react": "2.3.1",
......@@ -24,7 +26,9 @@
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10",
"@types/react-scroll": "^1.8.4",
"bignumber.js": "^9.1.0",
"dayjs": "^1.11.5",
"ethers": "^5.7.1",
"framer-motion": "^6",
"lodash": "^4.0.0",
"next": "12.2.5",
......@@ -40,6 +44,7 @@
"use-font-face-observer": "^1.2.1"
},
"devDependencies": {
"@playwright/experimental-ct-react": "^1.26.1",
"@types/node": "17.0.36",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
......@@ -51,6 +56,7 @@
"eslint-plugin-regexp": "^1.7.0",
"husky": "^8.0.0",
"lint-staged": ">=10",
"playwright": "^1.26.1",
"svgo": "^2.8.0",
"typescript": "4.7.2"
},
......
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/internal-transactions`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/logs`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/raw-trace`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react';
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './',
testMatch: /.*\.pw\.tsx/,
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: './__snapshots__',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: Boolean(process.env.CI),
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
headless: true,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
],
};
export default config;
import { ChakraProvider } from '@chakra-ui/react';
import type { ColorMode } from '@chakra-ui/react';
import React from 'react';
import theme from '../theme';
type Props = {
children: React.ReactNode;
colorMode?: ColorMode;
}
const RenderWithChakra = ({ children, colorMode = 'light' }: Props) => {
return (
<ChakraProvider theme={{ ...theme, config: { ...theme.config, initialColorMode: colorMode } }}>
{ children }
</ChakraProvider>
);
};
export default RenderWithChakra;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/playwright/index.ts"></script>
</body>
</html>
// Import styles, initialize component theme here.
// import '../src/common.css';
export {};
#!/bin/sh
yarn install
yarn test-ct
\ No newline at end of file
import { formAnatomy as parts } from '@chakra-ui/anatomy';
import {
createMultiStyleConfigHelpers,
} from '@chakra-ui/styled-system';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor, mode } from '@chakra-ui/theme-tools';
import type { Dict } from '@chakra-ui/utils';
import getDefaultFormColors from '../utils/getDefaultFormColors';
import FormLabel from './FormLabel';
import Input from './Input';
import Textarea from './Textarea';
const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys);
const getActiveLabelStyles = (theme: Dict, fc: string, bc: string, size: 'md' | 'lg') => {
const baseStyles = {
backgroundColor: bc,
color: getColor(theme, fc),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
};
switch (size) {
case 'md': {
return {
...baseStyles,
padding: '10px 16px 2px 16px',
};
}
case 'lg': {
return {
...baseStyles,
padding: '16px 24px 2px 24px',
};
}
}
};
const getDefaultLabelStyles = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return {
fontSize: 'md',
lineHeight: '20px',
padding: '18px 16px',
right: '18px',
};
}
case 'lg': {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 24px',
right: '26px',
};
}
}
};
const getPaddingX = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return '16px';
}
function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) {
const { theme } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
case 'lg': {
return '24px';
const activeLabelStyles = {
...FormLabel.variants?.floating?.(props)._focusWithin,
...FormLabel.sizes?.[size](props)._focusWithin,
} || {};
const activeInputStyles = (() => {
switch (size) {
case 'md': {
return {
paddingTop: '26px',
paddingBottom: '10px',
};
}
case 'lg': {
return {
paddingTop: '38px',
paddingBottom: '18px',
};
}
}
}
};
})();
const getActiveInputStyles = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return {
paddingTop: '26px',
paddingBottom: '10px',
};
}
const inputPx = (() => {
switch (size) {
case 'md': {
return '16px';
}
case 'lg': {
return {
paddingTop: '38px',
paddingBottom: '18px',
};
case 'lg': {
return '24px';
}
}
}
};
const variantFloating = definePartsStyle((props) => {
const { theme, backgroundColor, size = 'md' } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
const px = getPaddingX(size);
const activeInputStyles = getActiveInputStyles(size);
const activeLabelStyles = getActiveLabelStyles(theme, fc, bc, size);
})();
return {
container: {
// active styles
_focusWithin: {
label: {
...activeLabelStyles,
},
'input, textarea': {
...activeInputStyles,
},
'label .chakra-form__required-indicator': {
color: getColor(theme, fc),
},
},
// label's styles
label: {
...getDefaultLabelStyles(size),
left: '2px',
top: '2px',
zIndex: 2,
position: 'absolute',
borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent',
pointerEvents: 'none',
margin: 0,
transformOrigin: 'top left',
transitionProperty: 'font-size, line-height, padding, top, background-color',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': {
...activeLabelStyles,
label: activeLabelStyles,
'input, textarea': activeInputStyles,
},
// label styles
label: FormLabel.sizes?.[size](props) || {},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': activeLabelStyles,
'input[aria-invalid=true] + label, textarea[aria-invalid=true] + label': {
color: getColor(theme, ec),
},
// input's styles
// input styles
input: Input.sizes?.[size].field,
textarea: Textarea.sizes?.[size],
'input, textarea': {
padding: px,
padding: inputPx,
},
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
'input[disabled] + label, textarea[disabled] + label': {
backgroundColor: 'transparent',
},
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': {
...activeInputStyles,
},
// indicator's styles
// indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
color: getColor(theme, fc),
},
......@@ -153,6 +84,11 @@ const variantFloating = definePartsStyle((props) => {
color: getColor(theme, ec),
},
},
};
}
const baseStyle = definePartsStyle((props) => {
return {
requiredIndicator: {
marginStart: 0,
color: mode('gray.500', 'whiteAlpha.400')(props),
......@@ -160,12 +96,42 @@ const variantFloating = definePartsStyle((props) => {
};
});
const variantFloating = definePartsStyle((props) => {
return {
container: {
label: FormLabel.variants?.floating(props) || {},
},
};
});
const sizes = {
lg: definePartsStyle((props) => {
if (props.variant === 'floating') {
return getFloatingVariantStylesForSize('lg', props);
}
return {};
}),
md: definePartsStyle((props) => {
if (props.variant === 'floating') {
return getFloatingVariantStylesForSize('md', props);
}
return {};
}),
};
const variants = {
floating: variantFloating,
};
const Form = defineMultiStyleConfig({
baseStyle,
variants,
sizes,
defaultProps: {
size: 'md',
},
});
export default Form;
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({
fontSize: 'md',
marginEnd: '3',
mb: '2',
fontWeight: 'medium',
transitionProperty: 'common',
transitionDuration: 'normal',
opacity: 1,
_disabled: {
opacity: 0.4,
},
});
const variantFloating = defineStyle((props) => {
const { theme, backgroundColor } = props;
const { focusColor: fc } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
return {
left: '2px',
top: '2px',
zIndex: 2,
position: 'absolute',
borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent',
pointerEvents: 'none',
margin: 0,
transformOrigin: 'top left',
transitionProperty: 'font-size, line-height, padding, top, background-color',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
_focusWithin: {
backgroundColor: bc,
color: getColor(theme, fc),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
},
};
});
const variants = {
floating: variantFloating,
};
const sizes = {
lg: defineStyle((props) => {
if (props.variant === 'floating') {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 24px',
right: '26px',
_focusWithin: {
padding: '16px 24px 2px 24px',
},
};
}
return {};
}),
md: defineStyle((props) => {
if (props.variant === 'floating') {
return {
fontSize: 'md',
lineHeight: '20px',
padding: '18px 16px',
right: '18px',
_focusWithin: {
padding: '10px 16px 2px 16px',
},
};
}
return {};
}),
};
const FormLabel = defineStyleConfig({
variants,
baseStyle,
sizes,
});
export default FormLabel;
......@@ -4,6 +4,7 @@ import Button from './Button';
import Checkbox from './Checkbox';
import Drawer from './Drawer';
import Form from './Form';
import FormLabel from './FormLabel';
import Heading from './Heading';
import Input from './Input';
import Link from './Link';
......@@ -27,6 +28,7 @@ const components = {
Heading,
Input,
Form,
FormLabel,
Link,
Modal,
Popover,
......
import type { AddressParam } from 'types/api/addressParams';
import type { Reward } from 'types/api/reward';
export interface Block {
height: number;
timestamp: string;
tx_count: number;
miner: AddressParam;
size: number;
hash: string;
parent_hash: string;
difficulty: number;
total_difficulty: number;
gas_used: number;
gas_limit: number;
nonce: number;
base_fee_per_gas: number | null;
burnt_fees: number | null;
priority_fee: number | null;
extra_data: string | null;
state_root: string | null;
rewards?: Array<Reward>;
gas_target_percentage: number | null;
gas_used_percentage: number | null;
burnt_fees_percentage: number | null;
type: 'block' | 'reorg' | 'uncle';
tx_fees: string | null;
uncles_hashes: Array<string>;
}
export interface BlockResponse {
items: Array<Block>;
next_page_params: {
block_number: number;
items_count: number;
};
}
......@@ -8,4 +8,5 @@ export interface DecodedInputParams {
name: string;
type: string;
value: string;
indexed?: boolean;
}
import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction {
error?: string;
error: string | null;
success: boolean;
type: string;
type: TxInternalsType;
transaction_hash: string;
from: AddressParam;
to: AddressParam;
......
......@@ -6,7 +6,7 @@ export interface Log {
topics: Array<string>;
data: string;
index: number;
decoded?: DecodedInput;
decoded: DecodedInput | null;
}
export interface LogsResponse {
......
......@@ -11,7 +11,7 @@ export interface RawTrace {
gasUsed: string;
output: string;
};
error?: string;
error: string | null;
subtraces: number;
traceAddress: Array<number>;
type: string;
......
export interface Reward {
reward: number;
type: 'Miner Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
}
......@@ -11,5 +11,5 @@ export interface TokenTransfer {
total: {
value: string;
};
exchangeRate: number;
exchange_rate: string;
}
......@@ -6,7 +6,7 @@ export type Tokenlist = {
export type TokenlistItem = {
balance: number;
contractAddress: string;
decimals?: number;
decimals: number | null;
id: number;
name: string;
symbol: string;
......
......@@ -7,7 +7,7 @@ export interface Transaction {
hash: string;
result: string;
confirmations: number;
status: string;
status: 'ok' | 'error' | null;
block: number;
timestamp: string;
confirmation_duration: Array<number>;
......@@ -18,24 +18,24 @@ export interface Transaction {
fee: Fee;
gas_price: number;
type: number;
gas_used: number;
gas_limit: number;
max_fee_per_gas?: number;
max_priority_fee_per_gas?: number;
priority_fee?: number;
base_fee_per_gas?: number;
tx_burnt_fee?: number;
gas_used: string;
gas_limit: string;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
priority_fee: number | null;
base_fee_per_gas: number | null;
tx_burnt_fee: number | null;
nonce: number;
position: number;
revert_reason?: {
revert_reason: {
raw: string;
decoded: string;
};
} | null;
raw_input: string;
decoded_input?: DecodedInput;
token_transfers?: Array<TokenTransfer>;
decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: number;
exchange_rate: string;
}
export interface TransactionsResponse {
......
export type TxInternalsType = 'call' | 'delegate_call' | 'static_call' | 'create' | 'create2' | 'self_destruct' | 'reward'
export enum MarketplaceCategoryNames {
export enum MarketplaceCategoryId {
'all',
'favorites',
'defi',
'exchanges',
'finance',
......@@ -11,16 +13,20 @@ export enum MarketplaceCategoryNames {
'yieldFarming',
}
export type MarketplaceCategoriesIds = keyof typeof MarketplaceCategoryId;
export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string }
export type AppItemPreview = {
id: string;
title: string;
logo: string;
shortDescription: string;
categories: Array<keyof typeof MarketplaceCategoryNames>;
categories: Array<MarketplaceCategoriesIds>;
}
export type AppItemOverview = AppItemPreview & {
chainId: number;
chainIds: Array<number>;
author: string;
url: string;
description: string;
......
......@@ -28,7 +28,6 @@ const AppCard = ({ id,
isFavorite,
onFavoriteClick,
}: Props) => {
const categoriesLabel = categories.map(c => APP_CATEGORIES[c]).filter(notEmpty).join(', ');
const handleInfoClick = useCallback((event: MouseEvent) => {
......@@ -55,6 +54,7 @@ const AppCard = ({ id,
padding={{ base: 3, sm: '20px' }}
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
role="group"
>
<Box
display={{ base: 'grid', sm: 'block' }}
......@@ -71,6 +71,7 @@ const AppCard = ({ id,
h={{ base: '64px', sm: '96px' }}
>
<Image
borderRadius={ 8 }
src={ logo }
alt={ `${ title } app icon` }
/>
......@@ -110,9 +111,8 @@ const AppCard = ({ id,
position="absolute"
right={{ base: 3, sm: '20px' }}
bottom={{ base: 3, sm: '20px' }}
paddingTop={ 1 }
paddingLeft={ 8 }
bgGradient={ `linear(to-r, transparent, ${ useColorModeValue('white', 'black') } 20%)` }
bgGradient={ `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)` }
>
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
......@@ -134,9 +134,11 @@ const AppCard = ({ id,
</Box>
<IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }}
_groupHover={{ display: 'block' }}
position="absolute"
right={{ base: 3, sm: '20px' }}
top={{ base: 3, sm: '20px' }}
right={{ base: 3, sm: '10px' }}
top={{ base: 3, sm: '14px' }}
aria-label="Mark as favorite"
title="Mark as favorite"
variant="ghost"
......
import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import { Grid, GridItem, Heading, VisuallyHidden } from '@chakra-ui/react';
import React from 'react';
import type { AppItemPreview } from 'types/client/apps';
......@@ -14,37 +14,11 @@ type Props = {
onAppClick: (id: string) => void;
displayedAppId: string | null;
onModalClose: () => void;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
function getFavoriteApps() {
try {
return JSON.parse(localStorage.getItem('favoriteApps') || '[]');
} catch (e) {
return [];
}
}
const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
const result = favoriteApps.filter((appId: string) => appId !== id);
setFavoriteApps(result);
localStorage.setItem('favoriteApps', JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem('favoriteApps', JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps, onFavoriteClick }: Props) => {
return (
<>
<VisuallyHidden>
......@@ -54,7 +28,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
{ apps.length > 0 ? (
<Grid
templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
sm: 'repeat(auto-fill, minmax(178px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
......@@ -72,7 +46,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
shortDescription={ app.shortDescription }
categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ handleFavoriteClick }
onFavoriteClick={ onFavoriteClick }
/>
</GridItem>
)) }
......@@ -86,7 +60,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
id={ displayedAppId }
onClose={ onModalClose }
isFavorite={ favoriteApps.includes(displayedAppId) }
onFavoriteClick={ handleFavoriteClick }
onFavoriteClick={ onFavoriteClick }
/>
) }
</>
......
......@@ -5,7 +5,7 @@ import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
export const AppListSkeleton = () => {
const AppListSkeleton = () => {
return (
<Grid
templateColumns={{
......@@ -25,3 +25,5 @@ export const AppListSkeleton = () => {
</Grid>
);
};
export default AppListSkeleton;
......@@ -5,7 +5,7 @@ import {
import NextLink from 'next/link';
import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoryNames } from 'types/client/apps';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import linkIcon from 'icons/link.svg';
......@@ -100,9 +100,6 @@ const AppModal = ({
fontWeight="medium"
lineHeight={ 1 }
color="blue.600"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
>
{ title }
</Heading>
......@@ -161,7 +158,7 @@ const AppModal = ({
</Heading>
<Box marginBottom={ 2 }>
{ categories.map((category: keyof typeof MarketplaceCategoryNames) => APP_CATEGORIES[category] && (
{ categories.map((category: MarketplaceCategoriesIds) => APP_CATEGORIES[category] && (
<Tag
colorScheme="blue"
marginRight={ 2 }
......
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem';
import { APP_CATEGORIES } from './constants';
const categoriesList = Object.keys(APP_CATEGORIES).map((id: string) => ({
id: id,
name: APP_CATEGORIES[id as MarketplaceCategoriesIds],
})) as Array<MarketplaceCategory>;
type Props = {
selectedCategoryId: MarketplaceCategoriesIds;
onSelect: (category: MarketplaceCategoriesIds) => void;
}
const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId);
return (
<Menu>
<MenuButton
as={ Button }
mb={{ base: 2, sm: 0 }}
mr={{ base: 0, sm: 2 }}
size="md"
variant="outline"
colorScheme="gray"
flexShrink={ 0 }
>
<Box
as="span"
display="flex"
alignItems="center"
>
{ selectedCategory?.name }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
<MenuList zIndex={ 3 }>
{ categoriesList.map((category: MarketplaceCategory) => (
<CategoriesMenuItem
key={ category.id }
id={ category.id }
name={ category.name }
onClick={ onSelect }
/>
)) }
</MenuList>
</Menu>
);
};
export default React.memo(CategoriesMenu);
import { Icon, MenuItem } from '@chakra-ui/react';
import type { FunctionComponent, SVGAttributes } from 'react';
import React, { useCallback } from 'react';
import type { MarketplaceCategoriesIds } from 'types/client/apps';
import starFilledIcon from 'icons/star_filled.svg';
type Props = {
id: MarketplaceCategoriesIds;
name: string;
onClick: (category: MarketplaceCategoriesIds) => void;
}
const ICONS = {
favorites: starFilledIcon,
} as { [key in MarketplaceCategoriesIds]: FunctionComponent<SVGAttributes<SVGElement>> };
const CategoriesMenuItem = ({ id, name, onClick }: Props) => {
const handleSelection = useCallback(() => {
onClick(id);
}, [ id, onClick ]);
return (
<MenuItem
key={ id }
onClick={ handleSelection }
display="flex"
alignItems="center"
>
{ id in ICONS && (
<Icon mr={ 3 } as={ ICONS[id] } w={ 4 } h={ 4 } color="blackAlpha.800"/>
) }
{ name }
</MenuItem>
);
};
export default CategoriesMenuItem;
import type { MarketplaceCategoryNames } from 'types/client/apps';
import type { MarketplaceCategoriesIds } from 'types/client/apps';
export const APP_CATEGORIES: {[key in keyof typeof MarketplaceCategoryNames]: string} = {
export const APP_CATEGORIES: {[key in MarketplaceCategoriesIds]: string} = {
favorites: 'Favorites',
all: 'All apps',
defi: 'DeFi',
exchanges: 'Exchanges',
finance: 'Finance',
......
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import useNetwork from 'lib/hooks/useNetwork';
const favoriteAppsLocalStorageKey = 'favoriteApps';
function getFavoriteApps() {
try {
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]');
} catch (e) {
return [];
}
}
function isAppNameMatches(q: string, app: AppItemOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOverview, favoriteApps: Array<string>) {
return category === 'all' ||
(category === 'favorites' && favoriteApps.includes(app.id)) ||
app.categories.includes(category);
}
export default function useMarketplaceApps() {
const selectedNetwork = useNetwork();
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const [ category, setCategory ] = useState<MarketplaceCategoriesIds>('all');
const [ filterQuery, setFilterQuery ] = useState('');
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
const result = favoriteApps.filter((appId: string) => appId !== id);
setFavoriteApps(result);
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => setFilterQuery(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
const filterApps = useCallback((q: string, category: MarketplaceCategoriesIds) => {
const apps = defaultAppList
?.filter(app => {
return isAppNameMatches(q, app) && isAppCategoryMatches(category, app, favoriteApps);
});
setDisplayedApps(apps || []);
}, [ defaultAppList, favoriteApps ]);
const handleCategoryChange = useCallback((newCategory: MarketplaceCategoriesIds) => {
setCategory(newCategory);
}, []);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
useEffect(() => {
filterApps(filterQuery, category);
}, [ filterQuery, category, filterApps ]);
useEffect(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainIds.includes(selectedNetwork?.chainId))
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
return React.useMemo(() => ({
category,
handleCategoryChange,
debounceFilterApps,
isLoading,
displayedApps,
showAppInfo,
displayedAppId,
clearDisplayedAppId,
favoriteApps,
handleFavoriteClick,
}), [ category,
clearDisplayedAppId,
debounceFilterApps,
displayedAppId, displayedApps,
favoriteApps,
handleCategoryChange,
handleFavoriteClick,
isLoading,
showAppInfo,
]);
}
import { Icon, Link } from '@chakra-ui/react';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import { Box, Icon, Link } from '@chakra-ui/react';
import React from 'react';
import type { AppItemOverview } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import PlusIcon from 'icons/plus.svg';
import useNetwork from 'lib/hooks/useNetwork';
import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput';
import { AppListSkeleton } from '../apps/AppListSkeleton';
import useMarketplaceApps from '../apps/useMarkeplaceApps';
const Apps = () => {
const selectedNetwork = useNetwork();
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
function filterApps(q: string) {
const apps = defaultAppList
?.filter(app => app.title.toLowerCase().includes(q.toLowerCase()));
setDisplayedApps(apps || []);
}
useEffect(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainId === selectedNetwork?.chainId)
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
const {
isLoading,
category,
handleCategoryChange,
debounceFilterApps,
showAppInfo,
displayedApps,
displayedAppId,
clearDisplayedAppId,
favoriteApps,
handleFavoriteClick,
} = useMarketplaceApps();
return (
<>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
<Box
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
>
<CategoriesMenu
selectedCategoryId={ category }
onSelect={ handleCategoryChange }
/>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
</Box>
{ isLoading ? <AppListSkeleton/> : (
<AppList
......@@ -57,6 +43,8 @@ const Apps = () => {
onAppClick={ showAppInfo }
displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId }
favoriteApps={ favoriteApps }
onFavoriteClick={ handleFavoriteClick }
/>
) }
......
......@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import useNetwork from 'lib/hooks/useNetwork';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
type Props = {
......@@ -24,13 +25,15 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' +
'allow-top-navigation-by-user-activation';
'allow-top-navigation-by-user-activation allow-popups';
const allowAttributeValue = 'clipboard-read; clipboard-write;';
useEffect(() => {
if (app) {
ref?.current?.contentWindow?.postMessage({ colorMode, chaindId: network?.chainId }, app.url);
if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: network?.chainId }, app.url);
}
}, [ app, colorMode, network, ref ]);
}, [ isFrameLoading, app, colorMode, network, ref ]);
return (
<Page wrapChildren={ false }>
......@@ -40,16 +43,12 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
paddingTop={{ base: '138px', lg: 0 }}
>
{ (isFrameLoading) && (
<Center
h="100%"
w="100%"
>
Loading...
</Center>
<ContentLoader/>
) }
{ app && (
<Box
allow={ allowAttributeValue }
ref={ ref }
sandbox={ sandboxAttributeValue }
as="iframe"
......
......@@ -25,7 +25,6 @@ const MyProfile = () => {
<UserAvatar size={ 64 } data={ data }/>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.name || '' }
......@@ -34,7 +33,6 @@ const MyProfile = () => {
</FormControl>
<FormControl variant="floating" id="nickname" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.nickname || '' }
......@@ -43,7 +41,6 @@ const MyProfile = () => {
</FormControl>
<FormControl variant="floating" id="email" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.email }
......
......@@ -13,13 +13,14 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState';
// import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> },
{ id: 'state', title: 'State', component: <TxState/> },
// will be implemented later, api is not ready
// { id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
];
......
import type { InputProps } from '@chakra-ui/react';
import { IconButton, Icon, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
......@@ -17,7 +18,7 @@ interface Props {
error?: FieldError;
onAddFieldClick: (e: React.SyntheticEvent) => void;
onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void;
size?: string;
size?: InputProps['size'];
}
const MAX_INPUTS_NUM = 10;
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
......@@ -12,7 +13,7 @@ const TEXT_INPUT_MAX_LENGTH = 255;
interface Props {
control: Control<Inputs>;
error?: FieldError;
size?: string;
size?: InputProps['size'];
}
export default function PublicTagFormComment({ control, error, size }: Props) {
......@@ -22,7 +23,6 @@ export default function PublicTagFormComment({ control, error, size }: Props) {
<Textarea
{ ...field }
isInvalid={ Boolean(error) }
size={ size }
/>
<FormLabel>
{ getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) }
......
......@@ -16,7 +16,6 @@ import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
......@@ -57,9 +56,8 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const fetch = useFetch();
const inputSize = isMobile ? 'md' : 'lg';
const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: {
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path, Control } from 'react-hook-form';
......@@ -14,7 +15,7 @@ interface Props<TInputs extends FieldValues> {
control: Control<TInputs, object>;
pattern?: RegExp;
error?: FieldError;
size?: string;
size?: InputProps['size'];
}
export default function PublicTagsFormInput<Inputs extends FieldValues>({
......@@ -31,7 +32,6 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
<FormControl variant="floating" id={ field.name } isRequired={ required } size={ size }>
<Input
{ ...field }
size={ size }
required={ required }
isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH }
......
import type { InputProps } from '@chakra-ui/react';
import {
Input,
FormControl,
......@@ -11,7 +12,7 @@ import { ADDRESS_LENGTH } from 'lib/validations/address';
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
size?: string;
size?: InputProps['size'];
placeholder?: string;
backgroundColor?: string;
error?: FieldError;
......@@ -31,7 +32,6 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
size={ size }
/>
<FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel>
</FormControl>
......
import { Box, Text, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import { WEI, GWEI } from 'lib/consts';
interface Props {
value: string;
unit?: 'wei' | 'gwei' | 'ether';
currency?: string;
exchangeRate?: string;
className?: string;
accuracy?: number;
accuracyUsd?: number;
}
const CurrencyValue = ({ value, currency = '', unit = 'wei', exchangeRate, className, accuracy, accuracyUsd }: Props) => {
let unitBn: BigNumber.Value;
switch (unit) {
case 'wei':
unitBn = WEI;
break;
case 'gwei':
unitBn = GWEI;
break;
default:
unitBn = new BigNumber(1);
}
const valueBn = new BigNumber(value);
const valueCurr = valueBn.dividedBy(unitBn);
const exchangeRateBn = new BigNumber(exchangeRate || 0);
const usdBn = valueCurr.times(exchangeRateBn);
return (
<Box as="span" className={ className }>
<Text as="span">
{ accuracy ? valueCurr.toFixed(accuracy) : valueCurr.toFixed() }{ currency ? ` ${ currency }` : '' }
</Text>
{ exchangeRate !== undefined && exchangeRate !== null &&
// TODO: mb need to implement rounding to the first significant digit
<Text as="span" variant="secondary" whiteSpace="pre" fontWeight={ 400 }> (${ accuracyUsd ? usdBn.toFixed(accuracyUsd) : usdBn.toFixed() })</Text>
}
</Box>
);
};
export default React.memo(chakra(CurrencyValue));
import { Tag, TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg';
export interface Props {
status: 'success' | 'failed' | 'pending';
errorText?: string;
status: Transaction['status'];
errorText?: string | null;
}
const TxStatus = ({ status, errorText }: Props) => {
......@@ -16,17 +18,17 @@ const TxStatus = ({ status, errorText }: Props) => {
let colorScheme;
switch (status) {
case 'success':
case 'ok':
label = 'Success';
icon = successIcon;
colorScheme = 'green';
break;
case 'failed':
case 'error':
label = 'Failed';
icon = errorIcon;
colorScheme = 'red';
break;
case 'pending':
case null:
label = 'Pending';
icon = pendingIcon;
// FIXME: it's not gray on mockups
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import RenderWithChakra from '../../playwright/RenderWithChakra';
import Utilization from './Utilization';
test.use({ viewport: { width: 100, height: 50 } });
test('Utilization light', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('Utilization dark', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
import { Flex, Icon, Text } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer';
import rightArrowIcon from 'icons/arrows/east.svg';
import { space } from 'lib/html-entities';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet';
interface Props {
from: string;
to: string;
amount: number;
usd: number;
token: {
symbol: string;
hash: string;
name: string;
};
}
type Props = TTokenTransfer
const TokenTransfer = ({ from, to, amount, usd, token }: Props) => {
const TokenTransfer = ({ from, to, total, exchange_rate: exchangeRate, ...token }: Props) => {
return (
<Flex alignItems="center" flexWrap="wrap" columnGap={ 3 } rowGap={ 3 }>
<Flex alignItems="center">
<AddressLink fontWeight="500" hash={ from } truncation="constant"/>
<AddressLink fontWeight="500" hash={ from.hash } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLink fontWeight="500" hash={ to } truncation="constant"/>
<AddressLink fontWeight="500" hash={ to.hash } truncation="constant"/>
</Flex>
<Text fontWeight={ 500 } as="span">For:{ space }
<Text fontWeight={ 600 } as="span">{ amount }</Text>{ space }
<Text fontWeight={ 400 } variant="secondary" as="span">(${ usd.toFixed(2) })</Text>
<CurrencyValue value={ total.value.replaceAll(',', '') } unit="ether" exchangeRate={ exchangeRate } fontWeight={ 600 }/>
</Text>
<TokenSnippet { ...token }/>
<TokenSnippet symbol={ token.token_symbol } hash={ token.token_address } name="Foo"/>
</Flex>
);
};
......
import { Flex, Text, Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { DecodedInput } from 'types/api/decodedInput';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -39,14 +41,16 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
>
{ type }
</GridItem>
<GridItem
pr={ GAP }
pt={ GAP }
pb={ isLast ? PADDING : 0 }
bgColor={ bgColor }
>
{ indexed ? 'true' : 'false' }
</GridItem>
{ indexed !== undefined && (
<GridItem
pr={ GAP }
pt={ GAP }
pb={ isLast ? PADDING : 0 }
bgColor={ bgColor }
>
{ indexed ? 'true' : 'false' }
</GridItem>
) }
<GridItem
pr={ PADDING }
pt={ GAP }
......@@ -60,14 +64,28 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
);
};
const TxDecodedInputData = () => {
interface Props {
data: DecodedInput;
}
const TxDecodedInputData = ({ data }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hasIndexed = data.parameters.some(({ indexed }) => indexed !== undefined);
const gridTemplateColumns = hasIndexed ?
'minmax(80px, auto) minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)' :
'minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)';
const colNumber = hasIndexed ? 4 : 3;
return (
<Grid gridTemplateColumns="minmax(80px, auto) minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)" fontSize="sm" lineHeight={ 5 } w="100%">
<Grid gridTemplateColumns={ gridTemplateColumns } fontSize="sm" lineHeight={ 5 } w="100%">
{ /* FIRST PART OF BLOCK */ }
<GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: 4, lg: undefined }}>Method Id</GridItem>
<GridItem colSpan={{ base: 4, lg: 3 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}>0xddf252ad</GridItem>
<GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: colNumber, lg: undefined }}>
Method Id
</GridItem>
<GridItem colSpan={{ base: colNumber, lg: colNumber - 1 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}>
{ data.method_id }
</GridItem>
<GridItem
py={ 2 }
mt={ 2 }
......@@ -76,7 +94,7 @@ const TxDecodedInputData = () => {
fontWeight={ 600 }
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px"
colSpan={{ base: 4, lg: undefined }}
colSpan={{ base: colNumber, lg: undefined }}
>
Call
</GridItem>
......@@ -84,13 +102,13 @@ const TxDecodedInputData = () => {
py={{ base: 0, lg: 2 }}
mt={{ base: 0, lg: 2 }}
mb={{ base: 2, lg: 0 }}
colSpan={{ base: 4, lg: 3 }}
colSpan={{ base: colNumber, lg: colNumber - 1 }}
pr={{ base: 0, lg: PADDING }}
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth={{ base: '0px', lg: '1px' }}
whiteSpace="normal"
>
Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
{ data.method_call }
</GridItem>
{ /* TABLE INSIDE OF BLOCK */ }
<GridItem
......@@ -112,15 +130,17 @@ const TxDecodedInputData = () => {
>
Type
</GridItem>
<GridItem
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
Inde<wbr/>xed?
</GridItem>
{ hasIndexed && (
<GridItem
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
Inde<wbr/>xed?
</GridItem>
) }
<GridItem
pr={ PADDING }
pt={ PADDING }
......@@ -130,24 +150,23 @@ const TxDecodedInputData = () => {
>
Data
</GridItem>
<TableRow name="from" type="address">
<Address justifyContent="space-between">
<AddressLink hash="0x0000000000000000000000000000000000000000"/>
<CopyToClipboard text="0x0000000000000000000000000000000000000000"/>
</Address>
</TableRow>
<TableRow name="to" type="address" indexed>
<Address justifyContent="space-between">
<AddressLink hash="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
<CopyToClipboard text="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
</Address>
</TableRow>
<TableRow name="tokenId" type="uint256" isLast>
<Flex alignItems="center" justifyContent="space-between">
<Text>116842</Text>
<CopyToClipboard text="116842"/>
</Flex>
</TableRow>
{ data.parameters.map(({ name, type, value, indexed }, index) => {
return (
<TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }>
{ type === 'address' ? (
<Address justifyContent="space-between">
<AddressLink hash={ value }/>
<CopyToClipboard text={ value }/>
</Address>
) : (
<Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all">
<Text>{ value }</Text>
<CopyToClipboard text={ value }/>
</Flex>
) }
</TableRow>
);
}) }
</Grid>
);
};
......
This diff is collapsed.
import { Box, Flex } from '@chakra-ui/react';
import { Box, Flex, Alert, Show } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { TxInternalsType } from 'types/api/tx';
import type ArrayElement from 'types/utils/ArrayElement';
import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction';
import { data } from 'data/txInternal';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop';
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
import type { Sort, SortField } from 'ui/tx/internals/utils';
......@@ -26,7 +30,7 @@ const getNextSortValue = (field: SortField) => (prevValue: Sort | undefined) =>
return sequence[nextIndex];
};
const sortFn = (sort: Sort | undefined) => (a: ArrayElement<typeof data>, b: ArrayElement<typeof data>) => {
const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalTransaction) => {
switch (sort) {
case 'value-desc': {
const result = a.value > b.value ? -1 : 1;
......@@ -38,22 +42,23 @@ const sortFn = (sort: Sort | undefined) => (a: ArrayElement<typeof data>, b: Arr
return a.value === b.value ? 0 : result;
}
case 'gas-limit-desc': {
const result = a.gasLimit > b.gasLimit ? -1 : 1;
return a.gasLimit === b.gasLimit ? 0 : result;
}
// no gas limit in api yet
// case 'gas-limit-desc': {
// const result = a.gasLimit > b.gasLimit ? -1 : 1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
case 'gas-limit-asc': {
const result = a.gasLimit > b.gasLimit ? 1 : -1;
return a.gasLimit === b.gasLimit ? 0 : result;
}
// case 'gas-limit-asc': {
// const result = a.gasLimit > b.gasLimit ? 1 : -1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
default:
return 0;
}
};
const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): boolean => {
const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
......@@ -61,9 +66,19 @@ const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): bool
};
const TxInternals = () => {
const router = useRouter();
const fetch = useFetch();
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ 'tx-internals', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/internal-transactions`),
{
enabled: Boolean(router.query.id),
},
);
const isMobile = useIsMobile();
......@@ -77,8 +92,25 @@ const TxInternals = () => {
};
}, []);
if (isLoading) {
return (
<>
<Show below="lg"><TxInternalsSkeletonMobile/></Show>
<Show above="lg"><TxInternalsSkeletonDesktop/></Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) {
return <Alert>There are no internal transactions for this transaction.</Alert>;
}
const content = (() => {
const filteredData = data
const filteredData = data.items
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
.filter(searchFn(searchTerm))
.sort(sortFn(sort));
......
import { Box } from '@chakra-ui/react';
import { Box, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { data } from 'data/txLogs';
import type { LogsResponse } from 'types/api/log';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
const TxLogs = () => {
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ 'tx-log', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/logs`),
{
enabled: Boolean(router.query.id),
},
);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<Box>
<TxLogSkeleton/>
<TxLogSkeleton/>
</Box>
);
}
if (data.items.length === 0) {
return <Alert>There are no logs for this transaction.</Alert>;
}
return (
<Box>
{ data.map((item, index) => <TxLogItem key={ index } { ...item } index={ index }/>) }
{ data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) }
</Box>
);
};
......
import { Flex, Textarea } from '@chakra-ui/react';
import { Flex, Textarea, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace';
import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
const data = [
{
action: {
callType: 'delegatecall',
from: '0x296033cb983747b68911244ec1a3f01d7708851b',
gas: '0x1AB35C9',
// eslint-disable-next-line max-len
input: '0x6a76120200000000000000000000000099759357a9923bb164a7ae8b85703a6882cb84ea0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000014466d2a64d000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000013ef0000000000000000000000000000000000000000000000000000000000001bfb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000186b900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000f4e5b62da2eee3b5811dae1fae480f7623bd4cd000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000',
to: '0x3e5c63644e683549055b9be8653de26e0b4cd36e',
value: '0x0',
},
result: {
blockHash: '0x43dd926aa138a58d3f4740dae387bcff3c7bc525db2d0a449f323f8b8f92a229',
blockNumber: '0xa4f285',
from: '0xea8a7ef30f894bce23b42314613458d13f9d43ea',
gas: '0x30d40',
gasPrice: '0x2e90edd000',
hash: '0x72ee43a3784cc6749f64fad1ecf0bbd51a54dd6892ae0573f211566809e0d511',
input: '0x',
nonce: '0x1e7',
to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
transactionIndex: '0x0',
value: '0xde0b6b3a7640000',
v: '0x25',
r: '0x7e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093f',
s: '0x49634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995',
},
},
{
action: {
callType: 'delegatecall',
from: '0x296033cb983747b68911244ec1a3f01d7708851b',
gas: '0x1AB35C9',
// eslint-disable-next-line max-len
input: '0x6a76120200000000000000000000000099759357a9923bb164a7ae8b85703a6882cb84ea0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000014466d2a64d000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000013ef0000000000000000000000000000000000000000000000000000000000001bfb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000186b900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000f4e5b62da2eee3b5811dae1fae480f7623bd4cd000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000',
to: '0x3e5c63644e683549055b9be8653de26e0b4cd36e',
value: '0x0',
},
result: {
blockHash: '0x43dd926aa138a58d3f4740dae387bcff3c7bc525db2d0a449f323f8b8f92a229',
blockNumber: '0xa4f285',
from: '0xea8a7ef30f894bce23b42314613458d13f9d43ea',
gas: '0x30d40',
gasPrice: '0x2e90edd000',
hash: '0x72ee43a3784cc6749f64fad1ecf0bbd51a54dd6892ae0573f211566809e0d511',
input: '0x',
nonce: '0x1e7',
to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
transactionIndex: '0x0',
value: '0xde0b6b3a7640000',
v: '0x25',
r: '0x7e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093f',
s: '0x49634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995',
const TxRawTrace = () => {
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>(
[ 'tx-raw-trace', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/raw-trace`),
{
enabled: Boolean(router.query.id),
},
},
];
);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Flex justifyContent="end" mb={ 2 }>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="500px"/>
</>
);
}
const TxRawTrace = () => {
const text = JSON.stringify(data, undefined, 4);
return (
<>
<Flex justifyContent="end" mb={ 2 }>
......@@ -69,7 +45,7 @@ const TxRawTrace = () => {
</Flex>
<Textarea
variant="filledInactive"
height="570px"
minHeight="500px"
p={ 4 }
value={ text }
/>
......
import { Grid, GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const SkeletonRow = ({ w = '100%' }: { w?: string }) => (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<SkeletonCircle h={ 5 } w={ 5 }/>
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/>
</GridItem>
</>
);
const TxDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<SkeletonRow/>
<SkeletonRow w="20%"/>
<SkeletonRow w="50%"/>
<SkeletonRow/>
<SkeletonRow w="70%"/>
<SkeletonRow w="70%"/>
{ sectionGap }
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default TxDetailsSkeleton;
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { TxInternalsType } from 'types/api/tx';
import type { TxInternalsType } from 'types/api/internalTransaction';
import FilterButton from 'ui/shared/FilterButton';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { data as txData } from 'data/txInternal';
import type { InternalTransaction } from 'types/api/internalTransaction';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: typeof txData}) => {
const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => {
const selectedNetwork = useNetwork();
return (
<Box mt={ 6 }>
{ data.map((item) => <TxInternalsListItem key={ item.id } { ...item } currency={ selectedNetwork?.currency }/>) }
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item } currency={ selectedNetwork?.currency }/>) }
</Box>
);
};
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { InternalTransaction } from 'types/api/internalTransaction';
import type { data } from 'data/txInternal';
import eastArrowIcon from 'icons/arrows/east.svg';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = ArrayElement<typeof data> & { currency?: string };
type Props = InternalTransaction & { currency?: string };
const TxInternalsListItem = ({ type, from, to, value, currency, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency }: Props) => {
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex>
<Tag colorScheme="cyan" mr={ 2 }>{ capitalize(type) }</Tag>
<TxStatus status={ status }/>
<Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag>
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
......@@ -36,10 +37,11 @@ const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency
<Text fontSize="sm" fontWeight={ 500 }>Value { currency }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack>
<HStack spacing={ 3 }>
{ /* no gas limit in api yet */ }
{ /* <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
<Text fontSize="sm" variant="secondary">{ gasLimit.toLocaleString('en') }</Text>
</HStack>
</HStack> */ }
</AccountListItemMobile>
);
};
......
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '28%', '28%', '24px', '28%', '16%' ] }/>
</>
);
};
export default TxInternalsSkeletonDesktop;
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
</Flex>
)) }
</Box>
</>
);
};
export default TxInternalsSkeletonMobile;
import { Table, Thead, Tbody, Tr, Th, TableContainer, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { data as txData } from 'data/txInternal';
import type { InternalTransaction } from 'types/api/internalTransaction';
import arrowIcon from 'icons/arrows/east.svg';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils';
interface Props {
data: typeof txData;
data: Array<InternalTransaction>;
sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void;
}
......@@ -23,26 +24,27 @@ const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
<Thead>
<Tr>
<Th width="28%">Type</Th>
<Th width="20%">From</Th>
<Th width="28%">From</Th>
<Th width="24px" px={ 0 }/>
<Th width="20%">To</Th>
<Th width="28%">To</Th>
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { selectedNetwork?.currency }
</Link>
</Th>
<Th width="16%" isNumeric>
{ /* no gas limit in api yet */ }
{ /* <Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }>
{ sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Gas limit
</Link>
</Th>
</Th> */ }
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TxInternalsTableItem key={ item.id } { ...item }/>
<TxInternalsTableItem key={ item.transaction_hash } { ...item }/>
)) }
</Tbody>
</Table>
......
import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -8,16 +10,9 @@ import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
interface Props {
type: string;
status: 'success' | 'failed' | 'pending';
from: { hash: string; alias?: string};
to: { hash: string; alias?: string};
value: number;
gasLimit: number;
}
type Props = InternalTransaction
const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) => {
const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
return (
......@@ -28,12 +23,12 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props)
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
</Box>
) }
<TxStatus status={ status }/>
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Td>
<Td>
<Address>
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.alias } flexGrow={ 1 }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td px={ 0 }>
......@@ -42,15 +37,16 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props)
<Td>
<Address>
<AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.alias } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/>
</Address>
</Td>
<Td isNumeric>
{ value }
</Td>
<Td isNumeric>
{ /* no gas limit in api yet */ }
{ /* <Td isNumeric>
{ gasLimit.toLocaleString('en') }
</Td>
</Td> */ }
</Tr>
);
};
......
import type { TxInternalsType } from 'types/api/tx';
import type { TxInternalsType } from 'types/api/internalTransaction';
export type Sort = 'value-asc' | 'value-desc' | 'gas-limit-asc' | 'gas-limit-desc';
export type SortField = 'value' | 'gas-limit';
......@@ -10,10 +10,10 @@ interface TxInternalsTypeItem {
export const TX_INTERNALS_ITEMS: Array<TxInternalsTypeItem> = [
{ title: 'Call', id: 'call' },
{ title: 'Delegate call', id: 'delegate_call' },
{ title: 'Static call', id: 'static_call' },
{ title: 'Delegate call', id: 'delegatecall' },
{ title: 'Static call', id: 'staticcall' },
{ title: 'Create', id: 'create' },
{ title: 'Create2', id: 'create2' },
{ title: 'Self-destruct', id: 'self_destruct' },
{ title: 'Self-destruct', id: 'selfdestruct' },
{ title: 'Reward', id: 'reward' },
];
import { Text, Grid, GridItem, Link, Tooltip, Button, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { Log } from 'types/api/log';
import searchIcon from 'icons/search.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -8,12 +10,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
import TxLogTopic from 'ui/tx/logs/TxLogTopic';
import DecodedInputData from 'ui/tx/TxDecodedInputData';
interface Props {
address: string;
topics: Array<{ hex: string }>;
data: string;
index: number;
}
type Props = Log;
const RowHeader = ({ children }: { children: React.ReactNode }) => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }}>
......@@ -21,7 +18,8 @@ const RowHeader = ({ children }: { children: React.ReactNode }) => (
</GridItem>
);
const TxLogItem = ({ address, index, topics, data }: Props) => {
const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
......@@ -41,27 +39,31 @@ const TxLogItem = ({ address, index, topics, data }: Props) => {
<RowHeader>Address</RowHeader>
<GridItem display="flex" alignItems="center">
<Address>
<AddressIcon hash={ address }/>
<AddressLink hash={ address } ml={ 2 }/>
<AddressIcon hash={ address.hash }/>
<AddressLink hash={ address.hash } alias={ address.name } ml={ 2 }/>
</Address>
<Tooltip label="Find matches topic">
<Link ml={ 2 } display="inline-flex">
<Link ml={ 2 } mr={{ base: 9, lg: 0 }} display="inline-flex">
<Icon as={ searchIcon } boxSize={ 5 }/>
</Link>
</Tooltip>
<Tooltip label="Log index">
<Button variant="outline" colorScheme="gray" isActive ml={{ base: 9, lg: 'auto' }} size="sm" fontWeight={ 400 }>
<Button variant="outline" colorScheme="gray" isActive ml="auto" size="sm" fontWeight={ 400 }>
{ index }
</Button>
</Tooltip>
</GridItem>
<RowHeader>Decode input data</RowHeader>
<GridItem>
<DecodedInputData/>
</GridItem>
{ decoded && (
<>
<RowHeader>Decode input data</RowHeader>
<GridItem>
<DecodedInputData data={ decoded }/>
</GridItem>
</>
) }
<RowHeader>Topics</RowHeader>
<GridItem>
{ topics.map((item, index) => <TxLogTopic key={ index } { ...item } index={ index }/>) }
{ topics.filter(Boolean).map((item, index) => <TxLogTopic key={ index } hex={ item } index={ index }/>) }
</GridItem>
<RowHeader>Data</RowHeader>
<GridItem p={ 4 } fontSize="sm" borderRadius="md" bgColor={ dataBgColor }>
......
import { Flex, Grid, GridItem, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const RowHeader = () => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }} _first={{ alignSelf: 'center' }}>
<Skeleton h={ 6 } borderRadius="full" w="150px"/>
</GridItem>
);
const TopicRow = () => (
<Flex columnGap={ 3 }>
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } w="70px" borderRadius="full"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full"/>
</Flex>
);
const TxLogSkeleton = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Grid
gap={{ base: 2, lg: 8 }}
templateColumns={{ base: '1fr', lg: '200px 1fr' }}
py={ 8 }
_notFirst={{
borderTopWidth: '1px',
borderTopColor: borderColor,
}}
_first={{
pt: 0,
}}
>
<RowHeader/>
<GridItem display="flex" alignItems="center">
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full" ml={ 2 } mr={ 9 }/>
<Skeleton h={ 8 } w={ 8 } borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="150px" w="100%" borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem display="flex" flexDir="column" rowGap={ 3 }>
<TopicRow/>
<TopicRow/>
<TopicRow/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="60px" w="100%" borderRadius="base"/>
</GridItem>
</Grid>
);
};
export default TxLogSkeleton;
......@@ -23,22 +23,18 @@ const TxLogTopic = ({ hex, index }: Props) => {
<Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }>
{ index }
</Button>
{ /* temporary condition juse to show different states of the component */ }
{ /* delete when ther will be real data */ }
{ index > 0 && (
<Select
size="sm"
borderRadius="base"
value={ selectedDataType }
onChange={ handleSelectChange }
focusBorderColor="none"
w="75px"
mr={ 3 }
flexShrink={ 0 }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
) }
<Select
size="sm"
borderRadius="base"
value={ selectedDataType }
onChange={ handleSelectChange }
focusBorderColor="none"
w="75px"
mr={ 3 }
flexShrink={ 0 }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
<Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ hex }/>
</Box>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment