phase2-r2: collapse to canonical app/bridge tenant

Phase 1.5 inlined the bridge UI into pkg/bridge/src/app/ and added a
slim consumer at app/lux-bridge/. With the @luxbridge/app workspace
cycle severed there is no longer any reason to keep two tenant
directories or the legacy 400-file UI under app/bridge/.

This commit picks Path A:
  - delete legacy app/bridge/ (406-file @luxbridge/app, now lives in
    pkg/bridge/src/app/ as 16-file inlined SDK UI)
  - delete unused app/bridge3/ (Remix @luxbridge/app-v3, never shipped)
  - move app/lux-bridge/ → app/bridge/ (canonical tenant dir matches
    bridge.lux.network DNS; "lux-bridge" was a temporary worktree name
    needed while the legacy dir still existed)
  - delete root Dockerfile.bridge-ui (referenced deleted app/bridge3
    paths and the old Next.js standalone output; superseded by
    app/bridge/Dockerfile which builds the Vite SPA into ghcr.io/hanzoai/spa)

After this commit:
  - one and only one bridge tenant: app/bridge/ (slim, consumes @luxfi/bridge)
  - SDK: pkg/bridge/ (publishes @luxfi/bridge with inlined UI)
  - zero references to @luxbridge/app or @luxbridge/app-v3
  - zero references to app/bridge3 or app/lux-bridge
This commit is contained in:
2026-05-21 15:10:50 -07:00
parent 8aaa483452
commit 95a19813f4
550 changed files with 43 additions and 43801 deletions
-67
View File
@@ -1,67 +0,0 @@
# Build stage
FROM node:22-alpine AS builder
RUN apk add --no-cache libc6-compat python3 make g++
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare [email protected] --activate
# Copy root workspace files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
# Copy ALL workspace package.json files for proper resolution
COPY app/bridge/package.json ./app/bridge/
COPY app/bridge3/package.json ./app/bridge3/
COPY app/explorer/package.json ./app/explorer/
COPY app/server/package.json ./app/server/
COPY pkg/core/package.json ./pkg/core/
COPY pkg/settings/package.json ./pkg/settings/
COPY pkg/threshold/package.json ./pkg/threshold/
COPY pkg/ui/package.json ./pkg/ui/
COPY pkg/utila/package.json ./pkg/utila/
COPY contracts/package.json ./contracts/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy all source code (needed for workspace deps at build time)
COPY . .
# Set Next.js to standalone mode
ENV NEXT_TELEMETRY_DISABLED=1
# Build the bridge app
RUN cd app/bridge && NODE_OPTIONS=--max_old_space_size=2048 pnpm run build
# Runtime stage
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Add non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy necessary files from builder
COPY --from=builder /app/app/bridge/public ./public
COPY --from=builder /app/app/bridge/package.json ./package.json
# Copy standalone build
COPY --from=builder --chown=nextjs:nodejs /app/app/bridge/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/app/bridge/.next/static ./app/bridge/.next/static
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Start the application
CMD ["node", "app/bridge/server.js"]
-38
View File
@@ -1,38 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
/dist/
/prisma/schema.prisma
/prisma/dev.sqlite
/prisma/schema.prisma
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
-38
View File
@@ -1,38 +0,0 @@
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
env: () => ({
NEXT_PUBLIC_LS_BRIDGE_API: "https://bridge-api-dev.layerswap.cloud",
NEXT_PUBLIC_API_VERSION: "sandbox",
NEXT_PUBLIC_IDENTITY_API: "/",
NEXT_PUBLIC_RESOURCE_STORAGE_URL: "https://prodlslayerswapbridgesa.blob.core.windows.net",
}),
stories: [
"../stories/**/*.mdx",
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
"@storybook/addon-mdx-gfm",
"storybook-addon-mock"
],
framework: {
name: "@storybook/nextjs",
options: {},
},
docs: {
autodocs: "tag",
},
webpackFinal: async (config) => {
config.module?.rules?.push({
test: /\.scss$/,
use: ["style-loader", "css-loader", "postcss-loader", "sass-loader"],
});
return config;
},
};
export default config;
-25
View File
@@ -1,25 +0,0 @@
import type { Preview } from "@storybook/react";
import "../styles/globals.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
};
const preview: Preview = {
loaders: [
async () => ({
settings: await (await fetch(`${process.env.NEXT_PUBLIC_BACKEND_API}/api/settings?version=sandbox`)).json(),
}),
],
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
-3
View File
@@ -1,3 +0,0 @@
{
"WillLuke.nextjs.hasPrompted": true
}
-15
View File
@@ -1,15 +0,0 @@
# syntax=docker/dockerfile:1.7
#
# Bridge — Vite SPA served by ghcr.io/hanzoai/spa
# Builds inside the monorepo (context = repo root) so workspace deps resolve.
FROM node:22-alpine AS build
RUN apk add --no-cache libc6-compat python3 make g++ git
RUN corepack enable
WORKDIR /app
COPY . .
RUN pnpm install --frozen-lockfile || pnpm install
RUN pnpm -C app/bridge build
FROM ghcr.io/hanzoai/spa
COPY --from=build /app/app/bridge/dist /public
-21
View File
@@ -1,21 +0,0 @@
FROM node:18-alpine
WORKDIR /app
# Install dependencies
RUN apk add --no-cache git
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Expose port
EXPOSE 3000
# Start development server
CMD ["npm", "run", "dev"]
-3
View File
@@ -1,3 +0,0 @@
# bridge
Lux Network Bridge deployed at https://bridge.lux.network
@@ -1,9 +0,0 @@
const noConditionalLiterals = require("./rules/no-conditional-literals-in-jsx");
const noUnwrappedJsxText = require("./rules/no-unwrapped-jsx-text");
const plugin = {
rules: {
"no-conditional-literals-in-jsx": noConditionalLiterals,
"no-unwrapped-jsx-text": noUnwrappedJsxText
},
};
module.exports = plugin;
@@ -1,15 +0,0 @@
{
"name": "eslint-plugin-no-conditional-literals-in-jsx",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "node tests.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^8.49.0"
}
}
@@ -1,56 +0,0 @@
// From https://gist.github.com/azirbel/51518d919de979197a7c5c25c54a56d6
module.exports = {
meta: {
docs: {
description:
'Browser auto-translation will break if pieces of text nodes are be rendered conditionally.',
},
schema: [],
messages: {
unexpected:
'Conditional expression is a sibling of raw text and must be wrapped in <div> or <span>',
},
},
create: function (context) {
return {
// Imagine evaluating <div>text {conditional && 'string'}</div>
JSXExpressionContainer(node) {
// We start at the expression {conditional && 'string'}
if (node.expression.type !== 'LogicalExpression') return
// "text" is one of the siblingTextNodes.
const siblingTextNodes = (node.parent.children || []).filter(n => {
// In normal code these are 'Literal', but in test code they are 'JSXText'
const isText = n.type === 'Literal' || n.type === 'JSXText'
// Skip empty text nodes, like " \n " -- these may be JSX artifacts
return isText && !!n.value.trim()
})
// If we were evaluting
// <div>{property} {conditional && 'string'}</div>
// Then {property} would be one of the siblingExpressionNodes
const siblingExpressionNodes = (node.parent.children || []).filter(
n =>
n.type === 'JSXExpressionContainer' &&
(n.expression.type === 'Identifier' ||
n.expression.type === 'MemberExpression')
)
// Operands of {conditional && 'string'} -- the conditional and the
// literal. We want to make sure we have a text literal, otherwise we'd
// trigger this rule on the (safe) {conditional && <div>string</div>}.
const expressionOperandTypes = [
node.expression.left.type,
node.expression.right.type,
]
if (
siblingTextNodes.concat(siblingExpressionNodes).length > 0 &&
expressionOperandTypes.includes('Literal')
) {
context.report({ node, messageId: 'unexpected' })
}
},
}
},
}
@@ -1,106 +0,0 @@
// From https://github.com/sayari-analytics/eslint-plugin-sayari/tree/main
'use strict'
const {
removeSpecialCharacters, filterSiblings, jsxTextFixer
} = require('../utils/jsx')
const {
LITERAL, TEMPLATE_LITERAL, LOGICAL_EXPRESSION, CONDITIONAL_EXPRESSION, JSX_ELEMENT, JSX_TEXT
} = require('../utils/constants')
/***
*** CONSTANTS
*/
const RULE_DESCRIPTION = 'JSX text that share a common parent with other elements should be wrapped by a <span> tag'
/***
*** RULE DEFINITION
*/
module.exports = {
meta: {
type: 'problem',
fixable: 'code',
docs: {
description: RULE_DESCRIPTION,
category: 'Possible Errors',
url: 'https://github.com/sayari-analytics/graph-ui/issues/901'
},
messages: {
noUnwrappedJSX: 'No unwrapped JSX text'
}
},
create: (context) => ({
JSXExpressionContainer(element) {
const { expression, range, parent } = element
if (filterSiblings(range, parent.children).length === 0) {
return null
}
if (expression.type === LOGICAL_EXPRESSION) {
const { right } = expression
if (right.type === LITERAL) {
return (
context.report({
node: expression.right,
messageId: 'noUnwrappedJSX',
fix: (fixer) => fixer.replaceText(right, `<span>${right.value}</span>`)
})
)
} else if (right.type === TEMPLATE_LITERAL) {
return (
context.report({
node: element,
messageId: 'noUnwrappedJSX',
fix: (fixer) => ([
fixer.insertTextBefore(element, '<span>'),
fixer.insertTextAfter(element, '</span>')
])
})
)
}
} else if (expression.type === TEMPLATE_LITERAL || (
expression.type === CONDITIONAL_EXPRESSION &&
expression.consequent.type !== JSX_ELEMENT &&
expression.alternate.type !== JSX_ELEMENT
)) {
return (
context.report({
node: element,
messageId: 'noUnwrappedJSX',
fix: (fixer) => ([
fixer.insertTextBefore(element, '<span>'),
fixer.insertTextAfter(element, '</span>')
])
})
)
}
return null
},
JSXText(element) {
const { range, parent } = element
const siblings = filterSiblings(range, parent.children)
if (siblings.length === 0 || siblings.every(({ type }) => type === JSX_TEXT)) {
return null
}
if (removeSpecialCharacters(element.value)) {
const { loc, text } = jsxTextFixer(element)
return context.report({
loc,
node: element,
messageId: 'noUnwrappedJSX',
fix: (fixer) => fixer.replaceText(element, text)
})
}
}
})
}
@@ -1,102 +0,0 @@
const noConditionalLiterals = require('./rules/no-conditional-literals-in-jsx')
const noUnwrappedJsxText = require('./rules/no-unwrapped-jsx-text')
const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
})
const errors = [{ messageId: 'unexpected' }]
ruleTester.run('no-conditional-literals-in-jsx', noConditionalLiterals, {
valid: [
{
code: `<div>{conditional && 'string'}</div>`,
errors,
},
{
code: `<div>{conditional || 'string'}</div>`,
errors,
},
{
// The error happens when DOM elements are added or removed; swapping
// is ok
code: `<div>{conditional ? 'a' : 'b'}</div>`,
errors,
},
{
// As long as conditionally-rendered stuff is wrapped in div or span, it's fine
code: `<div>text {conditional && <div>wrapped is ok</div>}</div>`,
errors,
},
{
// Logic within an attribute doesn't affect the DOM
code: `<Avatar alt={conditional && 'string'} />`,
errors,
},
{
// JSX auto-adds whitespace when there are newlines. Make sure they don't trigger
code: `<div>
{conditional && 'string'}
</div>`,
errors,
},
],
invalid: [
{
code: `<div>text {conditional && 'string'}</div>`,
errors,
},
{
code: `<div>text {conditional || 'string'}</div>`,
errors,
},
{
code: `<div>{conditional && 'string'} text</div>`,
errors,
},
{
code: `<div>{conditional || 'string'} text</div>`,
errors,
},
{
// More complicated logic
code: `<div>text {(conditional1 && conditional2) || 'string'}</div>`,
errors,
},
{
// This results in 2 text nodes with no JSX containers -- dangerous
code: `<div>{property} {conditional && 'string'}</div>`,
errors,
},
{
// This results in 2 text nodes with no JSX containers -- dangerous
code: `<div>{object.property} {conditional && 'string'}</div>`,
errors,
},
],
})
ruleTester.run('no-unwrapped-jsx-text', noUnwrappedJsxText, {
valid: [
{
code: `<div>{conditional && <img></img>} <span>'string'</span></div>`,
errors: [{'messageId': "noUnwrappedJSX"}]
}
],
invalid: [
{
code: `<div>{conditional && <img></img>} 'text'</div>`,
errors: [{'messageId': "noUnwrappedJSX"}],
output: `<div>{conditional && <img></img>}<span>&nbsp;'text'</span></div>`
},
],
})
console.log("All tests passed!");
@@ -1,10 +0,0 @@
module.exports = {
JSX_TEXT: 'JSXText',
JSX_ELEMENT: 'JSXElement',
JSX_EXPRESSION_CONTAINER: 'JSXExpressionContainer',
LITERAL: 'Literal',
IDENTIFIER: 'Identifier',
TEMPLATE_LITERAL: 'TemplateLiteral',
CONDITIONAL_EXPRESSION: 'ConditionalExpression',
LOGICAL_EXPRESSION: 'LogicalExpression',
}
@@ -1,111 +0,0 @@
const {
LITERAL,
JSX_TEXT,
JSX_ELEMENT,
TEMPLATE_LITERAL,
LOGICAL_EXPRESSION,
CONDITIONAL_EXPRESSION,
JSX_EXPRESSION_CONTAINER,
} = require('./constants')
const specialCharacters = /(\n)|(\t)|(\s)|(\r)|(\f)|(\v)/
const removeSpecialCharacters = (text) => (
text.replace(/(\n)|(\t)|(\s)|(\r)|(\f)|(\v)|(&[a-z]+;)/gm, '')
)
const isWhitespace = (text) => (/\s/).test(text)
const shouldLintExpression = (expression) => (
expression.type === TEMPLATE_LITERAL || expression.type === CONDITIONAL_EXPRESSION || (
expression.type === LOGICAL_EXPRESSION && (
expression.right.type === LITERAL ||
expression.right.type === TEMPLATE_LITERAL ||
expression.right.type === JSX_ELEMENT
)
)
)
const filterSiblings = (self, children = []) => (
children.filter(({ range, type, value, expression }) => (
(range[0] !== self[0] && range[1] !== self[1]) && (
type === JSX_TEXT && removeSpecialCharacters(value) ||
type === JSX_EXPRESSION_CONTAINER && shouldLintExpression(expression) ||
type === JSX_ELEMENT
)
))
)
const jsxTextFixer = (element) => {
let loc = {
start: { line: undefined, column: undefined },
end: { line: undefined, column: undefined },
}
const byLine = element.raw.split('\n')
const numLines = byLine.length
let fixed = byLine.slice()
let open = false
let close = false
let index = 0
while (index < numLines && open === false) {
const line = byLine[index]
if (removeSpecialCharacters(line)) {
for (let i = 0; i < line.length; i += 1) {
if (specialCharacters.test(line[i])) continue
if (index === 0) {
if (isWhitespace(line[0])) {
fixed[index] = '&nbsp;' + line.slice(1)
}
fixed[index] = '<span>' + fixed[index]
loc.start = element.loc.start
} else {
fixed[index] = line.slice(0, i) + '<span>' + line.slice(i)
loc.start = { line: element.loc.start.line + index, column: i }
}
open = true
break
}
}
index += 1
}
index = numLines - 1
while (index >= 0 && close === false) {
let line = fixed[index]
if (removeSpecialCharacters(line)) {
if (isWhitespace(line[line.length - 1])) {
line = line.slice(0, line.length - 1) + '&nbsp;'
}
fixed[index] = line + '</span>'
if (numLines - 1 === index) {
loc.end = element.loc.end
} else {
loc.end = {
line: element.loc.end.line - ((numLines - 1) - index),
column: loc.start.column + byLine[index].length
}
}
close = true
}
index -= 1
}
return { loc, text: fixed.join('\n') }
}
module.exports = { jsxTextFixer, removeSpecialCharacters, filterSiblings }
+7 -8
View File
@@ -1,15 +1,14 @@
<!doctype html>
<html lang="en" class="dark" style="background-color: #000">
<html lang="en" style="background-color: #0a0a0a">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>Bridge</title>
<meta name="description" content="Cross-chain bridge" />
<!-- Basel Grotesk — loaded via @font-face in styles.css -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%235b8def'/></svg>" />
<title>Lux Bridge</title>
<meta name="description" content="Cross-chain bridge for the Lux Network." />
</head>
<body>
<div id="root"></div>
<div id="bridge-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+12 -235
View File
@@ -1,251 +1,28 @@
{
"name": "@luxbridge/app",
"version": "2.0.0",
"name": "@luxbridge/lux-tenant",
"version": "1.0.0",
"private": true,
"description": "bridge.lux.network — Lux-branded tenant build consuming @luxfi/bridge + @luxfi/brand.",
"license": "MIT",
"author": "Lux Industries, Inc.",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./App": "./src/App.tsx",
"./package.json": "./package.json"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"start": "vite preview --port 3000 --host 0.0.0.0",
"preview": "vite preview --port 3000 --host 0.0.0.0",
"tc": "tsc --noEmit",
"typecheck": "tsc --noEmit",
"clean": "rm -rf ./dist ./node_modules ./.next",
"storybook": "storybook dev -p 6006",
"build:storybook": "storybook build"
"preview": "vite preview --host --port 3001",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@eth-optimism/contracts-ts": "^0.15.0",
"@hanzo/ui": "catalog:",
"@headlessui/react": "^1.7.3",
"@imtbl/imx-sdk": "2.1.1",
"@loopring-web/loopring-sdk": "3.3.5",
"@loopring-web/web3-provider": "1.4.13",
"@luxfi/ui": "catalog:",
"@metamask/jazzicon": "^2.0.0",
"@prisma/client": "^5.21.1",
"@rainbow-me/rainbowkit": "^2.0.5",
"@sentry/browser": "^7.80.1",
"@solana/spl-token": "^0.4.9",
"@solana/wallet-adapter-base": "^0.9.23",
"@solana/wallet-adapter-coinbase": "^0.1.19",
"@solana/wallet-adapter-glow": "^0.1.18",
"@solana/wallet-adapter-phantom": "^0.9.24",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/wallet-adapter-react-ui": "^0.9.35",
"@solana/wallet-adapter-solflare": "0.6.28",
"@solana/wallet-adapter-walletconnect": "^0.1.16",
"@solana/web3.js": "1.95.4",
"@stripe/crypto": "^0.0.4",
"@stripe/stripe-js": "^1.54.2",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/forms": "^0.5.0",
"@tailwindcss/typography": "^0.5.2",
"@tanstack/react-query": "^5.59.16",
"@ton/core": "^0.59.0",
"@tonconnect/ui-react": "^2.0.9",
"@uidotdev/usehooks": "^2.4.1",
"@wagmi/core": "1.4.13",
"axios": "^1.7.9",
"bn.js": "^5.2.0",
"cmdk": "^0.2.0",
"copy-to-clipboard": "^3.3.3",
"crypto-js": "^4.1.1",
"ethers": "5.7.2",
"formik": "^2.2.9",
"framer-motion": "^10.16.15",
"joi": "^17.13.3",
"jotai": "^2.9.3",
"js-sha3": "^0.8.0",
"lucide-react": "catalog:",
"markdown-to-jsx": "^7.4.0",
"multicoin-address-validator": "^0.5.21",
"next-themes": "^0.2.1",
"qrcode.react": "^3.1.0",
"@luxfi/bridge": "workspace:*",
"@luxfi/brand": "^1.0.0",
"react": "catalog:",
"react-dom": "catalog:",
"react-error-boundary": "^4.0.11",
"react-hot-toast": "^2.2.0",
"react-swipeable": "^7.0.0",
"react-toastify": "^10.0.6",
"react-use-intercom": "^2.0.0",
"swr": "^2.1.1",
"tailwindcss-animate": "^1.0.5",
"ton": "^13.9.0",
"use-async-effect": "^2.2.7",
"uuid": "^9.0.0",
"viem": "^2.9.9",
"wagmi": "^2.5.19",
"web3": "^4.11.1",
"wouter": "^3.0.0",
"zksync": "^0.13.1",
"zustand": "^4.4.1"
"react-dom": "catalog:"
},
"devDependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/addon-essentials": "^7.4.5",
"@storybook/addon-interactions": "^7.4.5",
"@storybook/addon-links": "^7.4.5",
"@storybook/addon-mdx-gfm": "^7.4.5",
"@storybook/addon-onboarding": "^1.0.8",
"@storybook/blocks": "^7.4.5",
"@storybook/react": "^7.4.5",
"@storybook/react-vite": "^7.4.5",
"@storybook/testing-library": "^0.2.1",
"@vitejs/plugin-react": "^4.3.4",
"vite": "^8.0.0",
"@types/bn.js": "^5.1.0",
"@types/crypto-js": "^4.1.1",
"@types/lodash.merge": "^4.6.9",
"@types/mdx": "^2.0.9",
"@types/multicoin-address-validator": "^0.5.3",
"@types/node": "^20.10.7",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"autoprefixer": "^10.4.5",
"cross-env": "^7.0.3",
"postcss": "^8.4.33",
"postcss-import": "^16.0.0",
"postcss-nesting": "^12.1.5",
"tailwindcss": "catalog:",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "catalog:",
"webpack-watch-files-plugin": "^1.2.1"
},
"pnpm": {
"overrides": {
"color-string@<1.5.5": ">=1.5.5",
"tar@<6.2.1": ">=6.2.1",
"axios@>=0.8.1 <0.28.0": ">=0.28.0",
"tough-cookie@<4.1.3": ">=4.1.3",
"ws@>=8.0.0 <8.17.1": ">=8.17.1",
"ws@>=7.0.0 <7.5.10": ">=7.5.10",
"ws@>=2.1.0 <5.2.4": ">=5.2.4",
"zod@<=3.22.2": ">=3.22.3",
"next@>=14.0.0 <14.2.21": ">=14.2.21",
"undici@>=6.0.0 <6.21.1": ">=6.21.1",
"undici@>=4.5.0 <5.28.5": ">=5.28.5",
"esbuild@<=0.24.2": ">=0.25.0",
"vite@>=5.0.0 <=5.4.11": ">=5.4.12",
"elliptic@<=6.6.0": ">=6.6.1",
"vite@>=5.0.0 <5.4.15": ">=5.4.15",
"@remix-run/express@>=2.11.1 <2.16.3": ">=2.16.3",
"estree-util-value-to-estree@<3.3.3": ">=3.3.3",
"vite@>=5.0.0 <5.4.18": ">=5.4.18",
"cookie@<0.7.0": ">=0.7.0",
"@babel/runtime@<7.26.10": ">=7.26.10",
"@babel/helpers@<7.26.10": ">=7.26.10",
"vite@>=5.0.0 <=5.4.18": ">=5.4.19",
"base-x@<=3.0.10": ">=3.0.11",
"base-x@=4.0.0": ">=4.0.1",
"vite@>=5.0.0 <5.4.17": ">=5.4.17",
"image-size@>=1.1.0 <1.2.1": ">=1.2.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=1.1.12",
"brace-expansion@>=2.0.0 <=2.0.1": ">=2.0.2",
"next@>=13.0 <14.2.30": ">=14.2.30",
"pbkdf2@>=3.0.10 <=3.1.2": ">=3.1.3",
"@eslint/plugin-kit@<0.3.4": ">=0.3.4",
"on-headers@<1.1.0": ">=1.1.0",
"next@>=0.9.9 <14.2.31": ">=14.2.31",
"vite@<=5.4.19": ">=5.4.20",
"next@>=0.9.9 <14.2.32": ">=14.2.32",
"@metamask/sdk-communication-layer@>=0.16.0 <=0.33.0": ">=0.33.1",
"@metamask/sdk@>=0.16.0 <=0.33.0": ">=0.33.1",
"next@>=0.9.9 <14.2.24": ">=14.2.24",
"vite@>=5.2.6 <=5.4.20": ">=5.4.21",
"validator@<13.15.20": ">=13.15.20",
"tar-fs@>=2.0.0 <2.1.4": ">=2.1.4",
"tar-fs@>=3.0.0 <3.1.1": ">=3.1.1",
"sha.js@<=2.4.11": ">=2.4.12",
"tmp@<=0.2.3": ">=0.2.4",
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
"form-data@<2.5.4": ">=2.5.4",
"tar-fs@>=3.0.0 <3.0.9": ">=3.0.9",
"tar-fs@>=2.0.0 <2.1.3": ">=2.1.3",
"tar-fs@>=3.0.0 <3.0.7": ">=3.0.7",
"tar-fs@>=2.0.0 <2.1.2": ">=2.1.2",
"elliptic@>=4.0.0 <=6.5.6": ">=6.5.7",
"elliptic@>=2.0.0 <=6.5.6": ">=6.5.7",
"elliptic@>=5.2.1 <=6.5.6": ">=6.5.7",
"min-document@<=2.19.0": ">=2.19.1",
"glob@>=10.2.0 <10.5.0": ">=10.5.0",
"valibot@>=0.31.0 <1.2.0": ">=1.2.0",
"node-forge@<1.3.2": ">=1.3.2",
"axios@<0.30.0": ">=0.30.0",
"axios@>=1.0.0 <1.8.2": ">=1.8.2",
"elliptic@<6.6.0": ">=6.6.0",
"elliptic@<6.5.6": ">=6.5.6",
"jws@<3.2.3": ">=3.2.3",
"next@>=13.3.0 <14.2.34": ">=14.2.34",
"@remix-run/server-runtime@<=2.17.2": ">=2.17.3",
"@remix-run/router@<=1.23.1": ">=1.23.2",
"@remix-run/react@<2.17.3": ">=2.17.3",
"react-router@>=6.0.0 <6.30.2": ">=6.30.2",
"@remix-run/node@<=2.17.1": ">=2.17.2",
"@remix-run/react@>=1.15.0 <=2.17.0": ">=2.17.1",
"next@>=13.3.1-canary.0 <14.2.35": ">=14.2.35",
"h3@<=1.15.4": ">=1.15.5",
"axios@>=1.0.0 <1.12.0": ">=1.12.0",
"@coinbase/wallet-sdk@>=4.0.0-beta.0 <4.3.0": ">=4.3.0",
"lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23",
"lodash@>=4.0.0 <=4.17.22": ">=4.17.23",
"undici@<6.23.0": ">=6.23.0",
"vite@>=5.0.0 <5.4.16": ">=5.4.16",
"next@>=10.0.0 <15.5.10": ">=15.5.10",
"next@>=13.0.0 <15.0.8": ">=15.0.8",
"tar@<7.5.7": ">=7.5.7",
"diff@>=4.0.0 <4.0.4": ">=4.0.4",
"diff@>=5.0.0 <5.2.2": ">=5.2.2",
"validator@<13.15.22": ">=13.15.22",
"js-yaml@<3.14.2": ">=3.14.2",
"js-yaml@>=4.0.0 <4.1.1": ">=4.1.1",
"semver@>=2.0.0-alpha <5.7.2": ">=5.7.2",
"webpack@>=5.49.0 <=5.104.0": ">=5.104.1",
"webpack@>=5.49.0 <5.104.0": ">=5.104.0",
"mdast-util-to-hast@>=13.0.0 <13.2.1": ">=13.2.1",
"undici@>=6.0.0 <6.21.2": ">=6.21.2",
"undici@<5.29.0": ">=5.29.0",
"qs@>=6.7.0 <=6.14.1": ">=6.14.2",
"pbkdf2@>=1.0.0 <=3.1.2": ">=3.1.3",
"axios@<=0.30.2": ">=0.30.3",
"axios@>=1.0.0 <=1.13.4": ">=1.13.5",
"tar@<=7.5.2": ">=7.5.3",
"tar@<7.5.8": ">=7.5.8",
"bn.js@>=5.0.0 <5.2.3": ">=5.2.3",
"bn.js@<4.12.3": ">=4.12.3",
"minimatch@<3.1.3": ">=3.1.3",
"minimatch@>=5.0.0 <5.1.7": ">=5.1.7",
"minimatch@>=9.0.0 <9.0.6": ">=9.0.6",
"rollup@>=4.0.0 <4.59.0": ">=4.59.0",
"rollup@<2.80.0": ">=2.80.0",
"minimatch@>=5.0.0 <5.1.8": ">=5.1.8",
"minimatch@>=9.0.0 <9.0.7": ">=9.0.7",
"minimatch@<3.1.4": ">=3.1.4",
"serialize-javascript@<=7.0.2": ">=7.0.3",
"next@>=14.0.0 <14.2.25": ">=14.2.25",
"ajv@<6.14.0": ">=6.14.0",
"ajv@>=7.0.0-alpha.0 <8.18.0": ">=8.18.0",
"qs@<6.14.1": ">=6.14.1",
"svgo@>=3.0.0 <3.3.3": ">=3.3.3",
"svgo@>=2.1.0 <2.8.1": ">=2.8.1",
"tar@<=7.5.9": ">=7.5.10",
"tar@<=7.5.10": ">=7.5.11",
"flatted@<3.4.0": ">=3.4.0",
"undici@>=6.0.0 <6.24.0": ">=6.24.0",
"undici@<6.24.0": ">=6.24.0",
"tar@<=7.5.3": ">=7.5.4",
"next@>=9.5.0 <15.5.13": ">=15.5.13",
"next@>=10.0.0 <16.1.7": ">=16.1.7",
"h3@<1.15.6": ">=1.15.6",
"socket.io-parser@>=4.0.0 <4.2.6": ">=4.2.6"
}
"vite": "^5.4.11"
}
}
-8
View File
@@ -1,8 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': 'postcss-nesting',
tailwindcss: {},
autoprefixer: {},
},
}
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 40" fill="none">
<text x="0" y="30" font-family="system-ui, -apple-system, sans-serif" font-size="28" font-weight="700" fill="white">
<tspan fill="#4f46e5">L</tspan>iquidity
</text>
</svg>

