diff --git a/.env b/.env
new file mode 100644
index 00000000..09bff4ca
--- /dev/null
+++ b/.env
@@ -0,0 +1,4 @@
+NEXT_PUBLIC_PREFIX = bridge
+NEXT_PUBLIC_APP_NAME = bridge
+NEXT_PUBLIC_APP_ENV = testnet
+NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID = 5c5740c548328b21cf14472d580f3e2a
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..4edf267c
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,7 @@
+NEXT_PUBLIC_PREFIX = bridge
+NEXT_PUBLIC_APP_NAME = bridge
+# NEXT_PUBLIC_SENTRY_DNS = https://example.ingest.sentry.io/0000000
+# NEXT_PUBLIC_ANALYTICS_ID = G-XXXXXXXXX
+# NEXT_PUBLIC_APP_ENV = testnet
+
+NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID = 5c5740c548328b21cf14472d580f3e2a
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100755
index 00000000..f6418f34
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,31 @@
+{
+ "root": true,
+ "parser": "@typescript-eslint/parser",
+ "extends": [
+ "eslint:recommended",
+ "next/core-web-vitals",
+ "plugin:@typescript-eslint/recommended",
+ "eslint-config-prettier",
+ "plugin:prettier/recommended"
+ ],
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true
+ }
+ },
+ "rules": {
+ "@typescript-eslint/no-explicit-any": 0,
+ "@typescript-eslint/no-unsafe-assignment": 0,
+ "@typescript-eslint/ban-ts-comment": 0,
+ "react-hooks/exhaustive-deps": [
+ "warn",
+ {
+ "additionalHooks": "^use(LockCallback|Interval|ThrottleCallback|DebounceCallback)$"
+ }
+ ]
+ },
+ "env": {
+ "browser": true,
+ "node": true
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..ea59817c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: 👾 Bug Report
+about: Report a bug or issue with the project.
+title: ''
+labels: 'bug'
+assignees: ''
+
+---
+
+### Description
+A clear and concise description of what the bug is.
+
+### Steps To Reproduce
+1. Log in...
+2. Ensure that...
+3. Allow a long period of inactivity to pass...
+4. Observe that...
+5. Attempt to log in...
+
+### Current Behavior
+- After the period of inactivity...
+- When the user tries to log in using another method...
+- This causes a bug due to...
+
+### Expected Behavior
+- After a long period of inactivity...
+- When a user logs in successfully...
+- This ensures that only...
+
+### Environment
+- Platform: PC
+- Node: v18.18.0
+- Browser: Chrome 126.0.6478.56
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..163c1c43
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,10 @@
+blank_issues_enabled: false
+issue_template:
+ - name: 👾 Bug Report
+ description: Report a bug or issue with the project.
+ labels: ["bug"]
+ template: bug_report.md
+ - name: 💡 Feature Request
+ description: Create a new ticket for a new feature request.
+ labels: ["enhancement"]
+ template: feature_request.md
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..a4b81753
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,27 @@
+---
+name: 💡 Feature Request
+about: Create a new ticket for a new feature request
+title: ''
+labels: 'enhancement'
+assignees: ''
+
+---
+
+### Expected Behavior
+Describe the expected behavior here.
+
+### Specifications
+As a `user`, I would like to `action` so that `reason`.
+
+**Features:**
+- describe feature details here.
+
+**Development Tasks:**
+- [ ] Task 1
+- [ ] Task 2
+
+### Dependencies
+List any dependencies that are required for this feature by providing links to the issues or repositories.
+
+### References
+List any references that are related to this feature request.
\ No newline at end of file
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
new file mode 100644
index 00000000..696893f4
--- /dev/null
+++ b/.github/actions/setup/action.yml
@@ -0,0 +1,21 @@
+name: Setup
+runs:
+ using: composite
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ registry-url: https://registry.npmjs.org
+ cache: yarn
+
+ - uses: actions/cache@v3
+ id: install-cache
+ with:
+ path: node_modules/
+ key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
+
+ - if: steps.install-cache.outputs.cache-hit != 'true'
+ run: yarn install --frozen-lockfile --ignore-scripts
+ shell: bash
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
new file mode 100644
index 00000000..965b466b
--- /dev/null
+++ b/.github/workflows/codecov.yml
@@ -0,0 +1,41 @@
+name: Run tests and upload coverage
+
+on:
+ push:
+ branches:
+ - dev
+ - master
+ # - release/*
+ - feature/ut
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ test:
+ name: Run tests and collect coverage
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20.18.2'
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Run tests
+ run: yarn vitest run --coverage
+
+ - name: Upload results to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: coverage/lcov.info
+ flags: unittests
+ name: codecov-umbrella
diff --git a/.github/workflows/mainnet-package.yaml b/.github/workflows/mainnet-package.yaml
new file mode 100644
index 00000000..f4fdbba8
--- /dev/null
+++ b/.github/workflows/mainnet-package.yaml
@@ -0,0 +1,43 @@
+name: Build and Push Image
+on:
+ push:
+ tags:
+ - 'v*.*.*'
+jobs:
+ build-and-push-image:
+ runs-on: eBridge-runner
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Create image tag
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ secrets.REPOSITORY_REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/${{ secrets.REPOSITORY }}/ebridge-web # change this to your project frontend name
+ tags: |
+ type=ref,event=tag
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ env:
+ NEXT_PUBLIC_SENTRY_DNS: ${{ secrets.MAINNET_NEXT_PUBLIC_SENTRY_DNS }} # testnet mainnet
+ NEXT_PUBLIC_ANALYTICS_ID: ${{ secrets.MAINNET_NEXT_NEXT_PUBLIC_ANALYTICS_ID }}
+ NEXT_PUBLIC_APP_ENV: mainnet
+ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ${{ secrets.MAINNET_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID }}
+ NEXT_PUBLIC_TON_API_KEY: ${{ secrets.MAINNET_NEXT_PUBLIC_TON_API_KEY }}
+ with:
+ context: .
+ push: true
+ platforms: linux/amd64
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ NEXT_PUBLIC_SENTRY_DNS=${{ secrets.MAINNET_NEXT_PUBLIC_SENTRY_DNS }}
+ NEXT_PUBLIC_ANALYTICS_ID=${{ secrets.MAINNET_NEXT_NEXT_PUBLIC_ANALYTICS_ID }}
+ NEXT_PUBLIC_APP_ENV=mainnet
+ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=${{ secrets.MAINNET_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID }}
+ NEXT_PUBLIC_TON_API_KEY=${{ secrets.MAINNET_NEXT_PUBLIC_TON_API_KEY }}
+ ENV_NAME=mainnet
\ No newline at end of file
diff --git a/.github/workflows/nextLint.yml b/.github/workflows/nextLint.yml
new file mode 100644
index 00000000..276c550d
--- /dev/null
+++ b/.github/workflows/nextLint.yml
@@ -0,0 +1,33 @@
+name: Next Lint Check
+#
+
+on: [push, pull_request]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: '20' # Your node version, default 20 in 2024
+
+ - name: Cache node modules
+ uses: actions/cache@v3
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - name: Install Dependencies
+ run: yarn
+
+ - name: Run ESLint
+ run: yarn lint
\ No newline at end of file
diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml
new file mode 100644
index 00000000..fdc395ff
--- /dev/null
+++ b/.github/workflows/security.yaml
@@ -0,0 +1,23 @@
+name: Security Review
+
+permissions:
+ pull-requests: write
+ contents: read
+
+on:
+ pull_request:
+
+jobs:
+ security:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 2
+
+ - uses: Portkey-Wallet/claude-code-security-review@main
+ with:
+ comment-pr: true
+ claude-api-key: ${{ secrets.CLAUDE_API_KEY }}
+ run-every-commit: true
\ No newline at end of file
diff --git a/.github/workflows/sonarqube.yaml b/.github/workflows/sonarqube.yaml
new file mode 100644
index 00000000..2a257c10
--- /dev/null
+++ b/.github/workflows/sonarqube.yaml
@@ -0,0 +1,21 @@
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+name: PR Static Code Analysis
+jobs:
+ static-code-analysis:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Code Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: SonarQube Scan
+ uses: sonarsource/sonarqube-scan-action@v2.1.0
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
+ with:
+ args: >
+ -Dsonar.projectKey=ebridge-web
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..fafa759d
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,15 @@
+name: Test
+
+on:
+ push:
+ branches: ['dev', 'master']
+ pull_request:
+ branches: ['dev', 'master']
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/actions/setup
+ - run: yarn lint
\ No newline at end of file
diff --git a/.github/workflows/testnet-package.yaml b/.github/workflows/testnet-package.yaml
new file mode 100644
index 00000000..34631e6c
--- /dev/null
+++ b/.github/workflows/testnet-package.yaml
@@ -0,0 +1,67 @@
+
+name: Build and Push Image
+on:
+ push:
+ branches: ['release/*']
+jobs:
+ build-and-push-image:
+ runs-on: eBridge-runner
+ permissions:
+ contents: read
+ outputs:
+ short_sha: ${{ steps.vars.outputs.short_sha }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set short git commit SHA
+ id: vars
+ run: |
+ calculatedSha=$(git rev-parse --short ${{ github.sha }})
+ echo "short_sha=$calculatedSha" >> "$GITHUB_OUTPUT"
+ - name: Create image tag
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ secrets.REPOSITORY_REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/${{ secrets.REPOSITORY }}/ebridge-web # change this to your project frontend name
+ tags: |
+ type=sha
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ env:
+ NEXT_PUBLIC_SENTRY_DNS: ${{ secrets.TESTNET_NEXT_PUBLIC_SENTRY_DNS }} # testnet mainnet
+ NEXT_PUBLIC_ANALYTICS_ID: ${{ secrets.TESTNET_NEXT_NEXT_PUBLIC_ANALYTICS_ID }}
+ NEXT_PUBLIC_APP_ENV: testnet
+ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ${{ secrets.TESTNET_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID }}
+ with:
+ context: .
+ push: true
+ platforms: linux/amd64
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ NEXT_PUBLIC_SENTRY_DNS=${{ secrets.TESTNET_NEXT_PUBLIC_SENTRY_DNS }}
+ NEXT_PUBLIC_ANALYTICS_ID=${{ secrets.TESTNET_NEXT_NEXT_PUBLIC_ANALYTICS_ID }}
+ NEXT_PUBLIC_APP_ENV=testnet
+ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=${{ secrets.TESTNET_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID }}
+ ENV_NAME=testnet
+ dispatch:
+ runs-on: eBridge-runner
+ needs: build-and-push-image
+ steps:
+ - name: Deploy
+ uses: actions/github-script@v6
+ with:
+ github-token: ${{ secrets.TOK }}
+ script: |
+ await github.rest.actions.createWorkflowDispatch({
+ owner: 'eBridgeCrosschain',
+ repo: 'devops',
+ workflow_id: 'deployment_testnet.yaml',
+ ref: 'main',
+ inputs: {
+ appName: 'ebridge-web-server',
+ commit_sha: 'sha-${{ needs.build-and-push-image.outputs.short_sha }}',
+ }
+ })
diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml
new file mode 100644
index 00000000..ae8dbcd3
--- /dev/null
+++ b/.github/workflows/unit-test.yaml
@@ -0,0 +1,27 @@
+name: Test Badge
+
+permissions:
+ contents: write
+
+on:
+ push:
+ branches:
+ - dev
+ - master
+ # - release/*
+ - feature/ut
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Run frontend ci action
+ uses: eBridgeCrosschain/frontend-ci-action@v0.0.1
+ with:
+ node-version: '20.18.2'
+ commit-token: ${{ secrets.COMMIT_TOKEN }}
+ branch-name: 'feature/ut-badge'
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 00000000..eb05bd51
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+**/jest-report.xml
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+
+# shell
+*.sh
+!postinstall.sh
+.eslintcache
\ No newline at end of file
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100755
index 00000000..4c49ae6a
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+yarn commitlint --edit $1
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 00000000..36af2198
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx lint-staged
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..3cc7493a
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,21 @@
+{
+ "jsxBracketSameLine": true,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 120,
+ "endOfLine": "lf",
+ "overrides": [
+ {
+ "files": "*.css",
+ "options": {
+ "parser": "css"
+ }
+ },
+ {
+ "files": "*.less",
+ "options": {
+ "parser": "less"
+ }
+ }
+ ]
+}
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 00000000..bf80ce92
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,10 @@
+{
+ "extends": ["stylelint-config-standard", "stylelint-config-prettier"],
+ "customSyntax": "postcss-less",
+ "rules": {
+ "font-family-name-quotes": null,
+ "rule-empty-line-before": null,
+ "selector-pseudo-class-no-unknown": null,
+ "no-descending-specificity": null
+ }
+}
diff --git a/.vscode/codeSegment.code-snippets b/.vscode/codeSegment.code-snippets
new file mode 100644
index 00000000..54addfaa
--- /dev/null
+++ b/.vscode/codeSegment.code-snippets
@@ -0,0 +1,62 @@
+{
+ "App prefix": {
+ "scope": "less",
+ "prefix": "v",
+ "body": ["~'var(--@{app-prefix}-$1)'"],
+ "description": "less prefix"
+ },
+ "Class prefix": {
+ "scope": "less",
+ "prefix": "c",
+ "body": [".@{app-prefix}-$1"],
+ "description": "less class prefix"
+ },
+ "Css Module Global": {
+ "scope": "less",
+ "prefix": "g",
+ "body": [":global {$1}"],
+ "description": "css module global"
+ },
+ "import styles": {
+ "scope": "javascriptreact, typescriptreact",
+ "prefix": "styles",
+ "body": ["import styles from './styles.module.less';"],
+ "description": "import styles"
+ },
+ "xs media": {
+ "scope": "less",
+ "prefix": "xs",
+ "body": ["@media screen and (max-width: @xs-width) {$1}"],
+ "description": "xs media"
+ },
+ "md media": {
+ "scope": "less",
+ "prefix": "md",
+ "body": ["@media screen and (max-width: @md-width) {$1}"],
+ "description": "md media"
+ },
+ "xl media": {
+ "scope": "less",
+ "prefix": "xl",
+ "body": ["@media screen and (max-width: @xl-width) {$1}"],
+ "description": "xl media"
+ },
+ "xxl media": {
+ "scope": "less",
+ "prefix": "xl",
+ "body": ["@media screen and (max-width: @xxl-width) {$1}"],
+ "description": "xl media"
+ },
+ "font medium": {
+ "scope": "less",
+ "prefix": "medium",
+ "body": [".font-family-medium();"],
+ "description": "font medium"
+ },
+ "font regular": {
+ "scope": "less",
+ "prefix": "regular",
+ "body": [".font-family-regular();"],
+ "description": "font regular"
+ }
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..9c46fb72
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,29 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Next.js: debug server-side",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "npm run dev"
+ },
+ {
+ "name": "Next.js: debug client-side",
+ "type": "pwa-chrome",
+ "request": "launch",
+ "url": "http://localhost:3000"
+ },
+ {
+ "name": "Next.js: debug full stack",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "npm run dev",
+ "console": "integratedTerminal",
+ "serverReadyAction": {
+ "pattern": "started server on .+, url: (https?://.+)",
+ "uriFormat": "%s",
+ "action": "debugWithChrome"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..c6630b52
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,22 @@
+{
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "[json]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[css]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[less]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "cSpell.words": ["stylelint", "antd", "Cascader", "clsx", "fundpools", "Httpclient", "rehype", "Sider", "ewell"],
+ "cSpell.enableFiletypes": ["typescriptreact"],
+
+ "px-to-rem.px-per-rem": 14
+}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..9bfa9d3b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,49 @@
+FROM node:18 AS base
+
+ARG web=/opt/workspace/aelf-bridge-frontend
+
+# Rebuild the source code only when needed
+FROM base AS builder
+ARG NEXT_PUBLIC_SENTRY_DNS
+ARG NEXT_PUBLIC_ANALYTICS_ID
+ARG NEXT_PUBLIC_APP_ENV
+ARG NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
+ARG NEXT_PUBLIC_TON_API_KEY
+ARG ENV_NAME
+ENV NEXT_PUBLIC_SENTRY_DNS=${NEXT_PUBLIC_SENTRY_DNS}
+ENV NEXT_PUBLIC_ANALYTICS_ID=${NEXT_PUBLIC_ANALYTICS_ID}
+ENV NEXT_PUBLIC_APP_ENV=${NEXT_PUBLIC_APP_ENV}
+ENV NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=${NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID}
+ENV NEXT_PUBLIC_TON_API_KEY=${NEXT_PUBLIC_TON_API_KEY}
+ENV ENV_NAME=${ENV_NAME}
+
+# Production image, copy all the files and run next
+FROM base AS runner
+ARG NEXT_PUBLIC_SENTRY_DNS
+ARG NEXT_PUBLIC_ANALYTICS_ID
+ARG NEXT_PUBLIC_APP_ENV
+ARG NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
+ARG NEXT_PUBLIC_TON_API_KEY
+ARG ENV_NAME
+ENV NEXT_PUBLIC_SENTRY_DNS=${NEXT_PUBLIC_SENTRY_DNS}
+ENV NEXT_PUBLIC_ANALYTICS_ID=${NEXT_PUBLIC_ANALYTICS_ID}
+ENV NEXT_PUBLIC_APP_ENV=${NEXT_PUBLIC_APP_ENV}
+ENV NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=${NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID}
+ENV NEXT_PUBLIC_TON_API_KEY=${NEXT_PUBLIC_TON_API_KEY}
+ENV ENV_NAME=${ENV_NAME}
+WORKDIR ${web}
+
+COPY . ${web}
+
+# # add git
+# RUN apk add git
+
+# # add python
+# RUN apk add --no-cache python3 make g++
+
+RUN yarn \
+ && NEXT_PUBLIC_SENTRY_DNS=${NEXT_PUBLIC_SENTRY_DNS} NEXT_PUBLIC_ANALYTICS_ID=${NEXT_PUBLIC_ANALYTICS_ID} NEXT_PUBLIC_APP_ENV=${NEXT_PUBLIC_APP_ENV} NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=${NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID} ENV_NAME=${ENV_NAME} yarn build
+
+ENTRYPOINT yarn start
+
+EXPOSE 3000
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index dc8b916f..28db76be
--- a/README.md
+++ b/README.md
@@ -1 +1,46 @@
-# ebridge-web
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+
+
+
+
+
+
+## Getting Started
+
+First, setup env files refer to `.env.example`:
+
+- `.env.local` for all local environment
+- `.env.mainnet.local` for mainnet
+- `.env.testnet.local` for testnet
+
+Run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
+
+[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
+
+The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 00000000..422b1944
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1 @@
+module.exports = { extends: ['@commitlint/config-conventional'] };
diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md
new file mode 100644
index 00000000..abc2699a
--- /dev/null
+++ b/docs/PROJECT_STRUCTURE.md
@@ -0,0 +1,266 @@
+# eBridge-web Project Architecture Documentation
+
+## Project Overview
+
+eBridge-web is a frontend application built with Next.js framework, designed to implement cross-chain bridge functionality. The project uses TypeScript as the development language and employs a modern frontend technology stack and toolchain.
+
+## Technology Stack
+
+- **Framework**: Next.js
+- **Language**: TypeScript
+- **Package Manager**: Yarn
+- **UI Library**: Ant Design
+- **State Management**: React Query
+- **Web3 Libraries**:
+ - aelf-sdk
+ - aelf-sdk-cross-chain
+ - wagmi
+ - viem
+- **Code Standards**: ESLint, Prettier
+- **Testing**: Vitest
+- **CI/CD**: GitHub Actions
+
+## Testing
+
+The project uses Vitest for unit testing. Test files are located alongside the source files with the `.test.ts` or `.test.tsx` extension.
+
+### Test Structure
+
+- Test files follow the naming convention: `[filename].test.ts` or `[filename].test.tsx`
+- Tests are organized using the `describe` and `it` blocks
+- Each test file should be placed in the same directory as the component/function it tests
+
+### Test Coverage
+
+The project maintains a minimum test coverage requirement:
+- Statement coverage: 80%
+- Branch coverage: 70%
+- Function coverage: 80%
+- Line coverage: 80%
+
+### Running Tests
+
+```bash
+# Run all tests
+yarn test
+
+# Run tests without watch mode
+yarn test:not-watch
+
+# Update test snapshots
+yarn test:update
+
+# Run tests with coverage report
+yarn test:coverage
+
+# Run tests for CI
+yarn test:ci
+```
+
+### Test Utilities
+
+The project provides several test utilities:
+- `@testing-library/react` for component testing
+- `@testing-library/react-hooks` for hook testing
+- `@testing-library/jest-dom` for DOM testing
+- `happy-dom` for browser environment simulation
+
+### Testing Best Practices
+
+1. **Component Testing**
+ - Test component rendering
+ - Test user interactions
+ - Test component props
+ - Test component state changes
+
+2. **Hook Testing**
+ - Test hook initialization
+ - Test hook state changes
+ - Test hook side effects
+ - Test hook cleanup
+
+3. **Utility Function Testing**
+ - Test input/output
+ - Test edge cases
+ - Test error handling
+
+4. **Integration Testing**
+ - Test component interactions
+ - Test API calls
+ - Test state management
+
+### Mocking
+
+The project uses Vitest's mocking capabilities for:
+- API calls
+- Web3 interactions
+- Contract calls
+- External dependencies
+
+Example of a test file:
+
+```typescript
+import { render, screen } from '@testing-library/react';
+import { MyComponent } from './MyComponent';
+import { describe, it, expect } from 'vitest';
+
+describe('MyComponent', () => {
+ it('renders correctly', () => {
+ render();
+ expect(screen.getByText('Hello World')).toBeInTheDocument();
+ });
+
+ it('handles user interaction', () => {
+ render();
+ const button = screen.getByRole('button');
+ fireEvent.click(button);
+ expect(screen.getByText('Clicked!')).toBeInTheDocument();
+ });
+});
+```
+
+## Package Management
+
+The project uses Yarn as its package manager. Key Yarn-related files and configurations:
+
+- `yarn.lock`: Lock file that ensures consistent dependency versions across all installations
+- `.yarnrc.yml`: Yarn configuration file for managing package registry and other settings
+- `.yarn/`: Directory containing Yarn's cache and other internal files
+
+### Yarn Commands
+
+Common Yarn commands used in the project:
+
+```bash
+# Install dependencies
+yarn install
+
+# Run development server
+yarn dev
+
+# Run development server for mainnet
+yarn dev:mainnet
+
+# Run development server for test1
+yarn dev:test1
+
+# Build for production
+yarn build
+
+# Build for mainnet
+yarn build:mainnet
+
+# Start production server
+yarn start
+
+# Start production server for mainnet
+yarn start:mainnet
+
+# Run linting
+yarn lint
+
+# Run bundle analysis
+yarn dev:analyze
+```
+
+## Directory Structure
+
+```
+ebridge-web/
+├── src/ # Source code directory
+│ ├── api/ # API interface definitions and request handling
+│ ├── assets/ # Static resources (images, fonts, etc.)
+│ ├── components/ # Reusable React components
+│ ├── constants/ # Constant definitions
+│ ├── contexts/ # React Context related code
+│ ├── contracts/ # Smart contract related code
+│ ├── hooks/ # Custom React Hooks
+│ ├── i18n/ # Internationalization config and translation files
+│ ├── modals/ # Modal components
+│ ├── page-components/ # Page-level components
+│ ├── pages/ # Next.js page components
+│ ├── styles/ # Global styles and theme configuration
+│ ├── types/ # TypeScript type definitions
+│ ├── utils/ # Utility functions
+│ └── wagmiConnectors/ # Web3 wallet connection related code
+├── public/ # Static resources directory
+├── docs/ # Project documentation
+└── ... # Other configuration files
+
+```
+
+## Main Directory Descriptions
+
+### src/api
+Contains all API interface definitions and network request handling logic.
+
+### src/components
+Contains reusable React components that are used across multiple pages.
+
+### src/contexts
+React Context related code for global state management.
+
+### src/contracts
+Smart contract related code, including contract ABI and contract interaction logic.
+
+### src/hooks
+Custom React Hooks that provide reusable state logic.
+
+### src/i18n
+Internationalization configuration and translation files, supporting multiple languages.
+
+### src/modals
+Modal components for displaying popups, dialogs, etc.
+
+### src/page-components
+Page-level components that are typically associated with specific pages.
+
+### src/pages
+Next.js page components that define the application's routing structure.
+
+### src/styles
+Global style definitions and theme configuration.
+
+### src/types
+TypeScript type definition files.
+
+### src/utils
+General utility functions and helper methods.
+
+### src/wagmiConnectors
+Web3 wallet connection related code for handling blockchain wallet interactions.
+
+## Configuration Files
+
+- `next.config.js`: Next.js configuration
+- `tsconfig.json`: TypeScript configuration
+- `.eslintrc.json`: ESLint configuration
+- `.prettierrc`: Prettier code formatting configuration
+- `vite.config.mts`: Vite configuration
+- `vitest.setup.ts`: Vitest test configuration
+
+## Development Tools and Scripts
+
+The project uses multiple development tools and scripts to ensure code quality:
+
+- ESLint for code quality checking
+- Prettier for code formatting
+- Husky for Git hooks
+- Vitest for unit testing
+- Next.js bundle analyzer for performance optimization
+
+## Deployment
+
+The project includes a Dockerfile for containerized deployment. Additionally, automated build and deployment processes are implemented through GitHub Actions.
+
+## Environment Configuration
+
+The project supports multiple environments:
+- Mainnet
+- Test1
+- Development
+
+Each environment has corresponding rewrite configuration files:
+- `rewrites.mainnet.js`
+- `rewrites.testnet.js`
+- `rewrites.test1.js`
\ No newline at end of file
diff --git a/docs/TEST_ARCHITECTURE.md b/docs/TEST_ARCHITECTURE.md
new file mode 100644
index 00000000..9d30abce
--- /dev/null
+++ b/docs/TEST_ARCHITECTURE.md
@@ -0,0 +1,122 @@
+# eBridge-web Test Architecture
+
+## Overview
+
+The project uses Vitest as the primary testing framework, along with React Testing Library for component testing. The test setup includes various configurations to handle browser-specific APIs and DOM manipulation.
+
+## Test Framework
+
+- **Vitest**: Main testing framework
+- **React Testing Library**: For component testing
+- **Jest DOM**: For DOM testing utilities
+- **Happy DOM**: For browser environment simulation
+
+## Test Configuration
+
+The test configuration is primarily set up in `vitest.setup.ts`, which includes:
+
+1. DOM environment setup
+2. Mock implementations for browser APIs:
+ - TextEncoder
+ - ResizeObserver
+ - matchMedia
+ - Canvas context
+3. Global cleanup after each test
+4. React global setup
+
+## Running Tests
+
+The project provides several npm scripts for running tests:
+
+```bash
+# Run tests in watch mode (default)
+yarn test
+
+# Run tests without watch mode
+yarn test:not-watch
+
+# Update snapshots
+yarn test:update
+
+# Run tests with coverage
+yarn test:coverage
+
+# Run tests in CI environment
+yarn test:ci
+```
+
+## Test Structure
+
+Tests should be placed in the same directory as the component or module they're testing, with the following naming convention:
+- `*.test.ts` or `*.test.tsx` for component tests
+- `*.spec.ts` or `*.spec.tsx` for utility/function tests
+
+## Test Dependencies
+
+Key testing dependencies in `package.json`:
+
+```json
+{
+ "devDependencies": {
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.2.0",
+ "@testing-library/react-hooks": "^8.0.1",
+ "@vitest/coverage-v8": "^3.0.5",
+ "happy-dom": "^17.1.0",
+ "vitest": "^3.0.5"
+ }
+}
+```
+
+## Best Practices
+
+1. **Component Testing**:
+ - Use React Testing Library for component tests
+ - Focus on testing component behavior rather than implementation details
+ - Use meaningful test names that describe the test's purpose
+
+2. **Utility Testing**:
+ - Test pure functions with clear inputs and outputs
+ - Use appropriate test cases to cover edge cases
+ - Mock external dependencies when necessary
+
+3. **Test Coverage**:
+ - Aim for meaningful test coverage
+ - Focus on testing critical business logic
+ - Use coverage reports to identify untested areas
+
+4. **Mocking**:
+ - Use Vitest's built-in mocking capabilities
+ - Mock external services and APIs
+ - Keep mocks simple and focused
+
+## CI Integration
+
+Tests are automatically run in the CI environment using the `test:ci` script, which includes coverage reporting. The CI configuration ensures that:
+
+1. All tests pass before merging
+2. Coverage thresholds are met
+3. Test results are properly reported
+
+## Debugging Tests
+
+To debug tests:
+
+1. Use the `debug()` function from React Testing Library
+2. Add `console.log` statements in test files
+3. Use the `--inspect` flag with Vitest for Node.js debugging
+
+## Common Issues and Solutions
+
+1. **DOM Environment Issues**:
+ - Use the provided mock implementations in `vitest.setup.ts`
+ - Ensure proper cleanup after each test
+
+2. **Async Testing**:
+ - Use `waitFor` from React Testing Library
+ - Handle promises and async operations properly
+
+3. **Mocking Issues**:
+ - Ensure mocks are properly reset between tests
+ - Use appropriate mock implementations for different scenarios
\ No newline at end of file
diff --git a/feature-ut-badge-utils-test-results.json b/feature-ut-badge-utils-test-results.json
new file mode 100644
index 00000000..783f58f0
--- /dev/null
+++ b/feature-ut-badge-utils-test-results.json
@@ -0,0 +1,6 @@
+{
+ "schemaVersion": 1,
+ "label": "tests",
+ "message": "89 tests, 0 failures, 0 errors",
+ "color": "brightgreen"
+}
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100755
index 00000000..4f11a03d
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/next.config.js b/next.config.js
new file mode 100755
index 00000000..5ce2d89c
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,82 @@
+/* eslint-disable */
+/** @type {import('next').NextConfig} */
+const { NEXT_PUBLIC_PREFIX, ANALYZE, NODE_ENV } = process.env;
+const withLess = require('next-with-less');
+const withPlugins = require('next-compose-plugins');
+const withBundleAnalyzer = require('@next/bundle-analyzer')({
+ enabled: ANALYZE === 'true',
+});
+const { rewriteConstants, getRewrites, rewriteEnv } = require('./rewriteENV');
+rewriteEnv();
+rewriteConstants();
+const plugins = [
+ [withBundleAnalyzer],
+ [
+ withLess,
+ {
+ lessLoaderOptions: {
+ lessOptions: {
+ modifyVars: {
+ '@app-prefix': NEXT_PUBLIC_PREFIX,
+ '@ant-prefix': NEXT_PUBLIC_PREFIX,
+ },
+ },
+ },
+ },
+ ],
+];
+
+const nextConfig = {
+ reactStrictMode: false,
+ swcMinify: false,
+ // webpack(config) {
+ // config.resolve.alias['bn.js'] = path.resolve(process.cwd(), 'node_modules', 'bn.js');
+ // return config;
+ // },
+ compiler: {
+ removeConsole: false,
+ },
+ async rewrites() {
+ return getRewrites();
+ },
+ images: {
+ domains: [
+ 'raw.githubusercontent.com',
+ 'forest-mainnet.s3.ap-northeast-1.amazonaws.com',
+ 'forest-testnet.s3.ap-northeast-1.amazonaws.com',
+ 'portkey-did.s3.ap-northeast-1.amazonaws.com',
+ ],
+ },
+};
+
+const productionConfig = {
+ ...nextConfig,
+ swcMinify: false,
+ compiler: {
+ removeConsole: {
+ exclude: ['error'],
+ },
+ },
+ experimental: {
+ 'react-use': {
+ transform: 'react-use/lib/{{member}}',
+ },
+ lodash: {
+ transform: 'lodash/{{member}}',
+ },
+ },
+ resolve: {},
+};
+
+const withTM = require('next-transpile-modules')([
+ 'antd-mobile',
+ '@aelf-web-login/wallet-adapter-base',
+ '@aelf-web-login/wallet-adapter-react',
+ '@aelf-web-login/wallet-adapter-bridge',
+ '@portkey/did-ui-react',
+]);
+
+module.exports = withPlugins(
+ plugins,
+ ANALYZE === 'true' || process.env.NEXT_PUBLIC_APP_ENV === 'mainnet' ? withTM(productionConfig) : withTM(nextConfig),
+);
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..ad1cf995
--- /dev/null
+++ b/package.json
@@ -0,0 +1,129 @@
+{
+ "name": "ebridge-web",
+ "version": "2.13.3",
+ "private": true,
+ "scripts": {
+ "dev": "PORT=3000 next dev",
+ "dev:mainnet": "PORT=4200 ENV_NAME=mainnet next dev",
+ "dev:test1": "ENV_NAME=test1 next dev",
+ "build": "next build",
+ "build:mainnet": "ENV_NAME=mainnet next build",
+ "start": "next start",
+ "start:mainnet": "ENV_NAME=mainnet next start",
+ "lint": "next lint",
+ "dev:analyze": "PORT=4000 ANALYZE=true next dev",
+ "postinstall": "bash scripts/postinstall.sh",
+ "test": "vitest",
+ "test:not-watch": "vitest run",
+ "test:update": "vitest -u",
+ "test:coverage": "vitest --coverage",
+ "test:ci": "vitest --coverage"
+ },
+ "dependencies": {
+ "@aelf-react/core": "^0.1.19",
+ "@aelf-react/types": "^0.1.10",
+ "@aelf-web-login/utils": "^0.4.0-alpha.10",
+ "@aelf-web-login/wallet-adapter-base": "^0.4.0-alpha.10",
+ "@aelf-web-login/wallet-adapter-bridge": "^0.4.0-alpha.10",
+ "@aelf-web-login/wallet-adapter-fairy-vault-discover": "^0.4.0-alpha.10",
+ "@aelf-web-login/wallet-adapter-night-elf": "^0.4.0-alpha.10",
+ "@aelf-web-login/wallet-adapter-portkey-discover": "^0.4.0-alpha.10",
+ "@aelf-web-login/wallet-adapter-portkey-web": "^0.4.0-alpha.10",
+ "@aelf-web-login/wallet-adapter-react": "^0.4.0-alpha.10",
+ "@ant-design/icons": "^5.3.0",
+ "@apollo/client": "^3.7.3",
+ "@portkey/contracts": "^2.19.0-alpha.10",
+ "@portkey/did": "^2.19.0-alpha.24",
+ "@portkey/did-ui-react": "^2.19.0-alpha.10",
+ "@portkey/types": "^2.19.0-alpha.10",
+ "@portkey/utils": "^2.19.0-alpha.10",
+ "@sentry/nextjs": "^7.48.0",
+ "@tanstack/query-core": "^5.62.7",
+ "@tanstack/react-query": "^5.62.7",
+ "@ton/core": "^0.59.0",
+ "@ton/crypto": "^3.3.0",
+ "@tonconnect/ui-react": "^2.0.9",
+ "@walletconnect/ethereum-provider": "^2.9.2",
+ "@walletconnect/modal-ui": "git+https://github.com/kled-bridge/modal-ui#37b7aedc0aa659d44f8abb72e56f75108940732f",
+ "aelf-bridge": "git+https://github.com/mason-hz/aelf-bridge",
+ "aelf-sdk": "^3.4.15",
+ "aelf-sdk-cross-chain": "^1.0.14",
+ "antd": "^4.21.7",
+ "axios": "^1.5.0",
+ "bignumber.js": "^9.1.0",
+ "clsx": "^1.2.1",
+ "graphql": "^16.8.1",
+ "i18next": "^21.8.13",
+ "lodash": "^4.17.21",
+ "next": "12.2.0",
+ "next-compose-plugins": "^2.2.1",
+ "next-plugin-antd-less": "^1.8.0",
+ "next-transpile-modules": "^10.0.1",
+ "next-with-less": "^2.0.5",
+ "query-string": "^7.1.1",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-ga": "^3.3.1",
+ "react-i18next": "^11.18.0",
+ "react-infinite-scroll-component": "^6.1.0",
+ "react-use": "^17.4.0",
+ "tonweb": "^0.0.66",
+ "viem": "2.29.x",
+ "wagmi": "2.11.2",
+ "web3": "^1.7.4",
+ "web3-core": "^1.7.4"
+ },
+ "devDependencies": {
+ "@coinbase/wallet-sdk": "^3.3.0",
+ "@commitlint/cli": "^17.6.1",
+ "@commitlint/config-conventional": "^17.6.1",
+ "@next/bundle-analyzer": "^12.2.4",
+ "@next/eslint-plugin-next": "^13.4.1",
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.2.0",
+ "@testing-library/react-hooks": "^8.0.1",
+ "@types/jest": "^29.5.14",
+ "@types/lodash": "^4.14.201",
+ "@types/node": "18.0.1",
+ "@types/react": "18.2.51",
+ "@types/react-dom": "^18.2.18",
+ "@types/testing-library__jest-dom": "^6.0.0",
+ "@typescript-eslint/eslint-plugin": "^5.30.4",
+ "@typescript-eslint/parser": "^5.30.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "@vitest/coverage-v8": "^3.0.5",
+ "eslint": "8.19.0",
+ "eslint-config-next": "12.2.0",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "happy-dom": "^17.1.0",
+ "husky": "^8.0.3",
+ "less": "^4.1.3",
+ "less-loader": "^11.0.0",
+ "lint-staged": "^13.2.1",
+ "postcss-less": "^6.0.0",
+ "prettier": "^2.7.1",
+ "stylelint": "^14.9.1",
+ "stylelint-config-prettier": "^9.0.3",
+ "stylelint-config-standard": "^26.0.0",
+ "typescript": "5.0.4",
+ "vconsole": "^3.14.6",
+ "vite-tsconfig-paths": "^5.1.4",
+ "vitest": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx}": "eslint --cache --fix",
+ "*.{css,less}": "stylelint --fix"
+ },
+ "resolutions": {
+ "@types/react": "18.2.0",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "viem": "2.29.x",
+ "@walletconnect/modal-ui": "git+https://github.com/kled-bridge/modal-ui#37b7aedc0aa659d44f8abb72e56f75108940732f"
+ }
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 00000000..f5d998c9
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/font/Poppins-Bold.ttf b/public/font/Poppins-Bold.ttf
new file mode 100644
index 00000000..00559eeb
Binary files /dev/null and b/public/font/Poppins-Bold.ttf differ
diff --git a/public/font/Poppins-Medium.ttf b/public/font/Poppins-Medium.ttf
new file mode 100644
index 00000000..6bcdcc27
Binary files /dev/null and b/public/font/Poppins-Medium.ttf differ
diff --git a/public/font/Poppins-Regular.ttf b/public/font/Poppins-Regular.ttf
new file mode 100644
index 00000000..9f0c71b7
Binary files /dev/null and b/public/font/Poppins-Regular.ttf differ
diff --git a/public/tonconnect-manifest.json b/public/tonconnect-manifest.json
new file mode 100644
index 00000000..ae53d7da
--- /dev/null
+++ b/public/tonconnect-manifest.json
@@ -0,0 +1,7 @@
+{
+ "url": "https://ebridge.exchange",
+ "name": "eBridge",
+ "iconUrl": "https://ebridge.exchange/favicon.ico",
+ "termsOfUseUrl": "https://ebridge.exchange/teamOfService",
+ "privacyPolicyUrl": "https://ebridge.exchange/privacyPolicy"
+ }
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
new file mode 100755
index 00000000..fbf0e25a
--- /dev/null
+++ b/public/vercel.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/rewriteENV.js b/rewriteENV.js
new file mode 100644
index 00000000..256db2e1
--- /dev/null
+++ b/rewriteENV.js
@@ -0,0 +1,36 @@
+/* eslint-disable */
+const path = require('path');
+const fs = require('fs');
+
+function rewriteConstants() {
+ const appEnv = process.env.NEXT_PUBLIC_APP_ENV || 'testnet';
+ const IS_MAINNET = appEnv === 'mainnet';
+ const constantsPath = path.resolve(__dirname, `./src/constants/index.ts`);
+ fs.writeFileSync(
+ constantsPath,
+ `export const IS_MAINNET = ${IS_MAINNET};\n
+export * from './${appEnv}';\n`,
+ );
+}
+
+function getRewrites() {
+ const appEnv = process.env.NEXT_PUBLIC_APP_ENV || 'testnet';
+ return require(`./rewrites.${appEnv}`);
+}
+
+function rewriteEnv() {
+ const name = process.env.ENV_NAME;
+ try {
+ const data = fs.readFileSync(`.env.${name}.local`, 'utf8');
+ data.split('\n').forEach(function (item) {
+ const [key, value] = item.split('=');
+ process.env[key.trim()] = value.trim();
+ });
+ } catch (error) {
+ console.log(`Failed to load environment variables from file ${name}`);
+ }
+}
+
+exports.rewriteConstants = rewriteConstants;
+exports.getRewrites = getRewrites;
+exports.rewriteEnv = rewriteEnv;
diff --git a/rewrites.mainnet.js b/rewrites.mainnet.js
new file mode 100644
index 00000000..9e6e8048
--- /dev/null
+++ b/rewrites.mainnet.js
@@ -0,0 +1,9 @@
+const host = 'https://ebridge.exchange';
+module.exports = [
+ { source: '/api/:path*', destination: `${host}/api/:path*` },
+ { source: '/cms/:path*', destination: `https://cms.ebridge.exchange/:path*` },
+ {
+ source: '/AElfIndexer_eBridge/:path*',
+ destination: `https://dapp-indexer.ebridge.exchange/AElfIndexer_eBridge/:path*`,
+ },
+];
diff --git a/rewrites.test1.js b/rewrites.test1.js
new file mode 100644
index 00000000..b7f3223b
--- /dev/null
+++ b/rewrites.test1.js
@@ -0,0 +1,6 @@
+const host = 'http://192.168.66.78:7030';
+module.exports = [
+ { source: '/api/:path*', destination: `${host}/api/:path*` },
+ { source: '/cms/:path*', destination: `https://cms.ebridge.exchange/:path*` },
+ { source: '/AElfIndexer_eBridge/:path*', destination: `http://192.168.66.78:7022/AElfIndexer_eBridge/:path*` },
+];
diff --git a/rewrites.testnet.js b/rewrites.testnet.js
new file mode 100644
index 00000000..82d05ce2
--- /dev/null
+++ b/rewrites.testnet.js
@@ -0,0 +1,9 @@
+const host = 'https://test.ebridge.exchange';
+module.exports = [
+ { source: '/api/:path*', destination: `${host}/api/:path*` },
+ { source: '/cms/:path*', destination: `https://test-cms.ebridge.exchange/:path*` },
+ {
+ source: '/AElfIndexer_eBridge/:path*',
+ destination: `https://test-indexer.ebridge.exchange/AElfIndexer_eBridge/:path*`,
+ },
+];
diff --git a/scripts/fix-web-login.js b/scripts/fix-web-login.js
new file mode 100644
index 00000000..550d0db6
--- /dev/null
+++ b/scripts/fix-web-login.js
@@ -0,0 +1,16 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const fs = require('fs');
+const path = require('path');
+const filePath = path.resolve(
+ __dirname,
+ `../node_modules/@aelf-web-login/wallet-adapter-portkey-web/dist/esm/index.js`,
+);
+
+fs.readFile(filePath, 'utf-8', (err, data) => {
+ if (err) throw err;
+ let newData = data.replace(`import '@portkey/connect-web-wallet/dist/assets/index.css';`, '');
+ fs.writeFile(filePath, newData, (err) => {
+ if (err) throw err;
+ console.log('The file has been updated');
+ });
+});
diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh
new file mode 100644
index 00000000..791dff1c
--- /dev/null
+++ b/scripts/postinstall.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+husky install
+echo "husky install success"
+
+node scripts/fix-web-login.js
+echo "fix fix-web-login success"
+
diff --git a/src/api/__tests__/index.test.ts b/src/api/__tests__/index.test.ts
new file mode 100644
index 00000000..fe3c04be
--- /dev/null
+++ b/src/api/__tests__/index.test.ts
@@ -0,0 +1,109 @@
+import { describe, expect, test, vi } from 'vitest';
+import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
+import { baseRequest, request, requestGql } from 'api/index';
+import { service, spliceUrl } from 'api/utils';
+import myServer from 'api/server';
+import { IBaseRequest } from 'api/types';
+
+// Mock dependencies
+vi.mock('api/utils', () => ({
+ service: vi.fn(),
+ spliceUrl: vi.fn(),
+}));
+
+vi.mock('api/server', () => {
+ return {
+ default: {
+ parseRouter: vi.fn(),
+ base: {},
+ },
+ };
+});
+
+vi.mock('api/list', () => ({
+ BASE_APIS: { test: 'test' },
+ BASE_REQ_TYPES: {},
+ DEFAULT_METHOD: 'GET',
+ EXPAND_APIS: { expand: 'expand' },
+ EXPAND_REQ_TYPES: {},
+}));
+
+// Mock @apollo/client
+vi.mock('@apollo/client', () => ({
+ ApolloClient: vi.fn(),
+ HttpLink: vi.fn(),
+ InMemoryCache: vi.fn(),
+}));
+
+describe('baseRequest', () => {
+ test('should call service with correct parameters', async () => {
+ const mockResponse = { data: 'success' };
+ (service as any).mockResolvedValue(mockResponse);
+
+ const config: IBaseRequest = {
+ url: 'https://example.com',
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ };
+
+ const result = await baseRequest(config);
+
+ expect(spliceUrl).toHaveBeenCalledWith(config.url, config.query);
+ expect(service).toHaveBeenCalled();
+ expect(result).toEqual(mockResponse);
+ });
+
+ test('should use default method if method is not provided', async () => {
+ const mockResponse = { data: 'success' };
+ (service as any).mockResolvedValue(mockResponse);
+
+ const config: IBaseRequest = {
+ url: 'https://example.com',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ };
+
+ const result = await baseRequest(config);
+
+ expect(spliceUrl).toHaveBeenCalledWith(config.url, config.query);
+ expect(service).toHaveBeenCalled();
+ expect(result).toEqual(mockResponse);
+ });
+});
+
+describe('request', () => {
+ test('should assign base and expand requests correctly', () => {
+ const baseRequestMock = { test: vi.fn() };
+ const expandRequestMock = { expand: vi.fn() };
+
+ vi.mocked(myServer).base = baseRequestMock;
+ vi.mocked(myServer).expand = expandRequestMock;
+
+ expect(request).toHaveProperty('parseRouter');
+ expect(request).toHaveProperty('base');
+ });
+});
+
+describe('requestGql', () => {
+ test('should create ApolloClient with correct configuration', () => {
+ const uri = 'https://example.com/graphql';
+ const client = requestGql({ uri });
+
+ expect(ApolloClient).toHaveBeenCalledWith({
+ cache: new (InMemoryCache as any)(),
+ queryDeduplication: false,
+ defaultOptions: {
+ watchQuery: {
+ fetchPolicy: 'cache-and-network',
+ },
+ query: {
+ fetchPolicy: 'network-only',
+ },
+ },
+ link: new (HttpLink as any)({ uri }),
+ });
+
+ expect(client).toBeInstanceOf(ApolloClient);
+ });
+});
diff --git a/src/api/__tests__/server/index.test.ts b/src/api/__tests__/server/index.test.ts
new file mode 100644
index 00000000..0a4d2cfb
--- /dev/null
+++ b/src/api/__tests__/server/index.test.ts
@@ -0,0 +1,216 @@
+import { describe, expect, test, vi } from 'vitest';
+import myServer from 'api/server';
+import { DEFAULT_METHOD } from 'api/list';
+import { BaseConfig, UrlObj, RequestConfig } from 'api/types';
+import { service, getRequestConfig } from 'api/utils';
+
+// Mock dependencies
+vi.mock('api/utils', () => ({
+ service: vi.fn(),
+ spliceUrl: vi.fn(),
+ getRequestConfig: vi.fn(),
+}));
+
+vi.mock('api/list', () => ({
+ DEFAULT_METHOD: 'GET',
+}));
+
+describe('myServer', () => {
+ let serverInstance: any;
+
+ beforeEach(() => {
+ serverInstance = Object.create(myServer);
+ vi.clearAllMocks();
+ });
+
+ describe('parseRouter', () => {
+ test('should create methods on the server instance', () => {
+ const name = 'test';
+ const urlObj: UrlObj = {
+ method1: { target: 'https://example.com/method1', baseConfig: {} },
+ method2: { target: 'https://example.com/method2', baseConfig: {} },
+ };
+
+ serverInstance.parseRouter(name, urlObj);
+
+ expect(serverInstance[name]).toBeDefined();
+ expect(serverInstance[name].method1).toBeInstanceOf(Function);
+ expect(serverInstance[name].method2).toBeInstanceOf(Function);
+ });
+
+ test('should bind send method with correct base config', () => {
+ const name = 'test';
+ const urlObj: UrlObj = {
+ method1: { target: 'https://example.com/method1', baseConfig: {} },
+ };
+
+ serverInstance.parseRouter(name, urlObj);
+
+ expect(serverInstance[name].method1).toBeInstanceOf(Function);
+ });
+ });
+
+ describe('send', () => {
+ test('should call service with correct parameters', async () => {
+ const base: BaseConfig = { target: 'https://example.com', baseConfig: {} };
+ const config: RequestConfig = {
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const mockResponse = { data: 'success' };
+
+ (getRequestConfig as any).mockReturnValue({
+ method: 'POST',
+ query: 'test=1',
+ url: 'https://example.com',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ (service as any).mockResolvedValue(mockResponse);
+
+ const result = await serverInstance.send(base, config);
+
+ expect(getRequestConfig).toHaveBeenCalledWith(base, config);
+ expect(service).toHaveBeenCalledWith({
+ method: 'POST',
+ url: 'https://example.com',
+ headers: { 'Content-Type': 'application/json' },
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ test('should use default method if method is not provided', async () => {
+ const base: BaseConfig = { target: 'https://example.com', baseConfig: {} };
+ const config: RequestConfig = { query: 'test=1', headers: { 'Content-Type': 'application/json' } };
+ const mockResponse = { data: 'success' };
+
+ (getRequestConfig as any).mockReturnValue({
+ method: DEFAULT_METHOD,
+ query: 'test=1',
+ url: 'https://example.com',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ (service as any).mockResolvedValue(mockResponse);
+
+ const result = await serverInstance.send(base, config);
+
+ expect(getRequestConfig).toHaveBeenCalledWith(base, config);
+ expect(service).toHaveBeenCalledWith({
+ method: DEFAULT_METHOD,
+ url: 'https://example.com',
+ headers: { 'Content-Type': 'application/json' },
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ test('should handle string base correctly', async () => {
+ const base: BaseConfig = 'https://example.com';
+ const config: RequestConfig = {
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const mockResponse = { data: 'success' };
+
+ (getRequestConfig as any).mockReturnValue({
+ method: 'POST',
+ query: 'test=1',
+ url: 'https://example.com',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ (service as any).mockResolvedValue(mockResponse);
+
+ const result = await serverInstance.send(base, config);
+
+ expect(getRequestConfig).toHaveBeenCalledWith(base, config);
+ expect(service).toHaveBeenCalledWith({
+ method: 'POST',
+ url: 'https://example.com',
+ headers: { 'Content-Type': 'application/json' },
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ test('should executes successfully when the config not have url and base is string', async () => {
+ const base: BaseConfig = 'https://example.com';
+ const config: RequestConfig = {
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const mockResponse = { data: 'success' };
+
+ (getRequestConfig as any).mockReturnValue({
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ (service as any).mockResolvedValue(mockResponse);
+
+ const result = await serverInstance.send(base, config);
+
+ expect(getRequestConfig).toHaveBeenCalledWith(base, config);
+ expect(service).toHaveBeenCalledWith({
+ method: 'POST',
+ url: undefined,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ test('should executes successfully when the config not have url and base have target', async () => {
+ const base: BaseConfig = { target: 'https://example.com', baseConfig: {} };
+ const config: RequestConfig = {
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const mockResponse = { data: 'success' };
+
+ (getRequestConfig as any).mockReturnValue({
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ (service as any).mockResolvedValue(mockResponse);
+
+ const result = await serverInstance.send(base, config);
+
+ expect(getRequestConfig).toHaveBeenCalledWith(base, config);
+ expect(service).toHaveBeenCalledWith({
+ method: 'POST',
+ url: undefined,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ test('should executes successfully when the getRequestConfig method returns undefined', async () => {
+ const base: BaseConfig = { target: 'https://example.com', baseConfig: {} };
+ const config: RequestConfig = {
+ method: 'POST',
+ query: 'test=1',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const mockResponse = { data: 'success' };
+
+ (getRequestConfig as any).mockReturnValue(undefined);
+
+ (service as any).mockResolvedValue(mockResponse);
+
+ const result = await serverInstance.send(base, config);
+
+ expect(getRequestConfig).toHaveBeenCalledWith(base, config);
+ expect(service).toHaveBeenCalledWith({
+ method: 'GET',
+ url: undefined,
+ });
+ expect(result).toEqual(mockResponse);
+ });
+ });
+});
diff --git a/src/api/__tests__/utils.test.ts b/src/api/__tests__/utils.test.ts
new file mode 100644
index 00000000..d1507f1b
--- /dev/null
+++ b/src/api/__tests__/utils.test.ts
@@ -0,0 +1,204 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+import { describe, expect, test, vi } from 'vitest';
+import { service, spliceUrl, getRequestConfig, isDeniedRequest } from 'api/utils';
+
+// Mock eBridgeEventBus and axios
+vi.mock('utils/eBridgeEventBus');
+
+vi.mock('axios', () => {
+ const BASE_URL = 'https://ebridge.exchange';
+ return {
+ default: {
+ create: vi.fn().mockReturnValue({
+ interceptors: {
+ request: {
+ use: vi.fn().mockImplementation((config: any) => {
+ config();
+ }),
+ handlers: [
+ {
+ fulfilled: vi.fn().mockImplementation((config: any) => {
+ return config;
+ }),
+ rejected: vi.fn(),
+ },
+ ],
+ },
+ response: {
+ use: vi.fn().mockImplementation((response: any) => {
+ response({ data: { code: '200' } });
+ }),
+ },
+ },
+ post: vi.fn().mockImplementation((config: any) => {
+ return config;
+ }),
+ get: vi.fn().mockImplementation((config: any) => {
+ return config;
+ }),
+ defaults: {
+ baseURL: BASE_URL,
+ timeout: 50000,
+ },
+ }),
+ },
+ };
+});
+
+const BASE_URL = 'https://ebridge.exchange';
+
+describe('service', () => {
+ test('should create axios instance with correct baseURL and timeout', () => {
+ expect(service.defaults.baseURL).toBe(BASE_URL);
+ expect(service.defaults.timeout).toBe(50000);
+ });
+
+ test('should pass configuration through the GET request', async () => {
+ const config = {
+ url: '/test',
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ const response = await service.get(config.url);
+ expect(response).toEqual('/test');
+ });
+
+ test('should pass configuration through the POST request', async () => {
+ const config = {
+ url: '/test',
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ const response = await service.post(config.url);
+ expect(response).toEqual('/test');
+ });
+});
+
+describe('isDeniedRequest', () => {
+ test('returns true if error message contains "401"', () => {
+ const error = { message: 'Request failed with status code 401' };
+ const result = isDeniedRequest(error);
+ expect(result).toBe(true);
+ });
+
+ test('returns false if error message does not contain "401"', () => {
+ const error = { message: 'Request failed with status code 404' };
+ const result = isDeniedRequest(error);
+ expect(result).toBe(false);
+ });
+
+ test('handles errors without message property', () => {
+ // @ts-ignore
+ const error = {};
+ const result = isDeniedRequest(error as any);
+ expect(result).toBe(false);
+ });
+
+ test('logs warnings if an error occurs within isDeniedRequest', () => {
+ // Mock console.log
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ // @ts-ignore
+ isDeniedRequest(null);
+ expect(console.log).toHaveBeenCalled();
+ });
+});
+
+describe('spliceUrl', () => {
+ test('should return baseUrl when extendArg is not provided', () => {
+ const baseUrl = 'https://example.com';
+ const result = spliceUrl(baseUrl);
+
+ expect(result).toBe(baseUrl);
+ });
+
+ test('should return baseUrl with extendArg appended', () => {
+ const baseUrl = 'https://example.com';
+ const extendArg = 'test';
+ const result = spliceUrl(baseUrl, extendArg);
+
+ expect(result).toBe(`${baseUrl}/${extendArg}`);
+ });
+});
+
+describe('getRequestConfig', () => {
+ test('should return config when base is a string', () => {
+ const base = 'https://example.com';
+ const config = { method: 'GET', params: { id: 1 } };
+ const result = getRequestConfig(base, config);
+
+ expect(result).toEqual(config);
+ });
+
+ test('should merge baseConfig and config correctly', () => {
+ const base = {
+ target: '',
+ baseConfig: {
+ method: 'POST',
+ params: { page: 1 },
+ data: { key: 'value' },
+ query: '?sort=asc',
+ },
+ };
+ const config = {
+ method: 'GET',
+ params: { id: 1 },
+ data: { newKey: 'newValue' },
+ query: '&filter=true',
+ };
+ const result = getRequestConfig(base, config);
+
+ expect(result).toEqual({
+ ...config,
+ ...base.baseConfig,
+ query: `${base.baseConfig.query}${config.query}`,
+ method: config.method,
+ params: { ...base.baseConfig.params, ...config.params },
+ data: { ...base.baseConfig.data, ...config.data },
+ });
+ });
+
+ test('should handle undefined base and config', () => {
+ const result = getRequestConfig(undefined as any, undefined);
+
+ expect(result).toEqual({
+ data: {},
+ method: undefined,
+ params: {},
+ query: '',
+ });
+ });
+
+ test('should handle undefined baseConfig', () => {
+ const base = { target: '/test', baseConfig: {} };
+ const config = { method: 'GET', params: { id: 1 } };
+ const result = getRequestConfig(base, config);
+
+ expect(result).toEqual({
+ data: {},
+ ...config,
+ query: '',
+ });
+ });
+
+ test('should handle undefined config', () => {
+ const base = {
+ target: '/test',
+ baseConfig: {
+ method: 'POST',
+ params: { page: 1 },
+ data: { key: 'value' },
+ query: '?sort=asc',
+ },
+ };
+ const result = getRequestConfig(base);
+
+ expect(result).toEqual(base.baseConfig);
+ });
+});
diff --git a/src/api/index.ts b/src/api/index.ts
new file mode 100644
index 00000000..23b25584
--- /dev/null
+++ b/src/api/index.ts
@@ -0,0 +1,42 @@
+import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
+import { BASE_APIS, BASE_REQ_TYPES, DEFAULT_METHOD, EXPAND_APIS, EXPAND_REQ_TYPES } from './list';
+import myServer from './server';
+import { IBaseRequest } from './types';
+import { spliceUrl, service } from './utils';
+
+function baseRequest({ url, method = DEFAULT_METHOD, query = '', ...c }: IBaseRequest) {
+ return service({
+ ...c,
+ url: spliceUrl(url, query),
+ method,
+ });
+}
+
+myServer.parseRouter('base', BASE_APIS);
+
+Object.entries(EXPAND_APIS).forEach(([key, value]) => {
+ myServer.parseRouter(key, value);
+});
+
+const request: BASE_REQ_TYPES & EXPAND_REQ_TYPES = Object.assign({}, myServer.base, myServer);
+
+const requestGql = ({ uri }: { uri: string }) => {
+ return new ApolloClient({
+ cache: new InMemoryCache(),
+ queryDeduplication: false,
+ defaultOptions: {
+ watchQuery: {
+ fetchPolicy: 'cache-and-network',
+ },
+ query: {
+ fetchPolicy: 'network-only',
+ },
+ // ...defaultOptions,
+ },
+ link: new HttpLink({
+ uri,
+ }),
+ });
+};
+
+export { baseRequest, request, requestGql };
diff --git a/src/api/list.ts b/src/api/list.ts
new file mode 100644
index 00000000..9392cdc0
--- /dev/null
+++ b/src/api/list.ts
@@ -0,0 +1,90 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { API_REQ_FUNCTION, UrlObj } from './types';
+
+export const DEFAULT_METHOD = 'GET';
+
+/**
+ * api request configuration directory
+ * @example
+ * upload: {
+ * target: '/api/file-management/file-descriptor/upload',
+ * baseConfig: { method: 'POST', },
+ * },
+ * or:
+ * upload:'/api/file-management/file-descriptor/upload'
+ *
+ * @description api configuration default method is from DEFAULT_METHOD
+ * @type {UrlObj} // The type of this object from UrlObj.
+ */
+export const BASE_APIS = {
+ getCurrentBlockHeight: '',
+};
+
+const CrossApiList = {
+ getCrossChainTransfers: '/api/app/cross-chain-transfers',
+ getCrossChainIndexing: '/api/app/cross-chain-indexing/progress',
+ getDailyLimits: 'api/app/limiter/dailyLimits',
+ getRateLimits: 'api/app/limiter/rateLimits',
+};
+
+const ApplicationApiList = {
+ getTokenList: '/api/ebridge/application/tokens',
+ getTokenDetail: '/api/ebridge/application/token-detail',
+ commitTokenInfo: {
+ target: '/api/ebridge/application/commit-basic-info',
+ baseConfig: { method: 'POST' },
+ },
+ getTokenInfo: '/api/ebridge/application/user-token-access-info',
+ getChainStatus: '/api/ebridge/application/check-chain-access-status',
+ addChain: {
+ target: '/api/ebridge/application/add-chain',
+ baseConfig: { method: 'POST' },
+ },
+ prepareBindIssue: {
+ target: '/api/ebridge/application/prepare-binding-issue',
+ baseConfig: { method: 'POST' },
+ },
+ getIssue: {
+ target: '/api/ebridge/application/issue-binding',
+ baseConfig: { method: 'POST' },
+ },
+ getMyApplicationList: '/api/ebridge/application/list',
+ getApplicationDetail: '/api/ebridge/application/detail',
+ getTokenConfig: '/api/ebridge/application/token/config',
+};
+
+const CommonApiList = {
+ getTokenWhiteList: '/api/ebridge/application/token-white-list',
+ getTokenPrice: '/api/ebridge/application/token/price',
+};
+
+const CMSApiList = {
+ getToggleResultOfMask: '/cms/items/home',
+};
+
+const PoolApiList = {
+ overview: '/api/ebridge/application/pool-overview',
+ detail: '/api/ebridge/application/pool-detail',
+ list: '/api/ebridge/application/pool-list',
+};
+/**
+ * api request extension configuration directory
+ * @description object.key // The type of this object key comes from from @type {UrlObj}
+ */
+export const EXPAND_APIS = {
+ cross: CrossApiList,
+ cms: CMSApiList,
+ application: ApplicationApiList,
+ common: CommonApiList,
+ pool: PoolApiList,
+};
+
+export type BASE_REQ_TYPES = {
+ [x in keyof typeof BASE_APIS]: API_REQ_FUNCTION;
+};
+
+export type EXPAND_REQ_TYPES = {
+ [X in keyof typeof EXPAND_APIS]: {
+ [K in keyof (typeof EXPAND_APIS)[X]]: API_REQ_FUNCTION;
+ };
+};
diff --git a/src/api/server/index.ts b/src/api/server/index.ts
new file mode 100644
index 00000000..b5aab143
--- /dev/null
+++ b/src/api/server/index.ts
@@ -0,0 +1,34 @@
+import { DEFAULT_METHOD } from 'api/list';
+import { BaseConfig, UrlObj, RequestConfig } from 'api/types';
+import { spliceUrl, service, getRequestConfig } from 'api/utils';
+
+const myServer = new Function();
+
+/**
+ * @method parseRouter
+ * @param {string} name
+ * @param {UrlObj} urlObj
+ */
+myServer.prototype.parseRouter = function (name: string, urlObj: UrlObj) {
+ const obj: any = (this[name] = {});
+ Object.keys(urlObj).forEach((key) => {
+ obj[key] = this.send.bind(this, urlObj[key]);
+ });
+};
+
+/**
+ * @method send
+ * @param {BaseConfig} base
+ * @param {object} config
+ * @return {Promise}
+ */
+myServer.prototype.send = function (base: BaseConfig, config: RequestConfig) {
+ const { method = DEFAULT_METHOD, query = '', url, ...axiosConfig } = getRequestConfig(base, config) || {};
+ return service({
+ ...axiosConfig,
+ url: url || spliceUrl(typeof base === 'string' ? base : base.target, query),
+ method,
+ });
+};
+
+export default myServer.prototype;
diff --git a/src/api/types.ts b/src/api/types.ts
new file mode 100644
index 00000000..db941843
--- /dev/null
+++ b/src/api/types.ts
@@ -0,0 +1,15 @@
+import { AxiosRequestConfig, AxiosResponse } from 'axios';
+
+export type RequestConfig = {
+ query?: string; //this for url parameter; example: test/:id
+} & AxiosRequestConfig;
+
+export type IBaseRequest = {
+ url: string;
+} & RequestConfig;
+
+export type BaseConfig = string | { target: string; baseConfig: RequestConfig };
+
+export type UrlObj = { [key: string]: BaseConfig };
+
+export type API_REQ_FUNCTION = (config?: RequestConfig) => Promise>;
diff --git a/src/api/utils.ts b/src/api/utils.ts
new file mode 100644
index 00000000..d0aee870
--- /dev/null
+++ b/src/api/utils.ts
@@ -0,0 +1,65 @@
+import axios from 'axios';
+import { BaseConfig, RequestConfig } from './types';
+import { BASE_URL } from 'constants/index';
+import eBridgeEventBus from 'utils/eBridgeEventBus';
+
+export const isDeniedRequest = (error: { message: string }) => {
+ try {
+ const message: string = error.message;
+ if (message?.includes('401')) return true;
+ } catch (error) {
+ console.log(error);
+ }
+ return false;
+};
+
+const axiosInstance = axios.create({
+ baseURL: BASE_URL,
+ timeout: 50000,
+});
+
+axiosInstance.interceptors.request.use(
+ (config) => {
+ return config;
+ },
+ (error) => {
+ Promise.reject(error);
+ },
+);
+
+axiosInstance.interceptors.response.use(
+ (response) => {
+ const res = response.data;
+ return res;
+ },
+ (error) => {
+ console.log('api error', error, isDeniedRequest(error));
+ if (isDeniedRequest(error)) {
+ eBridgeEventBus.Unauthorized.emit();
+ }
+ return Promise.reject(error);
+ },
+);
+
+export const service = axiosInstance;
+
+export function spliceUrl(baseUrl: string, extendArg?: string) {
+ return extendArg ? baseUrl + '/' + extendArg : baseUrl;
+}
+
+export function getRequestConfig(base: BaseConfig, config?: RequestConfig) {
+ if (typeof base === 'string') {
+ return config;
+ } else {
+ const { baseConfig } = base || {};
+ const { query, method, params, data } = config || {};
+ return {
+ ...config,
+ ...baseConfig,
+ query: (baseConfig?.query || '') + (query || ''),
+ method: method ? method : baseConfig?.method,
+ params: Object.assign({}, baseConfig?.params, params),
+ data: Object.assign({}, baseConfig?.data, data),
+ };
+ }
+}
diff --git a/src/assets/images/arrow-blue.svg b/src/assets/images/arrow-blue.svg
new file mode 100644
index 00000000..3ee8488e
--- /dev/null
+++ b/src/assets/images/arrow-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/arrow-gray.svg b/src/assets/images/arrow-gray.svg
new file mode 100644
index 00000000..e5627d17
--- /dev/null
+++ b/src/assets/images/arrow-gray.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/arrow.svg b/src/assets/images/arrow.svg
new file mode 100644
index 00000000..0c7a1eb4
--- /dev/null
+++ b/src/assets/images/arrow.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/arrow_right_gray.svg b/src/assets/images/arrow_right_gray.svg
new file mode 100644
index 00000000..ce11d7c7
--- /dev/null
+++ b/src/assets/images/arrow_right_gray.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/arrow_right_white.svg b/src/assets/images/arrow_right_white.svg
new file mode 100644
index 00000000..d648d433
--- /dev/null
+++ b/src/assets/images/arrow_right_white.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/back.svg b/src/assets/images/back.svg
new file mode 100644
index 00000000..497c53b8
--- /dev/null
+++ b/src/assets/images/back.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/background.png b/src/assets/images/background.png
new file mode 100644
index 00000000..dd21266d
Binary files /dev/null and b/src/assets/images/background.png differ
diff --git a/src/assets/images/bg-mobile.png b/src/assets/images/bg-mobile.png
new file mode 100644
index 00000000..3fe90014
Binary files /dev/null and b/src/assets/images/bg-mobile.png differ
diff --git a/src/assets/images/bg-pc.png b/src/assets/images/bg-pc.png
new file mode 100644
index 00000000..99ed9502
Binary files /dev/null and b/src/assets/images/bg-pc.png differ
diff --git a/src/assets/images/bridge-blue.svg b/src/assets/images/bridge-blue.svg
new file mode 100644
index 00000000..34a711b6
--- /dev/null
+++ b/src/assets/images/bridge-blue.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/bridge.svg b/src/assets/images/bridge.svg
new file mode 100644
index 00000000..4fdb9d31
--- /dev/null
+++ b/src/assets/images/bridge.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/change.svg b/src/assets/images/change.svg
new file mode 100644
index 00000000..9c0dc45f
--- /dev/null
+++ b/src/assets/images/change.svg
@@ -0,0 +1,70 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/check-blue.svg b/src/assets/images/check-blue.svg
new file mode 100644
index 00000000..16c01790
--- /dev/null
+++ b/src/assets/images/check-blue.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/check-filled.svg b/src/assets/images/check-filled.svg
new file mode 100644
index 00000000..cd119b89
--- /dev/null
+++ b/src/assets/images/check-filled.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/checkFilled16.svg b/src/assets/images/checkFilled16.svg
new file mode 100644
index 00000000..2ba1e5d5
--- /dev/null
+++ b/src/assets/images/checkFilled16.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/clear-icon.svg b/src/assets/images/clear-icon.svg
new file mode 100644
index 00000000..37f83710
--- /dev/null
+++ b/src/assets/images/clear-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/close.svg b/src/assets/images/close.svg
new file mode 100644
index 00000000..7afc1600
--- /dev/null
+++ b/src/assets/images/close.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/closeFilled16.svg b/src/assets/images/closeFilled16.svg
new file mode 100644
index 00000000..f67a2691
--- /dev/null
+++ b/src/assets/images/closeFilled16.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/coinbaseWalletIcon.svg b/src/assets/images/coinbaseWalletIcon.svg
new file mode 100644
index 00000000..04e9f49b
--- /dev/null
+++ b/src/assets/images/coinbaseWalletIcon.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/community.svg b/src/assets/images/community.svg
new file mode 100644
index 00000000..cfdba9d9
--- /dev/null
+++ b/src/assets/images/community.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/complete.svg b/src/assets/images/complete.svg
new file mode 100644
index 00000000..1612a12d
--- /dev/null
+++ b/src/assets/images/complete.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/discord-dark.svg b/src/assets/images/discord-dark.svg
new file mode 100644
index 00000000..449bb817
--- /dev/null
+++ b/src/assets/images/discord-dark.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/discord.svg b/src/assets/images/discord.svg
new file mode 100644
index 00000000..9c5021d7
--- /dev/null
+++ b/src/assets/images/discord.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/down.svg b/src/assets/images/down.svg
new file mode 100644
index 00000000..b8b626fa
--- /dev/null
+++ b/src/assets/images/down.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/downBig.svg b/src/assets/images/downBig.svg
new file mode 100644
index 00000000..7d35097f
--- /dev/null
+++ b/src/assets/images/downBig.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/downSmall.svg b/src/assets/images/downSmall.svg
new file mode 100644
index 00000000..c078e705
--- /dev/null
+++ b/src/assets/images/downSmall.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/empty-wallet.svg b/src/assets/images/empty-wallet.svg
new file mode 100644
index 00000000..2c8ad29f
--- /dev/null
+++ b/src/assets/images/empty-wallet.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/empty.svg b/src/assets/images/empty.svg
new file mode 100644
index 00000000..e8285395
--- /dev/null
+++ b/src/assets/images/empty.svg
@@ -0,0 +1,27 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/error-filled.svg b/src/assets/images/error-filled.svg
new file mode 100644
index 00000000..e0f66cd3
--- /dev/null
+++ b/src/assets/images/error-filled.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/ethereum.svg b/src/assets/images/ethereum.svg
new file mode 100644
index 00000000..88ee23a3
--- /dev/null
+++ b/src/assets/images/ethereum.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/assets/images/group.svg b/src/assets/images/group.svg
new file mode 100644
index 00000000..9070fb87
--- /dev/null
+++ b/src/assets/images/group.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts
new file mode 100644
index 00000000..bb1f2c00
--- /dev/null
+++ b/src/assets/images/index.ts
@@ -0,0 +1,107 @@
+import metamask from './metamask.png';
+import walletConnectIcon from './walletConnectIcon.svg';
+import coinbaseWalletIcon from './coinbaseWalletIcon.svg';
+import nightElfIcon from './nightElf.png';
+import ebridgeLog from './logo-eBridge.svg';
+import aelfLogo from './logo-aelf.svg';
+import aelfChainLogo from './logo-aelf-chain.svg';
+import ethereumLogo from './ethereum.svg';
+import groupIcon from './group.svg';
+import tonLogo from './ton.svg';
+import clearIcon from './clear-icon.svg';
+import discordIcon from './discord.svg';
+import discordDarkIcon from './discord-dark.svg';
+import telegramIcon from './telegram.svg';
+import telegramDarkIcon from './telegram-dark.svg';
+import twitterIcon from './twitter.svg';
+import twitterDarkIcon from './twitter-dark.svg';
+import translateIcon from './translate.svg';
+import translateBlueIcon from './translate-blue.svg';
+import communityIcon from './community.svg';
+import legalIcon from './legal.svg';
+import arrowIcon from './arrow.svg';
+import arrowGrayIcon from './arrow-gray.svg';
+import downIcon from './down.svg';
+import downBigIcon from './downBig.svg';
+import downSmallIcon from './downSmall.svg';
+import arrowBlueIcon from './arrow-blue.svg';
+import arrowRightWhiteIcon from './arrow_right_white.svg';
+import arrowRightGrayIcon from './arrow_right_gray.svg';
+import backIcon from './back.svg';
+import checkBlueIcon from './check-blue.svg';
+import checkFilledIcon from './check-filled.svg';
+import checkFilled16Icon from './checkFilled16.svg';
+import closeFilled16Icon from './closeFilled16.svg';
+import errorFilledIcon from './error-filled.svg';
+import menuIcon from './menu.svg';
+import bridgeIcon from './bridge.svg';
+import bridgeBlueIcon from './bridge-blue.svg';
+import transactionsIcon from './transactions.svg';
+import transactionsBlueIcon from './transactions-blue.svg';
+import closeIcon from './close.svg';
+import infoCircleIcon from './info-circle.svg';
+import questionFilledIcon from './question-filled.svg';
+import loadingIcon from './loading.png';
+import completeIcon from './complete.svg';
+import infoCircleFillIcon from './info-circle-fill.svg';
+import infoCircleFillGrayIcon from './info-circle-fill-gray.svg';
+import viewProgressIcon from './view-progress.svg';
+import poolsIcon from './pools.svg';
+import poolsBlueIcon from './pools-blue.svg';
+import listingIcon from './listing.svg';
+import searchIcon from './search.svg';
+
+export {
+ metamask,
+ walletConnectIcon,
+ coinbaseWalletIcon,
+ nightElfIcon,
+ ebridgeLog,
+ aelfLogo,
+ aelfChainLogo,
+ ethereumLogo,
+ groupIcon,
+ clearIcon,
+ discordIcon,
+ discordDarkIcon,
+ telegramIcon,
+ telegramDarkIcon,
+ twitterIcon,
+ twitterDarkIcon,
+ translateIcon,
+ translateBlueIcon,
+ communityIcon,
+ legalIcon,
+ arrowIcon,
+ arrowGrayIcon,
+ downIcon,
+ downBigIcon,
+ downSmallIcon,
+ arrowBlueIcon,
+ arrowRightWhiteIcon,
+ arrowRightGrayIcon,
+ backIcon,
+ checkBlueIcon,
+ checkFilledIcon,
+ checkFilled16Icon,
+ closeFilled16Icon,
+ errorFilledIcon,
+ menuIcon,
+ bridgeIcon,
+ bridgeBlueIcon,
+ transactionsIcon,
+ transactionsBlueIcon,
+ closeIcon,
+ infoCircleIcon,
+ infoCircleFillIcon,
+ infoCircleFillGrayIcon,
+ questionFilledIcon,
+ loadingIcon,
+ tonLogo,
+ completeIcon,
+ viewProgressIcon,
+ poolsIcon,
+ poolsBlueIcon,
+ listingIcon,
+ searchIcon,
+};
diff --git a/src/assets/images/info-circle-fill-gray.svg b/src/assets/images/info-circle-fill-gray.svg
new file mode 100644
index 00000000..bff68e73
--- /dev/null
+++ b/src/assets/images/info-circle-fill-gray.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/info-circle-fill.svg b/src/assets/images/info-circle-fill.svg
new file mode 100644
index 00000000..9752fadf
--- /dev/null
+++ b/src/assets/images/info-circle-fill.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/info-circle.svg b/src/assets/images/info-circle.svg
new file mode 100644
index 00000000..ea13d4f0
--- /dev/null
+++ b/src/assets/images/info-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/legal.svg b/src/assets/images/legal.svg
new file mode 100644
index 00000000..0288bc4a
--- /dev/null
+++ b/src/assets/images/legal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/images/listing.svg b/src/assets/images/listing.svg
new file mode 100644
index 00000000..f1f24aa5
--- /dev/null
+++ b/src/assets/images/listing.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/loading.png b/src/assets/images/loading.png
new file mode 100644
index 00000000..1861216d
Binary files /dev/null and b/src/assets/images/loading.png differ
diff --git a/src/assets/images/logo-aelf-chain.svg b/src/assets/images/logo-aelf-chain.svg
new file mode 100644
index 00000000..6b085a9a
--- /dev/null
+++ b/src/assets/images/logo-aelf-chain.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/logo-aelf.svg b/src/assets/images/logo-aelf.svg
new file mode 100644
index 00000000..99b5656b
--- /dev/null
+++ b/src/assets/images/logo-aelf.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/images/logo-eBridge.svg b/src/assets/images/logo-eBridge.svg
new file mode 100644
index 00000000..799a3d1b
--- /dev/null
+++ b/src/assets/images/logo-eBridge.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/menu.svg b/src/assets/images/menu.svg
new file mode 100644
index 00000000..d5ffac71
--- /dev/null
+++ b/src/assets/images/menu.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/metamask.png b/src/assets/images/metamask.png
new file mode 100644
index 00000000..85714ea2
Binary files /dev/null and b/src/assets/images/metamask.png differ
diff --git a/src/assets/images/nightElf.png b/src/assets/images/nightElf.png
new file mode 100644
index 00000000..0f764541
Binary files /dev/null and b/src/assets/images/nightElf.png differ
diff --git a/src/assets/images/pools-blue.svg b/src/assets/images/pools-blue.svg
new file mode 100644
index 00000000..c32d6e2e
--- /dev/null
+++ b/src/assets/images/pools-blue.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/pools.svg b/src/assets/images/pools.svg
new file mode 100644
index 00000000..004edf8b
--- /dev/null
+++ b/src/assets/images/pools.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/question-filled.svg b/src/assets/images/question-filled.svg
new file mode 100644
index 00000000..8a3d67de
--- /dev/null
+++ b/src/assets/images/question-filled.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/images/search.svg b/src/assets/images/search.svg
new file mode 100644
index 00000000..e5f39512
--- /dev/null
+++ b/src/assets/images/search.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/telegram-dark.svg b/src/assets/images/telegram-dark.svg
new file mode 100644
index 00000000..b8ec1abd
--- /dev/null
+++ b/src/assets/images/telegram-dark.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/telegram.svg b/src/assets/images/telegram.svg
new file mode 100644
index 00000000..1a6c12bc
--- /dev/null
+++ b/src/assets/images/telegram.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/ton.svg b/src/assets/images/ton.svg
new file mode 100644
index 00000000..819a1b04
--- /dev/null
+++ b/src/assets/images/ton.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/assets/images/transactions-blue.svg b/src/assets/images/transactions-blue.svg
new file mode 100644
index 00000000..4cb7baf0
--- /dev/null
+++ b/src/assets/images/transactions-blue.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/transactions.svg b/src/assets/images/transactions.svg
new file mode 100644
index 00000000..f87de16e
--- /dev/null
+++ b/src/assets/images/transactions.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/translate-blue.svg b/src/assets/images/translate-blue.svg
new file mode 100644
index 00000000..632a3f38
--- /dev/null
+++ b/src/assets/images/translate-blue.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/translate.svg b/src/assets/images/translate.svg
new file mode 100644
index 00000000..d2883c9e
--- /dev/null
+++ b/src/assets/images/translate.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/src/assets/images/twitter-dark.svg b/src/assets/images/twitter-dark.svg
new file mode 100644
index 00000000..48d47e43
--- /dev/null
+++ b/src/assets/images/twitter-dark.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/twitter.svg b/src/assets/images/twitter.svg
new file mode 100644
index 00000000..0df88b21
--- /dev/null
+++ b/src/assets/images/twitter.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/view-progress.svg b/src/assets/images/view-progress.svg
new file mode 100644
index 00000000..acb0cd94
--- /dev/null
+++ b/src/assets/images/view-progress.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/walletConnectIcon.svg b/src/assets/images/walletConnectIcon.svg
new file mode 100644
index 00000000..f78a7b85
--- /dev/null
+++ b/src/assets/images/walletConnectIcon.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/theme/color.less b/src/assets/theme/color.less
new file mode 100644
index 00000000..48333e51
--- /dev/null
+++ b/src/assets/theme/color.less
@@ -0,0 +1,54 @@
+@primary-color: ~'var(--@{app-prefix}-primary-color)';
+@primary-color-hover: ~'var(--@{app-prefix}-primary-color-hover)';
+
+@font-1: @primary-color;
+@font-2: #262626;
+@font-3: #8f97a6;
+@font-4: #000000;
+@font-5: #364153;
+@font-6: #e13d3d;
+@font-7: #faad14;
+@font-8: #f28736;
+@font-9: #52c41a;
+@font-10: #d8dce2;
+@font-11: #a2b0f2;
+@font-12: #4d6aec;
+@font-13: #808080;
+@font-14: #127fff;
+@font-15: #1a1a1a;
+@font-16: #285eff;
+@font-17: #252525;
+@font-18: #d6d6d6;
+
+@bg-1: #f7f7f9;
+@bg-2: #ffffff;
+@bg-3: #f3f4f7;
+@bg-4: #eceef2;
+@bg-5: #dee2e8;
+@bg-6: @primary-color;
+@bg-7: #f6f7fa;
+@bg-8: #edf0fd;
+@bg-9: #d27974;
+@bg-10: #fef6e7;
+@bg-11: #d8e0ff;
+@bg-12: #4f77ff;
+@bg-13: #edf0fd;
+@bg-14: #f9fcff;
+@bg-15: #f8f8f8;
+@bg-16: #f5f5f5;
+@bg-17: #127fff;
+@bg-18: #00b73e;
+@bg-19: #f0f0f0;
+@bg-20: #ff9900;
+@bg-21: #f53f3f;
+@bg-22: #fee8e8;
+
+@border-1: #c5cbd5;
+@border-2: @primary-color;
+@border-3: #dee2e8;
+@border-4: #faad14;
+@border-5: #e0e0e0;
+@border-6: #f0f0f0;
+
+@shadow-1: #6a83d1;
+@shadow-2: #0000000d;
diff --git a/src/assets/theme/common-class.less b/src/assets/theme/common-class.less
new file mode 100644
index 00000000..39395b2f
--- /dev/null
+++ b/src/assets/theme/common-class.less
@@ -0,0 +1,117 @@
+.flex {
+ display: flex;
+}
+.flex-1 {
+ flex: 1;
+}
+.flex-shrink-0 {
+ flex-shrink: 0;
+}
+.flex-row {
+ display: flex;
+ flex-direction: row;
+}
+.flex-row-reverse {
+ display: flex;
+ flex-direction: row-reverse;
+}
+.flex-column {
+ display: flex;
+ flex-direction: column;
+}
+.align-end {
+ align-items: flex-end;
+}
+.flex-column-center {
+ align-items: center;
+ .flex-column();
+}
+.flex-row-wrap {
+ flex-wrap: wrap;
+ .flex-row();
+}
+.flex-row-center {
+ align-items: center;
+ .flex-row();
+}
+.flex-row-center-between {
+ justify-content: space-between;
+ .flex-row-center();
+}
+.flex-row-content-center {
+ justify-content: center;
+ .flex-row();
+}
+.flex-row-content-end {
+ justify-content: flex-end;
+ .flex-row();
+}
+.row-center {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
+.flex-center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+.flex-row-between {
+ justify-content: space-between;
+ .flex-row();
+}
+.cursor-pointer {
+ cursor: pointer;
+}
+.margin-auto {
+ margin: auto;
+}
+.margin-top-10 {
+ margin-top: 10px;
+}
+
+.text-center {
+ text-align: center;
+}
+.text-right {
+ text-align: right;
+}
+.gap-8 {
+ gap: 8px;
+}
+.gap-4 {
+ gap: 4px;
+}
+
+
+/* set list-style */
+ul,
+ol {
+ list-style: none;
+}
+
+/* set text-decoration */
+a,
+a:hover {
+ text-decoration: none;
+}
+
+/* clear margin & padding */
+body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements */
+ dl, dt, dd, ul, ol, li, /* list elements */
+ pre, /* text formatting elements */
+ fieldset, button, input, textarea, /* form elements */
+ th, td {
+ /* table elements */
+ margin: 0;
+ padding: 0;
+}
+.pointer-events-none {
+ pointer-events: none !important;
+}
+
+.list-style-decimal {
+ list-style: decimal;
+ padding-left: 16px;
+}
diff --git a/src/assets/theme/constants.less b/src/assets/theme/constants.less
new file mode 100644
index 00000000..c22aab69
--- /dev/null
+++ b/src/assets/theme/constants.less
@@ -0,0 +1,17 @@
+
+@xl-width: 1179px;
+@md-width: 799px;
+
+@max-width: ~'var(--@{app-prefix}-max-width)';
+@footer-height: 120px;
+@header-height: ~'var(--@{app-prefix}-header-height)';
+
+.text-overflow(@n) {
+ text-overflow: -o-ellipsis-lastline;
+ overflow: hidden;
+ /* stylelint-disable-next-line declaration-block-no-duplicate-properties */
+ text-overflow: ellipsis;
+ -webkit-box-orient: vertical;
+ line-clamp: @n;
+ -webkit-line-clamp: @n;
+}
diff --git a/src/assets/theme/customize.less b/src/assets/theme/customize.less
new file mode 100644
index 00000000..ad5b0a7b
--- /dev/null
+++ b/src/assets/theme/customize.less
@@ -0,0 +1,33 @@
+@font-face {
+ font-family: Poppins-Bold;
+ src: url('/font/Poppins-Bold.ttf');
+}
+@font-face {
+ font-family: Poppins-Medium;
+ src: url('/font/Poppins-Medium.ttf');
+}
+@font-face {
+ font-family: Poppins-Regular;
+ src: url('/font/Poppins-Regular.ttf');
+}
+html {
+ --@{app-prefix}-primary-color: #127fff;
+ --@{app-prefix}-primary-color-hover: #127fff;
+ // border-hover
+ --@{app-prefix}-primary-5: #127fff;
+ // border-hover-shadow
+ --@{app-prefix}-primary-color-outline: #506ef514;
+ --@{app-prefix}-primary-1: #edf0fd;
+ --@{app-prefix}-error-color: #ce342a;
+ // constants
+ --@{app-prefix}-max-width: 1000px;
+ --@{app-prefix}-header-height: 86px;
+
+ @media screen and (max-width: 1160px) {
+ --@{app-prefix}-max-width: calc(100vw - 120px);
+ }
+}
+
+html[data-theme='dark'] {
+ --@{app-prefix}-primary-color: red;
+}
diff --git a/src/assets/theme/font.less b/src/assets/theme/font.less
new file mode 100644
index 00000000..d2223a0c
--- /dev/null
+++ b/src/assets/theme/font.less
@@ -0,0 +1,28 @@
+@import '/src/assets/theme/color.less';
+.font-family-semibold {
+ font-weight: bold;
+ font-family: Poppins-Bold, sans-serif;
+}
+.font-family-medium {
+ font-weight: 500;
+ font-family: Poppins-Medium, sans-serif;
+}
+.font-family-regular {
+ font-weight: 400;
+ font-family: Poppins-Regular, sans-serif;
+}
+.font-2 {
+ color: @font-2;
+}
+.font-15 {
+ color: @font-15;
+}
+// .font-family-ultralight {
+// font-family: PingFangSC-Ultralight, PingFang SC, sans-serif;
+// }
+// .font-family-light {
+// font-family: PingFangSC-Light, PingFang SC, sans-serif;
+// }
+// .font-family-thin {
+// font-family: PingFangSC-Thin, PingFang SC, sans-serif;
+// }
diff --git a/src/components/CommonAmountRow/AmountInput/index.tsx b/src/components/CommonAmountRow/AmountInput/index.tsx
new file mode 100644
index 00000000..70e35ad5
--- /dev/null
+++ b/src/components/CommonAmountRow/AmountInput/index.tsx
@@ -0,0 +1,31 @@
+import { Input, InputProps, Row } from 'antd';
+import { useState } from 'react';
+import { parseInputChange } from 'utils/input';
+import { isValidPositiveNumber } from 'utils/reg';
+import styles from './styles.module.less';
+export default function AmountInput({
+ value: propsValue,
+ onChange,
+ className,
+ maxLength,
+ placeholder = '0',
+}: InputProps) {
+ const [value, setValue] = useState('');
+ return (
+
+ {
+ const { value } = e.target;
+ if (value && !isValidPositiveNumber(value)) return;
+ propsValue === undefined && setValue(parseInputChange(value));
+ onChange?.(e);
+ }}
+ />
+
+ );
+}
diff --git a/src/components/CommonAmountRow/AmountInput/styles.module.less b/src/components/CommonAmountRow/AmountInput/styles.module.less
new file mode 100644
index 00000000..2ca235bd
--- /dev/null
+++ b/src/components/CommonAmountRow/AmountInput/styles.module.less
@@ -0,0 +1,22 @@
+@import '/src/assets/theme/color.less';
+
+.amount-input {
+ flex: 1;
+ :global {
+ .@{app-prefix}-input,
+ .@{app-prefix}-input-affix-wrapper {
+ padding: 0;
+ border: 0;
+ color: @font-17;
+ font-size: 20px;
+ font-weight: 500;
+ line-height: 24px;
+ &:focus,
+ &:not(.input-affix-wrapper-disabled),
+ &:not(.input-affix-wrapper-disabled):hover {
+ border: 0;
+ box-shadow: none;
+ }
+ }
+ }
+}
diff --git a/src/components/CommonAmountRow/index.tsx b/src/components/CommonAmountRow/index.tsx
new file mode 100644
index 00000000..62647634
--- /dev/null
+++ b/src/components/CommonAmountRow/index.tsx
@@ -0,0 +1,73 @@
+import { useLanguage } from 'i18n';
+import clsx from 'clsx';
+import { Button, InputProps } from 'antd';
+import styles from './styles.module.less';
+import AmountInput from './AmountInput';
+import { ReactNode } from 'react';
+
+export type TCommonAmountRowProps = {
+ onClickMAX?: () => void;
+ showBalance?: boolean;
+ leftHeaderTitle?: string;
+ rightHeaderTitle?: string;
+ leftHeaderEle?: ReactNode;
+ rightInputEle?: ReactNode;
+ rightHeaderEle?: ReactNode;
+ value?: InputProps['value'];
+ onAmountInputChange?: InputProps['onChange'];
+ showError?: boolean;
+ maxLength?: number;
+ className?: string;
+ inputWrapClassName?: string;
+ placeholder?: InputProps['placeholder'];
+};
+
+export default function CommonAmountRow({
+ onClickMAX,
+ showBalance,
+ leftHeaderTitle,
+ rightHeaderTitle,
+ leftHeaderEle,
+ rightHeaderEle,
+ rightInputEle,
+ value,
+ showError,
+ maxLength = 25,
+ onAmountInputChange,
+ className,
+ inputWrapClassName,
+ placeholder,
+}: TCommonAmountRowProps) {
+ const { t } = useLanguage();
+
+ return (
+
+
+ {leftHeaderEle ? leftHeaderEle :
{leftHeaderTitle}}
+ {showBalance && (
+
+ {rightHeaderEle ? rightHeaderEle : {rightHeaderTitle}}
+
+
+ )}
+
+
+
+ {rightInputEle ? (
+ <>
+
+ {rightInputEle}
+ >
+ ) : null}
+
+
+ );
+}
diff --git a/src/components/CommonAmountRow/styles.module.less b/src/components/CommonAmountRow/styles.module.less
new file mode 100644
index 00000000..d40ea7bb
--- /dev/null
+++ b/src/components/CommonAmountRow/styles.module.less
@@ -0,0 +1,43 @@
+@import '/src/assets/theme/color.less';
+
+.amount-row {
+ gap: 8px;
+}
+
+.amount-label-wrap {
+ gap: 16px;
+ .amount-label {
+ color: @font-13;
+ font-size: 16px;
+ line-height: 22px;
+ }
+ .balance-wrap {
+ gap: 16px;
+ }
+ .balance {
+ color: @font-15;
+ font-size: 16px;
+ line-height: 22px;
+ }
+ .max-button {
+ padding: 0;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 22px;
+ }
+}
+
+.amount-input-wrap {
+ gap: 12px;
+ padding: 16px 12px;
+ border-radius: 6px;
+ border: 1px solid @border-5;
+ background: @bg-2;
+ .amount-input-red input {
+ color: @font-6;
+ }
+ .divider {
+ height: 24px;
+ border-left: 1px solid @border-6;
+ }
+}
diff --git a/src/components/CommonButton/index.tsx b/src/components/CommonButton/index.tsx
new file mode 100644
index 00000000..aa0d8391
--- /dev/null
+++ b/src/components/CommonButton/index.tsx
@@ -0,0 +1,35 @@
+import { Button } from 'antd';
+import type { ButtonProps } from 'antd/lib/button/button';
+import clsx from 'clsx';
+import React from 'react';
+import styles from './styles.module.less';
+declare const ButtonTypes: ['default', 'primary', 'ghost', 'dashed', 'link', 'text', 'select'];
+
+export interface CommonButtonProps {
+ type?: (typeof ButtonTypes)[number];
+ children?: React.ReactNode;
+ icon?: React.ReactNode;
+ shape?: ButtonProps['shape'];
+ size?: ButtonProps['size'];
+ disabled?: boolean;
+ loading?:
+ | boolean
+ | {
+ delay?: number;
+ };
+ prefixCls?: string;
+ className?: string;
+ ghost?: boolean;
+ danger?: boolean;
+ block?: boolean;
+ onClick?: ButtonProps['onClick'];
+}
+export default function CommonButton({ className, type, ...props }: CommonButtonProps) {
+ let c = '';
+ let btnType = type;
+ if (type === 'select') {
+ c = styles['select-btn'];
+ btnType = undefined;
+ }
+ return ;
+}
diff --git a/src/components/CommonButton/styles.module.less b/src/components/CommonButton/styles.module.less
new file mode 100644
index 00000000..cfa0e692
--- /dev/null
+++ b/src/components/CommonButton/styles.module.less
@@ -0,0 +1,30 @@
+@import '/src/assets/theme/color.less';
+.select-btn {
+ background: @bg-1;
+ border: 1px solid @border-1;
+ color: @font-5;
+ :global {
+ .anticon {
+ font-size: 12px;
+ }
+ }
+ &:hover {
+ background: @bg-4;
+ border-color: @border-1;
+ color: @font-5;
+ }
+ &:checked {
+ background: @bg-5;
+ border-color: @border-1;
+ }
+ &:disabled {
+ background: @bg-4;
+ border-color: @border-1;
+ color: @font-3;
+ }
+ &:focus {
+ background: @bg-1;
+ border-color: @border-1;
+ color: @font-5;
+ }
+}
diff --git a/src/components/CommonImage/index.tsx b/src/components/CommonImage/index.tsx
new file mode 100644
index 00000000..32402168
--- /dev/null
+++ b/src/components/CommonImage/index.tsx
@@ -0,0 +1,10 @@
+import clsx from 'clsx';
+import Image, { ImageProps } from 'next/image';
+import styles from './styles.module.less';
+export default function CommonImage({ className, style, layout = 'fill', alt = 'img', ...props }: ImageProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/CommonImage/styles.module.less b/src/components/CommonImage/styles.module.less
new file mode 100644
index 00000000..dd65c731
--- /dev/null
+++ b/src/components/CommonImage/styles.module.less
@@ -0,0 +1,3 @@
+.common-img {
+ position: relative;
+}
diff --git a/src/components/CommonLink/index.tsx b/src/components/CommonLink/index.tsx
new file mode 100644
index 00000000..6970125f
--- /dev/null
+++ b/src/components/CommonLink/index.tsx
@@ -0,0 +1,40 @@
+import { LinkOutlined } from '@ant-design/icons';
+import { Button } from 'antd';
+import React from 'react';
+import { isUrl } from '../../utils/reg';
+import clsx from 'clsx';
+import { useMobile } from 'contexts/useStore/hooks';
+export default function CommonLink({
+ href,
+ children,
+ className,
+ showIcon = true,
+ isTagA,
+}: {
+ href: string;
+ children?: React.ReactNode;
+ className?: string;
+ showIcon?: boolean;
+ isTagA?: boolean;
+}) {
+ const isMobile = useMobile();
+ const target = isMobile ? '_self' : '_blank';
+ if (isTagA)
+ return (
+
+ {children}
+
+ );
+ return (
+
+ );
+}
diff --git a/src/components/CommonMessage/index.tsx b/src/components/CommonMessage/index.tsx
new file mode 100644
index 00000000..548e201b
--- /dev/null
+++ b/src/components/CommonMessage/index.tsx
@@ -0,0 +1,18 @@
+import { message } from 'antd';
+import i18n from 'i18next';
+
+class CommonMessage {
+ error(text: any) {
+ if (
+ /((You closed the prompt with out any action)|(Operation canceled)|(You closed the prompt without any action))/.test(
+ JSON.stringify(text),
+ )
+ ) {
+ const txt = i18n.t('Request rejected. eBridge needs your permission to continue');
+ return message.error(txt);
+ }
+ message.error(text);
+ }
+}
+// eslint-disable-next-line import/no-anonymous-default-export
+export default new CommonMessage();
diff --git a/src/components/CommonModal/index.tsx b/src/components/CommonModal/index.tsx
new file mode 100644
index 00000000..04df112c
--- /dev/null
+++ b/src/components/CommonModal/index.tsx
@@ -0,0 +1,58 @@
+import { Col, Modal, ModalProps, Row } from 'antd';
+import clsx from 'clsx';
+import LeftOutlined from '@ant-design/icons/LeftOutlined';
+import { ReactNode } from 'react';
+import useMediaQueries from 'hooks/useMediaQueries'; // Cannot import useMobile
+import { prefixCls } from 'constants/misc';
+import CommonImage from 'components/CommonImage';
+import { closeIcon } from 'assets/images';
+import { TelegramPlatform } from 'utils/telegram/telegram';
+
+export interface ICommonModalProps extends ModalProps {
+ children?: any;
+ className?: string;
+ leftCallBack?: () => void;
+ leftElement?: ReactNode;
+ transitionName?: string;
+ type?: 'pop-bottom' | 'default';
+}
+
+export default function CommonModal(props: ICommonModalProps) {
+ const { leftCallBack, width, title, leftElement, transitionName, type } = props;
+ const isMd = useMediaQueries('md');
+ return (
+ }
+ maskClosable={false}
+ centered={props.centered ? props.centered : !isMd}
+ destroyOnClose
+ footer={null}
+ {...props}
+ width={width ? width : '800px'}
+ className={clsx(
+ 'common-modals',
+ {
+ 'common-modal-center': isMd && props.centered,
+ 'common-bottom-modals': type === 'pop-bottom',
+ 'tg-common-modals': TelegramPlatform.isTelegramPlatform(),
+ 'tg-common-bottom-modals': type === 'pop-bottom' && TelegramPlatform.isTelegramPlatform(),
+ },
+ props.className,
+ )}
+ transitionName={transitionName ?? isMd ? `${prefixCls}-move-down` : undefined}
+ title={
+
+ {leftCallBack || leftElement ? (
+
+ {leftElement || }
+
+ ) : null}
+
+ {title}
+
+ {leftCallBack || leftElement ? : null}
+
+ }
+ />
+ );
+}
diff --git a/src/components/CommonPopover/index.tsx b/src/components/CommonPopover/index.tsx
new file mode 100644
index 00000000..8bb3d32e
--- /dev/null
+++ b/src/components/CommonPopover/index.tsx
@@ -0,0 +1,6 @@
+import { Popover, PopoverProps } from 'antd';
+import clsx from 'clsx';
+import styles from './styles.module.less';
+export default function CommonPopover(props: PopoverProps) {
+ return ;
+}
diff --git a/src/components/CommonPopover/styles.module.less b/src/components/CommonPopover/styles.module.less
new file mode 100644
index 00000000..0c0ec2f8
--- /dev/null
+++ b/src/components/CommonPopover/styles.module.less
@@ -0,0 +1,19 @@
+@import '/src/assets/theme/color.less';
+.common-popover {
+ padding: 5px;
+ :global {
+ .@{app-prefix}-popover-inner {
+ border-radius: 12px;
+ }
+ .@{app-prefix}-popover-inner-content {
+ max-width: 280px;
+ padding: 20px;
+ box-shadow: 0 5px 12px 4px @shadow-2;
+ border-radius: 12px;
+ }
+ .@{app-prefix}-popover-arrow {
+ width: 16px;
+ height: 16px;
+ }
+ }
+}
diff --git a/src/components/CommonSelect/index.tsx b/src/components/CommonSelect/index.tsx
new file mode 100644
index 00000000..abb31fb1
--- /dev/null
+++ b/src/components/CommonSelect/index.tsx
@@ -0,0 +1,18 @@
+import { Select, SelectProps } from 'antd';
+import clsx from 'clsx';
+import React from 'react';
+import CommonImage from 'components/CommonImage';
+import { closeIcon, arrowIcon } from 'assets/images';
+import styles from './styles.module.less';
+export default function CommonSelect({ className, dropdownClassName, ...props }: SelectProps) {
+ return (
+ }
+ suffixIcon={}
+ getPopupContainer={(triggerNode) => triggerNode}
+ dropdownClassName={clsx(styles['select-dropdown'], dropdownClassName)}
+ className={clsx(styles.select, className)}
+ {...props}
+ />
+ );
+}
diff --git a/src/components/CommonSelect/styles.module.less b/src/components/CommonSelect/styles.module.less
new file mode 100644
index 00000000..7bbaf60c
--- /dev/null
+++ b/src/components/CommonSelect/styles.module.less
@@ -0,0 +1,42 @@
+@import '/src/assets/theme/color.less';
+.select {
+ :global {
+ .@{app-prefix}-select-selector {
+ border-radius: 6px !important;
+ height: 40px !important;
+ .@{app-prefix}-select-selection-item {
+ line-height: 38px;
+ }
+ .@{app-prefix}-select-selection-placeholder,
+ .@{app-prefix}-select-selection-search-input {
+ height: 40px !important;
+ line-height: 38px !important;
+ }
+ }
+ }
+}
+
+.icon {
+ width: 16px;
+ height: 16px;
+}
+
+.select-dropdown {
+ overflow: hidden;
+ border: 1px solid @border-5;
+ border-radius: 6px;
+ :global {
+ .@{app-prefix}-select-item {
+ min-height: 40px;
+ padding: 13px 12px;
+ &:hover:not(.@{app-prefix}-select-item-option-selected) {
+ background: @bg-3;
+ }
+ }
+ .@{app-prefix}-select-item-option-selected {
+ background: @bg-8;
+ color: @font-1;
+ font-weight: 500;
+ }
+ }
+}
diff --git a/src/components/CommonSelectTokenModal/index.tsx b/src/components/CommonSelectTokenModal/index.tsx
new file mode 100644
index 00000000..1720d348
--- /dev/null
+++ b/src/components/CommonSelectTokenModal/index.tsx
@@ -0,0 +1,128 @@
+import { useCallback, useEffect, useState } from 'react';
+import { useLanguage } from 'i18n';
+import clsx from 'clsx';
+import { Input, Row } from 'antd';
+import LinkForBlank from 'components/LinkForBlank';
+import CommonModal from 'components/CommonModal';
+import TokenLogo from 'components/TokenLogo';
+import Remind, { RemindType } from 'components/Remind';
+import { formatSymbol } from 'utils/token';
+import { getListingUrl } from 'utils/listingApplication';
+import { TBridgeChainId } from 'constants/chain';
+import { ListingStep } from 'constants/listingApplication';
+import styles from './styles.module.less';
+import CommonImage from 'components/CommonImage';
+import { searchIcon } from 'assets/images';
+
+type TToken = T & {
+ symbol: string;
+ icon?: string;
+ displaySymbol?: string;
+ status?: string;
+ isShowSuffix?: boolean;
+ disable?: boolean;
+};
+
+interface ISelectTokenProps {
+ selectSymbol?: string;
+ tokenList: TToken[];
+ chainId?: TBridgeChainId;
+ hideAddToken?: boolean;
+ remindContent?: string;
+ onSelect: (token: TToken) => void;
+}
+
+interface ISelectTokenModalProps extends ISelectTokenProps {
+ open?: boolean;
+ onClose: () => void;
+}
+
+function SelectToken({
+ selectSymbol,
+ tokenList,
+ chainId,
+ hideAddToken,
+ remindContent,
+ onSelect,
+}: ISelectTokenProps) {
+ const [searchList, setSearchList] = useState[]>();
+ const [value, setValue] = useState();
+ const { t } = useLanguage();
+
+ const onSearch = useCallback(() => {
+ if (value === undefined || typeof value !== 'string') return;
+ setSearchList(
+ tokenList.filter((i) => {
+ return i.symbol.toUpperCase().includes(value.toUpperCase());
+ }),
+ );
+ }, [tokenList, value]);
+
+ useEffect(() => {
+ if (value === '') onSearch();
+ }, [onSearch, value]);
+
+ return (
+ <>
+ {
+ setValue(e.target.value);
+ }}
+ onPressEnter={onSearch}
+ className={styles['input-search']}
+ placeholder={t('Search token')}
+ suffix={}
+ />
+ {remindContent && (
+
+ {remindContent}
+
+ )}
+
+ {(searchList || tokenList).map((item, k) => {
+ return (
+
onSelect(item)}
+ key={k}
+ className={clsx('flex-row-center-between', {
+ [styles['token-item']]: true,
+ [styles['token-item-selected']]: item.symbol === selectSymbol,
+ [styles['token-item-disable']]: item?.disable,
+ })}>
+
+
+ {formatSymbol(item.displaySymbol || item.symbol)}
+
+ {item?.isShowSuffix && {item?.status}
}
+
+ );
+ })}
+
+ {!hideAddToken && (
+
+ )}
+ >
+ );
+}
+
+export default function CommonSelectTokenModal({ open, onClose, ...selectTokenProps }: ISelectTokenModalProps) {
+ const { t } = useLanguage();
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/CommonSelectTokenModal/styles.module.less b/src/components/CommonSelectTokenModal/styles.module.less
new file mode 100644
index 00000000..b7b2c8bc
--- /dev/null
+++ b/src/components/CommonSelectTokenModal/styles.module.less
@@ -0,0 +1,134 @@
+@import '/src/assets/theme/color.less';
+@import '/src/assets/theme/font.less';
+@import '/src/assets/theme/constants.less';
+
+@margin-left-right-web: 24px;
+@margin-left-right-mobile: 16px;
+
+.select-modal {
+ height: 430px;
+ :global {
+ .@{app-prefix}-modal-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ .@{app-prefix}-modal-header {
+ .@{app-prefix}-modal-title .@{app-prefix}-col {
+ text-align: left !important;
+ }
+ }
+ .@{app-prefix}-modal-body {
+ overflow: hidden;
+ min-width: 448px;
+ max-width: 448px;
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ padding-top: 8px;
+ }
+ }
+ .input-search {
+ height: 40px;
+ border-radius: 6px;
+ width: calc(100% - @margin-left-right-web * 2);
+ margin-bottom: 8px;
+ align-self: center;
+ :global {
+ .@{app-prefix}-input-suffix {
+ font-size: 16px;
+ }
+ }
+ }
+ .input-search-suffix{
+ width: 16px;
+ height: 16px;
+ }
+ .token-tip {
+ margin-left: @margin-left-right-web;
+ margin-right: @margin-left-right-web;
+ margin-bottom: 8px;
+ }
+ .token-list {
+ overflow: auto;
+ .token-item {
+ flex-wrap: nowrap;
+ padding: 13px @margin-left-right-web;
+ line-height: 24px;
+ cursor: pointer;
+ .font-family-medium();
+ .token-logo {
+ flex: none;
+ width: 24px;
+ height: 24px;
+ margin-right: 8px;
+ border-radius: 50%;
+ }
+ .token-name {
+ overflow: hidden;
+ flex-shrink: 1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .token-suffix {
+ font-size: 14px;
+ color: @font-13;
+ .font-family-regular();
+ }
+ .token-info-disable {
+ opacity: 0.5;
+ }
+ &:hover {
+ background: @bg-15;
+ }
+ }
+ .token-item-selected {
+ background: @bg-15;
+ }
+ .token-item-disable {
+ cursor: not-allowed;
+ }
+ }
+ .token-list-add-token {
+ margin-bottom: 56px;
+ }
+ .bottom-row {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 8px 0 24px;
+ .row-body {
+ height: 48px;
+ a {
+ color: @font-1;
+ }
+ }
+ }
+}
+@media screen and (max-width: @md-width) {
+ .select-modal {
+ height: auto;
+ :global {
+ .@{app-prefix}-modal-header {
+ border-width: 0;
+ }
+ .@{app-prefix}-modal-content {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ }
+ }
+ .input-search {
+ width: calc(100% - @margin-left-right-mobile * 2);
+ }
+ .token-tip {
+ margin-left: @margin-left-right-mobile;
+ margin-right: @margin-left-right-mobile;
+ }
+ .token-list {
+ .token-item {
+ padding-left: @margin-left-right-mobile;
+ padding-right: @margin-left-right-mobile;
+ }
+ }
+ }
+}
diff --git a/src/components/CommonSteps/index.tsx b/src/components/CommonSteps/index.tsx
new file mode 100644
index 00000000..07dc2b68
--- /dev/null
+++ b/src/components/CommonSteps/index.tsx
@@ -0,0 +1,54 @@
+import { useMemo } from 'react';
+import clsx from 'clsx';
+import { Steps, StepsProps } from 'antd';
+import PartialLoading from 'components/PartialLoading';
+import { useMobile } from 'contexts/useStore/hooks';
+import styles from './styles.module.less';
+
+export interface ICommonStepsProps extends Omit {
+ stepItems: (Required['items'][number] & { isLoading?: boolean })[];
+ hideLine?: boolean;
+}
+
+export default function CommonSteps({
+ className,
+ stepItems,
+ current,
+ direction,
+ hideLine = false,
+ ...props
+}: ICommonStepsProps) {
+ const isMobile = useMobile();
+
+ const stepDirection = useMemo(() => {
+ const _default = isMobile ? 'vertical' : 'horizontal';
+ return direction || _default;
+ }, [direction, isMobile]);
+
+ const stepDirectionClassName = useMemo(() => {
+ return stepDirection === 'vertical' ? styles['common-steps-vertical'] : styles['common-steps-horizontal'];
+ }, [stepDirection]);
+
+ const stepItemsWithIcon = stepItems?.map(({ isLoading, ...item }) => ({
+ ...item,
+ icon: isLoading ? : undefined,
+ }));
+
+ return (
+
+ );
+}
diff --git a/src/components/CommonSteps/styles.module.less b/src/components/CommonSteps/styles.module.less
new file mode 100644
index 00000000..dfc00a59
--- /dev/null
+++ b/src/components/CommonSteps/styles.module.less
@@ -0,0 +1,196 @@
+@import 'src/assets/theme/color.less';
+@import 'src/assets/theme/constants.less';
+@import 'src/assets/theme/font.less';
+
+.common-steps.common-steps-weight {
+ :global {
+ .@{app-prefix}-steps-item {
+ .@{app-prefix}-steps-item-content {
+ min-width: 72px;
+
+ .@{app-prefix}-steps-item-title {
+ .font-family-medium();
+
+ font-size: 12px;
+ height: 20px;
+ line-height: 20px;
+ }
+ .@{app-prefix}-steps-item-description {
+ .font-family-medium();
+
+ color: @font-13;
+ font-size: 12px;
+ line-height: 20px;
+ }
+ }
+
+ &:not(.@{app-prefix}-steps-item-active) > .@{app-prefix}-steps-item-container[role='button']:hover {
+ .@{app-prefix}-steps-item-icon {
+ border-color: @bg-17;
+ .@{app-prefix}-steps-icon {
+ color: @bg-17;
+ }
+ }
+ .@{app-prefix}-steps-item-title {
+ color: @bg-17;
+ }
+ }
+
+ &.@{app-prefix}-steps-item-process {
+ .@{app-prefix}-steps-item-icon {
+ border: none;
+ border-radius: 50%;
+ background-color: @bg-17;
+ .@{app-prefix}-steps-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: @bg-2;
+ path {
+ stroke: @bg-2;
+ }
+ }
+ }
+ .@{app-prefix}-steps-item-tail {
+ &::after {
+ background-color: @border-5;
+ }
+ }
+ }
+
+ &.@{app-prefix}-steps-item-wait {
+ .@{app-prefix}-steps-item-icon {
+ border-color: @font-13;
+ .@{app-prefix}-steps-icon {
+ color: @font-13;
+ }
+ }
+ .@{app-prefix}-steps-item-tail {
+ &::after {
+ background-color: @border-5;
+ }
+ }
+ }
+
+ &.@{app-prefix}-steps-item-finish {
+ .@{app-prefix}-steps-item-icon {
+ border-color: @bg-17;
+ .@{app-prefix}-steps-finish-icon {
+ color: @bg-17;
+ }
+ }
+ .@{app-prefix}-steps-item-tail {
+ &::after {
+ background-color: @bg-17;
+ }
+ }
+ }
+
+ &.@{app-prefix}-steps-item-error {
+ .@{app-prefix}-steps-item-title {
+ color: @font-15;
+ }
+ }
+
+ &:has(+ .@{app-prefix}-steps-item-error) {
+ .@{app-prefix}-steps-item-tail {
+ &::after {
+ background-color: @font-6;
+ }
+ }
+ }
+
+ &:has(+ .@{app-prefix}-steps-item-finish),
+ &:has(+ .@{app-prefix}-steps-item-process) {
+ .@{app-prefix}-steps-item-tail {
+ &::after {
+ background-color: @bg-17;
+ }
+ }
+ }
+ }
+ }
+
+ &.common-steps-hide-line {
+ :global {
+ .@{app-prefix}-steps-item {
+ .@{app-prefix}-steps-item-tail {
+ &::after {
+ background-color: transparent !important;
+ }
+ }
+ }
+ }
+ }
+
+ &.common-steps-horizontal {
+ :global {
+ .@{app-prefix}-steps-item {
+ .@{app-prefix}-steps-item-tail {
+ margin-left: 12px;
+ padding: 3.5px 24px;
+ }
+ .@{app-prefix}-steps-item-icon {
+ margin-left: 0;
+ margin-right: 12px;
+ }
+
+ .@{app-prefix}-steps-item-content {
+ width: 100%;
+ // margin-right: 24px;
+ text-align: left;
+ }
+ &.@{app-prefix}-steps-item-process {
+ .@{app-prefix}-steps-item-icon {
+ .@{app-prefix}-steps-icon {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ &.common-steps-vertical {
+ .common-steps-loading {
+ width: 10px;
+ height: 10px;
+ }
+
+ :global {
+ .@{app-prefix}-steps-item {
+ &:last-child {
+ flex: none;
+ .@{app-prefix}-steps-item-content {
+ min-height: auto;
+ }
+ }
+ &:not(:last-child) {
+ .@{app-prefix}-steps-item-content {
+ min-height: 52px;
+ }
+ }
+ .@{app-prefix}-steps-item-tail {
+ left: 10px;
+ padding-top: 28px;
+ }
+ .@{app-prefix}-steps-item-icon {
+ width: 20px;
+ height: 20px;
+ margin-right: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ .@{app-prefix}-steps-icon {
+ font-size: 10px;
+ }
+ }
+ }
+ }
+
+ @media screen and (max-width: @md-width) {
+ width: auto;
+ }
+ }
+}
diff --git a/src/components/CommonTable/index.tsx b/src/components/CommonTable/index.tsx
new file mode 100644
index 00000000..9d511bbe
--- /dev/null
+++ b/src/components/CommonTable/index.tsx
@@ -0,0 +1,36 @@
+import { Table, TableProps } from 'antd';
+import React, { useMemo } from 'react';
+import EmptyWallet from 'assets/images/empty-wallet.svg';
+import Empty from 'assets/images/empty.svg';
+import { useWallet } from 'contexts/useWallet/hooks';
+import { Trans } from 'react-i18next';
+import EmptyDataBox from 'components/EmptyDataBox';
+
+export default function CommonTable(props: TableProps & { emptyText?: React.ReactNode }) {
+ const { fromWallet, toWallet } = useWallet();
+ const isConnect = !(fromWallet?.account || toWallet?.account);
+ const defaultEmptyText = useMemo(() => {
+ return !isConnect ? (
+ No cross-chain records found.
+ ) : (
+ <>
+ Connect2
+ wallet
+ to view transaction history
+ >
+ );
+ }, [isConnect]);
+
+ return (
+ {
+ return (
+
+ );
+ },
+ }}
+ />
+ );
+}
diff --git a/src/components/CommonTooltip/index.tsx b/src/components/CommonTooltip/index.tsx
new file mode 100644
index 00000000..c1a5bbf3
--- /dev/null
+++ b/src/components/CommonTooltip/index.tsx
@@ -0,0 +1,7 @@
+import { Tooltip, TooltipProps } from 'antd';
+import clsx from 'clsx';
+import styles from './styles.module.less';
+
+export default function CommonTooltip({ overlayClassName, ...props }: TooltipProps) {
+ return ;
+}
diff --git a/src/components/CommonTooltip/styles.module.less b/src/components/CommonTooltip/styles.module.less
new file mode 100644
index 00000000..4a7f94dc
--- /dev/null
+++ b/src/components/CommonTooltip/styles.module.less
@@ -0,0 +1,24 @@
+@import 'src/assets/theme/font.less';
+@import 'src/assets/theme/constants.less';
+
+.common-tooltip-overlay {
+ font-size: 12px;
+ line-height: 20px;
+
+ :global {
+ .@{app-prefix}-tooltip-inner {
+ padding: 8px;
+ border-radius: 8px;
+ }
+ }
+}
+
+@media screen and (max-width: @md-width) {
+ .common-tooltip-overlay {
+ :global {
+ .@{app-prefix}-tooltip-inner {
+ padding: 6px 8px;
+ }
+ }
+ }
+}
diff --git a/src/components/CommonTooltipSwitchModal/index.tsx b/src/components/CommonTooltipSwitchModal/index.tsx
new file mode 100644
index 00000000..571f6ce1
--- /dev/null
+++ b/src/components/CommonTooltipSwitchModal/index.tsx
@@ -0,0 +1,101 @@
+import { useMemo, useState, useImperativeHandle, forwardRef, useCallback, useEffect } from 'react';
+import clsx from 'clsx';
+import { TooltipProps } from 'antd';
+import CommonTooltip from 'components/CommonTooltip';
+import CommonModal, { ICommonModalProps } from 'components/CommonModal';
+import { GOT_IT } from 'constants/misc';
+import styles from './styles.module.less';
+import { closeIcon } from 'assets/images';
+import CommonImage from 'components/CommonImage';
+import useMediaQueries from 'hooks/useMediaQueries';
+import CommonButton from 'components/CommonButton';
+
+export interface ICommonTooltipSwitchModalRef {
+ open: () => void;
+}
+
+interface ICommonTooltipSwitchModalProps {
+ tooltipProps?: Pick;
+ modalProps?: Pick;
+ modalWidth?: number;
+ tip: React.ReactNode;
+ children: React.ReactNode;
+ isShowConfirmButton?: boolean;
+ confirmButtonText?: string;
+ confirmButtonClassName?: string;
+ onConfirm?: () => void;
+ autoOpen?: boolean;
+}
+
+const CommonTooltipSwitchModal = forwardRef(
+ (
+ {
+ tooltipProps,
+ modalProps,
+ modalWidth = 335,
+ tip,
+ children,
+ isShowConfirmButton = true,
+ confirmButtonText = GOT_IT,
+ confirmButtonClassName,
+ onConfirm,
+ autoOpen,
+ },
+ ref,
+ ) => {
+ const isMd = useMediaQueries('md');
+ const isTooltip = useMemo(() => !isMd, [isMd]);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const handleModalOpen = useCallback(() => {
+ if (!isTooltip) {
+ setIsModalOpen(true);
+ }
+ }, [isTooltip]);
+
+ const handleModalClose = useCallback(() => {
+ setIsModalOpen(false);
+ }, []);
+
+ useImperativeHandle(ref, () => ({
+ open: handleModalOpen,
+ }));
+
+ useEffect(() => {
+ if (!isMd) {
+ handleModalClose();
+ }
+ }, [handleModalClose, isMd]);
+
+ return (
+ <>
+
+ {autoOpen ? handleModalOpen()}>{children}
: children}
+
+ }
+ okText={confirmButtonText}
+ open={isModalOpen}
+ onOk={handleModalClose}
+ onCancel={handleModalClose}>
+ {tip}
+ {isShowConfirmButton && (
+
+ {confirmButtonText}
+
+ )}
+
+ >
+ );
+ },
+);
+
+CommonTooltipSwitchModal.displayName = 'CommonTooltipSwitchModal';
+
+export default CommonTooltipSwitchModal;
diff --git a/src/components/CommonTooltipSwitchModal/styles.module.less b/src/components/CommonTooltipSwitchModal/styles.module.less
new file mode 100644
index 00000000..28c8788d
--- /dev/null
+++ b/src/components/CommonTooltipSwitchModal/styles.module.less
@@ -0,0 +1,46 @@
+@import '/src/assets/theme/color.less';
+
+.common-tooltip-switch-modal {
+ :global {
+ .@{app-prefix}-modal-body {
+ color: @font-13;
+ padding: 0 24px 24px !important;
+ }
+ .@{app-prefix}-modal-content {
+ .@{app-prefix}-modal-close {
+ .@{app-prefix}-modal-close-x {
+ width: 40px;
+ height: 16px;
+ padding-top: 36px;
+ padding-right: 24px;
+ div {
+ width: 16px;
+ height: 16px;
+ }
+ svg path {
+ fill: @font-15;
+ }
+ }
+ }
+
+ .@{app-prefix}-modal-header {
+ min-height: 68px;
+ padding: 24px 24px 20px;
+ height: auto;
+ .@{app-prefix}-modal-title {
+ width: calc(100% - 40px);
+ font-size: 16px;
+ line-height: 24px;
+ word-wrap: break-word;
+ }
+ }
+ }
+ }
+
+ .confirm-button {
+ height: 48px;
+ font-size: 16px;
+ line-height: 24px;
+ margin-top: 16px;
+ }
+}
diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx
new file mode 100644
index 00000000..39903ebf
--- /dev/null
+++ b/src/components/ConnectWallet/index.tsx
@@ -0,0 +1,46 @@
+import clsx from 'clsx';
+import { Button } from 'antd';
+import { useLanguage } from 'i18n';
+import { ChainType, Web3Type } from 'types';
+import { shortenString } from 'utils';
+import { isELFChain } from 'utils/aelfUtils';
+import { formatAddress } from 'utils/chain';
+import styles from './styles.module.less';
+import { useConnect } from 'hooks/useConnect';
+import { TBridgeChainId } from 'constants/chain';
+
+interface IConnectWalletProps {
+ wallet?: Web3Type;
+ chainType?: ChainType;
+ chainId?: TBridgeChainId;
+ account?: string;
+ buttonText?: string;
+}
+
+export default function ConnectWallet({ wallet, chainType, chainId, account, buttonText }: IConnectWalletProps) {
+ const { t } = useLanguage();
+ const connect = useConnect();
+ const { chainId: walletChainId, account: walletAccount } = wallet || {};
+ const currentChainId = chainId || walletChainId;
+ const currentAccount = account || walletAccount;
+
+ return currentAccount ? (
+
+
+
+ {isELFChain(currentChainId)
+ ? shortenString(formatAddress(currentChainId, currentAccount), 7, 8)
+ : shortenString(currentAccount, 5, 3)}
+
+
+ ) : (
+
+ );
+}
diff --git a/src/components/ConnectWallet/styles.module.less b/src/components/ConnectWallet/styles.module.less
new file mode 100644
index 00000000..fa73bf63
--- /dev/null
+++ b/src/components/ConnectWallet/styles.module.less
@@ -0,0 +1,24 @@
+@import '/src/assets/theme/color.less';
+
+.wallet-address-wrap {
+ gap: 4px;
+ color: @font-13;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 22px;
+ .dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: @bg-18;
+ }
+}
+
+.connect-button {
+ height: auto;
+ padding: 0;
+ border: none;
+ font-size: 16px;
+ line-height: 22px;
+ color: @font-14;
+}
diff --git a/src/components/Copy/index.tsx b/src/components/Copy/index.tsx
new file mode 100644
index 00000000..6ad74183
--- /dev/null
+++ b/src/components/Copy/index.tsx
@@ -0,0 +1,25 @@
+import { useCopyToClipboard } from 'react-use';
+import IconFont from 'components/IconFont';
+export default function Copy({
+ toCopy,
+ children,
+ className,
+}: {
+ toCopy: string;
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ const [isCopied, setCopied] = useCopyToClipboard();
+ return (
+ setCopied(toCopy)} className={className}>
+ {isCopied.value ? (
+ 'Copied'
+ ) : (
+ <>
+
+ {children}
+ >
+ )}
+
+ );
+}
diff --git a/src/components/DisplayImage/index.tsx b/src/components/DisplayImage/index.tsx
new file mode 100644
index 00000000..4dbdb89e
--- /dev/null
+++ b/src/components/DisplayImage/index.tsx
@@ -0,0 +1,52 @@
+import styles from './styles.module.less';
+import clsx from 'clsx';
+import { useState } from 'react';
+import CommonImage from 'components/CommonImage';
+
+interface DisplayImageProps {
+ name: string;
+ src?: string;
+ className?: string;
+ defaultIconClassName?: string;
+ width?: number;
+ height?: number;
+ alt?: string;
+ isCircle?: boolean;
+}
+
+export default function DisplayImage({
+ name,
+ src,
+ className,
+ defaultIconClassName,
+ width = 24,
+ height = 24,
+ alt,
+ isCircle = true,
+}: DisplayImageProps) {
+ const [showIcon, setShowIcon] = useState(true);
+
+ return (
+
+
+ {name?.charAt(0)}
+
+ {showIcon && src && (
+
{
+ setShowIcon(false);
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/src/components/DisplayImage/styles.module.less b/src/components/DisplayImage/styles.module.less
new file mode 100644
index 00000000..e1a3b05d
--- /dev/null
+++ b/src/components/DisplayImage/styles.module.less
@@ -0,0 +1,29 @@
+@import '/src/assets/theme/color.less';
+
+.display-image {
+ position: relative;
+ flex-shrink: 0;
+}
+
+.display-image-circle {
+ border-radius: 50%;
+ overflow: hidden;
+
+ .common-image {
+ border-radius: 50%;
+ overflow: hidden;
+ }
+}
+
+.default-text {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 50%;
+ overflow: hidden;
+ width: 16px;
+ height: 16px;
+ border: 1px solid @border-1;
+ background: @bg-15;
+}
diff --git a/src/components/DynamicArrow/index.tsx b/src/components/DynamicArrow/index.tsx
new file mode 100644
index 00000000..7dd562d7
--- /dev/null
+++ b/src/components/DynamicArrow/index.tsx
@@ -0,0 +1,44 @@
+import { useMemo } from 'react';
+import clsx from 'clsx';
+import CommonImage from 'components/CommonImage';
+import { downBigIcon, downIcon, downSmallIcon } from 'assets/images';
+import styles from './styles.module.less';
+
+export type TDynamicArrowSize = 'Big' | 'Normal' | 'Small';
+
+type TDynamicArrow = {
+ className?: string;
+ iconClassName?: string;
+ isExpand?: boolean;
+ size?: TDynamicArrowSize;
+};
+
+export default function DynamicArrow({ className, iconClassName, isExpand = false, size = 'Normal' }: TDynamicArrow) {
+ const arrowIcon = useMemo(() => {
+ if (size === 'Big') {
+ return downBigIcon;
+ }
+ if (size === 'Small') {
+ return downSmallIcon;
+ }
+ return downIcon;
+ }, [size]);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/DynamicArrow/styles.module.less b/src/components/DynamicArrow/styles.module.less
new file mode 100644
index 00000000..ada88b6b
--- /dev/null
+++ b/src/components/DynamicArrow/styles.module.less
@@ -0,0 +1,24 @@
+.dynamic-arrow {
+ box-sizing: border-box;
+ width: 16px;
+ height: 16px;
+}
+
+.dynamic-arrow-big {
+ width: 20px;
+ height: 20px;
+}
+
+.dynamic-arrow-small {
+ width: 12px;
+ height: 12px;
+}
+
+.dynamic-arrow-icon {
+ width: 100%;
+ height: 100%;
+}
+
+.dynamic-arrow-icon-rotate {
+ transform: rotate(180deg);
+}
diff --git a/src/components/EmptyDataBox/index.tsx b/src/components/EmptyDataBox/index.tsx
new file mode 100644
index 00000000..fc55c08b
--- /dev/null
+++ b/src/components/EmptyDataBox/index.tsx
@@ -0,0 +1,18 @@
+import styles from './styles.module.less';
+import clsx from 'clsx';
+import Empty from 'assets/images/empty.svg';
+import CommonImage from 'components/CommonImage';
+
+export type TEmptyDataBoxProps = {
+ imageSrc?: string;
+ text?: React.ReactNode;
+};
+
+export default function EmptyDataBox({ imageSrc = Empty, text = 'No found' }: TEmptyDataBoxProps) {
+ return (
+
+ );
+}
diff --git a/src/components/EmptyDataBox/styles.module.less b/src/components/EmptyDataBox/styles.module.less
new file mode 100644
index 00000000..581b80ca
--- /dev/null
+++ b/src/components/EmptyDataBox/styles.module.less
@@ -0,0 +1,27 @@
+@import '/src/assets/theme/color.less';
+@import 'src/assets/theme/constants.less';
+
+.empty-data-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 72px 0;
+ color: @font-13;
+ .empty-data-box-icon {
+ width: 140px;
+ height: 140px;
+ }
+ .empty-data-box-text {
+ color: @font-13;
+ }
+}
+
+@media screen and (max-width: @md-width) {
+ .empty-data-box {
+ .empty-data-box-icon {
+ width: 30vw;
+ height: 30vw;
+ }
+ }
+}
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
new file mode 100644
index 00000000..4f198e97
--- /dev/null
+++ b/src/components/Footer/index.tsx
@@ -0,0 +1,55 @@
+import { memo } from 'react';
+import { useLanguage } from 'i18n';
+import Link from 'next/link';
+import CommonImage from 'components/CommonImage';
+import { aelfLogo } from 'assets/images';
+import styles from './styles.module.less';
+import { FOOTER_COMMUNITY_CONFIG, FOOTER_MENU_LIST_CONFIG } from 'constants/link';
+import useMediaQueries from 'hooks/useMediaQueries';
+
+function Footer() {
+ const { t } = useLanguage();
+ const isMd = useMediaQueries('md');
+
+ if (isMd) {
+ return null;
+ }
+
+ return (
+
+
+
+ Powered by
+
+
+
+ {FOOTER_COMMUNITY_CONFIG.map((item) => {
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+
+ {FOOTER_MENU_LIST_CONFIG.map((group, index) => (
+
+ {t(group.title)}
+ {group.menu.map((item) => {
+ return (
+
+ {t(item.label)}
+
+ );
+ })}
+
+ ))}
+
+
+ );
+}
+
+export default memo(Footer);
diff --git a/src/components/Footer/styles.module.less b/src/components/Footer/styles.module.less
new file mode 100644
index 00000000..e02aed71
--- /dev/null
+++ b/src/components/Footer/styles.module.less
@@ -0,0 +1,69 @@
+@import '/src/assets/theme/constants.less';
+@import '/src/assets/theme/color.less';
+@import '/src/assets/theme/font.less';
+
+.page-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ height: @footer-height;
+ padding: 24px 48px;
+ border-top: 1px solid @border-5;
+ background: rgb(255 255 255 / 95%);
+ color: @font-13;
+ font-size: 16px;
+
+ .page-footer-left {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 8px;
+ height: 100%;
+ }
+
+ .page-footer-powered {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ }
+
+ .aelf-logo {
+ width: 53px;
+ height: 16px;
+ }
+
+ .page-footer-community-group {
+ display: flex;
+ gap: 24px;
+ align-items: center;
+ }
+
+ .page-footer-community {
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+ }
+
+ .page-footer-right {
+ display: flex;
+ gap: 80px;
+ align-items: flex-start;
+ }
+
+ .page-footer-menu {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ font-size: 14px;
+ line-height: 16px;
+ .font-family-medium();
+ & > a {
+ color: @font-15;
+ }
+ .page-footer-menu-title {
+ color: @font-13;
+ }
+ }
+}
diff --git a/src/components/GoogleAnalytics/index.tsx b/src/components/GoogleAnalytics/index.tsx
new file mode 100644
index 00000000..a6675cff
--- /dev/null
+++ b/src/components/GoogleAnalytics/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import Script from 'next/script';
+export default function GoogleAnalytics({ id }: { id?: string }) {
+ return (
+ <>
+ `} />
+
+ >
+ );
+}
diff --git a/src/components/Header/images/logo.svg b/src/components/Header/images/logo.svg
new file mode 100644
index 00000000..77c37181
--- /dev/null
+++ b/src/components/Header/images/logo.svg
@@ -0,0 +1,43 @@
+
+
+
diff --git a/src/components/Header/images/testLogo.svg b/src/components/Header/images/testLogo.svg
new file mode 100644
index 00000000..001c7eee
--- /dev/null
+++ b/src/components/Header/images/testLogo.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
new file mode 100644
index 00000000..52a4acd5
--- /dev/null
+++ b/src/components/Header/index.tsx
@@ -0,0 +1,375 @@
+import styles from './styles.module.less';
+import logo from './images/logo.svg';
+import testLogo from './images/testLogo.svg';
+import {
+ translateIcon,
+ translateBlueIcon,
+ arrowGrayIcon,
+ arrowBlueIcon,
+ checkBlueIcon,
+ menuIcon,
+ closeIcon,
+ communityIcon,
+ legalIcon,
+ aelfLogo,
+ listingIcon,
+} from 'assets/images';
+import clsx from 'clsx';
+import { Button, Drawer, Dropdown, Menu } from 'antd';
+import { LOCAL_LANGUAGE } from 'i18n/config';
+import { useLanguage } from 'i18n';
+import useMediaQueries from 'hooks/useMediaQueries';
+import { useEffect, useMemo, useState } from 'react';
+import CommonImage from 'components/CommonImage';
+import { useRouter } from 'next/router';
+import Link from 'next/link';
+import { IS_MAINNET } from 'constants/index';
+import { NAV_LIST, HEADER_COMMUNITY_CONFIG, LEGAL_MENU_CONFIG, ROUTE_PATHS, LISTING_MENU_CONFIG } from 'constants/link';
+import { setAccountModal, setWalletsModal } from 'contexts/useModal/actions';
+import { useModalDispatch } from 'contexts/useModal/hooks';
+import { useWallet } from 'contexts/useWallet/hooks';
+import { AElfConnectorId, ChainType, EVMConnectorId, TONConnectorId } from 'types';
+import { useAelfLogin } from 'hooks/wallet';
+import WalletIcon from 'components/WalletIcon';
+import { shortenString } from 'utils';
+import { isELFChain } from 'utils/aelfUtils';
+import { formatAddress } from 'utils/chain';
+import { isPortkey } from 'utils/portkey';
+import { useConnect } from 'hooks/useConnect';
+import { isMobileDevices } from 'utils/isMobile';
+
+function SelectLanguage() {
+ const { language, changeLanguage } = useLanguage();
+ const menu = (
+