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
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"WillLuke.nextjs.hasPrompted": true
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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> '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] = ' ' + 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) + ' '
|
||||
}
|
||||
|
||||
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 }
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
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 |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 426 B |
|
Before Width: | Height: | Size: 831 B |
|
Before Width: | Height: | Size: 15 KiB |
@@ -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": ""
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 64 KiB |
@@ -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.
|
||||
@@ -1,4 +0,0 @@
|
||||
user-agent: *
|
||||
allow: /
|
||||
|
||||
sitemap: https://bridge.lux.network/sitemap.xml
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 +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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
// };
|
||||
// });
|
||||
// }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = {}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export type Partner = {
|
||||
display_name: string;
|
||||
logo_url: string;
|
||||
is_wallet: boolean;
|
||||
id: number;
|
||||
name: string
|
||||
}
|
||||
@@ -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 = ""
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 </span>{network?.display_name}<span>. </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 </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 </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 </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> tokens by transferring
|
||||
assets to
|
||||
<span>{network?.display_name || campaign.network}</span>. For each
|
||||
transaction, you'll receive
|
||||
<span>{campaign?.percentage}</span>% of Bridge fee back.
|
||||
<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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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. </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}</>
|
||||
|
||||
<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. </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, </span>
|
||||
:
|
||||
<span>Please try again. If the issue keeps happening, </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
|
||||
@@ -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
|
||||
@@ -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: </span>{isBalanceLoading ? <span className="ml-1 h-3 w-6 rounded-sm bg-gray-500 animate-pulse" /> : <span>{minAllowedAmount}</span>}
|
||||
<span> - Max: </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: </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> 
|
||||
<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
|
||||