Before

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,6 +0,0 @@
<svg width="126" height="34" viewBox="0 0 63 17" xmlns="http://www.w3.org/2000/svg">
<g fill="#ffffff" fill-rule="nonzero">
<polygon points="18 12.485 18 17 0 17 0 0 5.061 0 5.061 12.485"/>
<path d="M62.991,0 L56.069,0 L50.841,5.265 L45.64,0 L33.537,0 L33.537,8.355 C33.537,10.39 32.904,12.547 28.355,12.547 C23.805,12.547 23.173,10.418 23.173,8.355 L23.173,0 L18,0 L18,8.355 C18,14.199 21.107,17 28.364,17 C35.593,17 38.728,14.171 38.728,8.355 L38.728,0.327 C38.728,0.215 38.858,0.159 38.942,0.233 L47.25,8.374 L38.7,16.748 L45.649,16.748 L50.85,11.483 L56.078,16.748 L63,16.748 L54.478,8.374 L62.991,0 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

-77
View File
@@ -1,77 +0,0 @@
{
"brand": {
"name": "Lux Exchange",
"title": "Lux Exchange | Trade",
"description": "Multi-chain decentralized exchange powered by Lux Network",
"shortName": "LUX",
"labsName": "Lux Labs",
"legalEntity": "Lux Industries Inc.",
"walletName": "LUX Wallet",
"protocolName": "LUX Protocol",
"copyrightHolder": "Lux Industries Inc.",
"appDomain": "lux.exchange",
"docsDomain": "docs.lux.exchange",
"infoDomain": "info.lux.exchange",
"gatewayDomain": "gw.lux.exchange",
"wsDomain": "ws.lux.exchange",
"helpUrl": "https://docs.lux.exchange/help",
"termsUrl": "https://lux.exchange/terms",
"privacyUrl": "https://lux.exchange/privacy",
"downloadUrl": "https://lux.exchange/wallet",
"complianceEmail": "[email protected]",
"supportEmail": "[email protected]",
"twitter": "https://x.com/luxfi",
"farcaster": "",
"linkedin": "",
"tiktok": "",
"github": "https://github.com/luxfi",
"discord": "https://discord.gg/lux",
"logoUrl": "https://cdn.jsdelivr.net/npm/@luxfi/brand@latest/assets/logo/logo.svg",
"faviconUrl": "https://cdn.jsdelivr.net/npm/@luxfi/brand@latest/assets/logo/favicon.png",
"primaryColor": "#000000",
"theme": {
"light": {
"accent1": "#000000",
"surface1": "#FFFFFF",
"surface2": "#F9F9F9",
"neutral1": "#131313",
"neutral2": "rgba(19, 19, 19, 0.63)"
},
"dark": {
"accent1": "#FFFFFF",
"surface1": "#000000",
"surface2": "#1A1A1A",
"neutral1": "#FFFFFF",
"neutral2": "rgba(255, 255, 255, 0.65)"
}
}
},
"chains": {
"defaultChainId": 96369,
"supported": [96369, 96368, 96367, 200200, 1, 42161, 8453, 137, 43114]
},
"rpc": {
"96369": "https://api.lux.network/ext/bc/C/rpc",
"96368": "https://api.lux-test.network/ext/bc/C/rpc",
"96367": "https://api.lux-dev.network/ext/bc/C/rpc",
"200200": "https://api.zoo.network/ext/bc/C/rpc",
"200201": "https://api.zoo-test.network/ext/bc/C/rpc",
"1": "https://eth.llamarpc.com",
"42161": "https://arb1.arbitrum.io/rpc",
"8453": "https://mainnet.base.org",
"137": "https://polygon-rpc.com",
"43114": "https://api.avax.network/ext/bc/C/rpc"
},
"api": {
"graphql": "https://api.lux.exchange/v1/graphql",
"gateway": "https://gw.lux.exchange",
"insights": "https://insights.hanzo.ai"
},
"iam": {
"provider": "https://lux.id",
"clientId": "lux-exchange"
},
"walletConnect": {
"projectId": ""
}
}
-42
View File
@@ -1,42 +0,0 @@
### Partnership Inquiries
If you have partnership offers or questions related to collaboration with Bridge, please fill in <a href='https://t507xw0ywxn.typeform.com/to/hecTNbte#name=xxxxx' target='_blank'>this form</a> and we’ll be happy to talk to you.
---
### Bridge Integration Guide
To integrate Bridge in your wallet or app, direct users to Bridge and customize the initial values by using query parameters.
Example: [Loopring & Coinbase](https://bridge.lux.network/?destNetwork=LOOPRING_MAINNET&sourceExchangeName=coinbase).
- *sourceExchangeName* - Pre-select the source exchange. Available values are at the <a href='#bottom2'>bottom of the page</a>.
- *destNetwork* - Pre-select the destination network(L2, sidechain etc.). Available values are at the <a href='#bottom'>bottom of the page</a>.
- *lockAddress = true* - To lock the provided address, to not allow user to change it.
- *lockNetwork = true* - To lock the provided network.
- *asset* - To pre-select the asset. NOTE: available assets depend on the selected network, for example, the asset **LRC** is only available in **LOOPRING** network. Avilable values are: ***ETH***, ***USDC***, ***USDT*** and ***LRC***.
- [Full template](https://bridge.lux.network/?destNetwork=zksync_mainnet&destAddress=zksync%3A0x4d70500858f9705ddbd56d007d13bbc92c9c67d1&lockNetwork=true&lockAddress=true&appName=argent&email=tantushyan2736%40gmail.com).
---
### Centralized exchange or L2/side-chain integration
If you want Bridge to integrate your exchange or L2 you can reach out to hi@bridge.lux.network. Prerequisites are:
- exchange integration: availability of (free and instant) internal transactions and APIs to fetch deposit/withdrawal history
- L2 integrations: ability to transfer between accounts.
---
### Wallet Integration Source
If you're integrating Bridge to your wallet, and want your users to see where their address is coming from, you can reach out to hi@bridge.lux.network and provide the assets and information shown to the user.
<Image className='mx-auto' src="./images/argentIntegr.png" alt="Argent" width="500"/>
<Image className='mx-auto' src="./images/imTokenIntegr.png" alt="imToken" width="500"/>
<Image className='mx-auto' src="./images/tokenPocketIntegr.png" alt="TokenPocket" width="500"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

-9
View File
@@ -1,9 +0,0 @@
## How to transfer crypto from your exchange account to Arbitrum, zkSync, Loopring and many more L2 networks?
With [Bridge](/), you can bridge crypto from your crypto exchange account directly to a Layer 2 network with low fees. <br />
We support transfers of ETH, USDT, LRC and more, from 8+ of the biggest crypto exchanges, with more exchanges, assets, and networks added on weekly basis.
After filling in the **amount** and **asset** you want to send, the source **exchange** and destination **network**, your journey will differ based on the exchange you chose.
Select your exchange from the list below to continue.
File diff suppressed because one or more lines are too long
-4
View File
@@ -1,4 +0,0 @@
user-agent: *
allow: /
sitemap: https://bridge.lux.network/sitemap.xml
-19
View File
@@ -1,19 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/assets/lux-site-icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/lux-site-icons/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#000000",
"display": "standalone"
}
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://bridge.lux.network</loc>
</url>
<url>
<loc>https://bridge.lux.network/userguide</loc>
</url>
<url>
<loc>https://bridge.lux.network/about</loc>
</url>
</urlset>
-1
View File
@@ -1 +0,0 @@
hi
@@ -1,7 +0,0 @@
{
"url": "https://bridge.lux.network/app",
"name": "Lux Bridge",
"iconUrl": "https://bridge.lux.network/app/lux-site-icons/favivon-32x32.png",
"termsOfUseUrl": "https://docs.bridge.lux.network/user-docs/more-information/terms-of-services",
"privacyPolicyUrl": "https://docs.bridge.lux.network/user-docs/more-information/privacy-policy"
}
-119
View File
@@ -1,119 +0,0 @@
import { ChevronDown } from 'lucide-react'
import { lazy, Suspense, useEffect, useState, type FC } from 'react'
import { Route, Switch, useRoute } from 'wouter'
import Contexts from '@/components/Contexts'
import Header from '@/components/header'
import Main from '@/components/main'
import { BridgeAppSettings } from '@/Models/BridgeAppSettings'
import { type BridgeSettings } from '@/Models/BridgeSettings'
import getBridgeSettings from '@/util/getBridgeSettings'
import siteDef from '@/site-def'
import { fetchTenant, getTenant, type TenantConfig } from '@/lib/tenant'
import HomePage from '@/pages/HomePage'
import NotFoundPage from '@/pages/NotFoundPage'
const AuthPage = lazy(() => import('@/pages/AuthPage'))
const CampaignsPage = lazy(() => import('@/pages/CampaignsPage'))
const CampaignDetailPage = lazy(() => import('@/pages/CampaignDetailPage'))
const SalonPage = lazy(() => import('@/pages/SalonPage'))
const SwapPage = lazy(() => import('@/pages/SwapPage'))
const TeleporterSwapPage = lazy(() => import('@/pages/TeleporterSwapPage'))
const UtilaSwapPage = lazy(() => import('@/pages/UtilaSwapPage'))
const TransactionsPage = lazy(() => import('@/pages/TransactionsPage'))
const ConnectedWallets = lazy(() =>
import('@/components/ConnectedWallets').then((m) => ({ default: m.ConnectedWallets })),
)
// wouter route components take no params; pages read via useRoute + window.__NEXT_PARAMS__
function withParam<P extends string>(
Component: FC<{ params: Record<P, string> }>,
paramName: P,
): FC {
return function Wrapped() {
const [, params] = useRoute<Record<P, string>>(`/*`)
const value = (params as Record<P, string> | null)?.[paramName] ?? ''
return <Component params={{ [paramName]: value } as Record<P, string>} />
}
}
function RouteWithParams<P extends string>({ path, component: C, paramName }: {
path: string
component: FC<{ params: Record<P, string> }>
paramName: P
}) {
return (
<Route path={path}>
{(params: Record<string, string>) => (
<C params={{ [paramName]: params[paramName] ?? '' } as Record<P, string>} />
)}
</Route>
)
}
export function App() {
const [settings, setSettings] = useState<BridgeSettings | undefined>(undefined)
const [tenant, setTenant] = useState<TenantConfig>(() => getTenant())
useEffect(() => {
void getBridgeSettings().then(setSettings).catch(() => setSettings(undefined))
void fetchTenant().then(setTenant).catch(() => setTenant(getTenant()))
}, [])
const content = (
<Suspense fallback={null}>
<Switch>
<Route path="/" component={HomePage} />
<Route path="/auth" component={AuthPage} />
<Route path="/campaigns" component={CampaignsPage} />
<RouteWithParams path="/campaigns/:campaign" component={CampaignDetailPage} paramName="campaign" />
<Route path="/salon" component={SalonPage} />
<RouteWithParams path="/swap/:swapId" component={SwapPage} paramName="swapId" />
<RouteWithParams path="/swap/teleporter/:swapId" component={TeleporterSwapPage} paramName="swapId" />
<RouteWithParams path="/swap/v2/:swapId" component={UtilaSwapPage} paramName="swapId" />
<Route path="/transactions" component={TransactionsPage} />
<Route component={NotFoundPage} />
</Switch>
</Suspense>
)
if (!settings) {
// Settings not loaded yet — render minimal shell
return (
<Main>
<div className="flex items-center justify-center min-h-screen">
<p className="text-muted-foreground">Loading...</p>
</div>
</Main>
)
}
return (
<Contexts settings={settings}>
<Header siteDef={siteDef} logoVariant="logo-only" logoSrc={tenant.logoUrl}>
<Suspense fallback={null}>
<ConnectedWallets
connectButtonVariant="outline"
showWalletIcon={false}
connectButtonClx="pl-3 pr-2 flex rounded-lg relative"
>
<span className="pr-1.5">Connect</span>
<span className="pr-1">|</span>
<span>
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</span>
</ConnectedWallets>
</Suspense>
</Header>
<Main>{content}</Main>
<footer className="py-4 text-center text-xs text-muted-foreground">{tenant.name}</footer>
</Contexts>
)
}
// Suppress the unused warning for BridgeAppSettings import (used via SettingsProvider in Contexts)
void BridgeAppSettings
// Suppress unused — left here in case future routes need direct RouteWithParams wrapping via withParam helper
void withParam
-18
View File
@@ -1,18 +0,0 @@
export type ApiError = {
code: LSAPIKnownErrorCode | string,
message: string;
}
export enum LSAPIKnownErrorCode {
NOT_FOUND = "NOT_FOUND",
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
FUNDS_ON_HOLD = "FUNDS_ON_HOLD_ERROR",
COINBASE_AUTHORIZATION_LIMIT_EXCEEDED = "COINBASE_AUTHORIZATION_LIMIT_EXCEEDED",
COINBASE_INVALID_2FA = "COINBASE_INVALID_2FA",
ACTIVE_SWAP_LIMIT_EXCEEDED = "ACTIVE_SWAP_LIMIT_EXCEEDED",
NETWORK_ACCOUNT_ALREADY_EXISTS = "NETWORK_ACCOUNT_ALREADY_EXISTS",
BLACKLISTED_ADDRESS = "BLACKLISTED_ADDRESS",
INVALID_ADDRESS_ERROR = "INVALID_ADDRESS_ERROR",
UNACTIVATED_ADDRESS_ERROR = "UNACTIVATED_ADDRESS_ERROR"
}
-13
View File
@@ -1,13 +0,0 @@
import { type ApiError } from "./ApiError";
export class EmptyApiResponse {
constructor(error?: ApiError) {
this.error = error;
}
error?: ApiError;
}
export class ApiResponse<T> extends EmptyApiResponse {
data?: T
}
-43
View File
@@ -1,43 +0,0 @@
import { type Wallet } from "../stores/walletStore"
import { type CryptoNetwork, type NetworkCurrency } from "./CryptoNetwork"
export type BalanceProps = {
layer: CryptoNetwork,
address: string
}
export type GasProps = {
layer: CryptoNetwork,
currency: NetworkCurrency,
address?: `0x${string}`,
userDestinationAddress?: string,
wallet?: Wallet
}
export type Balance = {
network: string,
amount: number,
decimals: number,
isNativeCurrency: boolean,
token: string,
request_time: string,
}
export type Gas = {
token: string,
gas: number,
gasDetails?: {
gasLimit?: number,
maxFeePerGas?: number,
gasPrice?: number,
maxPriorityFeePerGas?: number
},
request_time: string
}
export type BalanceProvider = {
getBalance: ({ layer, address }: BalanceProps) => Promise<Balance[] | undefined> | undefined | void,
getGas: ({ layer, address, currency, userDestinationAddress, wallet }: GasProps) => Promise<Gas[] | undefined> | undefined | void,
supportedNetworks: string[],
name: string,
}
-137
View File
@@ -1,137 +0,0 @@
import type { Exchange } from "./Exchange";
import type { BridgeSettings, Route } from "@/Models/BridgeSettings";
import type { CryptoNetwork, NetworkCurrency } from "./CryptoNetwork";
export class BridgeAppSettings {
exchanges: Exchange[];
networks: CryptoNetwork[];
sourceRoutes: Route[];
destinationRoutes: Route[];
constructor(settings: BridgeSettings | any) {
// this.networks = BridgeAppSettings.ResolveLayers(
// settings.networks,
// settings.sourceRoutes,
// settings.destinationRoutes
// );
this.networks = settings.networks
this.exchanges = settings.exchanges;
this.sourceRoutes = settings.sourceRoutes;
this.destinationRoutes = settings.destinationRoutes;
}
/**
* get NetworkCurrency asset from exchange asset data
* @param layers
* @param exchange
* @param asset
* @returns
*/
getExchangeAsset = (
networks: CryptoNetwork[],
exchange?: Exchange,
asset?: string
): NetworkCurrency | undefined => {
if (!exchange || !asset) {
return undefined;
} else {
const currency = exchange?.currencies?.find((c) => c.asset === asset);
const network = networks.find((n) => n.internal_name === currency?.network);
return network?.currencies?.find((a) => a?.asset === asset);
}
};
/**
* get Network from Exchange and Asset
* @param layers
* @param exchange
* @param asset
* @returns
*/
getExchangeNetwork = (
networks: CryptoNetwork[],
exchange?: Exchange,
asset?: string
): CryptoNetwork | undefined => {
if (!exchange || !asset) {
return undefined;
} else {
const currency = exchange?.currencies?.find((c) => c.asset === asset);
const network = networks.find((n) => n.internal_name === currency?.network);
return network;
}
};
getTransactionExplorerTemplate = (
networks: CryptoNetwork[],
network?: CryptoNetwork,
exchange?: Exchange,
asset?: string
): string | undefined => {
if (network) {
return network?.transaction_explorer_template;
} else {
const currency = exchange?.currencies?.find((c) => c.asset === asset);
return networks.find((n) => n.internal_name === currency?.network)
?.transaction_explorer_template;
}
};
// static ResolveLayers(
// networks: CryptoNetwork[],
// sourceRoutes: Route[],
// destinationRoutes: Route[]
// ): Layer[] {
// const resource_storage_url = process.env.NEXT_PUBLIC_RESOURCE_STORAGE_URL;
// if (!resource_storage_url)
// throw new Error(
// "NEXT_PUBLIC_RESOURCE_STORAGE_URL is not set up in env vars"
// );
// const basePath = new URL(resource_storage_url);
// const networkLayers: Layer[] = networks?.map(
// (n): Layer => ({
// assets: BridgeAppSettings.ResolveNetworkL2Assets(
// n,
// sourceRoutes,
// destinationRoutes
// ),
// logo: `${
// process.env.NEXT_PUBLIC_CDN_URL
// }/bridge/networks/${n?.internal_name?.toLowerCase()}.png`,
// ...n,
// })
// );
// return networkLayers;
// }
// static ResolveNetworkL2Assets(
// network: CryptoNetwork,
// sourceRoutes: Route[],
// destinationRoutes: Route[]
// ): NetworkCurrency[] {
// return network?.currencies?.map((c) => {
// const is_deposit_enabled = sourceRoutes?.some(
// (r) => r.asset === c.asset && r.network === network.internal_name
// );
// const is_withdrawal_enabled = destinationRoutes?.some(
// (r) => r.asset === c.asset && r.network === network.internal_name
// );
// return {
// asset: c.asset,
// contract_address: c.contract_address,
// decimals: c.decimals,
// precision: c.precision,
// price_in_usd: c.price_in_usd,
// is_native: c.is_native,
// is_refuel_enabled: c.is_refuel_enabled,
// is_deposit_enabled,
// is_withdrawal_enabled,
// };
// });
// }
}
-20
View File
@@ -1,20 +0,0 @@
type AuthGetCodeResponse = {
data: {
next: Date,
already_sent: boolean
};
error: string;
}
type AuthConnectResponse = {
access_token: string;
expires_in: number;
token_type: string;
refresh_token: string;
scope: string;
}
export {
type AuthGetCodeResponse,
type AuthConnectResponse
}
-14
View File
@@ -1,14 +0,0 @@
import { type CryptoNetwork } from './CryptoNetwork'
import { type Exchange } from './Exchange'
export type BridgeSettings = {
exchanges: Exchange[]
networks: CryptoNetwork[]
sources?: Route[]
destinations?: Route[]
}
export type Route = {
network: string
asset: string
}
-67
View File
@@ -1,67 +0,0 @@
export enum NetworkType {
EVM = "evm",
Starknet = "starknet",
Solana = "solana",
Cosmos = "cosmos",
StarkEx = "stark_ex",
ZkSyncLite = "zk_sync_lite",
TON = "ton",
Bitocoin = "btc",
Cardano = "cardano",
}
export type CryptoNetwork = {
display_name: string
internal_name: string
logo?: string
is_testnet: boolean
is_featured: boolean
chain_id: string | null | undefined
status: 'active' | 'inactive'
type: NetworkType
native_currency:string,
transaction_explorer_template: string
account_explorer_template: string
currencies: NetworkCurrency[]
metadata: Metadata | null | undefined
managed_accounts: string[]
nodes: string[]
// managed_accounts: ManagedAccount[]
// nodes: NetworkNode[]
}
export type NetworkCurrency = {
name: string
asset: string
logo: string
contract_address: string | null | undefined
decimals: number
status: 'active' | 'inactive'
is_deposit_enabled: boolean
is_withdrawal_enabled: boolean
is_refuel_enabled: boolean
precision: number
price_in_usd: number
is_native: boolean
mint?: boolean
}
export type NetworkNode = {
url: string
}
export type ManagedAccount = {
address: `0x${string}`
}
export type Metadata = {
multicall3?: {
address: `0x${string}`
blockCreated: number
}
ensRegistry?: {
address: `0x${string}`
}
ensUniversalResolver?: {
address: `0x${string}`
}
WatchdogContractAddress?: `0x${string}`
L1Network?: string
}
-6
View File
@@ -1,6 +0,0 @@
export interface Currency {
asset: string;
usd_price: number;
precision: number;
img_url?: string
}
@@ -1,7 +0,0 @@
export type CurrencyExchange = {
exchange_id: string;
fee: number;
is_off_ramp_enabled: boolean;
min_withdrawal_amount: number;
off_ramp_max_amount:number;
}
-21
View File
@@ -1,21 +0,0 @@
export type Exchange = {
display_name: string;
internal_name: string;
is_featured: boolean;
type: "cex" | "fiat";
created_date: string;
currencies: {
asset: string,
is_default: boolean,
network: string,
status: string
} [];
metadata: ExchangeMetadata | null | undefined;
o_auth: {
connect_url: string,
authorize_url: string
} | null
img_url?: string
}
export type ExchangeMetadata = {}
-25
View File
@@ -1,25 +0,0 @@
import type { ManagedAccount, Metadata, NetworkCurrency, NetworkType } from './CryptoNetwork'
export type LayerStatus = 'active' | 'inactive' | 'insufficient_liquidity'
export type Layer = {
display_name: string
internal_name: string
logo: string
is_testnet: boolean
is_featured: boolean
chain_id: string | null | undefined
refuel_amount_in_usd: number
type: NetworkType,
transaction_explorer_template: string
account_explorer_template: string,
assets: NetworkCurrency[]
metadata: Metadata | null | undefined
managed_accounts: string[]
nodes: string[]
// managed_accounts: ManagedAccount[]
// nodes: NetworkNodes[]
}
export type NetworkNodes = {
url: string
}
-7
View File
@@ -1,7 +0,0 @@
export type Partner = {
display_name: string;
logo_url: string;
is_wallet: boolean;
id: number;
name: string
}
-40
View File
@@ -1,40 +0,0 @@
export class PersistantQueryParams {
from?: string = ""
to?: string = ""
lockAddress?: boolean = false
lockFrom?: boolean = false
lockTo?: boolean = false
lockAsset?: boolean = false
fromAsset?: string = ""
toAsset?: string = ""
destAddress?: string = ""
hideRefuel?: boolean = false
hideAddress?: boolean = false
hideFrom?: boolean = false
hideTo?: boolean = false
amount?: string = ""
externalId?: string = ""
signature?: string = ""
timestamp?: string = ""
apiKey?: string = ""
balances?: string = ""
account?: string = ""
actionButtonText?: string = ""
theme?: string = ""
appName?: string = ""
// Obsolate
sourceExchangeName?: string = ""
destNetwork?: string = ""
lockNetwork?: boolean = false
lockExchange?: boolean = false
addressSource?: string = ""
asset?: string = ""
teleport?: string = "false"
}
export class QueryParams extends PersistantQueryParams {
coinbase_redirect?: string = ""
}
-4
View File
@@ -1,4 +0,0 @@
export enum SwapFailReasons {
RECEIVED_LESS_THAN_VALID_RANGE = "received_less_than_valid_range",
RECEIVED_MORE_THAN_VALID_RANGE = "received_more_than_valid_range"
}
-59
View File
@@ -1,59 +0,0 @@
export type Network = {
id: number,
display_name: string | null,
internal_name: string | null,
native_currency: string | null,
is_testnet: boolean | null,
is_featured: boolean | null,
logo: string | null,
chain_id: string | null,
type: string | null,
average_completion_time: string | null,
transaction_explorer_template: string | null,
account_explorer_template: string | null,
listing_date: Date | null,
}
export type Currency = {
id: number,
name: string | null,
asset: string | null,
logo: string | null,
contract_address: string | null,
decimals: number | null,
price_in_usd: number | null,
precision: number | null,
is_native: boolean | null,
listing_date: Date,
network_id: number
}
export type Swap = {
id: string,
created_date: Date,
source_network_id: number,
source_network: Network,
source_exchange: string | null,
source_asset_id: number,
source_asset: Currency,
source_address: string,
destination_network_id: number,
destination_network: Network,
destination_exchange: string | null,
destination_asset_id: number,
destination_asset: Currency,
destination_address: string,
refuel: boolean | null,
use_deposit_address: boolean | null,
requested_amount: number | null,
status: string,
fail_reason: string | null,
metadata_sequence_number: number | null,
block_number: number,
deposit_address_id: number,
deposit_address: {
id: number,
type: string,
address: string
},
}
-24
View File
@@ -1,24 +0,0 @@
export enum SwapStatus {
Created = "created",
UserTransferPending = "user_transfer_pending",
UserTransferDelayed = "user_transfer_delayed",
UserDepositPending = "user_deposit_pending",
BridgeTransferPending = "bridge_transfer_pending",
Completed = "completed",
Failed = "failed",
Expired = "expired",
Cancelled = "cancelled",
TeleportProcessPending = "teleport_processing_pending",
UserPayoutPending = "user_payout_pending",
PayoutSuccess = "payout_success"
}
export const statusMapping: { [key: number]: SwapStatus } = {
"0": SwapStatus.Created,
"1": SwapStatus.UserTransferPending,
"2": SwapStatus.UserTransferDelayed,
"3": SwapStatus.BridgeTransferPending,
"4": SwapStatus.Completed,
"5": SwapStatus.Failed,
"6": SwapStatus.Expired,
"7": SwapStatus.Cancelled,
};
-243
View File
@@ -1,243 +0,0 @@
export type ThemeData = {
backdrop?: string,
actionButtonText: string,
logo: string,
placeholderText: string,
primary: ThemeColor,
secondary?: ThemeColor
}
export type ThemeColor = {
DEFAULT: string;
50: string;
100: string;
200: string;
300: string;
400: string;
500: string;
600: string;
700: string;
800: string;
900: string;
950?: string;
text: string,
textMuted?: string,
}
export const THEME_COLORS: { [key: string]: ThemeData } = {
"imxMarketplace": {
backdrop: "0, 121, 133",
actionButtonText: '0, 0, 0',
placeholderText: '140, 152, 192',
logo: '255, 255, 255',
primary: {
DEFAULT: '46, 236, 255',
'50': '230, 253, 255',
'100': '209, 251, 255',
'200': '168, 247, 255',
'300': '128, 243, 255',
'400': '87, 240, 255',
'500': '46, 236, 255',
'600': '0, 232, 255',
'700': '0, 172, 189',
'800': '0, 121, 133',
'900': '0, 70, 77',
'text': '255, 255, 255',
'textMuted': '86, 97, 123',
},
secondary: {
DEFAULT: '17, 29, 54',
'50': '49, 60, 155',
'100': '46, 59, 147',
'200': '35, 42, 112',
'300': '32, 41, 101',
'400': '28, 39, 89',
'500': '22, 37, 70',
'600': '20, 33, 62',
'700': '17, 29, 54',
'800': '15, 25, 47',
'900': '12, 21, 39',
'950': '11, 17, 35',
'text': '209, 251, 255',
},
},
"ea7df14a1597407f9f755f05e25bab42": {
backdrop: "0, 121, 133",
placeholderText: '198, 242, 246',
actionButtonText: '0, 0, 0',
logo: '255, 255, 255',
primary: {
DEFAULT: '128, 226, 235',
'50': '255, 255, 255',
'100': '255, 255, 255',
'200': '234, 250, 252',
'300': '198, 242, 246',
'400': '163, 234, 241',
'500': '128, 226, 235',
'600': '80, 215, 227',
'700': '34, 201, 217',
'800': '26, 156, 168',
'900': '19, 111, 120',
'950': '15, 89, 96',
'text': '255, 255, 255',
'textMuted': '86, 97, 123',
},
secondary: {
DEFAULT: '46, 89, 112',
'50': '193, 217, 230',
'100': '179, 208, 224',
'200': '150, 191, 212',
'300': '121, 173, 200',
'400': '92, 155, 188',
'500': '34, 66, 83',
'600': '16, 35, 49',
'700': '15, 29, 39',
'800': '34, 66, 83',
'900': '22, 43, 54',
'950': '14, 27, 34',
'text': '209, 251, 255',
}
},
"light": {
placeholderText: '134, 134, 134',
actionButtonText: '255, 255, 255',
logo: '255, 0, 147',
primary: {
DEFAULT: '228, 37, 117',
'50': '248, 200, 220',
'100': '246, 182, 209',
'200': '241, 146, 186',
'300': '237, 110, 163',
'400': '232, 73, 140',
'500': '228, 37, 117',
'600': '166, 51, 94',
'700': '136, 17, 67',
'800': '147, 8, 99',
'900': '196, 153, 175',
'text': '17, 17, 17',
'textMuted': '86, 97, 123',
},
secondary: {
DEFAULT: '240, 240, 240',
'50': '49, 60, 155',
'100': '46, 59, 147',
'200': '134, 134, 134',
'300': '139, 139, 139',
'400': '177, 177, 177',
'500': '218, 218, 218',
'600': '223, 223, 223',
'700': '240, 240, 240',
'800': '243, 244, 246',
'900': '250, 248, 248',
'950': '255, 255, 255',
'text': '108, 108, 108',
},
},
"default": {
backdrop: "62, 18, 64",
placeholderText: '140, 152, 192',
actionButtonText: '255, 255, 255',
logo: '255, 0, 147',
primary: {
DEFAULT: '228, 37, 117',
'50': '248, 200, 220',
'100': '246, 182, 209',
'200': '241, 146, 186',
'300': '237, 110, 163',
'400': '232, 73, 140',
'500': '228, 37, 117',
'600': '166, 51, 94',
'700': '136, 17, 67',
'800': '147, 8, 99',
'900': '110, 0, 64',
'text': '255, 255, 255',
'textMuted': '86, 97, 123',
},
secondary: {
DEFAULT: '17, 29, 54',
'50': '49, 60, 155',
'100': '46, 59, 147',
'200': '35, 42, 112',
'300': '32, 41, 101',
'400': '28, 39, 89',
'500': '22, 37, 70',
'600': '20, 33, 62',
'700': '17, 29, 54',
'800': '15, 25, 47',
'900': '12, 21, 39',
'950': '11, 17, 35',
'text': '171, 181, 209',
},
},
"evmos": {
placeholderText: '128, 110, 107',
actionButtonText: '255, 255, 255',
logo: '226, 49, 115',
primary: {
DEFAULT: '237, 78, 51',
'50': '248, 200, 220',
'100': '246, 182, 209',
'200': '241, 146, 186',
'300': '237, 110, 163',
'400': '232, 73, 140',
'500': '219, 211, 209',
'600': '166, 51, 94',
'700': '136, 17, 67',
'800': '147, 8, 99',
'900': '237, 78, 51',
'text': '74, 61, 59',
'textMuted': '86, 97, 123',
},
secondary: {
DEFAULT: '239, 239, 239',
'50': '49, 60, 155',
'100': '46, 59, 147',
'200': '134, 134, 134',
'300': '139, 139, 139',
'400': '177, 177, 177',
'500': '227, 227, 227',
'600': '223, 223, 223',
'700': '244, 243, 242',
'800': '248, 247, 247',
'900': '250, 248, 248',
'950': '255, 255, 255',
'text': '122, 91, 91',
},
},
"ton": {
placeholderText: '134, 134, 134',
actionButtonText: '255, 255, 255',
logo: '15, 15, 15',
primary: {
DEFAULT: '51, 144, 236',
'50': '248, 200, 220',
'100': '246, 182, 209',
'200': '241, 146, 186',
'300': '237, 110, 163',
'400': '232, 73, 140',
'500': '45, 148, 229',
'600': '166, 51, 94',
'700': '136, 17, 67',
'800': '45, 148, 229',
'900': '51, 144, 236',
'text': '15, 15, 15',
'textMuted': '86, 97, 123',
},
secondary: {
DEFAULT: '240, 240, 240',
'50': '190, 195, 200',
'100': '199, 201, 206',
'200': '208, 210, 211',
'300': '212, 214, 219',
'400': '220, 222, 226',
'500': '227, 230, 233',
'600': '229, 231, 235',
'700': '241, 243, 245',
'800': '243, 244, 246',
'900': '250, 248, 248',
'950': '255, 255, 255',
'text': '106, 119, 133',
},
}
}
@@ -1,5 +0,0 @@
export enum TransactionType {
Input = "input",
Output = "output",
Refuel = "refuel",
}
-73
View File
@@ -1,73 +0,0 @@
export type FormSteps = "SwapForm" | "Email" | "Code" | "OffRampExchangeOAuth" | "ExchangeOAuth" | "ExchangeApiCredentials" | "SwapConfirmation"
export type SwapSteps = "Email" | "Code" | "Overview" | "Withdrawal" | "OffRampWithdrawal" | "Processing" | "Success" | "Failed" | "ExternalPayment"
export type LoginSteps = "Email" | "Code"
export type BaseWizard = {
[key: string]: SwapCreateStep
}
export type FormWizardSteps = {
[Property in FormSteps]: SwapCreateStep
}
export type SwapWizardSteps = {
[Property in SwapSteps]: SwapCreateStep
}
export type LoginWizardSteps = {
[Property in LoginSteps]: SwapCreateStep
}
export enum SwapCreateStep {
MainForm = "MainForm",
Email = "Email",
Code = "Code",
PendingSwaps = "PendingSwaps",
AuthorizeCoinbaseWithdrawal = "AuthorizeCoinbaseWithdrawal",
OffRampOAuth = "OffRampOAuth",
ApiKey = "ApiKey",
TwoFactor = "TwoFactor",
ActiveSwapLimit = 'ActiveSwapLimit',
Error = "Error"
}
export enum SwapWithdrawalStep {
Withdrawal = "Withdrawal",
CoinbaseManualWithdrawal = "CoinbaseManualWithdrawal",
SwapProcessing = 'SwapProcessing',
ProcessingWalletTransaction = "ProcessingWalletTransaction",
Success = "Success",
Failed = "Failed",
Error = "Error",
Delay = "Delay",
OffRampWithdrawal = "OffRampWithdrawal",
WithdrawFromImtblx = "WithdrawFromImtblx",
WithdrawFromStarknet = "WithdrawFromStarknet",
SelectWithdrawalType = "SelectWithdrawalType",
CoinbaseInternalWithdrawal = "CoinbaseInternalWithdrawal",
}
export enum AuthStep {
Email = "Email",
Code = "Code",
PendingSwaps = 'PendingSwaps'
}
export type Steps = AuthStep | SwapWithdrawalStep | SwapCreateStep
export const ExchangeAuthorizationSteps: { [key: string]: SwapCreateStep } = {
"api_credentials": SwapCreateStep.ApiKey,
"o_auth2": SwapCreateStep.AuthorizeCoinbaseWithdrawal
}
export const OfframpExchangeAuthorizationSteps: { [key: string]: SwapCreateStep } = {
"api_credentials": SwapCreateStep.ApiKey,
"o_auth2": SwapCreateStep.OffRampOAuth
}
export interface WizardStep<T> {
Name: T;
Content: React.FC;
onBack?: () => void;
onNext?: (data?: any) => Promise<void>;
positionPercent: number;
}
-27
View File
@@ -1,27 +0,0 @@
'use client'
import { useEffect, useRef } from "react";
import Jazzicon from "@metamask/jazzicon";
const AddressIcon: React.FC<{
address: string;
size: number;
}> = ({
address,
size
}) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (address && ref.current) {
ref.current.innerHTML = "";
const iconElement = Jazzicon(size, parseInt(address.slice(2, 10), 16)) as HTMLElement
if(iconElement){
iconElement.style.display = 'block'
ref.current.appendChild(iconElement);
}
}
}, [address, size]);
return <div ref={ref as any} />
}
export default AddressIcon
-17
View File
@@ -1,17 +0,0 @@
'use client'
import React from 'react'
import AuthWizard from '@/components/Wizard/AuthWizard'
import { FormWizardProvider } from '@/context/formWizardProvider'
import { AuthStep } from '@/Models/Wizard'
import { SwapDataProvider } from '@/context/swap'
export default function AuthContent() {
return (
<SwapDataProvider>
<FormWizardProvider initialStep={AuthStep.Email} initialLoading={false}>
<AuthWizard />
</FormWizardProvider>
</SwapDataProvider>
)
}
-29
View File
@@ -1,29 +0,0 @@
import Image from 'next/image'
const AvatarGroup: React.FC<{
imageUrls: string[]
}
> = (({
imageUrls
}) => {
return (
<div className="">
<div className="isolate flex -space-x-1 overflow-hidden p-1">
{imageUrls.map(x => {
return (
<Image
key={x}
className="relative z-30 inline-block h-5 w-5 rounded-full ring-2 ring-foreground"
src={x}
width="60"
height={60}
alt=""
/>
)
})}
</div>
</div>
)
});
export default AvatarGroup;
@@ -1,39 +0,0 @@
'use client'
import { cn } from '@hanzo/ui/util'
import { ArrowUpRight } from 'lucide-react'
import Image from 'next/image'
import React from 'react'
interface IProps {
className?: string
}
const GotoExchange: React.FC<IProps> = ({ className }) => (
<div className={cn(className, 'w-full pb-4')}>
<div
onClick={() => window.open('https://lux.exchange/#/swap', '_self')}
className="flex w-full justify-between gap-3 bg-level-1 p-3 rounded-xl hover:opacity-70 cursor-pointer"
>
<div className="flex gap-3">
<div className="flex items-center">
<Image
src={'https://cdn.lux.network/bridge/networks/lux_mainnet.png'}
width={30}
height={30}
alt="avatar"
className="rounded-full flex-none"
/>
</div>
<div className="flex flex-col">
<h3 className="text-lg">Exchange</h3>
<h3 className="text-sm hidden sm:inline">Trade hyper liquid digital assets on LX with zero gas fees.</h3>
</div>
</div>
<div className="flex items-center">
<ArrowUpRight width={20} height={20} />
</div>
</div>
</div>
)
export default GotoExchange
@@ -1,90 +0,0 @@
import type { PropsWithChildren, ReactNode } from "react"
import { ChevronRight, ExternalLink } from "lucide-react"
import LinkWrapper from "../LinkWrapper"
import { cn } from '@hanzo/ui/util'
function Menu ({ children }: PropsWithChildren) {
return (
<div className="flex flex-col gap-3 mt-3">
{children}
<div style={{ height: '70px' }} />
</div>
)
}
const Group: React.FC<PropsWithChildren> = ({ children }) => (
<div className="divide-y divide-muted-3 rounded-md overflow-hidden bg-level-1">
{children}
</div>
)
const itemClass = 'flex justify-between cursor-pointer hover:bg-level-2 select-none w-full px-4 py-3 outline-none text-muted'
const Item: React.FC<{
pathname?: string
onClick?: React.MouseEventHandler<HTMLButtonElement>
icon: ReactNode
target?: '_blank' | '_self'
} & PropsWithChildren> = ({
children,
pathname,
onClick,
icon,
target = '_self'
}) => (
pathname ? (
<LinkWrapper
href={pathname}
target={target}
className={itemClass}
>
<div className="flex justify-start">
{icon}
<span className="pl-2">{children}</span>
</div>
{target === '_self' ?
( <ChevronRight className="h-5 w-5" /> )
:
( <ExternalLink className="h-5 w-5" /> )
}
</LinkWrapper>
) : (
<button
type="button"
onClick={onClick}
className={itemClass}
>
<div className="flex justify-start">
{icon}
<span className="pl-2">{children}</span>
</div>
<ChevronRight className="h-5 w-5" />
</button>
)
)
const Footer: React.FC<{
className?: string
} & PropsWithChildren> = ({
className='',
children
}) => (
<div className={cn(
'text-muted bg-background text-base ',
'sticky inset-x-0 bottom-0 ',
'w-full ',
'flex justify-center',
className
)}>
{children}
</div>
)
Menu.Group = Group
Menu.Item = Item
Menu.Footer = Footer
export default Menu
@@ -1,256 +0,0 @@
'use client'
import dynamic from 'next/dynamic'
import Menu from './Menu'
import Modal from '@/components/modal/modal'
import Popover from '../modal/popover'
import ChatIcon from '../icons/ChatIcon'
import inIframe from '../utils/inIframe'
import socialNav from './social'
import TokenService from '@/lib/TokenService'
import SendFeedback from '../sendFeedback'
import resolvePersistentQueryParams from '@/util/resolvePersistentQueryParams'
import React, { useCallback, useEffect, useState } from 'react'
import { useIntercom } from 'react-use-intercom'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import {
BookOpen,
Gift,
MenuIcon,
Home,
LogIn,
LogOut,
ScrollText,
LibraryIcon,
Shield,
Users,
MessageSquarePlus,
UserCircle2,
} from 'lucide-react'
import { shortenEmail } from '../utils/ShortenAddress'
import { Button, LinkElement } from '@hanzo/ui/primitives'
import {
useAuthDataUpdate,
useAuthState,
UserType,
} from '@/context/authContext'
const WalletsMenu = dynamic(
() => import('../ConnectedWallets').then((comp) => comp.WalletsMenu),
{
loading: () => <div />,
}
)
const BridgeMenu: React.FC = () => {
const { email, userType, userId } = useAuthState()
const { setUserType } = useAuthDataUpdate()
const { boot, show, update } = useIntercom()
const [embedded, setEmbedded] = useState<boolean>()
const [openTopModal, setOpenTopModal] = useState(false)
const [openFeedbackModal, setOpenFeedbackModal] = useState(false)
const path = usePathname()
const searchParams = useSearchParams()
const paramsString = searchParams.toString()
const router = useRouter()
useEffect(() => {
setEmbedded(inIframe())
}, [])
const updateWithProps = () => {
update({ email: email, userId: userId })
}
const handleLogout = useCallback(() => {
TokenService.removeAuthData()
if (path != '/') {
const paramString = resolvePersistentQueryParams(searchParams)
router.push('/' + (paramString ? '?' + paramString : ''))
} else {
router.refresh()
}
setUserType(UserType.NotAuthenticatedUser)
}, [paramsString])
const handleCloseFeedback = () => {
setOpenFeedbackModal(false)
}
const BurgerButton: React.FC = () => (
<Button
variant="outline"
size="square"
onClick={() => setOpenTopModal(true)}
aria-label="Main menu"
className="text-muted-2 p-0 flex items-center justify-center border-none"
>
<MenuIcon className="h-6 w-6" strokeWidth={2} />
</Button>
)
return (
<>
<BurgerButton />
<Modal
show={openTopModal}
setShow={setOpenTopModal}
header={
<h2 className="font-normal leading-none tracking-tight">Menu</h2>
}
>
<div className="text-sm font-medium focus:outline-none h-full">
<Menu>
<WalletsMenu />
<Menu.Group>
{path != '/' && (
<Menu.Item pathname="/" icon={<Home className="h-5 w-5" />}>
Home
</Menu.Item>
)}
{path != '/transactions' && (
<Menu.Item
pathname={`/transactions`}
icon={<ScrollText className="h-5 w-5" />}
>
Transfers
</Menu.Item>
)}
{!embedded && path != '/campaigns' && (
<Menu.Item
pathname={`/campaigns`}
icon={<Gift className="h-5 w-5" />}
>
Campaigns
</Menu.Item>
)}
</Menu.Group>
<Menu.Group>
<Menu.Item
onClick={() => {
boot()
show()
updateWithProps()
}}
target="_blank"
icon={<ChatIcon strokeWidth={2} className="h-5 w-5" />}
>
Help
</Menu.Item>
<Menu.Item
pathname="/docs/"
target="_blank"
icon={<BookOpen className="h-5 w-5" />}
>
Docs for Users
</Menu.Item>
<Menu.Item
pathname="/docs/partners-and-integrations"
target="_blank"
icon={<Users className="h-5 w-5" />}
>
Docs for Partners
</Menu.Item>
</Menu.Group>
<Menu.Group>
<Menu.Item
pathname="/docs/information/privacy-policy"
target="_blank"
icon={<Shield className="h-5 w-5" />}
>
Privacy Policy
</Menu.Item>
<Menu.Item
pathname="/docs/information/terms-of-services"
target="_blank"
icon={<LibraryIcon className="h-5 w-5" />}
>
Terms of Service
</Menu.Item>
</Menu.Group>
<Menu.Group>
<Popover
opener={
<Menu.Item
onClick={() => setOpenFeedbackModal(true)}
target="_blank"
icon={<MessageSquarePlus className="h-5 w-5" />}
>
Suggest a Feature
</Menu.Item>
}
isNested={true}
show={openFeedbackModal}
header="Suggest a Feature"
setShow={setOpenFeedbackModal}
>
<div className="p-0 md:max-w-md">
<SendFeedback onSend={handleCloseFeedback} />
</div>
</Popover>
</Menu.Group>
<div className="space-y-3 w-full">
<hr className="border-muted-3" />
<p className="text-muted-2 flex justify-center my-3">
Media links & suggestions:
</p>
</div>
<div className="grid grid-row-2 grid-cols-3 gap-1">
{socialNav.map(({ className, ...rest }, index) => (
<LinkElement
def={{
...rest,
variant: 'outline',
size: 'sm',
}}
className={className + ' font-sans text-muted-2'}
key={index}
/>
))}
</div>
{path != '/auth' && (
<Menu.Footer className='xs:border-t-0 md:border-t md:border-muted-2'>
{userType == UserType.AuthenticatedUser ? (
<div className="gap-4 flex justify-between items-center relative select-none outline-none w-full">
<div className="font-normal flex gap-2 items-center">
<UserCircle2 className="h-5 w-5" />
{email && <span>{shortenEmail(email, 22)}</span>}
</div>
<LinkElement
def={{
href: '',
title: 'Sign out',
icon: <LogOut className="h-5 w-5" />,
size: 'sm',
variant: 'outline',
}}
onClick={handleLogout}
className="font-sans font-semibold text-muted-2"
/>
</div>
) : (
<LinkElement
def={{
href: '/auth',
title: 'Sign in',
icon: <LogIn className="h-5 w-5" />,
size: 'default',
variant: 'primary',
}}
className="font-sans font-semibold xs:w-full md:w-auto"
/>
)}
</Menu.Footer>
)}
</Menu>
</div>
</Modal>
</>
)
}
export default BridgeMenu
@@ -1,48 +0,0 @@
import { type ComponentType } from 'react'
import YoutubeLogo from '../icons/YoutubeLogo'
import DiscordLogo from './../icons/DiscordLogo'
import GitHubLogo from './../icons/GitHubLogo'
import SubstackLogo from './../icons/SubstackLogo'
import TwitterLogo from './../icons/TwitterLogo'
import { Map as LucideMap} from 'lucide-react'
import { type LinkDef } from '@hanzo/ui/types'
export default [
{
title: 'Twitter',
href: '',
icon: <TwitterLogo className="h-5 w-5" aria-hidden="true"/>,
className: 'plausible-event-title=Twitter'
},
{
title: 'GitHub',
href: '',
icon: <GitHubLogo className="h-5 w-5" aria-hidden="true"/>,
className: 'plausible-event-title=GitHub'
},
{
title: 'Discord',
href: '',
icon: <DiscordLogo className="h-5 w-5" aria-hidden="true"/>,
className: 'plausible-event-title=Discord'
},
{
title: 'YouTube',
href: '',
icon: <YoutubeLogo className="h-5 w-5" aria-hidden="true"/>,
className: 'plausible-event-title=Youtube'
},
{
title: 'Substack',
href: '',
icon: <SubstackLogo className="h-5 w-5" aria-hidden="true"/>,
className: 'plausible-event-title=Substack'
},
{
title: 'Roadmap',
href: '',
icon: <LucideMap className="h-5 w-5" aria-hidden="true"/>,
className: 'plausible-event-title=Roadmap'
},
] satisfies (LinkDef & {className: string})[]
@@ -1,209 +0,0 @@
'use client'
import { useState } from "react"
import Image from 'next/image'
import Link from "next/link";
import { Trophy } from "lucide-react"
import useSWR from "swr"
import { useAccount } from "wagmi"
import { useSettings } from "@/context/settings"
import BridgeRPCClient, { type Campaign, type Leaderboard, type Reward } from "@/lib/BridgeRPCClient"
import { ApiResponse } from "@/Models/ApiResponse"
import { RewardsComponentLeaderboardSkeleton } from "../../Skeletons"
import ClickTooltip from "../../Tooltips/ClickTooltip"
import shortenAddress from "../../utils/ShortenAddress"
import { truncateDecimals } from "../../utils/RoundDecimals"
import AddressIcon from "../../AddressIcon";
import Modal from "../../modal/modal";
const Component: React.FC<{
campaign: Campaign
}> = ({
campaign
}) => {
const [openTopModal, setOpenTopModal] = useState(false)
const settings = useSettings()
const { address } = useAccount();
const handleOpenTopModal = () => {
setOpenTopModal(true)
}
const apiClient = new BridgeRPCClient()
const { data: leaderboardData, isLoading } = useSWR<ApiResponse<Leaderboard>>(`/campaigns/${campaign?.id}/leaderboard`, apiClient.fetcher, { dedupingInterval: 60000 })
const { data: rewardsData, isLoading: rewardsIsLoading } = useSWR<ApiResponse<Reward>>(`/campaigns/${campaign.id}/rewards/${address}`, apiClient.fetcher, { dedupingInterval: 60000 })
const leaderboard = leaderboardData?.data
if (isLoading) {
return <RewardsComponentLeaderboardSkeleton />
}
if (!leaderboard) {
//TODO handle
return <></>
}
const rewards = rewardsData?.data
const { networks } = settings
const network = networks.find(n => n.internal_name === campaign?.network)
const position = rewards?.user_reward.position || NaN
const campaignAsset = network?.currencies.find(c => c?.asset === campaign?.asset)
const leaderboardRewards = [
leaderboard.leaderboard_budget * 0.6,
leaderboard.leaderboard_budget * 0.3,
leaderboard.leaderboard_budget * 0.1
]
return <div className="space-y-2">
{leaderboard?.leaderboard?.length > 0 &&
<div className="flex items-center justify-between">
<p className="font-bold text-left leading-5">Leaderboard</p>
<button onClick={handleOpenTopModal} type="button" className=" leading-4 text-base text-muted underline hover:no-underline hover:text-foreground">
Top 10
</button>
</div>}
<p className="text-sm ">Users who earn the most throughout the program will be featured here.</p>
<div className=" border border-[#404040] hover:border-[#404040] transition duration-200 rounded-lg shadow-lg">
<div className="p-3">
{leaderboard?.leaderboard?.length > 0 ? <div className="space-y-6">
{
leaderboard?.leaderboard?.filter(u => u.position < 4).map(user => (
<div key={user.position} className="items-center flex justify-between text-muted">
<div className="flex items-center">
<p className="text-md font-medium w-fit mr-1 cursor-default">{user.position}.</p>
<div className="cols-start-2 flex items-center space-x-2">
<AddressIcon address={user.address} size={25} />
<div>
<div className="text-sm font-bold leading-3">
{user?.address && network?.account_explorer_template && <Link target="_blank" className="hover:opacity-80" href={network?.account_explorer_template?.replace("{0}", user?.address)}>
{user?.position === rewards?.user_reward?.position ? <span className="text-primary">You</span> : shortenAddress(user?.address)}
</Link>}
</div>
<p className="mt-1 text-sm font-medium leading-3 cursor-default">{truncateDecimals(user.amount, campaignAsset?.precision)} {campaign?.asset}</p>
</div>
</div >
</div >
{
leaderboard.leaderboard_budget > 0 && <div className="text-right flex items-center space-x-2">
<ClickTooltip
text={
<div className="flex items-center space-x-1">
<span>+</span>
<div className="h-3.5 w-3.5 relative">
<Image
src={network?.logo ?? ''}
alt="Project Logo"
height="40"
width="40"
loading="eager"
className="rounded-full object-contain" />
</div>
<p>
<span>{leaderboardRewards[user.position - 1]} {campaign?.asset}</span>
</p>
</div>
}
>
<div className='text-muted hover:cursor-pointer hover:muted-2 ml-0.5 hover:bg-level-4 active:ring-2 active:ring-gray-200 active:bg-level-3 focus:outline-none cursor-default p-1 rounded'>
<Trophy className="h-4 w-4" aria-hidden="true" />
</div>
</ClickTooltip>
</div>
}
</div >
))
}
{
position >= 4 && address && rewards?.user_reward &&
<div className={position > 4 ? "!mt-0 !pt-0" : ""}>
{position > 4 && < div className="text-2xl text-center leading-3 my-3">
...
</div>}
<div key={position} className="items-center flex justify-between">
<div className="flex items-center">
<p className="text-xl font-medium w-fit mr-1 cursor-default">{position}.</p>
<div className="cols-start-2 flex items-center space-x-2">
<AddressIcon address={address} size={25} />
<div>
<div className="text-sm font-bold leading-3">
{network?.account_explorer_template && <Link target="_blank" className="hover:opacity-80" href={network?.account_explorer_template?.replace("{0}", address)}>
<span className="">You</span>
</Link>}
</div>
<p className="mt-1 text-sm font-medium leading-3 cursor-default">{truncateDecimals(rewards.user_reward.total_amount, campaignAsset?.precision)} {campaign?.asset}</p>
</div>
</div >
</div >
</div >
</div >
}
</div >
:
<div className="h-8 flex flex-col justify-center items-center text-sm">
Here will be leaderboard.
</div>
}
</div >
</div >
<Modal height="full" header='Leaderboard' show={openTopModal} setShow={setOpenTopModal} >
<div className="bg-background border border-[#404040] mt-2 hover:border-[#404040] transition duration-200 rounded-lg shadow-lg text-muted">
<div className="p-3">
<div className="space-y-6">
{
leaderboard?.leaderboard?.map(user => (
<div key={user.position} className="items-center flex justify-between">
<div className="flex items-center">
<p className="text-md font-medium w-fit mr-1 cursor-default">{user.position}.</p>
<div className="cols-start-2 flex items-center space-x-2">
<AddressIcon address={user.address} size={25} />
<div>
<div className="text-sm font-bold leading-3">
{user?.address && network?.account_explorer_template && <Link target="_blank" className="hover:opacity-80" href={network?.account_explorer_template?.replace("{0}", user?.address)}>
{user.position === rewards?.user_reward?.position ? <span className="text-primary">You</span> : shortenAddress(user.address)}
</Link>}
</div>
<p className="mt-1 text-sm font-medium leading-3 cursor-default">{truncateDecimals(user.amount, campaignAsset?.precision)} {campaign?.asset}</p>
</div>
</div >
</div >
{
user.position < 4 && leaderboard.leaderboard_budget > 0 &&
<div className="text-right flex items-center space-x-2">
<ClickTooltip text={
<div className="flex items-center space-x-1">
<span>+</span>
<div className="h-3.5 w-3.5 relative">
<Image
src={network?.logo ?? ''}
alt="Address Logo"
height="40"
width="40"
loading="eager"
className="rounded-full object-contain" />
</div>
<p>
<span>{leaderboardRewards[user.position - 1]} {campaign?.asset}</span>
</p>
</div>
}>
<div className='hover:cursor-pointer hover:text-foreground ml-0.5 hover:bg-level-4 active:ring-2 active:ring-gray-200 active:bg-level-3 focus:outline-none cursor-default p-1 rounded'>
<Trophy className="h-4 w-4" aria-hidden="true" />
</div>
</ClickTooltip>
</div>
}
</div >
))
}
</div >
</div >
</div >
</Modal >
</div >
}
export default Component
@@ -1,180 +0,0 @@
'use client'
import Image from 'next/image'
import useSWR from "swr"
import { Clock } from "lucide-react"
import { useAccount } from "wagmi"
import { Progress } from "@hanzo/ui/primitives";
import { useSettings } from "@/context/settings"
import BackgroundField from "../../backgroundField";
import type { Campaign, Reward, RewardPayout } from "@/lib/BridgeRPCClient"
import BridgeRPCClient from "@/lib/BridgeRPCClient"
import { RewardsComponentSkeleton } from "../../Skeletons"
import { ApiResponse } from "@/Models/ApiResponse"
import ClickTooltip from "../../Tooltips/ClickTooltip"
import shortenAddress from "../../utils/ShortenAddress"
const Rewards: React.FC<{
campaign: Campaign
}> = ({
campaign
}) => {
const settings = useSettings()
const { networks } = settings
const { address } = useAccount();
const apiClient = new BridgeRPCClient()
const { data: rewardsData, isLoading: rewardsIsLoading } = useSWR<ApiResponse<Reward>>(`/campaigns/${campaign.id}/rewards/${address}`, apiClient.fetcher, { dedupingInterval: 60000 })
const { data: payoutsData, isLoading: payoutsIsLoading } = useSWR<ApiResponse<RewardPayout[]>>(`/campaigns/${campaign.id}/payouts/${address}`, apiClient.fetcher, { dedupingInterval: 60000 })
if (rewardsIsLoading || payoutsIsLoading) {
return <RewardsComponentSkeleton />
}
const payouts = payoutsData?.data || []
const totalBudget = campaign.total_budget
const network = networks.find(n => n.internal_name === campaign.network)
const rewards = rewardsData?.data
const campaignEndDate = new Date(campaign.end_date)
const now = new Date()
const next = rewards?.next_airdrop_date ? new Date(rewards?.next_airdrop_date) : null
const difference_in_days = next ?
Math.floor(Math.abs(((next.getTime() - now.getTime())) / (1000 * 3600 * 24))) : null
const difference_in_hours = (next && difference_in_days) ?
Math.round(Math.abs(((next.getTime() - now.getTime())) / (1000 * 3600) - (difference_in_days * 24)))
: null
const campaignIsEnded = (campaignEndDate.getTime() - now.getTime()) < 0 || campaign.status !== 'active'
const DistributedAmount = ((campaign.distributed_amount / campaign.total_budget) * 100)
const usdc_price = network?.currencies?.find(c => c.asset === campaign.asset)?.price_in_usd
const total_amount = rewards?.user_reward.total_amount
const total_in_usd = (total_amount && usdc_price) ? (usdc_price * total_amount).toFixed(2) : null
return <>
<div className="space-y-4">
<p className="">
<span>
<span>Onboarding incentives that are earned by transferring to&nbsp;</span>{network?.display_name}<span>.&nbsp;</span>
<a
target='_blank'
href="/docs/"
className=" underline hover:no-underline decoration-white cursor-pointer"
>Learn more</a>
</span>
</p>
<div className="bg-background divide-y divide-muted-3 rounded-lg shadow-lg border border-[#404040] hover:border-[#404040] transition duration-200">
{!campaignIsEnded &&
<BackgroundField header={<span className="flex justify-between"><span className="flex items-center"><span>Pending Earnings&nbsp;</span><ClickTooltip text={`${campaign?.asset} tokens that will be airdropped periodically.`} /> </span><span>Next Airdrop</span></span>} withoutBorder>
<div className="flex justify-between w-full text-2xl">
<div className="flex items-center space-x-1">
<div className="h-5 w-5 relative">
<Image
src={network?.logo ?? ''}
alt="Project Logo"
height="40"
width="40"
loading="eager"
className="rounded-full object-contain" />
</div>
<p>
{rewards?.user_reward.total_pending_amount} <span className="text-base sm:text-2xl">{campaign?.asset}</span>
</p>
</div>
<div className="flex items-center space-x-1">
<Clock className="h-5" />
<p>
{difference_in_days}d {difference_in_hours}h
</p>
</div>
</div>
</BackgroundField>
}
<BackgroundField header={<span className="flex justify-between"><span className="flex items-center"><span>Total Earnings&nbsp;</span><ClickTooltip text={`${campaign?.asset} tokens that you’ve earned so far (including Pending Earnings).`} /></span><span>Current Value</span></span>} withoutBorder>
<div className="flex justify-between w-full text-slate-300 text-2xl">
<div className="flex items-center space-x-1">
<div className="h-5 w-5 relative">
<Image
src={network?.logo ?? ''}
alt="Project Logo"
height="40"
width="40"
loading="eager"
className="rounded-full object-contain" />
</div>
<p>
{rewards?.user_reward.total_amount} <span className="text-base sm:text-2xl">{campaign?.asset}</span>
</p>
</div>
<p>
${total_in_usd}
</p>
</div>
</BackgroundField>
</div>
</div>
<div className=" rounded-lg shadow-lg border border-[#404040] hover:border-[#404040] transition duration-200">
<BackgroundField header={
<>
<p className="flex items-center"><span>{campaign?.asset} pool</span>
<ClickTooltip text={`The amount of ${campaign?.asset} to be distributed during this round of the campaign.`} />
</p>
</>
} withoutBorder>
<div className="flex flex-col w-full gap-2">
<Progress value={DistributedAmount === Infinity ? 0 : DistributedAmount} />
<div className="flex justify-between w-full font-semibold text-sm ">
<div className=""><span className="">{campaign?.distributed_amount.toFixed(0)}</span> <span>/</span> {totalBudget} {campaign?.asset}</div>
</div>
</div>
</BackgroundField>
</div>
{
payouts.length > 0 &&
<div className="space-y-1">
<p className="font-bold text-lg text-left">Payouts</p>
<div className=" divide-y divide-muted-3 rounded-lg shadow-lg border border-[#404040] hover:border-[#404040] transition duration-200">
<div className="inline-block min-w-full align-middle">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table className="min-w-full divide-y divide-muted-3">
<thead className="">
<tr>
<th scope="col" className="py-3.5 pl-4 text-left text-sm font-semibold sm:pl-6">
Tx Id
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold ">
Amount
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold ">
Date
</th>
</tr>
</thead>
<tbody className="divide-y divide-muted-3">
{payouts.map((payout) => (
<tr key={payout.transaction_id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium sm:pl-6 underline hover:no-underline">
<a target={"_blank"} href={network?.transaction_explorer_template?.replace("{0}", payout.transaction_id)}>{shortenAddress(payout.transaction_id)}</a>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-100">{payout.amount}</td>
<td className="px-3 py-4 text-sm text-gray-100">{new Date(payout.date).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
}
</>
}
export default Rewards
@@ -1,140 +0,0 @@
'use client'
import Image from 'next/image'
import { Gift } from 'lucide-react'
import { useAccount } from 'wagmi'
import useSWR from 'swr'
import { LinkElement } from '@hanzo/ui/primitives'
import { useSettings } from '@/context/settings'
import BridgeRPCClient, { type Campaign } from '@/lib/BridgeRPCClient'
import { type ApiResponse } from '@/Models/ApiResponse'
import RainbowKit from '../../Swap/Withdraw/Wallet/RainbowKit'
import SubmitButton from '../../buttons/submitButton';
import WalletIcon from '../../icons/WalletIcon';
import LinkWrapper from '../../LinkWrapper';
import Widget from '../../Widget';
import Leaderboard from './Leaderboard'
import Rewards from './Rewards';
import SpinIcon from '../../icons/spinIcon'
import type { CryptoNetwork } from '@/Models/CryptoNetwork'
const CampaignDetails: React.FC<{
campaign: string
}> = ({
campaign: campaignName
}) => {
const settings = useSettings()
const { networks } = settings
const { isConnected } = useAccount();
// :aa Shouldn't this be loaded server-side in the parent page??
const apiClient = new BridgeRPCClient()
const { data: campaignsData, isLoading } = useSWR<ApiResponse<Campaign[]>>('/campaigns', apiClient.fetcher)
const campaign = campaignsData?.data?.find((c) => (c.name === campaignName))
const network = networks.find(n => n.internal_name === campaign?.network)
if (isLoading) {
return <Loading />
}
if (!campaign) {
return <NotFound />
}
return (
<Widget>
<Widget.Content>
<div className='space-y-5'>
<div className='flex items-center gap-1'>
{network && (
<div className='h-7 w-7 relative'>
<Image
src={network.logo ?? ''}
alt='Project Logo'
height='40'
width='40'
loading='eager'
className='rounded-md object-contain'
/>
</div>
)}
<h2 className='font-bold text-xl text-left flex items-center'>
{network?.display_name && (<span>network.display_name&nbsp;</span>)}
Rewards
</h2>
</div>
{isConnected ? (
<Rewards campaign={campaign} />
) : (
<BriefInformation network={network} campaign={campaign} />
)}
<Leaderboard campaign={campaign} />
</div>
</Widget.Content>
{!isConnected ? (
<Widget.Footer>
<RainbowKit>
<SubmitButton isDisabled={false} isSubmitting={false} icon={<WalletIcon className='stroke-2 w-6 h-6' />}>
Connect a wallet
</SubmitButton>
</RainbowKit>
</Widget.Footer>
) : (<></>)}
</Widget >
)
}
const BriefInformation: React.FC<{
campaign: Campaign,
network?: CryptoNetwork
}> = ({
campaign,
network
}) => (
<p className="text-muted text-base">
You can earn $<span>{campaign?.asset}</span>&nbsp;tokens by transferring
assets to&nbsp;
<span>{network?.display_name || campaign.network}</span>. For each
transaction, you'll receive&nbsp;
<span>{campaign?.percentage}</span>% of Bridge fee back.&nbsp;
<LinkElement
def={{
title: "Learn more",
newTab: true,
href: "/docs/",
}}
className="inline-flex text-primary underline hover:no-underline"
/>
</p>
)
const Loading = () => (
<Widget className='min-h-[500px]'>
<Widget.Content>
<div className='absolute top-[calc(50%-5px)] left-[calc(50%-5px)]'>
<SpinIcon className='animate-spin h-5 w-5' />
</div>
</Widget.Content>
</Widget>
)
const NotFound = () => (
<Widget className='min-h-[500px]'>
<Widget.Content>
<div className='h-[364px] flex flex-col items-center justify-center space-y-4'>
<Gift className='h-20 w-20 ' />
<p className='font-bold text-center'>Campaign not found</p>
<LinkWrapper className='text-xs underline hover:no-underline' href='/campaigns'>See all campaigns</LinkWrapper>
</div>
</Widget.Content>
</Widget>
)
export default CampaignDetails
@@ -1,119 +0,0 @@
'use client'
import Image from 'next/image'
import useSWR from 'swr'
import { Gift } from 'lucide-react'
import { ApiResponse } from '@/Models/ApiResponse'
import BridgeRPCClient, { type Campaign } from '@/lib/BridgeRPCClient'
import { useSettings } from '@/context/settings'
import SpinIcon from '../icons/spinIcon'
import LinkWrapper from '../LinkWrapper'
import Widget from '../Widget'
import type { CryptoNetwork } from '@/Models/CryptoNetwork'
const Rewards = () => {
const { networks } = useSettings()
const apiClient = new BridgeRPCClient()
const { data: campaignsData, isLoading } = useSWR<ApiResponse<Campaign[]>>('/campaigns', apiClient.fetcher)
const campaigns = campaignsData?.data
const activeCampaigns = campaigns?.filter(IsCampaignActive) || []
const inactiveCampaigns = campaigns?.filter(c => !IsCampaignActive(c)) || []
return (
<Widget className='min-h-[520px]'>
<Widget.Content>
{!isLoading ?
<div className='space-y-5 h-full text-muted'>
<div className='space-y-2'>
<p className='font-bold text-left leading-5'>Campaigns</p>
<div className='border border-[#404040] transition duration-200 rounded-lg shadow-lg'>
<div className='p-3 space-y-4'>
{
activeCampaigns.length > 0 ?
activeCampaigns.map(c =>
<CampaignItem
campaign={c}
networks={networks}
key={c.id}
/>)
:
<div className='flex flex-col items-center justify-center space-y-2'>
<Gift className='h-10 w-10 text-secondary' />
<p className='font-bold text-center'>There are no active campaigns right now</p>
</div>
}
</div>
</div>
</div>
{
inactiveCampaigns.length > 0 &&
<div className='space-y-2'>
<p className='font-bold text-left leading-5'>Old campaigns</p>
<div className='border border-[#404040] transition duration-200 rounded-lg shadow-lg'>
<div className='p-3 dpsv flex flex-col space-y-4'>
{inactiveCampaigns.map(c =>
<CampaignItem
campaign={c}
networks={networks}
key={c.id}
/>)}
</div >
</div >
</div >
}
</div >
:
<div className='absolute top-[calc(50%-5px)] left-[calc(50%-5px)]'>
<SpinIcon className='animate-spin h-5 w-5' />
</div>
}
</Widget.Content>
</Widget>
)
}
const CampaignItem: React.FC<{
campaign: Campaign,
networks: CryptoNetwork[]
}> = ({
campaign,
networks
}) => {
const campaignLayer = networks.find(l => l.internal_name === campaign.network)
const campaignDaysLeft = ((new Date(campaign.end_date).getTime() - new Date().getTime()) / 86400000).toFixed()
const campaignIsActive = IsCampaignActive(campaign)
return <LinkWrapper href={`/campaigns/${campaign.name}`}
className='flex justify-between items-center'>
<span className='flex items-center gap-1 hover:text-foreground active:scale-90 duration-200 transition-all'>
<span className='h-5 w-5 relative'>
{campaignLayer && <Image
src={campaignLayer.logo ?? ''}
alt='Project Logo'
height='40'
width='40'
loading='eager'
className='rounded-md object-contain' />}
</span>
<span className='font-semibold text-base text-left flex items-center'>{campaign?.display_name} </span>
</span>
{
campaignIsActive &&
<span className=' text-right text-sm'>
{campaignDaysLeft} days left
</span>
}
</LinkWrapper>
}
function IsCampaignActive(campaign: Campaign) {
const now = new Date()
return campaign.status == 'active' && (new Date(campaign?.end_date).getTime() > now.getTime())
}
export default Rewards
-99
View File
@@ -1,99 +0,0 @@
'use client'
import React, {
type PropsWithChildren,
forwardRef,
useCallback,
useImperativeHandle,
useState
} from 'react'
import { useSwipeable } from 'react-swipeable'
const CarouselItem: React.FC<{
width: number
} & PropsWithChildren> = ({
children,
width
}) => (
<div
className={'rounded-xl inline-flex items-center justify-center flex-col pb-0 ' +
'bg-gradient-to-b from-background to-level-1 h-full'
}
style={{ width: width }}
>
{children}
</div>
)
export type CarouselRef = {
next: () => void
prev: () => void
goToLast: () => void
goToFirst: () => void
}
const Carousel = forwardRef<CarouselRef, {
starAtLast: boolean
onLast: (value: boolean) => void
onFirst: (value: boolean) => void
} & PropsWithChildren>(({
onFirst,
onLast,
children,
starAtLast
}, ref) => {
const [activeIndex, setActiveIndex] = useState(starAtLast ? React.Children.count(children) - 1 : 0)
const updateIndex = useCallback((newIndex: number) => {
onFirst(false)
onLast(false)
if (newIndex >= 0 && newIndex <= React.Children.count(children) - 1) {
setActiveIndex(newIndex)
}
if (newIndex >= React.Children.count(children) - 1)
onLast(true)
if (newIndex == 0)
onFirst(true)
}, [children, onFirst, onLast])
useImperativeHandle(ref, () => ({
next: () => {
updateIndex(activeIndex + 1)
},
prev: () => {
updateIndex(activeIndex - 1)
},
goToLast: () => {
updateIndex(React.Children.count(children) - 1)
},
goToFirst: () => {
updateIndex(0)
}
}), [activeIndex, children, updateIndex])
const handlers = useSwipeable({
onSwipedLeft: () => updateIndex(activeIndex + 1),
onSwipedRight: () => updateIndex(activeIndex - 1),
})
return (
<div {...handlers} className='overflow-hidden h-full' >
<div
className='whitespace-nowrap transition-transform duration-500 inner h-full'
style={{ transform: `translateX(-${activeIndex * 100}%)` }}
>
{children && React.Children.map(children, (child, index) => (
React.cloneElement(child as any, { width: '100%' })
))}
</div>
</div>
)
})
export {
Carousel as default,
CarouselItem
}
-58
View File
@@ -1,58 +0,0 @@
import { THEME_COLORS, type ThemeData } from "../Models/Theme";
type Props = {
themeData?: ThemeData | null
}
const ColorSchema: React.FC<{
themeData?: ThemeData | null
}> = ({
themeData
}) => {
themeData = themeData || THEME_COLORS.default
return (
<>
{themeData &&
<style global jsx>{`
:root {
--ls-colors-backdrop:${themeData.backdrop};
--ls-colors-logo: ${themeData.logo};
--ls-colors-primary: ${themeData.primary?.DEFAULT};
--ls-colors-primary-50: ${themeData.primary?.[50]};
--ls-colors-primary-100: ${themeData.primary?.[100]};
--ls-colors-primary-200: ${themeData.primary?.[200]};
--ls-colors-primary-300: ${themeData.primary?.[300]};
--ls-colors-primary-400: ${themeData.primary?.[400]};
--ls-colors-primary-500: ${themeData.primary?.[500]};
--ls-colors-primary-600: ${themeData.primary?.[600]};
--ls-colors-primary-700: ${themeData.primary?.[700]};
--ls-colors-primary-800: ${themeData.primary?.[800]};
--ls-colors-primary-900: ${themeData.primary?.[900]};
--ls-colors-actionButtonText: ${themeData.actionButtonText};
--ls-colors-text-placeholder: ${themeData.placeholderText};
--ls-colors-primary-text: ${themeData.primary?.text};
--ls-colors-primary-text-muted: ${themeData.primary?.textMuted};
--ls-colors-primary-logoColor: ${themeData.logo};
--ls-colors-secondary: ${themeData.secondary?.DEFAULT};
--ls-colors-secondary-50: ${themeData.secondary?.[50]};
--ls-colors-secondary-100: ${themeData.secondary?.[100]};
--ls-colors-secondary-200: ${themeData.secondary?.[200]};
--ls-colors-secondary-300: ${themeData.secondary?.[300]};
--ls-colors-secondary-400: ${themeData.secondary?.[400]};
--ls-colors-secondary-500: ${themeData.secondary?.[500]};
--ls-colors-secondary-600: ${themeData.secondary?.[600]};
--ls-colors-secondary-700: ${themeData.secondary?.[700]};
--ls-colors-secondary-800: ${themeData.secondary?.[800]};
--ls-colors-secondary-900: ${themeData.secondary?.[900]};
--ls-colors-secondary-950: ${themeData.secondary?.[950]};
--ls-colors-secondary-text: ${themeData.secondary?.text};
}
`}
</style>
}
</>
)
}
export default ColorSchema
@@ -1,19 +0,0 @@
const AverageCompletionTime: React.FC<{
minutes: number | undefined,
hours: number | undefined
}> = ({
minutes,
hours
}) => {
if (!minutes) {
return <span>~1-2 minutes</span>
}
if (minutes > 1 && minutes < 60) return <span><span>~</span><span>{minutes.toFixed()}</span> <span>minutes</span></span>
else if (minutes >= 60) return <span><span>~</span><span>{hours}</span> <span>{hours && hours > 1 ? 'hours' : 'hour'}</span> <span>{minutes > 0 ? ` ${minutes?.toFixed()} minutes` : ''}</span></span>
else return <span>~1-2 minutes</span>
}
export default AverageCompletionTime
@@ -1,12 +0,0 @@
const FormattedDate = ({ date }: { date: string }) => {
const swapDate = new Date(date);
const yyyy = swapDate.getFullYear();
let mm = swapDate.getMonth() + 1; // Months start at 0!
let dd = swapDate.getDate();
if (dd < 10) dd = 0 + dd;
if (mm < 10) mm = 0 + mm;
return <>{dd + '/' + mm + '/' + yyyy}</>;
}
export default FormattedDate
@@ -1,28 +0,0 @@
'use client'
import { type PropsWithChildren, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
const ReactPortal: React.FC<{
wrapperId: string
} & PropsWithChildren> = ({
children,
wrapperId = "react-portal-wrapper"
}) => {
const ref = useRef<Element | null>(null);
const [mounted, setMounted] = useState(false)
useEffect(() => {
let element = document.getElementById(wrapperId);
if (!element) {
element = document.createElement('div');
element.setAttribute("id", wrapperId);
document.body.appendChild(element);
}
ref.current = element
setMounted(true)
}, [wrapperId]);
return ref.current && mounted ? createPortal(children, ref.current) : null;
};
export default ReactPortal
@@ -1,33 +0,0 @@
import SubmitButton from './buttons/submitButton';
import { Link } from 'lucide-react';
const ConnectNetwork: React.FC<{
NetworkDisplayName: string;
AppURL: string;
}> = ({
NetworkDisplayName,
AppURL
}) => {
const connectButtonIcon = <Link className='h-5 w-5'></Link>
return (
<div className="relative inset-0 flex flex-col overflow-y-auto styled-scroll">
<div className="relative min-h-full items-center justify-center pt-0 text-center">
<h3 className='mb-4 pt-2 text-xl text-center md:text-left font-semibold'>
<p className='mb-10 pt-2 text-base text-center md:text-left font-light'>
{NetworkDisplayName} account with the provided address does not exist. To create one, go to {NetworkDisplayName} and connect your wallet.
</p>
</h3>
<div className="mt-3 sm:mt-6 text-sm">
<SubmitButton icon={connectButtonIcon} isDisabled={false} isSubmitting={false} onClick={() => window.open(AppURL, '_blank')}>
Connect
</SubmitButton>
</div>
</div>
</div>
)
}
export default ConnectNetwork;
@@ -1,270 +0,0 @@
'use client'
import { useState, type PropsWithChildren } from 'react'
import { ChevronDown, Plus } from 'lucide-react'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@hanzo/ui/primitives'
import WalletIcon from './icons/WalletIcon'
import shortenAddress from './utils/ShortenAddress'
import useWallet from '../hooks/useWallet'
import ConnectButton from './buttons/connectButton'
import AddressIcon from './AddressIcon'
import { type Wallet } from '../stores/walletStore'
import { cn } from '@hanzo/ui/util'
import { useEthersSigner } from '@/hooks/useEthersSigner'
import Image from 'next/image'
import { useSearchParams } from 'next/navigation'
import { useSettings } from '@/context/settings'
import type { CryptoNetwork } from '@/Models/CryptoNetwork'
const ConnectedWallets: React.FC<
{
connectButtonVariant?: 'outline' | 'primary'
showWalletIcon?: boolean
connectButtonClx?: string,
className?: string
} & PropsWithChildren
> = ({
children,
connectButtonVariant = 'outline',
showWalletIcon = true,
connectButtonClx = '',
className = ''
}) => {
const { wallets } = useWallet()
const [openDialog, setOpenDialog] = useState<boolean>(false)
//hooks
const { chainId, signer, address } = useEthersSigner()
const { networks } = useSettings()
console.log(wallets)
if (address && chainId) {
const network = networks.find(
(n: CryptoNetwork) => Number(n.chain_id) === chainId
)
return (
<div className={cn("flex gap-2 items-center", className)}>
<div className="flex gap-2 items-center">
<Image
src={
network?.logo ??
'https://cdn.lux.network/bridge/currencies/lux/lux.svg'
}
alt="Project Logo"
height="27"
width="27"
loading="eager"
className="rounded-full object-contain flex-none"
/>
<span className='hidden sm:flex'>{network && network.display_name}</span>
</div>
<div
onClick={() => setOpenDialog(true)}
className="flex gap-1 items-center cursor-pointer bg-level-2 py-[2px] px-2 sm:py-1 rounded-full text-xs sm:text-sm"
>
<WalletsIcons wallets={wallets} />
{shortenAddress(address)}
<ChevronDown className="h-5 w-5" aria-hidden="true" />
</div>
<ConnectedWalletsDialog
openDialog={openDialog}
setOpenDialog={setOpenDialog}
/>
</div>
)
}
if (wallets.length > 0) {
return (
<>
<Button
variant="outline"
size="square"
onClick={() => setOpenDialog(true)}
aria-label="Connect wallet"
className="text-muted-2 p-0 flex items-center justify-center"
>
<WalletsIcons wallets={wallets} />
</Button>
<ConnectedWalletsDialog
openDialog={openDialog}
setOpenDialog={setOpenDialog}
/>
</>
)
}
return (
<ConnectButton>
<button
aria-label="Connect wallet"
className={cn(
'flex items-center justify-center text-muted-2 p-0 py-[6px] border border-muted-4 hover:border-muted-1',
connectButtonClx
)}
>
{showWalletIcon && (
<WalletIcon className="h-5 w-5 mx-0.5" strokeWidth="1.5" />
)}
{children}
</button>
</ConnectButton>
)
}
const WalletsIcons = ({ wallets }: { wallets: Wallet[] }) => {
const firstWallet = wallets[0]
const secondWallet = wallets[1]
return (
<div className="-space-x-2 flex">
{firstWallet?.connector && (
<firstWallet.icon className="flex-shrink-0 h-6 w-6" />
)}
{secondWallet?.connector && (
<secondWallet.icon className="flex-shrink-0 h-6 w-6" />
)}
{wallets.length > 2 && (
<div className="h-6 w-6 flex-shrink-0 rounded-full justify-center p-1 overlfow-hidden text-xs">
<span>
<span>+</span>
{wallets.length - 2}
</span>
</div>
)}
</div>
)
}
const WalletsMenu = () => {
const [openDialog, setOpenDialog] = useState<boolean>(false)
const { wallets } = useWallet()
const wallet = wallets[0]
if (wallets.length > 0) {
return (
<>
<button
onClick={() => {
setOpenDialog(true)
}}
type="button"
className={
'py-3 px-4 relative flex items-center w-full rounded-md space-x-1 ' +
'bg-level-1 hover:bg-level-2 font-semibold ' +
'transform transition duration-200 ease-in-out'
}
>
{wallets.length === 1 ? (
<div className="flex gap-4 items-start">
<div className="inline-flex items-center relative">
<AddressIcon address={wallet.address} size={20} />
{wallet.connector && (
<span className="absolute -bottom-1 -right-2 ml-1 text-[10px] leading-4 font-semibold">
<wallet.icon className="w-4 h-4 border-2 rounded-full" />
</span>
)}
</div>
<p>{shortenAddress(wallet.address)}</p>
</div>
) : (
<>
<div className="flex justify-center w-full">
Connected wallets
</div>
<div className="place-items-end absolute left-2.5">
<WalletsIcons wallets={wallets} />
</div>
</>
)}
</button>
<ConnectedWalletsDialog
openDialog={openDialog}
setOpenDialog={setOpenDialog}
/>
</>
)
}
return (
<ConnectButton>
<Button
className="border-none !px-4 flex justify-center gap-2"
type="button"
>
<WalletIcon className="h-5 w-5" strokeWidth={2} />
<span>Connect a wallet</span>
</Button>
</ConnectButton>
)
}
const ConnectedWalletsDialog = ({
openDialog,
setOpenDialog,
}: {
openDialog: boolean
setOpenDialog: (open: boolean) => void
}) => {
const { wallets, disconnectWallet } = useWallet()
return (
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-center">Wallets</DialogTitle>
</DialogHeader>
<div className="flex flex-col justify-start space-y-2">
{wallets.map((wallet, index) => (
<div
key={index}
className="w-full relative items-center justify-between gap-2 flex rounded-md outline-none bg-level-1 p-3 border border-[#404040] "
>
<div className="flex space-x-4 items-center">
{wallet.connector && (
<div className="inline-flex items-center relative">
<wallet.icon className="w-8 h-8 p-0.5" />
</div>
)}
<p>{shortenAddress(wallet.address)}</p>
</div>
<button
onClick={() => {
disconnectWallet(wallet.providerName)
if (wallets.length === 1) {
setOpenDialog(false)
}
}}
className="p-1 hover:bg-level-2 text-xs hover:opacity-75"
>
Disconnect
</button>
</div>
))}
</div>
<DialogFooter>
<ConnectButton
onClose={() => {
setOpenDialog(false)
}}
>
<div className="hover:text-opacity-80 flex items-center gap-1 justify-end w-fit">
<Plus className="h-4 w-4" />
<span className="text-sm">Link a new wallet</span>
</div>
</ConnectButton>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export { ConnectedWalletsDialog as default, WalletsMenu, ConnectedWallets }
@@ -1,19 +0,0 @@
'use client'
import { useIntercom } from "react-use-intercom"
import { useAuthState } from "../context/authContext"
const ContactSupport: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { email, userId } = useAuthState()
const { boot, show, update } = useIntercom()
const updateWithProps = () => update({ email: email, userId: userId })
return <span onClick={() => {
boot();
show();
updateWithProps()
}}>
{children}
</span>
}
export default ContactSupport
-100
View File
@@ -1,100 +0,0 @@
'use client'
import { ErrorBoundary } from 'react-error-boundary'
import * as Sentry from '@sentry/nextjs'
import { TooltipProvider } from '@hanzo/ui/primitives'
import ErrorFallback from '@/components/ErrorFallback'
import ColorSchema from '@/components/ColorSchema'
import TonConnectProvider from './TonConnectProvider'
import RainbowKit from '@/components/RainbowKit'
import Solana from '@/components/SolanaProvider'
import { BridgeAppSettings } from '@/Models/BridgeAppSettings'
import { SendErrorMessage } from '@/lib/telegram'
import { THEME_COLORS, type ThemeData } from '../Models/Theme'
import { AuthProvider } from '@/context/authContext'
import { SettingsProvider } from '@/context/settings'
import { TenantProvider } from '@/context/tenantContext'
import { FeeProvider } from '@/context/feeContext'
import { JotaiProvider } from '@/context/jotaiContext'
import { EthersProvider } from '@/context/ethersContext'
import { type BridgeSettings } from '@/Models/BridgeSettings'
import { IntercomProvider } from 'react-use-intercom'
import { SWRConfig } from 'swr'
import type { ErrorInfo, PropsWithChildren } from 'react'
const INTERCOM_APP_ID = 'o1kmvctg'
import { ToastProvider } from '@/context/toast-provider'
import '@rainbow-me/rainbowkit/styles.css'
const Contexts: React.FC<
{
settings: BridgeSettings
} & PropsWithChildren
> = ({ settings, children }) => {
// :aa These were was all that getServerSideProps() ever returned.
// So seems clearest to just do it this way.
const _settings = settings ?? {
networks: [],
exchanges: [],
sourceRoutes: [],
destinationRoutes: [],
}
const themeData = THEME_COLORS.default // :aa TODO
function logErrorToService(error: Error, info: ErrorInfo) {
const transaction = Sentry.startTransaction({
name: 'error_boundary_handler',
})
Sentry.configureScope((scope) => {
scope.setSpan(transaction)
})
if (
process.env.NEXT_PUBLIC_VERCEL_ENV &&
!error.stack?.includes('chrome-extension')
) {
SendErrorMessage(
'UI error',
`env: ${process.env.NEXT_PUBLIC_VERCEL_ENV} %0A url: ${process.env.NEXT_PUBLIC_VERCEL_URL} %0A message: ${error?.message} %0A errorInfo: ${info?.componentStack} %0A stack: ${error?.stack ?? error.stack} %0A`
)
}
Sentry.captureException(error, { data: info })
transaction?.finish()
}
return (
<SWRConfig value={{ revalidateOnFocus: false }}>
<TenantProvider>
<IntercomProvider appId={INTERCOM_APP_ID} initializeDelay={2500}>
{themeData && <ColorSchema themeData={themeData} />}
<SettingsProvider settings={new BridgeAppSettings(_settings)}>
<AuthProvider>
<TooltipProvider delayDuration={500}>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={logErrorToService}
>
<ToastProvider>
<JotaiProvider>
<TonConnectProvider basePath={''} themeData={themeData}>
<RainbowKit>
<EthersProvider>
<Solana>
<FeeProvider>{children}</FeeProvider>
</Solana>
</EthersProvider>
</RainbowKit>
</TonConnectProvider>
</JotaiProvider>
</ToastProvider>
</ErrorBoundary>
</TooltipProvider>
</AuthProvider>
</SettingsProvider>
</IntercomProvider>
</TenantProvider>
</SWRConfig>
)
}
export default Contexts
@@ -1,3 +0,0 @@
export interface SwapConfirmationFormValues {
RightWallet: boolean;
}
@@ -1,16 +0,0 @@
import { type CryptoNetwork, type NetworkCurrency } from "@/Models/CryptoNetwork";
import { type Exchange } from "@/Models/Exchange";
import { type AssetGroup } from "../Input/CEXCurrencyFormField";
export type SwapFormValues = {
amount?: string;
destination_address?: string;
fromCurrency?: NetworkCurrency;
toCurrency?: NetworkCurrency;
refuel?: boolean;
from?: CryptoNetwork;
to?: CryptoNetwork;
fromExchange?: Exchange,
toExchange?: Exchange,
currencyGroup?: AssetGroup
}
@@ -1,97 +0,0 @@
'use client'
import React from 'react'
import useSWR from "swr"
import Image from 'next/image';
import { motion } from "framer-motion"
import BridgeApiClient, { type Campaign } from "@/lib/BridgeApiClient"
import { ApiResponse } from "@/Models/ApiResponse"
import { useSettings } from "@/context/settings"
import { truncateDecimals } from "../../utils/RoundDecimals"
import { type CryptoNetwork, type NetworkCurrency } from "@/Models/CryptoNetwork"
import ClickTooltip from "../../Tooltips/ClickTooltip"
const CampaignComponent: React.FC<{
destination: CryptoNetwork,
fee: number | undefined,
selected_currency: NetworkCurrency,
}> = ({
destination,
fee,
selected_currency
}) => {
const apiClient = new BridgeApiClient()
const { data: campaignsData } = useSWR<ApiResponse<Campaign[]>>('/campaigns', apiClient.fetcher)
const now = new Date().getTime()
const campaign = campaignsData
?.data
?.find(c =>
c?.network === destination?.internal_name
&& c.status == 'active'
&& new Date(c?.end_date).getTime() - now > 0)
if (!campaign || !fee)
return <></>
return <CampaignDisplay
campaign={campaign}
fee={fee}
selected_currency={selected_currency}
/>
}
const CampaignDisplay: React.FC<{
campaign: Campaign,
fee: number,
selected_currency: NetworkCurrency,
}> = ({
campaign,
fee,
selected_currency
}) => {
const { networks } = useSettings()
const network = networks.find(l => l.internal_name === campaign.network)
const campaignAsset = network?.currencies.find(c => c?.asset === campaign?.asset)
const feeinUsd = fee * selected_currency.price_in_usd
const reward = truncateDecimals(((feeinUsd * (campaign?.percentage || 0) / 100) / (campaignAsset?.price_in_usd || 1)), (campaignAsset?.precision || 0))
return <motion.div
initial={{ y: "-100%" }}
animate={{
y: 0,
transition: { duration: 0.3, ease: [0.36, 0.66, 0.04, 1] },
}}
exit={{
y: "-100%",
transition: { duration: 0.4, ease: [0.36, 0.66, 0.04, 1] },
}}
className='w-full flex items-center justify-between rounded-b-lg bg-level-1 relative bottom-2 z-0 pt-4 pb-2 px-3.5 text-right'>
<div className='flex items-center'>
<p>Est. {campaignAsset?.asset} Reward</p>
<ClickTooltip text={<span><span>The amount of onboarding reward that you’ll earn.&nbsp;</span><a target='_blank' href='/campaigns' className='text-primary underline hover:no-underline decoration-primary cursor-pointer'>Learn more</a></span>} />
</div>
{
Number(reward) > 0 &&
<div className="flex items-center space-x-1">
<span>+</span>
<div className="h-5 w-5 relative">
<Image
src={network?.logo ?? ''}
alt="Project Logo"
height="40"
width="40"
loading="eager"
className="rounded-md object-contain" />
</div>
<p>
{reward} {campaignAsset?.asset}
</p>
</div>
}
</motion.div>
}
export default CampaignComponent
@@ -1,99 +0,0 @@
'use client'
import { Clock9, Fuel } from "lucide-react";
import { useFormikContext } from "formik";
import { type CryptoNetwork, type NetworkCurrency } from "@/Models/CryptoNetwork";
import { truncateDecimals } from "../../utils/RoundDecimals";
import AverageCompletionTime from "../../Common/AverageCompletionTime";
import { useBalancesState } from "@/context/balances";
import { type SwapFormValues } from "../../DTOs/SwapFormValues";
import { type Fee, useFee } from "@/context/feeContext";
const DetailedEstimates: React.FC<{
networks: CryptoNetwork[]
source?: CryptoNetwork | null,
destination?: CryptoNetwork | null,
selected_currency?: NetworkCurrency | null,
}> = ({
source,
destination,
selected_currency
}) => {
const { values } = useFormikContext<SwapFormValues>();
const { fromCurrency } = values;
const { fee, isFeeLoading } = useFee()
const parsedFee = fee && parseFloat(Number(fee.walletFee).toFixed(fromCurrency?.precision))
const currencyName = fromCurrency?.asset || " "
return (
<div className="flex justify-between w-full items-center">
<div className="flex flex-row items-baseline justify-between gap-1 pr-1 text-muted-1">
<label className="inline-flex items-center text-left">
Fee:
</label>
<div className="text-right ">
{isFeeLoading ? (
<div className='h-[10px] w-10 inline-flex bg-gray-500 rounded-sm animate-pulse' />
) : (
<span>{parsedFee || 0}</span>
)}
<span>{currencyName}</span>
</div>
</div>
{source && selected_currency && (
<NetworkGas network={source} selected_currency={selected_currency} />
)}
<EstimatedArrival currency={selected_currency} destination={destination} fee={fee} />
</div>
)
}
const NetworkGas: React.FC<{
network: CryptoNetwork,
selected_currency: NetworkCurrency,
}> = ({
selected_currency,
network
}) => {
const { gases, isGasLoading } = useBalancesState()
const networkGas = network.internal_name ?
gases?.[network.internal_name]?.find(g => g?.token === selected_currency.asset)?.gas : null
if (!networkGas) return null
const source_native_currnecy = network.currencies.find(a => a.is_native)
const estimatedGas = (networkGas && source_native_currnecy) ?
truncateDecimals(networkGas, source_native_currnecy?.precision)
: truncateDecimals(networkGas, selected_currency?.precision)
return (
<div className="flex flex-row items-center gap-2 w-fit px-1">
<Fuel className="h-4 w-4" />
<div className="text-right flex items-center gap-1">
{isGasLoading ? <div className='h-[10px] w-10 bg-gray-500 rounded-sm animate-pulse' /> : estimatedGas} <span>{network?.currencies.find(a => a.is_native)?.asset ?? selected_currency.asset}</span>
</div>
</div>
)
}
const EstimatedArrival: React.FC<{
destination?: CryptoNetwork | null,
currency?: NetworkCurrency | null,
fee: Fee
}> = ({
fee
}) => (
<div className="flex flex-row items-center gap-2 w-fit pl-1 text-muted-1">
<Clock9 className="h-4 w-4" />
<span className="text-right">
<AverageCompletionTime hours={fee?.avgCompletionTime?.total_hours} minutes={fee?.avgCompletionTime?.total_minutes} />
</span>
</div>
)
export default DetailedEstimates
@@ -1,26 +0,0 @@
import type { PropsWithChildren } from "react"
const FeeDetails = ({ children }: PropsWithChildren) => (
<div className="flex flex-col divide-y-2 divide-level-2 rounded-lg bg-level-1 overflow-hidden text-sm">
{children}
</div>
)
const Item: React.FC<{
onClick?: React.MouseEventHandler<HTMLButtonElement>
icon?: JSX.Element
} & PropsWithChildren> = ({
children,
icon
}) => (
<div className='gap-4 flex relative items-center outline-none w-full px-4 py-3'>
{icon && (
<div>{icon}</div>
)}
{children}
</div>
)
FeeDetails.Item = Item
export default FeeDetails
@@ -1,84 +0,0 @@
import { GetDefaultAsset } from "@/util/settingsHelper";
import { CaluclateRefuelAmount } from "@/lib/fees";
import { truncateDecimals } from "../../utils/RoundDecimals";
import { type CryptoNetwork, type NetworkCurrency } from "@/Models/CryptoNetwork";
const ReceiveAmounts: React.FC<{
receive_amount?: number;
currency?: NetworkCurrency | null;
to: CryptoNetwork | undefined | null;
refuel: boolean
}> = ({
receive_amount,
currency,
to,
refuel
}) => {
const parsedReceiveAmount = parseFloat(receive_amount?.toFixed(currency?.precision) || "")
const destinationNetworkCurrency = (to && currency) ? GetDefaultAsset(to, currency.asset) : null
return <>
<span className="md:font-semibold text-sm md:text-base leading-8 md:leading-8 flex-1">
You will receive
</span>
<div className='flex items-center space-x-2'>
<span className="text-sm md:text-base">
{
parsedReceiveAmount > 0 ?
<div className="font-semibold md:font-bold text-right leading-4">
<p>
<>{parsedReceiveAmount}</>
&nbsp;
<span>
{destinationNetworkCurrency?.asset}
</span>
</p>
{refuel && <Refuel
currency={currency}
to={to}
refuel={refuel}
/>}
</div>
: '-'
}
</span>
</div>
</>
}
const Refuel: React.FC<{
currency?: NetworkCurrency | null;
to?: CryptoNetwork | null;
refuel: boolean
}> = ({
to,
currency,
refuel
}) => {
const destination_native_asset = to?.currencies.find(c => c.asset === to.native_currency)
const refuelCalculations = CaluclateRefuelAmount({
refuelEnabled: refuel,
currency,
to
})
const { refuelAmountInNativeCurrency } = refuelCalculations
const truncated_refuel = truncateDecimals(refuelAmountInNativeCurrency, destination_native_asset?.precision)
return <>
{
truncated_refuel > 0 ? <p className='text-[12px] text-muted-3'>
<>+</> <span>{truncated_refuel} {destination_native_asset?.asset}</span>
</p>
: null
}
</>
}
export {
ReceiveAmounts as default,
Refuel
}
@@ -1,33 +0,0 @@
'use client'
import { useFormikContext } from "formik";
import ClickTooltip from "../../Tooltips/ClickTooltip"
import ToggleButton from "../../buttons/toggleButton"
import { type SwapFormValues } from "../../DTOs/SwapFormValues";
const RefuelToggle = () => {
const {
values,
setFieldValue
} = useFormikContext<SwapFormValues>();
const { to: destination } = values
const destination_native_currency = destination?.currencies.find(c => c.asset === destination.native_currency)?.asset
const handleConfirmToggleChange = (value: boolean) => {
setFieldValue('refuel', value)
}
return (
<div className="flex items-center justify-between">
<div className="font- flex items-center -placeholder text-sm">
<span>Refuel</span>
<ClickTooltip text={`You will get a small amount of ${destination_native_currency} that you can use to pay for gas fees.`} />
</div>
<ToggleButton name="refuel" value={!!values?.refuel} onChange={handleConfirmToggleChange} />
</div>
)
}
export default RefuelToggle
@@ -1,69 +0,0 @@
'use client'
import { type SwapFormValues } from '../../DTOs/SwapFormValues';
import ReceiveAmounts from './ReceiveAmounts';
import DetailedEstimates from './DetailedEstimates';
import { useFee } from '@/context/feeContext';
import { useSettings } from '@/context/settings';
import RefuelToggle from './Refuel';
//import CEXNetworkFormField from '../../Input/CEXNetworkFormField';
import FeeDetails from './FeeDetailsComponent';
import { useQueryState } from '@/context/query';
import CampaignComponent from './Campaign';
import type React from 'react'
const FeeDetailsComponent: React.FC<{ values: SwapFormValues }> = ({
values
}) => {
const { toCurrency, from, to, refuel, fromExchange, toExchange } = values || {};
const { fee } = useFee()
const currency = toCurrency
const { networks } = useSettings()
const query = useQueryState();
return (<>
<FeeDetails>
{/* {
((fromExchange || toExchange) && (from || to)) &&
<FeeDetails.Item>
<CEXNetworkFormField direction={fromExchange ? 'from' : 'to'} />
</FeeDetails.Item>
} */}
{to && toCurrency && to.currencies.find(a => a.asset === toCurrency.asset)?.is_refuel_enabled && !query?.hideRefuel &&
<FeeDetails.Item>
<RefuelToggle />
</FeeDetails.Item>
}
{from && to &&
<FeeDetails.Item>
<DetailedEstimates
networks={networks}
selected_currency={currency}
source={from}
destination={to}
/>
</FeeDetails.Item>
}
<FeeDetails.Item>
<ReceiveAmounts
currency={currency}
to={to}
receive_amount={fee.walletReceiveAmount}
refuel={!!refuel}
/>
</FeeDetails.Item>
</FeeDetails>
{values.to && values.toCurrency && (
<CampaignComponent
destination={values.to}
selected_currency={values.toCurrency}
fee={fee.walletFee}
/>
)}
</>)
}
export default FeeDetailsComponent
@@ -1,65 +0,0 @@
import { Home, RefreshCcw } from "lucide-react";
import { Button } from '@hanzo/ui/primitives'
import ContactSupport from "./ContactSupport"
import MessageComponent from "./MessageComponent"
import Main from './main'
import { useGoHome } from "@/hooks/useGoHome"
export default function ErrorFallback({ error, resetErrorBoundary }: {error: any, resetErrorBoundary: () => void}) {
const extension_error = error.stack?.includes?.("chrome-extension", "app://")
const goHome = useGoHome()
return (
<Main className='mb-20'>
<MessageComponent>
<MessageComponent.Content icon="red">
<MessageComponent.Header>
Unable to complete the request
</MessageComponent.Header>
<MessageComponent.Description className='max-w-lg flex flex-col justify-between items-center mt-4'>
<p className='text-center'>
<span>Sorry, but we were unable to complete this request.&nbsp;</span>
{extension_error ?
<span>It seems that some of your extensions are preventing the app from running.</span>
:
<span>Are team as been informed, and will investigate the issue.</span>
}
</p>
<p className='text-center'>
{extension_error ?
<span>Please disable extensions and try again or open in incognito mode. If the issue keeps happening,&nbsp;</span>
:
<span>Please try again. If the issue keeps happening,&nbsp;</span>
}
<span className="underline cursor-pointer "><ContactSupport>contact our support team.</ContactSupport></span>
</p>
</MessageComponent.Description>
</MessageComponent.Content>
<MessageComponent.Buttons className="flex flex-row gap-4">
<Button
className="flex gap-2"
variant="outline"
onClick={goHome}
>
<Home className="h-5 w-5" aria-hidden="true" />
<span>Go home</span>
</Button>
<Button
className="flex gap-2"
variant='primary'
onClick={resetErrorBoundary}
>
<RefreshCcw className="h-5 w-5" aria-hidden="true" />
<span>Try Again</span>
</Button>
</MessageComponent.Buttons>
</MessageComponent>
<div id="modal_portal_root" />
</Main>
)
}
@@ -1,75 +0,0 @@
'use client'
import type React from 'react'
import { useIntercom } from 'react-use-intercom'
import { useAuthState } from '../../context/authContext'
import IconButton from '../buttons/iconButton'
import ToggleButton from '../buttons/toggleButton'
import GoHomeButton from '../utils/GoHome'
import ChatIcon from '../icons/ChatIcon'
import dynamic from 'next/dynamic'
import BridgeMenu from '../BridgeMenu'
import { cn } from '@hanzo/ui/util'
import { ArrowLeft } from 'lucide-react'
import { Button } from '@hanzo/ui/primitives'
//hooks
import { usePathname } from 'next/navigation'
import { useAtom } from 'jotai'
import { useTelepoterAtom } from '@/store/teleport'
const HeaderWithMenu: React.FC<{
goBack: (() => void) | undefined
}> = ({ goBack }) => {
// hooks
const { email, userId } = useAuthState()
const { boot, show, update } = useIntercom()
const pathname = usePathname()
const updateWithProps = () => {
update({ email: email, userId: userId })
}
const [useTeleporter, setUseTeleporter] = useAtom(useTelepoterAtom)
const handleBridgeTypeChange = (value: boolean) => {
setUseTeleporter(value)
}
const ChatButton: React.FC = () => (
<Button
variant="outline"
size="square"
onClick={() => {
boot()
show()
updateWithProps()
}}
aria-label="Open chat"
className="text-muted-2 hidden md:inline-block"
>
<ChatIcon className="h-6 w-6" strokeWidth="1.5" />
</Button>
)
return (
<div className="w-full xs:flex md:grid md:grid-cols-5 px-3 md:px-6 mt-3 items-center xs:justify-between">
<div>
{goBack ? (
<IconButton onClick={goBack} aria-label="Go back" icon={<ArrowLeft strokeWidth="3" />} />
) : (
<div className={cn('flex items-center gap-2', pathname !== '/' ? 'hidden' : '')}>
<ToggleButton value={useTeleporter} onChange={handleBridgeTypeChange} name="Teleport" />
Teleport
</div>
)}
</div>
<div className="col-start-5 justify-self-end self-center flex items-center gap-3">
{/* <ConnectWallets /> */}
{/* <ChatButton /> */}
<BridgeMenu />
</div>
</div>
)
}
export default HeaderWithMenu
-339
View File
@@ -1,339 +0,0 @@
'use client'
import { type ChangeEvent, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
import Image from 'next/image';
import { useFormikContext } from "formik";
import { Info } from "lucide-react";
import { RadioGroup } from "@headlessui/react";
import { type AddressBookItem } from "@/lib/BridgeApiClient";
import { type SwapFormValues } from "../DTOs/SwapFormValues";
import { useSwapDataUpdate } from "@/context/swap";
import KnownInternalNames from "@/lib/knownIds";
import { useSettings } from "@/context/settings";
import { isValidAddress } from "@/lib/addressValidator";
import { type Partner } from "@/Models/Partner";
import shortenAddress from "../utils/ShortenAddress";
import AddressIcon from "../AddressIcon";
import WalletIcon from "../icons/WalletIcon";
import useWallet from "@/hooks/useWallet";
import { cn } from "@hanzo/ui/util"
interface Input extends Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'as' | 'onChange'> {
hideLabel?: boolean;
disabled: boolean;
name: string;
children?: JSX.Element | JSX.Element[];
ref?: any;
close: () => void,
isPartnerWallet: boolean,
partnerImage?: string,
partner?: Partner,
canFocus?: boolean,
address_book?: AddressBookItem[]
}
const Address: React.FC<Input> = forwardRef<HTMLInputElement, Input>(function Address
({ name, canFocus, close, address_book, disabled, isPartnerWallet, partnerImage, partner }, ref) {
const {
values,
setFieldValue
} = useFormikContext<SwapFormValues>();
const [wrongNetwork, setWrongNetwork] = useState(false)
const inputReference = useRef<HTMLInputElement>(null);
const destination = values.to
const valid_addresses = address_book?.filter(a => a.networks?.some(n => destination?.internal_name === n) && isValidAddress(a.address, destination)) || []
const { setDepositeAddressIsfromAccount, setAddressConfirmed } = useSwapDataUpdate()
const placeholder = "Enter your address here"
const [inputValue, setInputValue] = useState<string | undefined>(values?.destination_address || "")
const [validInputAddress, setValidInputAddress] = useState<string | undefined>('')
const destinationIsStarknet = destination?.internal_name === KnownInternalNames.Networks.StarkNetGoerli
|| destination?.internal_name === KnownInternalNames.Networks.StarkNetMainnet
const { connectWallet, disconnectWallet, getAutofillProvider: getProvider } = useWallet()
const provider = useMemo(() => {
return values?.to && getProvider(values?.to)
}, [values?.to, getProvider])
const connectedWallet = provider?.getConnectedWallet()
const settings = useSettings()
useEffect(() => {
if (destination && isValidAddress(connectedWallet?.address, destination) && !values?.destination_address) {
//TODO move to wallet implementation
if (connectedWallet
&& connectedWallet.providerName === 'starknet'
&& (connectedWallet.chainId != destinationChainId)
&& destination) {
(async () => {
setWrongNetwork(true)
await disconnectWallet(connectedWallet.providerName)
})()
return
}
setInputValue(connectedWallet?.address)
setAddressConfirmed(true)
setFieldValue("destination_address", connectedWallet?.address)
}
}, [connectedWallet?.address, destination])
useEffect(() => {
if (canFocus) {
inputReference?.current?.focus()
}
}, [canFocus])
useEffect(() => {
values.destination_address && setInputValue(values.destination_address)
}, [values.destination_address])
const handleRemoveDepositeAddress = useCallback(async () => {
setDepositeAddressIsfromAccount(false)
setFieldValue("destination_address", '')
setInputValue("")
}, [setDepositeAddressIsfromAccount, setFieldValue, values.to])
const handleSelectAddress = useCallback((value: string) => {
setAddressConfirmed(true)
setFieldValue("destination_address", value)
close()
}, [close, setAddressConfirmed, setFieldValue])
const inputAddressIsValid = isValidAddress(inputValue, destination)
let errorMessage = '';
if (inputValue && !isValidAddress(inputValue, destination)) {
errorMessage = `Enter a valid ${values.to?.display_name} address`
}
const handleInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value)
setAddressConfirmed(false)
}, [])
useEffect(() => {
if (inputAddressIsValid) {
setValidInputAddress(inputValue)
}
}, [inputValue, inputAddressIsValid])
const handleSetNewAddress = useCallback(() => {
setAddressConfirmed(true)
setFieldValue("destination_address", validInputAddress)
close()
}, [validInputAddress])
const destinationAsset = values.toCurrency
const destinationChainId = values?.to?.chain_id
return (
<div className='w-full flex flex-col justify-between h-full '>
<div className='flex flex-col self-center grow w-full'>
<div className='flex flex-col self-center grow w-full space-y-3'>
<div className="text-left">
<label htmlFor={name}>Address</label>
{isPartnerWallet && partner && <span className='truncate text-sm text-indigo-200'> ({partner?.display_name})</span>}
<div className="flex flex-wrap flex-col md:flex-row">
<div className="relative flex grow rounded-lg shadow-sm mt-1.5 bg-level-1 border-[#404040] border focus-within:ring-0 focus-within:ring-foreground focus-within:border-foreground">
{isPartnerWallet &&
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
{
partnerImage &&
<Image alt="Partner logo" className='rounded-md object-contain' src={partnerImage} width="24" height="24"></Image>
}
</div>
}
<input
onChange={handleInputChange}
value={inputValue}
placeholder={placeholder}
autoCorrect="off"
type={"text"}
disabled={disabled || !!(connectedWallet && values.destination_address)}
name={name}
id={name}
ref={inputReference}
tabIndex={0}
className={(isPartnerWallet ? 'pl-11' : '') +
' disabled:cursor-not-allowed grow h-12 border-none leading-4 block font-semibold w-full bg-level-1 rounded-lg truncate hover:overflow-x-scroll focus:ring-0 focus:outline-none'}
/>
{
inputValue && !disabled &&
<span className="inline-flex items-center mr-2">
<div className="text-xs flex items-center space-x-2 md:ml-5 bg-level-2 rounded-md ">
<button
type="button"
className="p-0.5 duration-200 transition hover:bg-level-3 rounded-md"
onClick={handleRemoveDepositeAddress}
>
<div className="flex items-center px-2 text-sm py-1 font-semibold">
Clear
</div>
</button>
</div>
</span>
}
</div>
{errorMessage &&
<div className="basis-full text-xs text-primary">
{errorMessage}
</div>
}
{wrongNetwork && !inputValue &&
<div className="basis-full text-xs text-primary">
{
destination?.internal_name === KnownInternalNames.Networks.StarkNetMainnet
? <span>Please switch to Starknet Mainnet with your wallet and click Autofill again</span>
: <span>Please switch to Starknet Goerli with your wallet and click Autofill again</span>
}
</div>
}
</div>
</div>
{
validInputAddress &&
<div onClick={handleSetNewAddress} className='text-left min-h-12 cursor-pointer space-x-2 border border-muted bg-level-1 shadow-xl flex text-sm rounded-md items-center w-full transform hover:bg-level-2 transition duration-200 px-2 py-2 hover:border-foreground hover:shadow-xl'>
<div className='flex bg-level-3 flex-row items-left rounded-md p-2'>
{
destinationIsStarknet && connectedWallet ?
<connectedWallet.icon className='rounded-md' alt={connectedWallet?.address} width={25} height={25} />
:
<AddressIcon address={validInputAddress} size={25} />
}
</div>
<div className="flex flex-col grow">
<div className="text-md font-medium ">
{shortenAddress(validInputAddress)}
</div>
</div>
<div className='flex flex-row items-left px-2 py-1 rounded-md'>
Select
</div>
</div>
}
{
!disabled
&& !inputValue
&& destination
&& provider
&& !connectedWallet &&
<div onClick={() => { connectWallet(provider.name) }} className='min-h-12 text-left cursor-pointer space-x-2 bg-level-1 flex text-sm rounded-md items-center w-full transform transition duration-200 px-2 py-1.5 hover:bg-level-2 hover:shadow-xl'>
<div className='flexflex-row items-left bg-level-3 px-2 py-1 rounded-md'>
<WalletIcon className="w-6 h-6" />
</div>
<div className="flex flex-col">
<div className="block text-sm font-medium">
Autofill from wallet
</div>
<div className="text-gray-500">
Connect your wallet to fetch the address
</div>
</div>
</div>
}
{
!inputAddressIsValid
&& destinationAsset
&& values.toExchange
&&
<div className='text-left p-4 bg-level-1 rounded-lg border border-[#404040]'>
<div className="flex items-center">
<Info className='h-5 w-5 mr-3' />
<label className="block text-sm md:text-base font-medium leading-6">How to find your {values.toExchange.display_name} deposit address</label>
</div>
<ul className="list-disc font-light space-y-1 text-xs md:text-sm mt-2 ml-8">
<li>Go to the Deposits page</li>
<li>
<span>Select</span>
<span className="inline-block mx-1">
<span className='flex gap-1 items-baseline text-sm '>
<Image src={destinationAsset.logo ?? ''}
alt="Project Logo"
height="15"
width="15"
className='rounded-sm'
/>
<span>{destinationAsset.asset}</span>
</span>
</span>
<span>as asset</span>
</li>
<li>
<span>Select</span>
<span className="inline-block mx-1">
<span className='flex gap-1 items-baseline text-sm '>
<Image src={values?.toExchange?.img_url ?? ''}
alt="Project Logo"
height="15"
width="15"
className='rounded-sm'
/>
<span >{destination?.display_name}</span>
</span>
</span>
<span>as network</span>
</li>
</ul>
</div>
}
{
!disabled && valid_addresses?.length > 0 && !inputValue &&
<div className="text-left">
<label className="">Recently used</label>
<RadioGroup disabled={disabled} value={values.destination_address} onChange={handleSelectAddress}>
<div className="rounded-md overflow-y-auto styled-scroll max-h-[300px]">
{valid_addresses?.map((a) => (
<RadioGroup.Option
key={a.address}
value={a.address}
disabled={disabled}
className={({ disabled }) => (
cn(
disabled ? ' cursor-not-allowed ' : ' cursor-pointer ',
'relative flex focus:outline-none mt-2 mb-3 '
)
)}
>
{({ checked }) => {
const difference_in_days = Math.round(Math.abs(((new Date()).getTime() - new Date(a.date).getTime()) / (1000 * 3600 * 24)))
return (
<RadioGroup.Description
as="span"
className={`space-x-2 flex text-sm rounded-md items-center w-full transform transition duration-200 px-2 py-1.5 border border-[#404040] hover:border-[#404040] hover:bg-level-1 hover:shadow-xl ${checked && 'border-muted-3'}`}
>
<div className='flex bg-level-3 flex-row items-left rounded-md p-2'>
<AddressIcon address={a.address} size={20} />
</div>
<div className="flex flex-col">
<div className="block text-sm font-medium">
{shortenAddress(a.address)}
</div>
<div className="text-gray-500">
{
difference_in_days === 0 ?
<>Used today</>
:
(difference_in_days > 1 ?
<>Used {difference_in_days} days ago</>
: <>Used yesterday</>)
}
</div>
</div>
</RadioGroup.Description>
)
}}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>
}
</div>
</div>
</div>
)
})
export default Address
-167
View File
@@ -1,167 +0,0 @@
'use client'
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFormikContext } from "formik";
import { type SwapFormValues } from "../DTOs/SwapFormValues";
import NumericInput from "./NumericInput";
import SecondaryButton from "../buttons/secondaryButton";
import { useBalancesState, useBalancesUpdate } from "@/context/balances";
import { truncateDecimals } from "../utils/RoundDecimals";
import { useFee } from "@/context/feeContext";
import useWallet from "@/hooks/useWallet";
import useBalance from "@/hooks/useBalance";
const AmountField = forwardRef(function AmountField(_, ref: any) {
const { values, setFieldValue, handleChange } = useFormikContext<SwapFormValues>();
const [requestedAmountInUsd, setRequestedAmountInUsd] = useState<string>();
const { fromCurrency, from, to, destination_address, toCurrency, currencyGroup } = values || {};
const { minAllowedAmount, maxAllowedAmount: maxAmountFromApi } = useFee()
const [isAmountVisible, setIsAmountVisible] = useState(false);
const { balances, isBalanceLoading, gases, isGasLoading } = useBalancesState()
const { getAutofillProvider: getProvider } = useWallet()
const provider = useMemo(() => {
return values.from && getProvider(values.from)
}, [values.from, getProvider])
const wallet = provider?.getConnectedWallet()
const gasAmount = gases[from?.internal_name || '']?.find(g => g?.token === fromCurrency?.asset)?.gas || 0
const { fetchBalance, fetchGas } = useBalance()
const name = "amount"
const walletBalance = wallet && balances[wallet.address]?.find(b => b?.network === from?.internal_name && b?.token === fromCurrency?.asset)
const maxAllowedAmount = (walletBalance &&
maxAmountFromApi &&
minAllowedAmount &&
((walletBalance.amount - gasAmount) >= minAllowedAmount &&
(walletBalance.amount - gasAmount) <= maxAmountFromApi)) ?
walletBalance.amount - Number(gasAmount)
: maxAmountFromApi
const maxAllowedDisplayAmount = maxAllowedAmount && truncateDecimals(maxAllowedAmount, fromCurrency?.precision)
const placeholder = (fromCurrency && toCurrency && from && to && minAllowedAmount && !isBalanceLoading && !isGasLoading) ? `${minAllowedAmount} - ${maxAllowedDisplayAmount}` : '0.0'
const step = 1 / Math.pow(10, fromCurrency?.precision || 1)
const amountRef = useRef(ref)
const updateRequestedAmountInUsd = useCallback((requestedAmount: number) => {
if (fromCurrency?.price_in_usd && !isNaN(requestedAmount)) {
setRequestedAmountInUsd((fromCurrency?.price_in_usd * requestedAmount).toFixed(2));
} else {
setRequestedAmountInUsd(undefined);
}
}, [requestedAmountInUsd, fromCurrency]);
const handleSetMinAmount = () => {
setFieldValue(name, minAllowedAmount);
if (minAllowedAmount)
updateRequestedAmountInUsd(minAllowedAmount);
}
const handleSetMaxAmount = useCallback(async () => {
from && await fetchBalance(from);
from && fromCurrency && fetchGas(from, fromCurrency, destination_address || "");
setFieldValue(name, maxAllowedAmount);
if (maxAllowedAmount)
updateRequestedAmountInUsd(maxAllowedAmount)
}, [from, to, fromCurrency, destination_address, maxAllowedAmount])
useEffect(() => {
values.from && fetchBalance(values.from);
}, [values.from, values.destination_address, wallet?.address])
useEffect(() => {
values.to && fetchBalance(values.to);
}, [values.to, values.destination_address, wallet?.address])
const contract_address = values?.from?.currencies.find(a => a.asset === values?.fromCurrency?.asset)?.contract_address
useEffect(() => {
wallet?.address && values.from && values.fromCurrency && fetchGas(values.from, values.fromCurrency, values.destination_address || wallet.address)
}, [contract_address, values.from, values.fromCurrency, wallet?.address])
return (<>
{/* <AmountLabel detailsAvailable={!!(from && to && amount)}
maxAllowedAmount={maxAllowedDisplayAmount}
minAllowedAmount={minAllowedAmount}
isBalanceLoading={(isBalanceLoading || isGasLoading)}
/> */}
<div className="flex w-full justify-between items-center">
<div className="relative w-full">
<NumericInput
disabled={
(!values.from && !values.fromExchange) || (!values.fromCurrency && !values.currencyGroup) ||
(!values.to && !values.toExchange) || (!values.toCurrency && !values.currencyGroup)
}
placeholder={placeholder}
min={minAllowedAmount}
max={maxAllowedAmount}
step={isNaN(step) ? 0.01 : step}
name={name}
ref={amountRef}
precision={fromCurrency?.precision}
onFocus={() => {setIsAmountVisible(false)}}
onBlur={() => {setIsAmountVisible(true)}}
className="rounded-r-none w-full pl-0.5 p-0 focus:ring-0 h-fit"
onChange={e => {
/^[0-9]*[.,]?[0-9]*$/.test(e.target.value) && handleChange(e);
updateRequestedAmountInUsd(parseFloat(e.target.value));
}}
>
{/* {requestedAmountInUsd && isAmountVisible ? (
<span className="absolute text-xs right-0 bottom-[2px]">
(${requestedAmountInUsd})
</span>
) : null} */}
</NumericInput>
</div>
{
from && to && fromCurrency ?
<div className="flex flex-col justify-center">
<div className="text-xs flex flex-col items-center space-x-1 md:space-x-2 ml-2 md:ml-5 px-2">
<div className="flex">
<SecondaryButton disabled={!minAllowedAmount} onClick={handleSetMinAmount} size="xs">
MIN
</SecondaryButton>
<SecondaryButton disabled={!maxAllowedAmount} onClick={handleSetMaxAmount} size="xs" className="ml-1.5">
MAX
</SecondaryButton>
</div>
</div>
</div>
:
<></>
}
</div >
</>)
});
type AmountLabelProps = {
detailsAvailable: boolean;
minAllowedAmount: number | undefined;
maxAllowedAmount: number | undefined;
isBalanceLoading: boolean;
}
const AmountLabel = ({
detailsAvailable,
minAllowedAmount,
maxAllowedAmount,
isBalanceLoading,
}: AmountLabelProps) => {
return <div className="flex items-center w-full justify-between">
<div className="flex items-center space-x-2">
<p className="block font-semibold text-muted text-xs mb-1">Amount</p>
{/* {
detailsAvailable &&
<div className="text-xs hidden md:flex items-center">
<span>(Min:&nbsp;</span>{isBalanceLoading ? <span className="ml-1 h-3 w-6 rounded-sm bg-gray-500 animate-pulse" /> : <span>{minAllowedAmount}</span>}
<span>&nbsp;-&nbsp;Max:&nbsp;</span>{isBalanceLoading ? <span className="ml-1 h-3 w-6 rounded-sm bg-gray-500 animate-pulse" /> : <span>{maxAllowedAmount}</span>}<span>)</span>
</div>
} */}
</div>
</div>
}
export default AmountField
@@ -1,248 +0,0 @@
"use client";
import { useCallback, useEffect } from "react";
import { useFormikContext } from "formik";
import { useSettings } from "@/context/settings";
import { type SwapFormValues } from "../DTOs/SwapFormValues";
import { SelectMenuItem, type ISelectMenuItem } from "../Select/Shared/Props/selectMenuItem";
import PopoverSelectWrapper from "../Select/Popover/PopoverSelectWrapper";
import CurrencySettings from "@/lib/CurrencySettings";
import { SortingByOrder } from "@/lib/sorting";
import { useQueryState } from "@/context/query";
import { type CryptoNetwork, type NetworkCurrency } from "@/Models/CryptoNetwork";
import BridgeApiClient from "@/lib/BridgeApiClient";
import useSWR from "swr";
import { ApiResponse } from "@/Models/ApiResponse";
import { type Balance } from "@/Models/Balance";
const CurrencyGroupFormField: React.FC<{
direction: string
}> = ({
direction
}) => {
const {
values: { to, fromCurrency, toCurrency, from, currencyGroup, fromExchange, toExchange },
setFieldValue,
} = useFormikContext<SwapFormValues>();
console.log("cex currency form field =====", { to, fromCurrency, toCurrency, from, currencyGroup, fromExchange, toExchange })
const name = "currencyGroup";
const query = useQueryState();
const apiClient = new BridgeApiClient();
const version = BridgeApiClient.apiVersion;
// const sourceRoutesURL = `/sources${to && toCurrency
// ? `?destination_network=${to.internal_name}&destination_asset=${toCurrency.asset}&`
// : "?"
// }version=${version}`;
// const destinationRoutesURL = `/destinations${from && fromCurrency
// ? `?source_network=${from.internal_name}&source_asset=${fromCurrency.asset}&`
// : "?"
// }version=${version}`;
// const { data: sourceRoutes } = useSWR<
// ApiResponse<
// {
// network: string;
// asset: string;
// }[]
// >
// >(sourceRoutesURL, apiClient.fetcher);
// const { data: destinationRoutes } = useSWR<
// ApiResponse<
// {
// network: string;
// asset: string;
// }[]
// >
// >(destinationRoutesURL, apiClient.fetcher);
const destinationRoutesURL = `/destination_currencies${toExchange
? `?destination_network=${toExchange.internal_name}&destination_asset=${currencyGroup?.name}&`
: "?"
}version=${version}`;
const sourceRoutesURL = `/source_currencies${fromExchange
? `?source_network=${fromExchange.internal_name}&source_asset=${currencyGroup?.name}&`
: "?"
}version=${version}`;
const { data: sourceRoutes } = useSWR<
ApiResponse<
{
network: string;
asset: string;
}[]
>
>(sourceRoutesURL, apiClient.fetcher);
const { data: destinationRoutes } = useSWR<
ApiResponse<
{
network: string;
asset: string;
}[]
>
>(destinationRoutesURL, apiClient.fetcher);
const routes = direction === 'from' ? sourceRoutes?.data : destinationRoutes?.data
const assets = routes && groupBy(routes, ({ asset }) => asset);
const assetNames =
assets &&
Object.keys(assets).map((a) => ({ name: a, networks: assets[a] }));
const lockedCurrency = query?.lockAsset
? assetNames?.find(
(a) => a.name.toUpperCase() === query?.asset?.toUpperCase()
)
: undefined;
const filteredCurrencies = lockedCurrency ? [lockedCurrency] : assetNames;
const currencyMenuItems = GenerateCurrencyMenuItems(
filteredCurrencies!,
direction === "from" ? sourceRoutes?.data : destinationRoutes?.data,
lockedCurrency,
from,
to,
direction
);
const value = currencyMenuItems?.find((x) => x.id == currencyGroup?.name);
useEffect(() => {
if (currencyMenuItems?.length > 0) {
setFieldValue(name, currencyMenuItems[0].baseObject, true);
}
// setFieldValue(`${direction}Currency`, {
// "name": currencyGroup?.name,
// "asset": currencyGroup?.name,
// "contract_address": null,
// "decimals": 18,
// "status": "active",
// "is_deposit_enabled": true,
// "is_withdrawal_enabled": false,
// "is_refuel_enabled": false,
// "max_withdrawal_amount": 0,
// "deposit_fee": 0.2,
// "withdrawal_fee": 0.2,
// "source_base_fee": 1,
// "destination_base_fee": 1
// }, true);
// setFieldValue(`${direction}`, {
// ...fromExchange,
// "assets": [],
// }, true);
}, [currencyMenuItems?.length])
const handleSelect = useCallback(
(item: SelectMenuItem<AssetGroup>) => {
setFieldValue(name, item.baseObject, true);
// setFieldValue(`${direction}Currency`, {
// "name": currencyGroup?.name,
// "asset": currencyGroup?.name,
// "contract_address": null,
// "decimals": 18,
// "status": "active",
// "is_deposit_enabled": true,
// "is_withdrawal_enabled": false,
// "is_refuel_enabled": false,
// "max_withdrawal_amount": 0,
// "deposit_fee": 0.2,
// "withdrawal_fee": 0.2,
// "source_base_fee": 1,
// "destination_base_fee": 1
// }, true);
// setFieldValue(`${direction}`, {
// ...fromExchange,
// "assets": [],
// }, true);
},
[name, direction, toCurrency, fromCurrency, from, to]
)
return (
<div className="relative">
<PopoverSelectWrapper
placeholder="Asset"
values={currencyMenuItems}
value={value}
setValue={handleSelect as (v: ISelectMenuItem) => void}
disabled={!value?.isAvailable?.value}
/>
</div>
)
};
export function GenerateCurrencyMenuItems(
currencies: AssetGroup[],
routes?: { network: string; asset: string }[],
lockedCurrency?: AssetGroup | undefined,
from?: CryptoNetwork,
to?: CryptoNetwork,
direction?: string,
balances?: Balance[]
): SelectMenuItem<AssetGroup>[] {
let currencyIsAvailable = () => {
if (lockedCurrency) {
return {
value: false,
disabledReason: CurrencyDisabledReason.LockAssetIsTrue,
};
}
// else if (from && to && routes?.some(r => r.asset !== currency.asset && r.network !== (direction === 'from' ? from.internal_name : to.internal_name))) {
// return { value: false, disabledReason: CurrencyDisabledReason.InvalidRoute }
// }
else {
return { value: true, disabledReason: null };
}
};
const storageUrl = process.env.NEXT_PUBLIC_RESOURCE_STORAGE_URL;
return currencies
?.map((c) => {
const currency = c;
const displayName = lockedCurrency?.name ?? currency.name;
const res: SelectMenuItem<AssetGroup> = {
baseObject: c,
id: c.name,
name: displayName || "-",
order: CurrencySettings.KnownSettings[c.name]?.Order ?? 5,
// imgSrc: `${storageUrl}layerswap/currencies/${c.name.toLowerCase()}.png`,
imgSrc: `${process.env.NEXT_PUBLIC_CDN_URL}/bridge/currencies/${c.name.toLowerCase()}.png`,
isAvailable: currencyIsAvailable(),
type: "currency",
};
return res;
})
.sort(SortingByOrder);
}
export enum CurrencyDisabledReason {
LockAssetIsTrue = "",
InsufficientLiquidity = "Temporarily disabled. Please check later.",
InvalidRoute = "Invalid route",
}
const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) => {
return list.reduce((previous, currentItem) => {
const group = getKey(currentItem);
if (!previous[group]) previous[group] = [];
previous[group].push(currentItem);
return previous;
}, {} as Record<K, T[]>);
};
export type AssetGroup = {
name: string;
networks: {
network: string;
asset: string;
}[];
};
export default CurrencyGroupFormField;
@@ -1,199 +0,0 @@
'use client'
import { forwardRef, useCallback, useEffect, useState } from 'react'
import Image from 'next/image'
import useSWR from 'swr'
import { useFormikContext } from 'formik'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@hanzo/ui/primitives'
import { useSettings } from '@/context/settings'
import { type SwapFormValues } from '../DTOs/SwapFormValues'
import { SelectMenuItem } from '../Select/Shared/Props/selectMenuItem'
import { ApiResponse } from '@/Models/ApiResponse'
import BridgeApiClient from '@/lib/BridgeApiClient'
type SwapDirection = 'from' | 'to'
type Props = {
direction: SwapDirection
}
const CEXNetworkFormField = forwardRef(function CEXNetworkFormField(
{ direction }: Props,
ref: any
) {
const { values, setFieldValue } = useFormikContext<SwapFormValues>()
const name = direction
const { from, to, fromCurrency, toCurrency } = values
const { networks } = useSettings()
const filterWith = direction === 'from' ? to : from
const filterWithAsset =
direction === 'from' ? toCurrency?.asset : fromCurrency?.asset
const apiClient = new BridgeApiClient()
const version = BridgeApiClient.apiVersion
const routesEndpoint = `/${
direction === 'from' ? 'sources' : 'destinations'
}${
filterWith && filterWithAsset
? `?${direction === 'to' ? 'source_network' : 'destination_network'}=${
filterWith.internal_name
}&${
direction === 'to' ? 'source_asset' : 'destination_asset'
}=${filterWithAsset}&`
: '?'
}version=${version}`
const { data: routes, isLoading } = useSWR<
ApiResponse<
{
network: string
asset: string
}[]
>
>(routesEndpoint, apiClient.fetcher)
const [routesData, setRoutesData] = useState<
{
network: string
asset: string
}[]
>()
useEffect(() => {
if (!isLoading && routes?.data) setRoutesData(routes.data)
}, [routes])
const menuItems = routesData && GenerateMenuItems(routesData)
const handleSelect = useCallback(
(item: SelectMenuItem<{ network: string, asset: string }>) => {
// if (!item) return
// const layer = networks.find(
// (l) => l.internal_name === item.baseObject.network
// )
// const currency = layer?.currencies.find(
// (a) => a.asset === item.baseObject.asset
// )
// setFieldValue(name, layer, true)
// setFieldValue(`${name}Currency`, currency, true)
},
[name]
)
const value = menuItems?.find(
(item) =>
item.baseObject.asset ===
(direction === 'from' ? fromCurrency : toCurrency)?.asset &&
item.baseObject.network ===
(direction === 'from' ? from : to)?.internal_name
)
//Setting default value
useEffect(() => {
if (!menuItems) return
else if (value) return
const item = menuItems[0]
handleSelect(item)
}, [routesData])
if (!menuItems) return
return (
<div className='flex justify-between items-center w-fit gap-1'>
<label htmlFor={name} className='block '>
{direction === 'from' ? 'via' : 'in'}
</label>
<div className='w-fit' ref={ref}>
<Select
value={value?.id}
onValueChange={(v) =>
handleSelect(menuItems.find((m) => m.id === v)!)
}
>
<SelectTrigger className='w-full border-none !h-fit !p-0'>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel className=''>Networks</SelectLabel>
{menuItems?.map((route, index) => {
const network = networks.find(
(l) => l.internal_name === route.baseObject.network
)
const currency = network?.currencies.find(
(a) => a.asset === route.baseObject.asset
)
return (
<SelectItem key={index} value={route.id}>
<div className='flex justify-between gap-1'>
<div className='inline-flex items-center gap-1 w-full'>
<div className='flex-shrink-0 h-5 w-5 relative'>
<Image
src={network?.logo ?? ''}
alt='Network Logo'
height='40'
width='40'
loading='eager'
className='rounded-md object-contain'
/>
</div>
<p>{network?.display_name.slice(0, 3)}</p>
</div>
<div className='inline-flex items-center gap-1'>
<div className='flex-shrink-0 h-5 w-5 relative'>
<Image
src={currency?.logo ?? ''}
alt='Token Logo'
height='40'
width='40'
loading='eager'
className='rounded-md object-contain'
/>
</div>
<p>{currency?.asset}</p>
</div>
</div>
</SelectItem>
)
})}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
)
})
function GenerateMenuItems(
items: { network: string, asset: string }[]
): SelectMenuItem<{ network: string, asset: string }>[] {
const menuItems = items.map((e, index) => {
const res: SelectMenuItem<{ network: string, asset: string }> = {
baseObject: e,
id: index.toString(),
name: e.network,
order: 100,
imgSrc: '',
isAvailable: { value: true, disabledReason: null },
type: 'cex',
}
return res
})
return menuItems
}
export default CEXNetworkFormField
@@ -1,291 +0,0 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useFormikContext } from "formik";
import useSWR from "swr";
import { useSettings } from "@/context/settings";
import { type SwapFormValues } from "../DTOs/SwapFormValues";
import { SelectMenuItem, type ISelectMenuItem } from "../Select/Shared/Props/selectMenuItem";
import PopoverSelectWrapper from "../Select/Popover/PopoverSelectWrapper";
import CurrencySettings from "@/lib/CurrencySettings";
import { SortingByAvailability } from "@/lib/sorting";
import { useBalancesState } from "@/context/balances";
import { truncateDecimals } from "../utils/RoundDecimals";
import { useQueryState } from "@/context/query";
import { type CryptoNetwork, type NetworkCurrency } from "@/Models/CryptoNetwork";
import BridgeApiClient from "@/lib/BridgeApiClient";
import { ApiResponse } from "@/Models/ApiResponse";
import useWallet from "@/hooks/useWallet";
import { type Balance } from "@/Models/Balance";
const CurrencyFormField: React.FC<{
direction: string
}> = ({
direction
}) => {
const { values, setFieldValue } = useFormikContext<SwapFormValues>();
const { to, fromCurrency, toCurrency, from, currencyGroup } = values;
const [allRoutes, setAllRoutes] = useState<string[]>([]);
const name = direction === "from" ? "fromCurrency" : "toCurrency";
const query = useQueryState();
const { balances, isBalanceLoading } = useBalancesState();
const lockedCurrency = query?.lockAsset
? from?.currencies?.find(
(c) => c?.asset?.toUpperCase() === query?.asset?.toUpperCase()
)
: undefined;
const assets = direction === "from" ? from?.currencies : to?.currencies;
const { getAutofillProvider: getProvider } = useWallet();
const provider = useMemo(() => {
return from && getProvider(from);
}, [from, getProvider]);
const wallet = provider?.getConnectedWallet();
const walletBalance =
wallet &&
balances[wallet.address]?.find(
(b) =>
b?.network === from?.internal_name && b?.token === fromCurrency?.asset
);
const destinationBalance =
wallet &&
balances[wallet.address]?.find(
(b) => b?.network === to?.internal_name && b?.token === toCurrency?.asset
);
const walletBalanceAmount =
walletBalance?.amount &&
truncateDecimals(walletBalance?.amount, fromCurrency?.precision);
const destinationBalanceAmount =
destinationBalance?.amount &&
truncateDecimals(destinationBalance?.amount, toCurrency?.precision);
const apiClient = new BridgeApiClient();
const version = BridgeApiClient.apiVersion;
//destination_currencies?source_network=BINANCE&source_asset=undefined&version=mainnet
const destinationRoutesURL = `/destination_currencies${to ? `?destination_network=${to.internal_name}&destination_asset=${toCurrency?.asset}&` : "?"}version=${version}`;
//source_currencies?source_network=BINANCE&source_asset=undefined&version=mainnet
const sourceRoutesURL = `/source_currencies${from ? `?source_network=${from.internal_name}&source_asset=${fromCurrency?.asset}&` : "?"}version=${version}`;
const { data: sourceRoutes, error: sourceRoutesError } = useSWR<
ApiResponse<
{
network: string;
asset: string;
}[]
>
>(sourceRoutesURL, apiClient.fetcher);
const { data: destinationRoutes, error: destRoutesError } = useSWR<
ApiResponse<
{
network: string;
asset: string;
}[]
>
>(destinationRoutesURL, apiClient.fetcher);
const currencies = lockedCurrency ? [lockedCurrency] : assets;
const filteredCurrencies = currencies?.filter((currency) => {
if (direction === "from") {
return currency.is_deposit_enabled;
} else {
return currency.is_withdrawal_enabled;
}
});
const currencyMenuItems = GenerateCurrencyMenuItems(
currencies!,
values,
direction === "from" ? sourceRoutes?.data : destinationRoutes?.data,
lockedCurrency,
direction,
balances[wallet?.address || ""]
);
const currencyAsset = direction === "from" ? fromCurrency?.asset : toCurrency?.asset;
useEffect(() => {
let currencyIsAvailable =
(fromCurrency || toCurrency) &&
currencyMenuItems?.some((c) => c?.baseObject.asset === currencyAsset);
if (currencyIsAvailable) return;
const default_currency =
currencyMenuItems?.find(
(c) =>
c.baseObject?.asset?.toUpperCase() === query?.asset?.toUpperCase()
) || currencyMenuItems?.[0];
const selected_currency = currencyMenuItems?.find(
(c) =>
c.baseObject?.asset?.toUpperCase() ===
(
currencyGroup?.name ||
(direction === "to" ? fromCurrency?.asset : toCurrency?.asset)
)?.toUpperCase()
);
if (
direction === "to" &&
selected_currency &&
destinationRoutes?.data
?.filter((r) => r.network === to?.internal_name)
?.some((r) => r.asset === selected_currency.name)
) {
setFieldValue(name, selected_currency.baseObject);
} else if (
direction === "from" &&
selected_currency &&
sourceRoutes?.data
?.filter((r) => r.network === from?.internal_name)
?.some((r) => r.asset === selected_currency.name)
) {
setFieldValue(name, selected_currency.baseObject);
} else if (default_currency) {
setFieldValue(name, default_currency.baseObject);
}
}, [from, to, query]);
useEffect(() => {
if (direction === "to" && fromCurrency && toCurrency) {
if (
destinationRoutes &&
!destinationRoutes?.data
?.filter((r) => r.network === to?.internal_name)
?.some((r) => r.asset === toCurrency?.asset)
) {
setFieldValue(name, null);
} else if (destRoutesError) {
setFieldValue("toCurrency", null);
setFieldValue("to", null);
}
}
}, [fromCurrency, direction, to, destinationRoutes, destRoutesError]);
useEffect(() => {
if (direction === "from" && toCurrency && fromCurrency) {
if (
sourceRoutes?.data &&
!sourceRoutes?.data
?.filter((r) => r.network === from?.internal_name)
?.some((r) => r.asset === fromCurrency?.asset)
) {
setFieldValue(name, null);
} else if (sourceRoutesError) {
setFieldValue("fromCurrency", null);
setFieldValue("from", null);
}
}
}, [toCurrency, direction, from, sourceRoutes, sourceRoutesError]);
const value = currencyMenuItems?.find((x) => x.id == currencyAsset);
const handleSelect = useCallback(
(item: SelectMenuItem<NetworkCurrency>) => {
setFieldValue(name, item.baseObject, true);
},
[name, direction, toCurrency, fromCurrency, from, to]
);
const balanceAmount = direction === "from" ? walletBalanceAmount : destinationBalanceAmount;
return (
<div className="relative">
{/* {(direction === 'from' ? (from && fromCurrency) : (to && toCurrency)) && balanceAmount != undefined && !isNaN(balanceAmount) &&
<div className="text-xs text-right absolute right-0 -top-7">
<div className='bg-level-1 py-1.5 pl-2 text-xs'>
<div>
<span>Balance:&nbsp;</span>
{isBalanceLoading ?
<div className='h-[10px] w-10 inline-flex bg-gray-500 rounded-sm animate-pulse' />
:
<span>{balanceAmount}</span>}
</div>
</div>
</div>
} */}
<PopoverSelectWrapper
placeholder="Asset"
values={currencyMenuItems}
value={value}
setValue={handleSelect as (v: ISelectMenuItem) => void}
disabled={!value?.isAvailable?.value}
/>
</div>
);
};
export function GenerateCurrencyMenuItems(
currencies: NetworkCurrency[],
values: SwapFormValues,
routes?: { network: string; asset: string }[],
lockedCurrency?: NetworkCurrency,
direction?: string,
balances?: Balance[]
): SelectMenuItem<NetworkCurrency>[] {
const { to, from } = values;
let currencyIsAvailable = (currency: NetworkCurrency) => {
if (lockedCurrency) {
return {
value: false,
disabledReason: CurrencyDisabledReason.LockAssetIsTrue,
};
} else if (
(from || to) &&
!routes
?.filter(
(r) =>
r.network ===
(direction === "from" ? from?.internal_name : to?.internal_name)
)
.some((r) => r.asset === currency.asset)
) {
return {
value: true,
disabledReason: CurrencyDisabledReason.InvalidRoute,
};
} else {
return { value: true, disabledReason: null };
}
};
return currencies
?.map((c) => {
const currency = c;
const displayName = lockedCurrency?.asset ?? currency.asset;
const balance = balances?.find(
(b) => b?.token === c?.asset && from?.internal_name === b.network
);
const formatted_balance_amount = balance
? Number(truncateDecimals(balance?.amount, c.precision))
: "";
const res: SelectMenuItem<NetworkCurrency> = {
baseObject: c,
id: c.asset,
name: displayName || "-",
order: CurrencySettings.KnownSettings[c.asset]?.Order ?? 5,
imgSrc: c.logo ?? '',
isAvailable: currencyIsAvailable(c),
details: `${formatted_balance_amount}`,
type: "currency",
};
return res;
})
.sort(SortingByAvailability);
}
export enum CurrencyDisabledReason {
LockAssetIsTrue = "",
InsufficientLiquidity = "Temporarily disabled. Please check later.",
InvalidRoute = "Invalid route",
}
export default CurrencyFormField;
@@ -1,337 +0,0 @@
"use client"
import React, { forwardRef, useCallback, useEffect, useState } from "react"
import { useFormikContext } from "formik"
import useSWR from "swr"
import { useSettings } from "@/context/settings"
import { type SwapFormValues } from "../DTOs/SwapFormValues"
import {
type ISelectMenuItem,
SelectMenuItem,
} from "../Select/Shared/Props/selectMenuItem"
// types
import { type CryptoNetwork, type NetworkCurrency } from "@/Models/CryptoNetwork"
import { type Exchange } from "@/Models/Exchange"
// comps
import CommandSelectWrapper from "../Select/Command/CommandSelectWrapper"
import ExchangeSettings from "@/lib/ExchangeSettings"
import NetworkSettings from "@/lib/NetworkSettings"
import CurrencyFormField from "./CurrencyFormField"
import BridgeApiClient from "@/lib/BridgeApiClient"
import AmountField from "./Amount"
import CurrencyGroupFormField from "./CEXCurrencyFormField"
import { SortingByOrder } from "@/lib/sorting"
import { LayerDisabledReason } from "../Select/Popover/PopoverSelect"
import { SelectMenuItemGroup } from "../Select/Command/commandSelect"
import { useQueryState } from "@/context/query"
import { ApiResponse } from "@/Models/ApiResponse"
import { Refuel } from "../DisclosureComponents/FeeDetails/ReceiveAmounts"
import { useFee } from "@/context/feeContext"
type SwapDirection = "from" | "to"
type Props = {
direction: SwapDirection
label: string
className?: string
}
const GROUP_ORDERS = {
Popular: 1,
New: 2,
Fiat: 3,
Networks: 4,
Exchanges: 5,
Other: 10,
}
type OrderType = keyof typeof GROUP_ORDERS
const getGroupName = (value: CryptoNetwork | Exchange, type: "cex" | "layer") => {
if (value.is_featured) {
return "Popular"
} if (type === "layer") {
return "Networks"
} else if (type === "cex") {
return "Exchanges"
} else {
return "Other"
}
}
const NetworkFormField = forwardRef(function NetworkFormField(
{ direction, label, className }: Props,
ref: any
) {
const { values, setFieldValue } = useFormikContext<SwapFormValues>()
const name = direction
const {
from,
to,
fromCurrency,
toCurrency,
fromExchange,
toExchange,
currencyGroup,
refuel,
} = values
const { lockFrom, lockTo } = useQueryState()
const { networks, exchanges } = useSettings()
let placeholder = ""
let searchHint = ""
let filteredLayers: CryptoNetwork[]
let filteredExchanges: Exchange[]
let menuItems: SelectMenuItem<CryptoNetwork | Exchange>[]
let valueGrouper: (values: ISelectMenuItem[]) => SelectMenuItemGroup[]
const filterWith = direction === "from" ? to : from
const filterWithAsset = direction === "from" ? toCurrency?.asset : fromCurrency?.asset
const filterWithExchange = direction === "from" ? toExchange : fromExchange
const apiClient = new BridgeApiClient()
const version = BridgeApiClient.apiVersion
const routesEndpoint = `/${
direction === "from" ? "sources" : "destinations"
}${
filterWith
? `?${direction === "to" ? "source_network" : "destination_network"}=${
filterWith.internal_name
}&${
direction === "to" ? "source_asset" : "destination_asset"
}=${filterWithAsset}&`
: filterWithExchange
? `?${
direction === "from"
? "destination_asset_group"
: "source_asset_group"
}=${filterWithExchange?.internal_name}&`
: "?"
}version=${version}`
const { data: routes, isLoading } = useSWR<
ApiResponse<
{
network: string
asset: string
}[]
>
>(routesEndpoint, apiClient.fetcher)
const [routesData, setRoutesData] = useState<
{
network: string
asset: string
}[]
>()
useEffect(() => {
if (!isLoading && routes?.data) setRoutesData(routes?.data)
}, [routes])
if (direction === "from") {
placeholder = "Source"
searchHint = "Swap from"
filteredLayers = networks.filter(
(l) =>
// l.internal_name !== filterWith?.internal_name &&
routesData?.some((r) => r.network === l.internal_name)
)
filteredExchanges = exchanges.filter(
(e) =>
// e.internal_name !== filterWith?.internal_name &&
routesData?.some((r) => r.network === e.internal_name)
)
menuItems = GenerateMenuItems(
filteredLayers,
toExchange ? [] : exchanges,
direction,
!!(from && lockFrom)
)
} else {
placeholder = "Destination"
searchHint = "Swap to"
filteredLayers = networks.filter(
(l) =>
routesData?.some((r) => r.network === l.internal_name) &&
l.internal_name !== filterWith?.internal_name
)
filteredExchanges = exchanges.filter(
(e) =>
routesData?.some((r) => r.network === e.internal_name) &&
e.internal_name !== filterWith?.internal_name
)
menuItems = GenerateMenuItems(
filteredLayers,
fromExchange ? [] : filteredExchanges, // cex -> net
direction,
!!(to && lockTo)
)
}
valueGrouper = groupByType
const value = menuItems.find((x) =>
x.type === "layer"
? x.id == (direction === "from" ? from : to)?.internal_name
: x.id ==
(direction === "from" ? fromExchange : toExchange)?.internal_name
)
const handleSelect = useCallback(
(item: SelectMenuItem<CryptoNetwork | Exchange>) => {
if (item.type === "cex") {
setFieldValue(`${name}Exchange`, item.baseObject, true)
setFieldValue(name, null, true)
setFieldValue(`currencyGroup`, null, true)
setFieldValue(`${name}Currency`, null, true)
} else {
setFieldValue(name, item.baseObject, true)
setFieldValue(`${name}Exchange`, null, true)
setFieldValue(`${name}Currency`, null, true)
}
},
[name]
)
const { fee } = useFee()
const parsedReceiveAmount = parseFloat(
fee.manualReceiveAmount?.toFixed(toCurrency?.precision ?? 6) || ""
)
const destinationNetworkCurrency = to && toCurrency ? toCurrency : null
return (
<div className={className}>
<label htmlFor={name} className="block font-semibold text-xs">
{label}
</label>
<div className="border border-[#404040] bg-level-1 rounded-lg mt-1.5 pb-2">
<div ref={ref}>
<div className="w-full">
<CommandSelectWrapper
disabled={false}
valueGrouper={valueGrouper}
placeholder={placeholder}
setValue={handleSelect as (v: ISelectMenuItem) => void}
value={value}
values={menuItems}
searchHint={searchHint}
className="rounded-b-none border-t-0 border-x-0"
/>
</div>
</div>
<div className="flex justify-between items-center mt-2 pl-3 pr-4">
{direction === "from" ? (
<AmountField />
) : parsedReceiveAmount > 0 ? (
<div className="font-semibold md:font-bold text-right leading-4">
<p>
<span>{parsedReceiveAmount}</span>&nbsp
<span>{destinationNetworkCurrency?.asset}</span>
</p>
{refuel && (
<Refuel currency={toCurrency} to={to} refuel={refuel} />
)}
</div>
) : (
<>-</>
)}
{value?.type === "cex" ? (
<CurrencyGroupFormField direction={name} />
) : (
<CurrencyFormField direction={name} />
)}
</div>
</div>
</div>
)
})
function groupByType(values: ISelectMenuItem[]) {
let groups: SelectMenuItemGroup[] = []
values.forEach((v) => {
let group =
groups.find((x) => x.name == v.group) ||
new SelectMenuItemGroup({ name: v.group, items: [] })
group.items.push(v)
if (!groups.find((x) => x.name == v.group)) {
groups.push(group)
}
})
groups.forEach((group) => {
group.items.sort((a, b) => a.name.localeCompare(b.name))
})
groups.sort((a: SelectMenuItemGroup, b: SelectMenuItemGroup) => {
// Sort put networks first then exchanges
return (
(GROUP_ORDERS[a.name as OrderType] || GROUP_ORDERS.Other) -
(GROUP_ORDERS[b.name as OrderType] || GROUP_ORDERS.Other)
)
})
return groups
}
function GenerateMenuItems(
networks: CryptoNetwork[],
exchanges: Exchange[],
direction: SwapDirection,
lock: boolean
): SelectMenuItem<CryptoNetwork | Exchange>[] {
let layerIsAvailable = () => {
if (lock) {
return {
value: false,
disabledReason: LayerDisabledReason.LockNetworkIsTrue,
}
} else {
return { value: true, disabledReason: null }
}
}
const mappedLayers = networks
.map((l) => {
let orderProp: keyof NetworkSettings | keyof ExchangeSettings =
direction == "from" ? "OrderInSource" : "OrderInDestination"
const order = NetworkSettings.KnownSettings[l.internal_name]?.[orderProp]
const res: SelectMenuItem<CryptoNetwork> = {
baseObject: l,
id: l.internal_name,
name: l.display_name,
order: order || 100,
imgSrc: l?.logo || '',
isAvailable: layerIsAvailable(),
type: "layer",
group: getGroupName(l, "layer"),
}
return res
})
.sort(SortingByOrder)
const mappedExchanges = exchanges
.map((e) => {
let orderProp: keyof ExchangeSettings =
direction == "from" ? "OrderInSource" : "OrderInDestination"
const order =
ExchangeSettings.KnownSettings[e.internal_name]?.[orderProp]
const res: SelectMenuItem<Exchange> = {
baseObject: e,
id: e.internal_name,
name: e.display_name,
order: order || 100,
imgSrc: e?.img_url ?? '',
isAvailable: layerIsAvailable(),
type: "cex",
group: getGroupName(e, "cex"),
}
return res
})
.sort(SortingByOrder)
const items = [...mappedExchanges, ...mappedLayers]
return items
}
export default NetworkFormField

Some files were not shown because too many files have changed in this diff Show More