mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
feat: bridge UI part is motly finished
This commit is contained in:
+113
-49
@@ -4,10 +4,10 @@
|
||||
|---|---|
|
||||
| Repository | `github.com/luxfi/bridge` |
|
||||
| Owner | Lux Industries, Inc. |
|
||||
| Author of doc | (to be filled) |
|
||||
| Author of doc | Jackson Mori |
|
||||
| Date | 2026-05-22 |
|
||||
| Status | Draft for review (R4 — corrected against working tree) |
|
||||
| Supersedes | Prior R3 draft (contained stale tree + diagram claims) |
|
||||
| Status | Draft for review (R5 — refresh after R3 merge into `whispers/bridgev2`) |
|
||||
| Supersedes | R4 (pre-merge dep layout + pre-doc-cleanup; treated R3 client-side MPC as pending). R5 re-pins against `pkg/bridge` v1.0.3 + the now-merged client-side MPC / cosigner stack, and reflects the deletion of the legacy `docs/*.md` set. |
|
||||
|
||||
---
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
`bridge.lux.network` is Lux's cross-chain bridge UI. Historically it was a monolithic frontend bound to an older JavaScript API + standalone MPC service. Both have been replaced:
|
||||
|
||||
- The **API backend** is `app/server/` (Express + Prisma) and is live at `api.bridge.lux.network`.
|
||||
- The **API backend** is `app/server/` (Express + Prisma) and is live at `api.bridge.lux.network`. It also holds the cosigner glue (`src/domain/cosigners.ts`) that talks to external custodians (Utila, Fireblocks) on behalf of tenants — secret material never leaves the server.
|
||||
- The **MPC layer** is provided by `pkg/threshold/` (`@luxfi/threshold` SDK), deployed in two modes:
|
||||
- **Private MPC** — Lux treasury cluster, collects fees. Config under `config/mpc/config.yaml`.
|
||||
- **Public MPC** — `m-chain`, validator-powered, permissionless, deployed via `k8s/mpc-deployment.yaml`.
|
||||
- The **frontend** is packaged as a publishable white-label SDK `@luxfi/bridge` (`pkg/bridge/`). The bridge UI is **inlined** under `pkg/bridge/src/app/` — there is no longer a workspace `@luxbridge/app-v3` dependency. Each tenant (Lux, Zoo, Hanzo, ) consumes the SDK from a thin host shell plus its own `@<org>/brand` package.
|
||||
|
||||
This document is the authoritative project requirements set. The prior `requirements.txt` working-notes file is no longer at the repo root.
|
||||
This document is the authoritative project requirements set. There is no `requirements.txt` working-notes file in the repo (none has existed in git history); REQUIREMENTS.md is the canonical record.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
@@ -62,11 +62,14 @@ The following are **explicitly out of scope** for this initiative and must not c
|
||||
bridge/
|
||||
├── app/
|
||||
│ ├── bridge/ @luxbridge/lux-tenant v1.0.0 — canonical Lux tenant.
|
||||
│ │ Vite + React, ~120 LOC effective (bridge.config.ts ~88
|
||||
│ │ lines + main.tsx + index.html). Consumes
|
||||
│ │ @luxfi/bridge + @luxfi/brand + @luxfi/logo.
|
||||
│ │ Vite + React, thin shell (bridge.config.ts ~88 lines
|
||||
│ │ + main.tsx + index.html). Consumes @luxfi/bridge +
|
||||
│ │ @luxfi/brand + @luxfi/logo. See §5.5 for the
|
||||
│ │ transitive deps it currently re-declares.
|
||||
│ ├── explorer/ Bridge explorer app.
|
||||
│ └── server/ Bridge API (Express + Prisma). Live at api.bridge.lux.network.
|
||||
│ Holds cosigner glue (src/domain/cosigners.ts +
|
||||
│ __tests__/cosigners.test.ts + cosigners-fireblocks.test.ts).
|
||||
├── pkg/
|
||||
│ ├── bridge/ @luxfi/bridge — PUBLIC SDK (the canonical consumer entry point).
|
||||
│ │ ├── src/
|
||||
@@ -78,21 +81,36 @@ bridge/
|
||||
│ │ │ ├── mount.ts Imperative DOM-mount helper.
|
||||
│ │ │ ├── config.ts getConfig/setConfig/applyBrandMetadata.
|
||||
│ │ │ ├── types.ts BridgeConfig + BrandConfig/AuthConfig/
|
||||
│ │ │ │ KMSConfig/WalletConfig/MPCConfig blocks.
|
||||
│ │ │ ├── app/ Inlined bridge UI (BridgeApp.tsx + components/
|
||||
│ │ │ │ hooks/ lib/ styles/).
|
||||
│ │ │ └── __tests__/ Vitest specs.
|
||||
│ │ ├── package.json v1.0.1, scope public, registry npmjs.org.
|
||||
│ │ └── README.md Consumer-facing docs (kept in sync through R2/R3).
|
||||
│ │ │ │ KMSConfig/WalletConfig/MPCConfig blocks,
|
||||
│ │ │ │ plus BridgeUtilaConfig / BridgeFireblocksConfig
|
||||
│ │ │ │ cosigner blocks (nested under MPCConfig).
|
||||
│ │ │ ├── app/ Inlined bridge UI.
|
||||
│ │ │ │ ├── BridgeApp.tsx
|
||||
│ │ │ │ ├── components/ AssetInput, Card, ChainSelector, Header,
|
||||
│ │ │ │ │ SwapForm, TransferStatus, WalletConnect.
|
||||
│ │ │ │ ├── hooks/ useSwap, useTransfers, useWallet.
|
||||
│ │ │ │ ├── lib/ assets, bridge-api, chains, format,
|
||||
│ │ │ │ │ mpc-session (R3 client-side MPC),
|
||||
│ │ │ │ │ wagmi-config.
|
||||
│ │ │ │ └── styles/
|
||||
│ │ │ └── __tests__/ Vitest specs: bridge-api, config,
|
||||
│ │ │ mpc-session, useSwap, useTransfers,
|
||||
│ │ │ useTransfers-cosigners, useWallet,
|
||||
│ │ │ wagmi-config.
|
||||
│ │ ├── package.json v1.0.3, scope public, registry npmjs.org.
|
||||
│ │ └── README.md Consumer-facing docs (kept in sync through R2/R3/R5).
|
||||
│ ├── core/ Shared core utilities.
|
||||
│ ├── settings/ Network + asset registry (private, unpublished).
|
||||
│ ├── threshold/ @luxfi/threshold — MPC SDK. Consumed by app/server.
|
||||
│ ├── threshold/ @luxfi/threshold — MPC SDK. Consumed by app/server AND
|
||||
│ │ now by pkg/bridge (R3 client-side MPC session helper).
|
||||
│ ├── ui/ Shared UI primitives.
|
||||
│ └── utila/ Utila cosigner helpers (declarative, layered).
|
||||
├── docs/ Nextra site (pages/, theme.config.tsx, next.config.js,
|
||||
│ package.json) — not a flat bag of md files.
|
||||
│ Loose top-level *.md (BRIDGE-STATUS, LOCAL-SETUP, etc.)
|
||||
│ predate the Nextra migration and are being folded in.
|
||||
│ package.json + content/docs/*.mdx). The legacy loose
|
||||
│ top-level *.md set (BRIDGE-STATUS, LOCAL-SETUP,
|
||||
│ DEPLOYMENT, CI-CD-DOCKER-IMAGES, the MPC-* set) was
|
||||
│ deleted in the merge from main (R5). Only LLM.md
|
||||
│ and LUX-ID-INTEGRATION.md remain at docs/ root.
|
||||
├── config/mpc/config.yaml Private MPC cluster config.
|
||||
├── k8s/mpc-deployment.yaml Public MPC (m-chain) deployment manifests.
|
||||
└── REQUIREMENTS.md This document.
|
||||
@@ -102,8 +120,9 @@ Notable removals from prior drafts:
|
||||
|
||||
- `app/bridge3/` does **not** exist. The two non-tenant apps are `app/explorer/` and `app/server/`.
|
||||
- `pkg/bridge/src/luxbridge-app.d.ts` does **not** exist (the workspace-dep shim was removed when the UI was inlined).
|
||||
- Root `requirements.txt` is **not** present.
|
||||
- Root `requirements.txt` is **not** present (and never has been in git history).
|
||||
- The legacy `@luxbridge/app` v2.0.0 directory was retired; `app/bridge/` is now the new, thin `@luxbridge/lux-tenant`.
|
||||
- The legacy `docs/{BRIDGE-STATUS,LOCAL-SETUP,DEPLOYMENT,CI-CD-DOCKER-IMAGES,MIGRATION-TO-GO-MPC,MPC-GO-INTEGRATION,MPC-INTEGRATION-COMPLETE,MPC-MODERNIZATION-SUMMARY}.md` were deleted in the merge from main; their content has either been folded into the Nextra site or retired outright.
|
||||
|
||||
### 5.2 Component status
|
||||
|
||||
@@ -115,8 +134,9 @@ Notable removals from prior drafts:
|
||||
| Private MPC (treasury fees) | Live | same code, separate cluster, `config/mpc/config.yaml` |
|
||||
| r-chain relay (optional helper) | Live (in repo) | `app/server/cmd/`, hooks |
|
||||
| o-chain oracle (optional helper) | Live (in repo) | upstream `github.com/luxfi/bridge` |
|
||||
| `@luxfi/bridge` SDK (`pkg/bridge/`) | v1.0.1 on `whispers/bridgev2`, UI inlined | This repo |
|
||||
| Lux tenant app (`app/bridge/`) | `@luxbridge/lux-tenant` v1.0.0 — ~120 LOC | This repo |
|
||||
| `@luxfi/bridge` SDK (`pkg/bridge/`) | **v1.0.3** on `whispers/bridgev2`. UI inlined; **client-side MPC session merged**; declarative cosigner blocks (`utila`, `fireblocks`) exposed via `BridgeMPCConfig`. | This repo |
|
||||
| Server-side cosigner glue | **Merged** — `app/server/src/domain/cosigners.ts` + Fireblocks/native tests. End-to-end soak against a real Utila/Fireblocks tenant still pending. | `app/server/` |
|
||||
| Lux tenant app (`app/bridge/`) | `@luxbridge/lux-tenant` v1.0.0 — thin shell | This repo |
|
||||
| Zoo tenant app | Pending — needs new tenant repo (see §10) | TBD |
|
||||
| Liquidity backend wire-up | Later | After Lux/Zoo tenants ship |
|
||||
|
||||
@@ -155,7 +175,8 @@ interface BridgeConfig {
|
||||
auth?: BridgeAuthConfig // Hanzo IAM (OIDC) block, mirrors <Exchange auth={…}/>
|
||||
kms?: BridgeKMSConfig // Tenant KMS endpoint
|
||||
wallet?: BridgeWalletConfig // WalletConnect project id + chain set
|
||||
mpc?: BridgeMPCConfig // publicUrl (m-chain) + optional privateUrl + protocol
|
||||
mpc?: BridgeMPCConfig // optional publicUrl + privateUrl + protocol
|
||||
// + optional utila / fireblocks cosigner blocks
|
||||
clientId?: string // @deprecated — use auth.clientId
|
||||
iamOrg?: string // @deprecated — use auth.orgSlug
|
||||
}
|
||||
@@ -163,25 +184,54 @@ interface BridgeConfig {
|
||||
|
||||
`BridgeMPCConfig.protocol` accepts classical (`cggmp21`, `frost`, `bls`, `doerner`) and post-quantum lattice-based (`pulsar`, `corona`, `magnetar`) threshold-signature identifiers. All protocols are leaderless / permissionless-safe by design.
|
||||
|
||||
`BridgeMPCConfig` also accepts optional `utila?: BridgeUtilaConfig` and `fireblocks?: BridgeFireblocksConfig` cosigner blocks (the types live in `pkg/bridge/src/types.ts` and are reachable through `BridgeMPCConfig`). These are **declarative only**: the SDK carries the tenant-visible identifiers (Utila org id / vault id, Fireblocks API key id / vault account id), and the bridge backend (`app/server/src/domain/cosigners.ts`) holds the matching secrets and completes the cosign. The cosigner layer sits *on top of* native Lux MPC — the backend enforces 2-of-2 (native threshold + external cosigner) before releasing settlement.
|
||||
|
||||
`BridgeMPCConfig.publicUrl` is **optional**: tenants running pure-external custody (utila or fireblocks set, no native threshold) may omit it. The recommended layered mode keeps it set so native MPC remains the primary signer.
|
||||
|
||||
`BrandConfig` keeps the original shape: `name`, optional `logoUrl`, `faviconUrl`, `primaryColor`, `secondaryColor`, `supportEmail`, `docsUrl`.
|
||||
|
||||
### 5.4 SDK dependency layout (verified against `pkg/bridge/package.json` v1.0.1)
|
||||
### 5.4 SDK dependency layout (verified against `pkg/bridge/package.json` v1.0.3)
|
||||
|
||||
Direct prod `dependencies`: `@luxfi/brand`, `@hanzo/gui`, `@hanzogui/core`, `@hanzogui/helpers`, `@hanzogui/constants`, `@hanzogui/themes`, `@hanzogui/animate`, `@hanzogui/animate-presence`, `@hanzogui/button`, `@hanzogui/input`.
|
||||
Direct prod `dependencies` (15 total):
|
||||
|
||||
`peerDependencies` (host-supplied): `react`, `react-dom`, plus the ~20 leaf `@hanzogui/*` components (popover, select, dialog, card, text, stacks, spinner, toast, tabs, label, form, list-item, separator, tooltip, avatar, image, group, progress, visually-hidden, portal, sheet) — all marked `optional` via `peerDependenciesMeta`. This lets host apps tree-shake the parts of the bridge UI they don't render.
|
||||
- **UI primitives (`@hanzo` / `@hanzogui`)**: `@hanzo/gui`, `@hanzogui/core`, `@hanzogui/helpers`, `@hanzogui/constants`, `@hanzogui/themes`, `@hanzogui/config-default`, `@hanzogui/animate`, `@hanzogui/animate-presence`, `@hanzogui/button`, `@hanzogui/input`.
|
||||
- **Brand**: `@luxfi/brand` (`^1.0.0`, published on npm).
|
||||
- **Client-side MPC**: `@luxfi/threshold` (`workspace:*`) — drives `pkg/bridge/src/app/lib/mpc-session.ts`. This was added in the R3 merge; it's the only `workspace:*` runtime dep in the SDK.
|
||||
- **Web3 / data**: `@tanstack/react-query` `5.90.20`, `viem` `2.30.5`, `wagmi` `2.15.5`. All pinned exactly.
|
||||
|
||||
`devDependencies`: `typescript`, `vitest`, `@types/react`, `@types/react-dom`.
|
||||
`peerDependencies` (host-supplied): `react` (`>=18`), `react-dom` (`>=18`), plus 21 leaf `@hanzogui/*` components (popover, select, dialog, card, text, stacks, spinner, toast, tabs, label, form, list-item, separator, tooltip, avatar, image, group, progress, visually-hidden, portal, sheet) — all marked `optional` via `peerDependenciesMeta`. This lets host apps tree-shake the parts of the bridge UI they don't render.
|
||||
|
||||
The SDK **does not** workspace-depend on `@luxbridge/app-v3` (it does not exist) or on `@luxfi/threshold` directly. Today, threshold MPC is exercised from `app/server/` only; the SDK ships an `mpc?: BridgeMPCConfig` block so it can carry cluster URLs + protocol id forward, but does not import the threshold client itself.
|
||||
`devDependencies`: `typescript`, `vitest`, `@testing-library/react`, `happy-dom`, `jsdom`, `@types/react`, `@types/react-dom`.
|
||||
|
||||
> R3 design note: a follow-on change is in flight to add a client-side `mpc-session.ts` under `pkg/bridge/src/app/lib/` that imports `ThresholdClient` directly, plus a declarative `mpc.utila` / `mpc.fireblocks` cosigner layer. That work is **not yet merged into the working tree being documented here** — when it lands, §5.4 must be re-pinned and the SDK version bumped accordingly.
|
||||
The SDK **does not** workspace-depend on any `@luxbridge/app-*` package (those were retired with the inline UI). The single `workspace:*` runtime dep is `@luxfi/threshold`, which is the in-repo T-Chain SDK; it has no transitive workspace deps of its own.
|
||||
|
||||
### 5.5 Tenant shape (verified against `app/bridge/package.json`)
|
||||
|
||||
`@luxbridge/lux-tenant` v1.0.0, `private: true`. Dependencies: `@luxfi/brand ^1.0.0`, `@luxfi/bridge workspace:*`, `@luxfi/logo ^1.0.0`, plus `react` / `react-dom` (catalog) and `react-native-web` for RN-web parity. Scripts: `dev` / `build` / `preview` / `typecheck` against Vite + tsc.
|
||||
`@luxbridge/lux-tenant` v1.0.0, `private: true`.
|
||||
|
||||
Runtime config is supplied via `window.__ENV` (templated from `BRIDGE_*` env at container boot) with an `import.meta.env.VITE_*` build-time fallback for local dev. Tenants do not pass chain configs — they pass `apiHost` + `env` + `brand` + optional auth/kms/wallet/mpc blocks.
|
||||
Dependencies fall into two layers:
|
||||
|
||||
- **Tenant-owned**: `@luxfi/brand ^1.0.0`, `@luxfi/bridge workspace:*`, `@luxfi/logo ^1.0.0`, plus `react` / `react-dom` (catalog) and `react-native-web ^0.19.0` for RN-web parity.
|
||||
- **Pinned transitively** (workaround until §9.3.1 ships a compiled `dist/`): `@hanzogui/config-default ^7.0.0`, `@luxfi/threshold workspace:*`, `@tanstack/react-query 5.90.20`, `viem 2.30.5`, `wagmi 2.15.5`. The SDK is consumed at the source level (its `main`/`types` still point at `src/*`), so Vite's dep-scanner walks these from the tenant's `node_modules/` rather than from the SDK package's nested `node_modules/`. Once the SDK builds to `dist/` (Phase 3.1) and externalizes web3 + UI primitives, tenants will no longer need to re-declare these.
|
||||
|
||||
Scripts: `dev` / `build` / `preview` / `typecheck` against Vite + tsc. The Vite config aliases `react-native` → `react-native-web` and dedupes `react` / `react-dom` / `react-native-web`; downstream tenants must replicate this aliasing — see `pkg/bridge/README.md` ("Consuming this SDK from a Vite app").
|
||||
|
||||
Runtime config is supplied via `window.__ENV` (templated from `BRIDGE_*` env at container boot, served from `/__ENV.js`) with an `import.meta.env.VITE_*` build-time fallback for local dev. Tenants do not pass chain configs — they pass `apiHost` + `env` + `brand` + optional auth/kms/wallet/mpc blocks. Brand defaults are read from `@luxfi/brand/brand.json` so a brand bump propagates without string copies.
|
||||
|
||||
### 5.6 R3 client-side MPC integration (merged)
|
||||
|
||||
The R3 work that R4 listed as "in flight" is now in the working tree. Summary of what landed:
|
||||
|
||||
| Artifact | Location | Purpose |
|
||||
|---|---|---|
|
||||
| `mpc-session.ts` | `pkg/bridge/src/app/lib/mpc-session.ts` | Wraps `@luxfi/threshold`'s `ThresholdClient` so the UI can surface real session progress (sessionId / status / signature) instead of a setTimeout placeholder. Initiates sessions only — no key material on the client. |
|
||||
| `bridge-api.ts` | `pkg/bridge/src/app/lib/bridge-api.ts` | Typed bridge-server REST client. |
|
||||
| `wagmi-config.ts` | `pkg/bridge/src/app/lib/wagmi-config.ts` | Per-tenant wagmi + WalletConnect config derived from `BridgeWalletConfig`. |
|
||||
| `BridgeUtilaConfig` / `BridgeFireblocksConfig` | `pkg/bridge/src/types.ts` | Declarative tenant-visible identifiers for external cosigners. Reachable via `BridgeMPCConfig.utila` / `.fireblocks`. |
|
||||
| `app/server/src/domain/cosigners.ts` (+ tests) | server side | Holds the actual Utila / Fireblocks secret material and performs the cosign. Browser never sees the secret. |
|
||||
| Vitest specs | `pkg/bridge/src/__tests__/` | `mpc-session.test.ts`, `useTransfers-cosigners.test.tsx`, `bridge-api.test.ts`, `wagmi-config.test.ts`, `useSwap.test.tsx`, `useTransfers.test.tsx`, `useWallet.test.tsx` |
|
||||
|
||||
**Invariant (must not regress):** secret material for external cosigners lives on `app/server/` only. The SDK declares the tenant-public identifiers; it must never grow a code path that holds a Utila / Fireblocks secret on the client.
|
||||
|
||||
## 6. Target Architecture
|
||||
|
||||
@@ -192,14 +242,17 @@ Tenant app (~120 LOC) --> | exports: |
|
||||
imports SDK + brand | Bridge, mountBridge |
|
||||
e.g.: | BridgeConfig + sub-blocks|
|
||||
@luxfi/bridge | UI inlined at src/app/ |
|
||||
@luxfi/brand --> | no workspace lazy-load |
|
||||
@luxfi/logo +-----------------------------+
|
||||
@luxfi/brand --> | client-side MPC via |
|
||||
@luxfi/logo | @luxfi/threshold |
|
||||
+-----------------------------+
|
||||
|
|
||||
v
|
||||
+-----------------------------+
|
||||
| app/server (Express + |
|
||||
| Prisma) — Bridge API |
|
||||
| api.bridge.lux.network |
|
||||
| + cosigners.ts (utila / |
|
||||
| fireblocks secrets) |
|
||||
+-----------------------------+
|
||||
|
|
||||
+--------------------+--------------------+
|
||||
@@ -231,8 +284,8 @@ Status of original Phase 1 line items:
|
||||
| 1.1 | Bridge UI compiles and runs against `apiHost: api.bridge.lux.network` in `env: mainnet`. | ✅ — UI is inlined under `pkg/bridge/src/app/` and consumed by `app/bridge/`. |
|
||||
| 1.2 | Commit the in-flight SDK edits that resolved the `@luxbridge/app` workspace cycle. | ✅ — see Phase 1.5 history note in `pkg/bridge/src/Bridge.tsx`. |
|
||||
| 1.3 | Run the SDK end-to-end against Lux testnet. | 🟡 — verify on current branch before mainnet cutover. |
|
||||
| 1.4 | Deploy the Lux tenant app to `bridge.lux.network`. | 🟡 — `app/bridge/` shape is final; deploy pipeline TBC. |
|
||||
| 1.5 | Refresh `docs/BRIDGE-STATUS.md` and `docs/LOCAL-SETUP.md` to point at `app/bridge/`. | 🟡 — these files predate Nextra migration; fold them into `docs/pages/` and remove stale legacy mentions. |
|
||||
| 1.4 | Deploy the Lux tenant app to `bridge.lux.network`. | 🟡 — `app/bridge/` shape is final (`Dockerfile`, `docker-entrypoint.sh`, `__ENV.js` template all merged); deploy pipeline TBC. |
|
||||
| 1.5 | Refresh / fold the legacy `docs/*.md` (`BRIDGE-STATUS`, `LOCAL-SETUP`, etc.). | ✅ — the loose top-level set was deleted in the merge from main; only `docs/LLM.md` and `docs/LUX-ID-INTEGRATION.md` remain at docs root, and active docs now live under `docs/content/docs/`. |
|
||||
| 1.6 | Keep `pkg/bridge/README.md` accurate (it was updated through R2/R3). | ✅ — consumer-facing README documents `Bridge`, `mountBridge`, `applyBrandMetadata`, `getConfig`, `setConfig`. |
|
||||
|
||||
**Acceptance criteria** (recheck before sign-off):
|
||||
@@ -240,7 +293,7 @@ Status of original Phase 1 line items:
|
||||
- A user can reach `bridge.lux.network`, connect a wallet, get a quote on Lux testnet, and complete a bridge transaction signed by an m-chain MPC quorum.
|
||||
- Internal devs can run `pnpm dev` from `app/bridge/` against local `app/server/` and complete the same flow.
|
||||
- Treasury fee collection through the private MPC cluster fires on a real transfer.
|
||||
- `pkg/bridge/README.md` matches the current API surface in §5.3.
|
||||
- `pkg/bridge/README.md` matches the current API surface in §5.3 (including the cosigner block additions from R3).
|
||||
|
||||
## 8. Phase 2 — Multi-Tenant Embed
|
||||
|
||||
@@ -254,7 +307,7 @@ Status of original Phase 1 line items:
|
||||
| 2.2 | Publish `@zooai/brand` and `@zooai/logo` if not yet published; confirm `@hanzoai/brand` if needed. | Out-of-tree, coordinate with brand owners. |
|
||||
| 2.3 | Add an `@luxfi/bridge` mount inside `luxfi/exchange/apps/web/src/main.tsx` (mirroring the existing `@luxfi/exchange` SDK pattern). | Cross-repo. |
|
||||
| 2.4 | Same for `zooai/exchange/apps/web/src/main.tsx`. | Cross-repo. |
|
||||
| 2.5 | Confirm `BrandConfig` + the auth/kms/wallet/mpc blocks cover everything Zoo + Hanzo need without code changes in `pkg/bridge/`. If anything must be hardcoded, surface it as a new optional config field with semver-minor bump. | Drives any v1.1.x of the SDK. |
|
||||
| 2.5 | Confirm `BrandConfig` + the auth/kms/wallet/mpc blocks (including the cosigner sub-blocks) cover everything Zoo + Hanzo need without code changes in `pkg/bridge/`. If anything must be hardcoded, surface it as a new optional config field with semver-minor bump. | Drives any v1.1.x of the SDK. |
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
@@ -270,12 +323,12 @@ Status of original Phase 1 line items:
|
||||
|
||||
| # | Task | Notes |
|
||||
|---|---|---|
|
||||
| 3.1 | Switch `pkg/bridge/package.json` `main`/`types` from `src/*.ts` to compiled `dist/*.js` + `.d.ts` outputs. Add a real `build` step that emits both. | Today `main`/`types` point at `src/*` and `build` is `tsc --noEmit`. |
|
||||
| 3.2 | Externalize React + react-dom + `@luxfi/brand` from the published bundle. | Already peer-deps; verify after build step lands. |
|
||||
| 3.3 | Confirm there are no `workspace:*` deps left in the publishable surface. | UI is inlined; `@hanzogui/*` are already on npm; `@luxfi/brand` is already published `^1.0.0`. |
|
||||
| 3.1 | Switch `pkg/bridge/package.json` `main`/`types` from `src/*.ts` to compiled `dist/*.js` + `.d.ts` outputs. Add a real `build` step that emits both. | Today `main`/`types` point at `src/*` and `build` is `tsc --noEmit`. This is what forces tenants to re-declare web3 / `@hanzogui/config-default` / `@luxfi/threshold` in their own `package.json` (§5.5). |
|
||||
| 3.2 | Externalize React + react-dom + `@luxfi/brand` (and ideally `wagmi` / `viem` / `@tanstack/react-query`) from the published bundle. | React / react-dom are already peer-deps; verify the new web3 trio after the build step lands. |
|
||||
| 3.3 | Confirm there are no `workspace:*` deps left in the publishable surface. | `@luxfi/threshold` is currently a `workspace:*` runtime dep — it must be published to npm (or vendored / re-exported through `dist/`) before SDK publication. |
|
||||
| 3.4 | Publish `@luxfi/bridge` v1.x → npm under the `@luxfi` scope (public access). | `pnpm pub` from `pkg/bridge/`. |
|
||||
| 3.5 | Land the R3 client-side MPC integration (`mpc-session.ts` + `mpc.utila` / `mpc.fireblocks` declarative cosigner layer) and bump SDK accordingly. | This is the work referenced in §5.4 R3 design note. Secret material stays on `app/server/`. |
|
||||
| 3.6 | Document standalone-backend mode for : how to point `apiHost` at a non-Lux endpoint, what server API contract `app/server/` exposes, what's pluggable. | New doc under `docs/pages/`. |
|
||||
| 3.5 | ~~Land the R3 client-side MPC integration~~ — **done** (R5). `mpc-session.ts`, `bridge-api.ts`, `wagmi-config.ts`, `BridgeUtilaConfig` / `BridgeFireblocksConfig`, server-side `cosigners.ts` + tests are all merged; SDK bumped to v1.0.3. Remaining sub-task: end-to-end soak against a real Utila and a real Fireblocks tenant on testnet. | Secret material stays on `app/server/` — invariant must not regress. |
|
||||
| 3.6 | Document standalone-backend mode for : how to point `apiHost` at a non-Lux endpoint, what server API contract `app/server/` exposes, what's pluggable. | New doc under `docs/content/docs/`. |
|
||||
| 3.7 | Confirm can render their own securities (AAPL, etc.) via the existing tokens/assets registry (`@luxbridge/settings`) without forking. If not, add an injection point. | May surface a `tokens?` field on `BridgeConfig`. |
|
||||
|
||||
**Acceptance criteria**
|
||||
@@ -283,6 +336,7 @@ Status of original Phase 1 line items:
|
||||
- `npm install @luxfi/bridge` from a fresh project succeeds; the Quick Start in `pkg/bridge/README.md` produces a running bridge in <5 minutes.
|
||||
- A third party can stand up a bridge with their own `apiHost` and `BrandConfig` without modifying SDK source.
|
||||
- A new standalone-backend doc clearly describes the path.
|
||||
- The cosigner layer has been exercised end-to-end against at least one real external custodian on testnet.
|
||||
|
||||
## 10. Outstanding Tenant Work
|
||||
|
||||
@@ -292,24 +346,34 @@ The earlier `zoo/bridge` proxy-Go code and `lux-shim`/`zoo/bridge/app` overlay d
|
||||
|
||||
### 10.2 SDK working state
|
||||
|
||||
`pkg/bridge/` is checked in at v1.0.1 on `whispers/bridgev2`. Recent merges (see `git log`): `phase3-r2 stack (inline UI + cleanup + Tamagui swap)`, `phase1 SDK config blocks (auth/kms/wallet/mpc + PQ protocol union)`, `wire @luxfi/logo as canonical Lux logo source`. The R3 client-side MPC + cosigner work referenced in §5.4 / §9.3.5 is the next merge in flight.
|
||||
`pkg/bridge/` is at **v1.0.3** on `whispers/bridgev2`. Recent merges (see `git log`):
|
||||
|
||||
- `phase3-r2 stack (inline UI + cleanup + Tamagui swap)` — inlined the bridge UI.
|
||||
- `phase1 SDK config blocks (auth/kms/wallet/mpc + PQ protocol union)` — added the optional config blocks.
|
||||
- `wire @luxfi/logo as canonical Lux logo source` — brand wiring.
|
||||
- `merge: swarm/integration → main` (phase1-3 + `@luxfi/logo`) — bundled the R3 work into main.
|
||||
- `merge: main → whispers/bridgev2` (R5) — pulled the **R3 client-side MPC + cosigner stack** into this working tree: `mpc-session.ts`, `bridge-api.ts`, `wagmi-config.ts`, `BridgeUtilaConfig` / `BridgeFireblocksConfig`, `app/server/src/domain/cosigners.ts` + tests, plus tenant deploy artefacts (`Dockerfile`, `docker-entrypoint.sh`, `__ENV.js` template).
|
||||
- `chore: env has been set up successfully` — environment bring-up after the R3 merge: added the transitively-required deps to `app/bridge/package.json` (see §5.5) and a `prepare` script to `pkg/threshold/` so `dist/` builds on `pnpm install`.
|
||||
|
||||
The next outstanding SDK work is **Phase 3.1** (compile to `dist/`) so external consumers can install the package without source-level workarounds, and **Phase 3.5 remainder** (e2e soak of the cosigner layer against a real external custodian).
|
||||
|
||||
## 11. Risks & Open Questions
|
||||
|
||||
| # | Item | Risk / impact | Owner |
|
||||
|---|---|---|---|
|
||||
| R1 | Build step still emits no `dist/`. Publishing today would ship `src/*.ts` directly — works for workspace consumers, breaks downstream type resolution for non-pnpm users. | Blocks Phase 3.1/3.4. | SDK |
|
||||
| R2 | Top-level `docs/*.md` files (`BRIDGE-STATUS.md`, `LOCAL-SETUP.md`, the MPC-* set) predate the Nextra migration and still mention legacy paths. New contributors will follow stale instructions. | Wasted onboarding cycles. | Docs / Phase 1.5 |
|
||||
| R3 | The R3 cosigner layer (`mpc.utila` / `mpc.fireblocks`) is purely declarative in the SDK type. Secrets must stay on `app/server/`; a future implementer must not move them client-side by accident. | Security regression. | SDK + server, Phase 3.5 |
|
||||
| R1 | Build step still emits no `dist/`. Publishing today would ship `src/*.ts` directly — works for workspace consumers (since pnpm hoists transitive deps locally), breaks downstream type resolution for non-pnpm users and forces tenants to re-declare web3 deps. | Blocks Phase 3.1 / 3.4. | SDK |
|
||||
| R2 | `@luxfi/threshold` is a `workspace:*` runtime dep of `@luxfi/bridge`. Publishing `@luxfi/bridge` without first publishing (or vendoring) threshold will produce an unresolvable install for external consumers. | Blocks Phase 3.3 / 3.4. | SDK |
|
||||
| R3 | The cosigner layer (`mpc.utila` / `mpc.fireblocks`) is declarative on the SDK side and secret-holding on the server side. A future implementer must not move secret material client-side. The SDK type surface deliberately omits any field that would carry a secret. | Security regression. | SDK + server, Phase 3.5 e2e. |
|
||||
| R4 | 's securities (AAPL, etc.) may require asset-registry injection points the current `@luxbridge/settings` does not expose. | Could force a SDK breaking change. | Investigate during Phase 3.7. |
|
||||
| R5 | New `auth` / `kms` / `wallet` blocks are not yet exercised end-to-end by a non-Lux tenant; behavior under Zoo/Hanzo branding is unverified. | Auth break for Zoo/Hanzo. | Phase 2 e2e. |
|
||||
| R5 | New `auth` / `kms` / `wallet` / cosigner blocks are not yet exercised end-to-end by a non-Lux tenant; behavior under Zoo/Hanzo branding is unverified. | Auth + cosigner break for Zoo/Hanzo. | Phase 2 e2e. |
|
||||
| R6 | `app/bridge/package.json` currently re-declares deps that morally belong to `@luxfi/bridge` (web3 trio + `@hanzogui/config-default` + `@luxfi/threshold`). If a tenant lags an SDK upgrade, version drift between the tenant's declared pins and what the SDK actually uses can cause subtle runtime breaks. Phase 3.1 retires this risk. | Drift between tenant and SDK pins. | SDK, Phase 3.1. |
|
||||
|
||||
**Open questions for sponsor:**
|
||||
|
||||
1. Is Hanzo a tenant in this round, or only Lux + Zoo + (later) ?
|
||||
2. Target date for the R3 client-side MPC + cosigner merge (drives Phase 3 schedule).
|
||||
3. Should the loose top-level `docs/*.md` files be folded into the Nextra site under `docs/pages/`, or deleted outright?
|
||||
4. Should `pkg/utila/` (and any equivalent `pkg/fireblocks/` if added) ship as published `@luxfi/*` packages, or stay private workspace helpers consumed only by `app/server/`?
|
||||
2. Target date / acceptance bar for the Phase 3.5 cosigner e2e soak (Utila + Fireblocks on testnet).
|
||||
3. Should `pkg/utila/` (and any equivalent `pkg/fireblocks/` if added) ship as published `@luxfi/*` packages, or stay private workspace helpers consumed only by `app/server/`?
|
||||
4. Publication timing for `@luxfi/threshold` (gates SDK publication — see R2).
|
||||
|
||||
## 12. Out-of-Tree References
|
||||
|
||||
|
||||
@@ -21,5 +21,10 @@ window.__ENV = {
|
||||
BRIDGE_IAM_ORG: '${BRIDGE_IAM_ORG:-}',
|
||||
BRIDGE_LOGO_URL: '${BRIDGE_LOGO_URL:-}',
|
||||
WC_PROJECT_ID: '${WC_PROJECT_ID:-}',
|
||||
BRIDGE_WALLET_DEFAULT_CHAIN: '${BRIDGE_WALLET_DEFAULT_CHAIN:-}',
|
||||
BRIDGE_WALLET_SUPPORTED_CHAINS: '${BRIDGE_WALLET_SUPPORTED_CHAINS:-}',
|
||||
BRIDGE_MPC_PUBLIC_URL: '${BRIDGE_MPC_PUBLIC_URL:-}',
|
||||
BRIDGE_MPC_PRIVATE_URL: '${BRIDGE_MPC_PRIVATE_URL:-}',
|
||||
BRIDGE_MPC_PROTOCOL: '${BRIDGE_MPC_PROTOCOL:-}',
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -11,4 +11,9 @@ window.__ENV = window.__ENV || {
|
||||
BRIDGE_IAM_ORG: '',
|
||||
BRIDGE_LOGO_URL: '',
|
||||
WC_PROJECT_ID: '',
|
||||
BRIDGE_WALLET_DEFAULT_CHAIN: '',
|
||||
BRIDGE_WALLET_SUPPORTED_CHAINS: '',
|
||||
BRIDGE_MPC_PUBLIC_URL: '',
|
||||
BRIDGE_MPC_PRIVATE_URL: '',
|
||||
BRIDGE_MPC_PROTOCOL: '',
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@
|
||||
// 3. @luxfi/brand defaults — final fallback to the canonical Lux brand
|
||||
// package's brand.json values.
|
||||
|
||||
import type { BridgeConfig } from '@luxfi/bridge'
|
||||
import type {
|
||||
BridgeConfig,
|
||||
BridgeMPCConfig,
|
||||
BridgeWalletConfig,
|
||||
} from '@luxfi/bridge'
|
||||
import luxBrandJson from '@luxfi/brand/brand.json'
|
||||
import { getColorSVG } from '@luxfi/logo'
|
||||
|
||||
@@ -61,13 +65,98 @@ const env = (key: string, fallback?: string): string | undefined => {
|
||||
|
||||
const luxLogoDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(getColorSVG())}`
|
||||
|
||||
// Bridge endpoints belong to the bridge subdomain — independent of
|
||||
// @luxfi/brand's appDomain (which is "lux.exchange" for the exchange).
|
||||
const fallbackApiHost = 'https://api.bridge.lux.network'
|
||||
// Live bridge REST endpoint. Note the hostname is `bridge-api.lux.network`
|
||||
// (hyphenated) — this is the same Express backend the production
|
||||
// bridge.lux.network frontend uses today. The previous `api.bridge.lux.network`
|
||||
// fallback never resolved in DNS, which is why the UI used to surface
|
||||
// "Failed to fetch" on first load. When a public B-Chain (BridgeVM) JSON-RPC
|
||||
// endpoint is exposed, the SDK will try that first via `BridgeConfig.rpc`
|
||||
// and only fall back to this REST host on RPC failure.
|
||||
const fallbackApiHost = 'https://bridge-api.lux.network'
|
||||
const fallbackDocsUrl = 'https://docs.lux.network/bridge'
|
||||
|
||||
// MPC endpoints. publicUrl is the m-chain (public threshold network, used
|
||||
// for user wallets); privateUrl is the treasury cluster (used for fee
|
||||
// collection only and not always reachable from the tenant). Protocol
|
||||
// defaults to cggmp21 — the canonical classical ECDSA-CMP variant called
|
||||
// out as leaderless / permissionless-safe in REQUIREMENTS.md §5.3.
|
||||
const fallbackMpcPublicUrl = 'https://mpc.lux.network'
|
||||
const fallbackMpcProtocol: NonNullable<BridgeMPCConfig['protocol']> = 'cggmp21'
|
||||
|
||||
// Curated EVM allow-list for the Lux tenant. The SDK's full supported set
|
||||
// (mainnet, arbitrum, base, polygon, optimism) is intentionally narrowed
|
||||
// here so the user wallet only sees chains the bridge backend actually
|
||||
// settles to. Non-EVM destinations (Lux native, Solana) are handled by the
|
||||
// MPC threshold layer and don't appear in the wagmi connector.
|
||||
const fallbackSupportedChainIds = [1, 42161, 8453, 137, 10]
|
||||
const fallbackDefaultChainId = 1
|
||||
|
||||
/**
|
||||
* Parse a comma-separated list of numeric chain IDs from env, dropping
|
||||
* non-numeric entries. Empty / unset → `undefined` so the caller can fall
|
||||
* back to the static allow-list.
|
||||
*/
|
||||
const parseChainIds = (raw: string | undefined): number[] | undefined => {
|
||||
if (!raw) return undefined
|
||||
const ids = raw
|
||||
.split(',')
|
||||
.map((s) => Number.parseInt(s.trim(), 10))
|
||||
.filter((n) => Number.isFinite(n))
|
||||
return ids.length > 0 ? ids : undefined
|
||||
}
|
||||
|
||||
const parseChainId = (raw: string | undefined): number | undefined => {
|
||||
if (!raw) return undefined
|
||||
const n = Number.parseInt(raw, 10)
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}
|
||||
|
||||
const wallet: BridgeWalletConfig = {
|
||||
walletConnectProjectId: env('WC_PROJECT_ID'),
|
||||
defaultChainId: parseChainId(env('BRIDGE_WALLET_DEFAULT_CHAIN')) ?? fallbackDefaultChainId,
|
||||
supportedChainIds:
|
||||
parseChainIds(env('BRIDGE_WALLET_SUPPORTED_CHAINS')) ?? fallbackSupportedChainIds,
|
||||
}
|
||||
|
||||
// MPC protocol must be one of the union values in BridgeMPCConfig.protocol.
|
||||
// Anything else from env → fall back to cggmp21 rather than producing a
|
||||
// type-cast lie.
|
||||
const ALLOWED_MPC_PROTOCOLS: readonly NonNullable<BridgeMPCConfig['protocol']>[] = [
|
||||
'cggmp21',
|
||||
'frost',
|
||||
'bls',
|
||||
'doerner',
|
||||
'pulsar',
|
||||
'corona',
|
||||
'magnetar',
|
||||
]
|
||||
|
||||
const parseMpcProtocol = (
|
||||
raw: string | undefined,
|
||||
): NonNullable<BridgeMPCConfig['protocol']> => {
|
||||
if (!raw) return fallbackMpcProtocol
|
||||
const match = ALLOWED_MPC_PROTOCOLS.find((p) => p === raw)
|
||||
return match ?? fallbackMpcProtocol
|
||||
}
|
||||
|
||||
const mpc: BridgeMPCConfig = {
|
||||
publicUrl: env('BRIDGE_MPC_PUBLIC_URL', fallbackMpcPublicUrl),
|
||||
privateUrl: env('BRIDGE_MPC_PRIVATE_URL'),
|
||||
protocol: parseMpcProtocol(env('BRIDGE_MPC_PROTOCOL')),
|
||||
}
|
||||
|
||||
// In Vite dev mode we want apiHost to be same-origin so /api/* routes through
|
||||
// the Vite dev proxy (which injects the production Origin header — see
|
||||
// vite.config.ts). In production builds we use the explicit env value or the
|
||||
// canonical bridge-api.lux.network fallback (where the prod origin is on
|
||||
// the server's CORS allow-list).
|
||||
const devApiHost =
|
||||
import.meta.env.DEV && typeof window !== 'undefined'
|
||||
? window.location.origin
|
||||
: undefined
|
||||
|
||||
export const bridgeConfig: BridgeConfig = {
|
||||
apiHost: env('BRIDGE_API_HOST', fallbackApiHost)!,
|
||||
apiHost: devApiHost ?? env('BRIDGE_API_HOST', fallbackApiHost)!,
|
||||
env: env('BRIDGE_ENV', 'mainnet')!,
|
||||
clientId: env('BRIDGE_CLIENT_ID'),
|
||||
iamOrg: env('BRIDGE_IAM_ORG', 'lux'),
|
||||
@@ -84,4 +173,6 @@ export const bridgeConfig: BridgeConfig = {
|
||||
supportEmail: supportEmail ?? '[email protected]',
|
||||
docsUrl: fallbackDocsUrl,
|
||||
},
|
||||
wallet,
|
||||
mpc,
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -12,6 +12,11 @@ interface ImportMetaEnv {
|
||||
readonly VITE_BRIDGE_IAM_ORG?: string
|
||||
readonly VITE_BRIDGE_LOGO_URL?: string
|
||||
readonly VITE_WC_PROJECT_ID?: string
|
||||
readonly VITE_BRIDGE_WALLET_DEFAULT_CHAIN?: string
|
||||
readonly VITE_BRIDGE_WALLET_SUPPORTED_CHAINS?: string
|
||||
readonly VITE_BRIDGE_MPC_PUBLIC_URL?: string
|
||||
readonly VITE_BRIDGE_MPC_PRIVATE_URL?: string
|
||||
readonly VITE_BRIDGE_MPC_PROTOCOL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -22,7 +22,33 @@ export default defineConfig({
|
||||
'process.env.TAMAGUI_TARGET': JSON.stringify('web'),
|
||||
__DEV__: 'false',
|
||||
},
|
||||
server: { port: 3001, host: true },
|
||||
server: {
|
||||
port: 3001,
|
||||
host: true,
|
||||
// Dev-only proxy that fronts the production bridge REST API. Browsers
|
||||
// would otherwise be blocked: bridge-api.lux.network returns
|
||||
// `Access-Control-Allow-Origin: https://bridge.lux.network` and a 500
|
||||
// for any other origin. With this proxy in place the browser sees a
|
||||
// same-origin request (`/api/*` → localhost) and Vite forwards it to
|
||||
// bridge-api.lux.network with `Origin: https://bridge.lux.network`
|
||||
// injected. Production builds talk directly to the API (the prod
|
||||
// origin is whitelisted server-side) so this block has no effect on
|
||||
// the deployed image.
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://bridge-api.lux.network',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
headers: {
|
||||
// Tell bridge-api.lux.network this request came from the prod
|
||||
// origin so its CORS allow-list passes. changeOrigin already
|
||||
// rewrites the Host header; this overrides the Origin header
|
||||
// that node-http-proxy would otherwise forward verbatim.
|
||||
Origin: 'https://bridge.lux.network',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
preview: { port: 3001, host: true },
|
||||
build: { outDir: 'dist', sourcemap: true },
|
||||
})
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
VITE_SERVER_URI=https://api-bridge.lux.network
|
||||
VITE_NET_VERSION=testnet
|
||||
@@ -0,0 +1,41 @@
|
||||
export interface Brand {
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
shortName: string
|
||||
legalEntity: string
|
||||
appDomain: string
|
||||
twitter: string
|
||||
github: string
|
||||
discord: string
|
||||
logoUrl: string
|
||||
faviconUrl: string
|
||||
}
|
||||
|
||||
const defaults: Brand = {
|
||||
name: 'Lux Bridge',
|
||||
title: 'Lux Bridge',
|
||||
description: 'Cross-chain bridge powered by Lux Network',
|
||||
shortName: 'LUX',
|
||||
legalEntity: 'Lux Industries Inc.',
|
||||
appDomain: 'bridge.lux.network',
|
||||
twitter: 'https://x.com/luxfi',
|
||||
github: 'https://github.com/luxfi',
|
||||
discord: 'https://discord.gg/lux',
|
||||
logoUrl: '/favicon.ico',
|
||||
faviconUrl: '/favicon.ico',
|
||||
}
|
||||
|
||||
export let brand: Brand = { ...defaults }
|
||||
|
||||
export async function loadBrand(): Promise<Brand> {
|
||||
try {
|
||||
const res = await fetch('/brand.json')
|
||||
if (!res.ok) return brand
|
||||
const config = await res.json()
|
||||
if (config.brand) Object.assign(brand, config.brand)
|
||||
const baseName = brand.name.replace(/\s*bridge\s*/i, '').trim()
|
||||
if (!brand.shortName && baseName) brand.shortName = baseName.length <= 3 ? baseName.toUpperCase() : baseName
|
||||
} catch {}
|
||||
return brand
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type PropsWithChildren } from 'react'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
} from '@hanzo/ui/primitives-common'
|
||||
|
||||
const TooltipHelper: React.FC<{
|
||||
text: string
|
||||
tooltipClx?: string
|
||||
} & PropsWithChildren> = ({
|
||||
children,
|
||||
text,
|
||||
tooltipClx=''
|
||||
}) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={tooltipClx}>
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const formatAvailable = (v: number | null): string => {
|
||||
if (!v) return ''
|
||||
let str = v.toFixed(5)
|
||||
if (v - parseFloat(str) !== 0) {
|
||||
str = '~' + str
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
const AmountAvailable: React.FC<{
|
||||
amount?: number
|
||||
fromAssetName: string
|
||||
}> = ({
|
||||
amount,
|
||||
fromAssetName
|
||||
}) => {
|
||||
|
||||
const formattedAmount = formatAvailable(amount ?? null)
|
||||
const availableWasRounded = formattedAmount?.startsWith('~')
|
||||
|
||||
return (
|
||||
<span className='block shrink-0 cursor-default'>
|
||||
{availableWasRounded ? (
|
||||
<TooltipHelper text={`${amount} ${fromAssetName}`} tooltipClx='text-foreground'>
|
||||
<span>{formattedAmount} {fromAssetName} avail</span>
|
||||
</TooltipHelper>
|
||||
) : (<>
|
||||
<span>{formattedAmount} {fromAssetName} avail</span>
|
||||
</>)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AmountAvailable
|
||||
@@ -0,0 +1,32 @@
|
||||
import { type PropsWithChildren } from 'react'
|
||||
import { TooltipWrapper } from '@hanzo/ui/primitives-common'
|
||||
|
||||
import { formatToMaxChar } from '@hanzo/ui/util'
|
||||
|
||||
|
||||
const AssetQuantityAvailable: React.FC<{
|
||||
quantity: number
|
||||
maxChars: number
|
||||
fromAssetName: string
|
||||
}> = ({
|
||||
quantity,
|
||||
maxChars,
|
||||
fromAssetName
|
||||
}) => {
|
||||
|
||||
const { result: formatted, change } = formatToMaxChar(quantity, maxChars)
|
||||
|
||||
return (
|
||||
<span className='block shrink-0 cursor-default'>
|
||||
{change === 'rounded' ? (
|
||||
<TooltipWrapper text={`${quantity} ${fromAssetName}`} tooltipClx='text-foreground'>
|
||||
<span>~{formatted} {fromAssetName} avail</span>
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
<span>{quantity} {fromAssetName} avail</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssetQuantityAvailable
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { observable, reaction, type IObservableValue } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { TooltipWrapper } from '@hanzo/ui/primitives-common'
|
||||
|
||||
import { cn, type FormatThreshold, formatAndAbbreviateAsCurrency } from '@hanzo/ui/util'
|
||||
|
||||
import { useSwapState } from '@/contexts/swap-state'
|
||||
|
||||
const THRESHOLDS = [
|
||||
{
|
||||
from: 1000000000,
|
||||
use: 'M'
|
||||
},
|
||||
{
|
||||
from: 1000000000000,
|
||||
use: 'B'
|
||||
},
|
||||
] satisfies FormatThreshold[]
|
||||
|
||||
|
||||
const DollarValue: React.FC<{
|
||||
className?: string
|
||||
}> = observer(({
|
||||
className='',
|
||||
}) => {
|
||||
|
||||
const swapState = useSwapState()
|
||||
const valueRef = useRef<IObservableValue<string | null>>(observable.box(''))
|
||||
const tooltipRef = useRef<IObservableValue<string | null>>(observable.box(''))
|
||||
|
||||
useEffect(() => (
|
||||
reaction(
|
||||
() => ({
|
||||
quantity: swapState.fromAssetQuantity,
|
||||
price: swapState.fromAssetPriceUSD
|
||||
}),
|
||||
({quantity, price}) => {
|
||||
const value: number | null = (quantity > 0 && price) ? quantity * price : null
|
||||
const {
|
||||
result,
|
||||
change,
|
||||
full
|
||||
} = formatAndAbbreviateAsCurrency(value, THRESHOLDS)
|
||||
//console.log(`RES: ${result}, CH: ${change}, FULL: ${full}`)
|
||||
valueRef.current.set(((change === 'rounded') ? '~' : '') + result)
|
||||
tooltipRef.current.set((change === 'rounded') ? full : null)
|
||||
},
|
||||
{ fireImmediately: true }
|
||||
)
|
||||
), [])
|
||||
|
||||
return (
|
||||
<span className={cn('block cursor-default', className)}>
|
||||
{tooltipRef.current.get() ? (
|
||||
<TooltipWrapper text={tooltipRef.current.get()!} tooltipClx='text-foreground'>
|
||||
<span>{valueRef.current.get()}</span>
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
<span>{valueRef.current.get()}</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
export default DollarValue
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useRef } from 'react'
|
||||
import { observable, runInAction, type IObservableValue } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
import { Button, Input } from '@hanzo/ui/primitives-common'
|
||||
|
||||
import { useSwapState } from '@/contexts/swap-state'
|
||||
|
||||
import AssetCombobox from '../asset-combobox'
|
||||
import AssetQuantityAvailable from './asset-quantity-available'
|
||||
import DollarValue from './dollar-value'
|
||||
|
||||
const AssetCard: React.FC<{
|
||||
assetQuantityAvailable?: number
|
||||
onQuantityChanged?: (am: number) => void
|
||||
debounceQuantityChangesBy?: number
|
||||
className?: string
|
||||
}> = observer(({
|
||||
assetQuantityAvailable,
|
||||
onQuantityChanged,
|
||||
debounceQuantityChangesBy=600,
|
||||
className=''
|
||||
}) => {
|
||||
|
||||
const swapState = useSwapState()
|
||||
|
||||
// Use this extra state string to better handle initial decimal point
|
||||
// and other subtleties.
|
||||
const quantityDisplayRef = useRef<IObservableValue<string>>(observable.box(''))
|
||||
|
||||
const debouncedOnQuantityChanged = useDebouncedCallback(
|
||||
(v: number) => { onQuantityChanged && onQuantityChanged(v) },
|
||||
debounceQuantityChangesBy
|
||||
)
|
||||
|
||||
const onChange: React.ChangeEventHandler<HTMLInputElement> = (
|
||||
event
|
||||
) => {
|
||||
let str = event.target.value
|
||||
if (str === '.') {
|
||||
str = '0.'
|
||||
}
|
||||
else if (str === '0' && quantityDisplayRef.current.get() === '0.') {
|
||||
str = ''
|
||||
}
|
||||
const newValue = Number(str)
|
||||
if (!isNaN(newValue)) {
|
||||
runInAction(() => {
|
||||
quantityDisplayRef.current.set(str)
|
||||
})
|
||||
}
|
||||
swapState.setFromAssetQuantity(newValue)
|
||||
debouncedOnQuantityChanged(newValue)
|
||||
}
|
||||
|
||||
const onMaxClicked = () => {
|
||||
swapState.setFromAssetQuantity(assetQuantityAvailable!)
|
||||
debouncedOnQuantityChanged(assetQuantityAvailable!)
|
||||
runInAction(() => {
|
||||
quantityDisplayRef.current.set(assetQuantityAvailable!.toString())
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('border border-muted-4 py-2 pl-3 pr-1.5 has-[:focus]:border-muted', className)}>
|
||||
<div className='flex justify-between items-center gap-1'>
|
||||
<Input
|
||||
className={
|
||||
'min-w-0 text-foreground focus:text-accent bg-level-0 p-1 ' +
|
||||
'!border-none focus:!outline-none focus-visible:!ring-0 focus:!ring-0 text-xl !placeholder-muted-3'
|
||||
}
|
||||
lang="en"
|
||||
placeholder='0'
|
||||
min='0'
|
||||
type='text'
|
||||
value={quantityDisplayRef.current.get()}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<AssetCombobox
|
||||
assets={swapState.fromAssets}
|
||||
asset={swapState.fromAsset}
|
||||
setAsset={swapState.setFromAsset}
|
||||
buttonClx='shrink-0 border-none pr-0 -mr-1'
|
||||
popoverAlign='end'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-between items-center text-muted text-sm gap-1.5'>
|
||||
<DollarValue />
|
||||
<div className='flex items-center shrink-0'>
|
||||
{ (assetQuantityAvailable && swapState.fromAsset) ? (<>
|
||||
<AssetQuantityAvailable
|
||||
quantity={assetQuantityAvailable}
|
||||
maxChars={8}
|
||||
fromAssetName={swapState.fromAsset?.name ?? ''}
|
||||
/>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={onMaxClicked}
|
||||
size='xs'
|
||||
disabled={assetQuantityAvailable === swapState.fromAssetQuantity}
|
||||
className='font-sans text-muted bg-level-1 ml-2 h-6'
|
||||
>
|
||||
max
|
||||
</Button>
|
||||
</>) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default AssetCard
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Combobox, type ComboboxTriggerProps, type ListAdaptor } from '@hanzo/ui/primitives-common'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
import type { Asset } from '@luxfi/core'
|
||||
|
||||
const adaptor = {
|
||||
getValue: (el: Asset): string => (el.asset),
|
||||
equals: (el1: Asset, el2: Asset): boolean => (
|
||||
el1.asset.toUpperCase() === el2.asset.toUpperCase()
|
||||
),
|
||||
valueEquals: (el: Asset, v: string): boolean => (
|
||||
el.asset.toUpperCase() === v.toUpperCase()
|
||||
),
|
||||
getLabel: (el: Asset): string => (el.name),
|
||||
getImageUrl: (el: Asset): string => (el.logo),
|
||||
isDisabled: (el: Asset): boolean => (el.status !== 'active')
|
||||
|
||||
} satisfies ListAdaptor<Asset>
|
||||
|
||||
const AssetCombobox: React.FC<{
|
||||
assets: Asset[]
|
||||
asset: Asset | null
|
||||
setAsset: (Asset: Asset | null) => void
|
||||
placeholder?: string
|
||||
searchHint?: string
|
||||
disabled?: boolean
|
||||
buttonClx?: string
|
||||
popoverClx?: string
|
||||
popoverAlign? : "center" | "end" | "start"
|
||||
}> = ({
|
||||
assets,
|
||||
asset,
|
||||
setAsset,
|
||||
placeholder,
|
||||
searchHint,
|
||||
disabled=false,
|
||||
buttonClx='',
|
||||
popoverClx='',
|
||||
popoverAlign = 'center',
|
||||
}) => (
|
||||
|
||||
<Combobox<Asset, ComboboxTriggerProps<Asset>>
|
||||
elements={assets}
|
||||
current={asset}
|
||||
setCurrent={setAsset}
|
||||
adaptor={adaptor}
|
||||
noCheckmark
|
||||
searchPlaceholder={searchHint}
|
||||
popoverClx={cn('font-sans font-medium w-full', popoverClx)}
|
||||
popoverAlign={popoverAlign}
|
||||
listItemClx='bg-background hover:!bg-level-3'
|
||||
listItemSelectedClx='!bg-level-2' // fw default is level-3
|
||||
listItemDisabledClx='!opacity-30' // fw default is 35
|
||||
triggerProps={{
|
||||
open: false,
|
||||
current: (asset ?? null),
|
||||
currentLabel: null,
|
||||
imageUrl: null,
|
||||
placeholder,
|
||||
imageClx: 'rounded',
|
||||
buttonClx: cn('font-sans font-medium w-auto pl-1.5 pr-2', buttonClx),
|
||||
disabled
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
export default AssetCombobox
|
||||
@@ -0,0 +1,201 @@
|
||||
import React from 'react'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle
|
||||
} from '@hanzo/ui/primitives-common'
|
||||
|
||||
import type ChildMenu from '@/hanzo-ui-types/child-menu'
|
||||
import type LinkDefExtended from '@/hanzo-ui-types/link-def-extended'
|
||||
|
||||
const DesktopNav: React.FC<{
|
||||
links: LinkDefExtended[],
|
||||
isMenuOpened: boolean,
|
||||
setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}> = ({ links, isMenuOpened, setIsMenuOpen }) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
const preventScroll = (e: WheelEvent | TouchEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (isMenuOpened) {
|
||||
window.addEventListener('wheel', preventScroll, { passive: false })
|
||||
window.addEventListener('touchmove', preventScroll, { passive: false })
|
||||
window.addEventListener('keydown', preventScrollKeys, { passive: false })
|
||||
} else {
|
||||
window.removeEventListener('wheel', preventScroll)
|
||||
window.removeEventListener('touchmove', preventScroll)
|
||||
window.removeEventListener('keydown', preventScrollKeys)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('wheel', preventScroll)
|
||||
window.removeEventListener('touchmove', preventScroll)
|
||||
window.removeEventListener('keydown', preventScrollKeys)
|
||||
}
|
||||
}, [isMenuOpened])
|
||||
|
||||
const preventScrollKeys = (e: KeyboardEvent) => {
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseEnter = React.useCallback(() => {
|
||||
setIsMenuOpen(true)
|
||||
}, [setIsMenuOpen])
|
||||
|
||||
const handleMouseLeave = React.useCallback(() => {
|
||||
setIsMenuOpen(false)
|
||||
}, [setIsMenuOpen])
|
||||
|
||||
const handleContentMouseMove = () => {
|
||||
setIsMenuOpen(false)
|
||||
}
|
||||
|
||||
const menuHiddenClass = !isMenuOpened ? 'invisible' : ''
|
||||
|
||||
return links.length > 0 ? (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{links.map((el, index) => (
|
||||
<NavigationMenuItem key={index} className='!m-0'>
|
||||
{el.isAIMenu ? (
|
||||
<NavigationMenuLink href={el.href} className={cn(navigationMenuTriggerStyle(), ' text-muted-1 bg-transparent')}>
|
||||
{el.title}
|
||||
</NavigationMenuLink>
|
||||
) : el.title === 'Cards' ? (
|
||||
<>
|
||||
<NavigationMenuTrigger
|
||||
className='text-muted-1 bg-transparent'
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onFocus={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onBlur={handleMouseLeave}
|
||||
>
|
||||
<a href={el.href} >
|
||||
{el.title}
|
||||
</a>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent
|
||||
className={cn('fixed left-0 top-14 pt-6 w-screen h-full border-0 !backdrop-blur-3xl bg-transparent mt-0', menuHiddenClass)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className='flex justify-center items-start'>
|
||||
<div
|
||||
className='grid xl:grid-cols-3 w-full justify-center max-w-[750px]'
|
||||
onMouseLeave={handleContentMouseMove}
|
||||
>
|
||||
{GroupChildMenu({ childs: el.childMenu, isCards: true })}
|
||||
</div>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NavigationMenuTrigger
|
||||
className='text-muted-1 bg-transparent'
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onFocus={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onBlur={handleMouseLeave}
|
||||
>
|
||||
{
|
||||
el.href && el.href !== '' ?
|
||||
<a href={el.href} >
|
||||
{el.title}
|
||||
</a> : <>{el.title}</>
|
||||
}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent
|
||||
className={cn('fixed left-0 top-14 pt-6 w-screen h-full border-0 !backdrop-blur-3xl bg-transparent mt-0', menuHiddenClass)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
className='flex flex-row w-full justify-center'
|
||||
onMouseLeave={handleContentMouseMove}
|
||||
>
|
||||
{GroupChildMenu({ childs: el.childMenu })}
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</>
|
||||
)}
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default DesktopNav
|
||||
|
||||
const ListItem = React.forwardRef<
|
||||
React.ElementRef<'a'>,
|
||||
React.ComponentPropsWithoutRef<'a'>
|
||||
>(({ className, title, children, ...props }, ref) => (
|
||||
<li key={title}>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:text-accent-foreground focus:bg-level-1 focus:text-accent-foreground text-muted-1 hover:text-primary hover:bg-transparent duration-1000 ease-in-out',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className='text-sm font-medium leading-none'>{title}</div>
|
||||
<p className='line-clamp-3 text-sm leading-snug text-muted-1'>
|
||||
{children}
|
||||
</p>
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
))
|
||||
ListItem.displayName = 'ListItem'
|
||||
|
||||
const GroupChildMenu = (params: { childs: ChildMenu[] | undefined, isCards?: boolean }) => {
|
||||
const { childs, isCards = false } = params
|
||||
|
||||
if (!childs) {
|
||||
return null
|
||||
}
|
||||
|
||||
const groupedChildMenus = childs.reduce((grouped: Record<string, ChildMenu[]>, childLink) => {
|
||||
if (childLink.groupName) {
|
||||
grouped[childLink.groupName] = grouped[childLink.groupName] || []
|
||||
grouped[childLink.groupName].push(childLink)
|
||||
}
|
||||
return grouped
|
||||
}, {})
|
||||
|
||||
const getChildExtraClass = (index: number) => {
|
||||
if (isCards && (index === 3 || index === 4)) {
|
||||
return 'xl:-mt-32'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return Object.entries(groupedChildMenus).map(([groupName, childLinks], index) => (
|
||||
<div key={groupName} className={cn('py-4 px-4 ', getChildExtraClass(index))}>
|
||||
<h2 className='text-muted-1'>{groupName}</h2>
|
||||
<ul className='w-[200px] gap-3 md:w-[250px] lg:w-[250px]'>
|
||||
{childLinks.map((link) => (
|
||||
<div className='flex justify-start items-center' key={link.title}>
|
||||
{link.icon}
|
||||
<ListItem key={link.title} title={link.title} href={link.href} className='ml-[14px]'>
|
||||
{link.contents}
|
||||
</ListItem>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useState, type FC, type PropsWithChildren } from 'react'
|
||||
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
import Logo, { type LogoVariant } from '../logo'
|
||||
import DesktopNav from './desktop-nav-menu'
|
||||
|
||||
|
||||
const DesktopHeader: React.FC<{
|
||||
currentAs: string | undefined
|
||||
links: LinkDef[]
|
||||
className?: string
|
||||
logoVariant?: LogoVariant
|
||||
} & PropsWithChildren> = ({
|
||||
links,
|
||||
className = '',
|
||||
children,
|
||||
logoVariant='text-only'
|
||||
}) => {
|
||||
const [isMenuOpened, setIsMenuOpen] = useState(false);
|
||||
|
||||
// TODO move 13px into a size class and configure twMerge to recognize say, 'text-size-nav'
|
||||
// (vs be beat out by 'text-color-nav')
|
||||
return (
|
||||
<header id='DESKTOP_HEADER' className={cn(
|
||||
'bg-[rgba(0, 0, 0, 0.5)] !backdrop-blur-3xl fixed z-header top-0 left-0 right-0',
|
||||
className,
|
||||
isMenuOpened ? ' h-full' : ''
|
||||
)} >
|
||||
{/* md or larger */}
|
||||
<div className={
|
||||
'flex flex-row h-[80px] items-center justify-between ' +
|
||||
'mx-[24px] w-full max-w-screen'
|
||||
}>
|
||||
<Logo size={logoVariant === 'logo-only' ? 'lg' : 'md'} href='/' outerClx='hidden lg:flex' key='two' variant={logoVariant} />
|
||||
<Logo size='sm' href='/' outerClx='hidden md:flex lg:hidden' key='one' variant={logoVariant} />
|
||||
{/* md or larger */}
|
||||
<div className='flex w-full gap-4 items-center justify-center'>
|
||||
<DesktopNav links={links} isMenuOpened={isMenuOpened} setIsMenuOpen={setIsMenuOpen} />
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default DesktopHeader
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useState, type PropsWithChildren } from 'react'
|
||||
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
import type SiteDef from '@/hanzo-ui-types/site-def'
|
||||
|
||||
import DesktopHeader from './desktop'
|
||||
|
||||
import { type LogoVariant } from '../logo'
|
||||
|
||||
|
||||
/** children will render furthest right */
|
||||
const Header: React.FC<{
|
||||
siteDef: SiteDef
|
||||
className?: string
|
||||
logoVariant?: LogoVariant
|
||||
} & PropsWithChildren> = ({
|
||||
siteDef,
|
||||
className = '',
|
||||
children,
|
||||
logoVariant='text-only'
|
||||
}) => {
|
||||
|
||||
// TODO
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const { nav: { common, featured }, currentAs } = siteDef
|
||||
const links = (featured) ? [...common, ...featured] : common
|
||||
|
||||
return (
|
||||
<DesktopHeader
|
||||
className={cn(className, 'hidden md:flex')}
|
||||
links={links}
|
||||
currentAs={currentAs}
|
||||
logoVariant={logoVariant}
|
||||
>{children}</DesktopHeader>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { type LucideProps } from 'lucide-react'
|
||||
|
||||
const Avatar: React.FC<LucideProps> = (props: LucideProps) => (
|
||||
<svg width="17" height="21" viewBox="0 0 17 21" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M8.66112 10.3544C6.14668 10.3544 4.32664 6.91073 4.32664 4.59206C4.32664 2.12213 6.27238 0.115723 8.66112 0.115723C11.0499 0.115723 12.9956 2.12514 12.9956 4.59206C12.9956 6.91398 11.1756 10.3544 8.66112 10.3544ZM8.66112 1.1542C6.82539 1.1542 5.33242 2.69703 5.33242 4.59185C5.33242 6.65402 6.94715 9.31553 8.66112 9.31553C10.3761 9.31553 11.9898 6.65387 11.9898 4.5925C11.9898 2.69668 10.4969 1.15398 8.66112 1.15398V1.1542Z" fill="white" />
|
||||
<path d="M6.79776 11.3345H10.5242C13.7802 11.3345 16.4302 14.0712 16.4302 17.4338C16.4302 19.0831 15.1297 20.423 13.5356 20.423H3.78923C2.19216 20.423 0.894689 19.0831 0.894689 17.4338C0.893706 14.0712 3.54156 11.3345 6.79776 11.3345ZM3.78819 19.3844H13.5337C14.5749 19.3844 15.4225 18.509 15.4225 17.4338C15.4225 14.6453 13.2253 12.3731 10.5223 12.3731H6.79862C4.0956 12.3731 1.89839 14.6423 1.89839 17.4338C1.89937 18.509 2.74701 19.3844 3.78816 19.3844H3.78819Z" fill="white" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default Avatar
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import { type LucideProps } from 'lucide-react'
|
||||
|
||||
const BagIcon: React.FC<LucideProps> = (props: LucideProps) => (
|
||||
<svg fill="none" viewBox="0 0 1019 1281" {...props}>
|
||||
<path d="m977.243 365.553h-175.407v-90.629c0-151.923-131.968-274.924-292.334-274.924-160.367 0-292.337 123.002-292.337 274.924v90.629h-175.4023c-23.3886 0-41.7627 17.051-41.7627 38.756v775.121c0 55.8 48.4456 100.76 108.582 100.76h801.84c60.138 0 108.578-44.96 108.578-100.76v-775.121c0-21.705-18.37-38.756-41.757-38.756zm-676.553-90.629c0-108.517 93.546-196.9408 208.812-196.9408 115.265 0 208.813 88.4218 208.813 196.9408v89.08h-417.625z" fill="currentColor"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default BagIcon
|
||||
@@ -0,0 +1,6 @@
|
||||
export { default as Avatar } from './avatar'
|
||||
export { default as Bag } from './bag-icon'
|
||||
export { default as LeftArrow } from './left-arrow'
|
||||
export { default as LuxLogo } from './lux-logo'
|
||||
export { default as RightArrow } from './right-arrow'
|
||||
export { default as SocialIcon, type SocialIconProps } from './social-icon'
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { type LucideProps } from 'lucide-react'
|
||||
|
||||
const LeftArrow: React.FC<LucideProps> = (props: LucideProps) => (
|
||||
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M1.06975 7.44328C0.679224 7.83381 0.679224 8.46697 1.06975 8.8575L7.43371 15.2215C7.82423 15.612 8.4574 15.612 8.84792 15.2215C9.23845 14.8309 9.23845 14.1978 8.84792 13.8072L3.19107 8.15039L8.84792 2.49354C9.23845 2.10301 9.23845 1.46985 8.84792 1.07932C8.4574 0.688798 7.82423 0.688798 7.43371 1.07932L1.06975 7.44328ZM1.77686 9.15039H2.49037V7.15039H1.77686V9.15039Z" fill="white"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default LeftArrow
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import { type LucideProps } from 'lucide-react'
|
||||
|
||||
const LuxLogo: React.FC<LucideProps> = (props: LucideProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" {...props}>
|
||||
<polygon points="25,46.65 50,3.35 0,3.35" fill="white" stroke='black' strokeWidth={props.strokeWidth}/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default LuxLogo
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import { type LucideProps } from 'lucide-react'
|
||||
|
||||
const RightArrow: React.FC<LucideProps> = (props: LucideProps) => (
|
||||
<svg width="9" height="16" viewBox="0 0 9 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M8.33797 7.58293C8.72849 7.97346 8.72849 8.60662 8.33797 8.99715L1.97401 15.3611C1.58348 15.7516 0.950316 15.7516 0.559792 15.3611C0.169267 14.9706 0.169267 14.3374 0.559792 13.9469L6.21665 8.29004L0.559792 2.63318C0.169267 2.24266 0.169267 1.6095 0.559792 1.21897C0.950316 0.828447 1.58348 0.828447 1.97401 1.21897L8.33797 7.58293ZM7.63086 9.29004H6.91734V7.29004H7.63086V9.29004Z" fill="white"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default RightArrow
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
import { SocialIcon as BaseSocialIcon } from 'react-social-icons'
|
||||
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
import './social-svg.css'
|
||||
|
||||
interface SocialIconProps {
|
||||
// one of these: https://github.com/couetilc/react-social-icons/tree/main/db
|
||||
network: string
|
||||
size: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SocialIcon: React.FC<SocialIconProps> = ({
|
||||
network,
|
||||
size,
|
||||
className = ''
|
||||
}) => (
|
||||
<BaseSocialIcon
|
||||
network={network}
|
||||
as='div'
|
||||
// This is set up so the enclosing element sets the color.
|
||||
// For example, something like 'color-foreground hover:color-muted-1'
|
||||
className={cn('color-inherit', className)}
|
||||
bgColor='transparent'
|
||||
fgColor='currentColor'
|
||||
style={{height: size, width: size, flex: 'none' }}
|
||||
/>
|
||||
)
|
||||
|
||||
export {
|
||||
type SocialIconProps,
|
||||
SocialIcon as default
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.social-icon svg {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react'
|
||||
import { Link } from "@remix-run/react"
|
||||
|
||||
import { type TShirtSize } from '@hanzo/ui/types'
|
||||
|
||||
import { LuxLogo} from './icons'
|
||||
|
||||
const TEXT = 'LUX'
|
||||
type LogoVariant = 'text-only' | 'logo-only' | 'full'
|
||||
|
||||
const Logo: React.FC<{
|
||||
size?: TShirtSize
|
||||
variant?: LogoVariant
|
||||
onClick?: () => void
|
||||
href?: string
|
||||
outerClx?: string
|
||||
textClx?: string
|
||||
}> = ({
|
||||
size,
|
||||
href, // no default please!
|
||||
outerClx='',
|
||||
textClx='',
|
||||
variant='full',
|
||||
onClick,
|
||||
}) => {
|
||||
let classes: any = {}
|
||||
const toAdd = (variant === 'logo-only') ?
|
||||
{
|
||||
span: 'hidden',
|
||||
icon: ''
|
||||
}
|
||||
:
|
||||
(variant === 'text-only') ?
|
||||
{
|
||||
span: '',
|
||||
icon: 'hidden'
|
||||
}
|
||||
:
|
||||
{
|
||||
span: '',
|
||||
icon: ''
|
||||
}
|
||||
|
||||
if (size === 'lg' || size === 'xl' ) { // for safety
|
||||
classes.icon = 'h-10 w-10 mr-4 color-inherit '
|
||||
classes.span = 'text-3xl '
|
||||
}
|
||||
// match lux.network
|
||||
else if (size === 'md') {
|
||||
classes.icon = 'h-[32px] w-[32px] mr-[12px] color-inherit '
|
||||
classes.span = 'text-[1.8rem]/[1.8rem] tracking-tighter '
|
||||
}
|
||||
else if (size === 'sm' ) {
|
||||
classes.icon = 'h-6 w-6 mr-2 color-inherit '
|
||||
classes.span = 'text-lg '
|
||||
}
|
||||
// xs
|
||||
else {
|
||||
classes.icon = 'h-4 w-4 mr-1 color-inherit '
|
||||
classes.span = 'text-base '
|
||||
}
|
||||
|
||||
classes.icon += toAdd.icon
|
||||
classes.span += toAdd.span
|
||||
|
||||
|
||||
const outerClasses = 'flex flex-row items-center ' + outerClx
|
||||
const spanClasses = 'inline-block font-bold font-heading '
|
||||
+ textClx
|
||||
+ (href ? ' hover:text-accent cursor-pointer ' : ' cursor-default ')
|
||||
+ classes.span
|
||||
|
||||
return (
|
||||
href ? (
|
||||
<Link to={href} className={outerClasses} onClick={onClick} >
|
||||
<LuxLogo className={classes.icon} />
|
||||
<span className={spanClasses}>{TEXT}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span className={outerClasses} onClick={onClick}>
|
||||
<LuxLogo className={classes.icon} />
|
||||
<span className={spanClasses}>{TEXT}</span>
|
||||
</span>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Logo as default,
|
||||
type LogoVariant
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { type PropsWithChildren } from 'react'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
const Main: React.FC<PropsWithChildren & {
|
||||
id?: string
|
||||
className?: string
|
||||
headerSpace?: boolean
|
||||
standardWidth?: boolean
|
||||
}> = ({
|
||||
children,
|
||||
className='',
|
||||
headerSpace=true,
|
||||
standardWidth=true,
|
||||
id
|
||||
}) => (
|
||||
<main id={id ?? 'BRIDGE_MAIN'} className={cn(
|
||||
'flex flex-col justify-start items-center',
|
||||
'px-4 md:px-6 xl:px-0',
|
||||
standardWidth ? 'max-w-screen-xl w-full lg:mx-auto' : '',
|
||||
headerSpace ? 'pt-[44px] md:pt-[80px]' : 'py-4',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
|
||||
export default Main
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
import { type ListAdaptor } from '@hanzo/ui/primitives-common'
|
||||
import type { Network } from '@luxfi/core'
|
||||
|
||||
export default {
|
||||
getValue: (el: Network): string => (el.internal_name),
|
||||
equals: (el1: Network, el2: Network): boolean => (
|
||||
el1.internal_name.toUpperCase() === el2.internal_name.toUpperCase()
|
||||
),
|
||||
valueEquals: (el: Network, v: string): boolean => (
|
||||
el.internal_name.toUpperCase() === v.toUpperCase()
|
||||
),
|
||||
getLabel: (el: Network): string => (el.display_name),
|
||||
getImageUrl: (el: Network): string | null => ((el.logo ?? el.img_url) ?? null),
|
||||
|
||||
} satisfies ListAdaptor<Network>
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Combobox } from '@hanzo/ui/primitives-common'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
import type { Network } from '@luxfi/core'
|
||||
|
||||
import adaptor from './adaptor'
|
||||
import NetworkComboboxTrigger, { type NetworkTriggerProps } from './trigger'
|
||||
|
||||
const ICON_SIZE = 40
|
||||
|
||||
const NetworkCombobox: React.FC<{
|
||||
networks: Network[]
|
||||
network: Network | null
|
||||
setNetwork: (network: Network | null) => void
|
||||
searchHint?: string
|
||||
popoverClx?: string
|
||||
buttonClx?: string
|
||||
popoverAlign? : "center" | "end" | "start"
|
||||
label: string
|
||||
rightJustified?: boolean
|
||||
}> = ({
|
||||
networks,
|
||||
network,
|
||||
setNetwork,
|
||||
searchHint,
|
||||
popoverClx='',
|
||||
popoverAlign = 'center',
|
||||
buttonClx='',
|
||||
label,
|
||||
rightJustified=false
|
||||
}) => (
|
||||
|
||||
<Combobox<Network, NetworkTriggerProps>
|
||||
elements={networks}
|
||||
current={network}
|
||||
setCurrent={setNetwork}
|
||||
adaptor={adaptor}
|
||||
searchPlaceholder={searchHint}
|
||||
noCheckmark
|
||||
popoverClx={cn('font-sans font-medium w-full', popoverClx)}
|
||||
popoverAlign={popoverAlign}
|
||||
listItemClx='bg-background hover:!bg-level-3'
|
||||
listItemSelectedClx='!bg-level-2'
|
||||
Trigger={NetworkComboboxTrigger}
|
||||
triggerProps={{
|
||||
open: false,
|
||||
current: (network ?? null),
|
||||
currentLabel: null,
|
||||
imageUrl: null,
|
||||
buttonClx: cn('font-sans font-medium w-full px-2', buttonClx),
|
||||
disabled: false,
|
||||
imageSize: ICON_SIZE,
|
||||
label,
|
||||
rightJustified
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
export default NetworkCombobox
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Button, Skeleton, LoadingSpinner, type ComboboxTriggerProps } from '@hanzo/ui/primitives-common'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
import type { Network } from '@luxfi/core'
|
||||
|
||||
interface NetworkTriggerProps extends ComboboxTriggerProps<Network>{
|
||||
label: string
|
||||
rightJustified?: boolean
|
||||
}
|
||||
|
||||
const NetworkComboboxTriggerInner = ({
|
||||
current,
|
||||
currentLabel,
|
||||
imageUrl,
|
||||
buttonClx,
|
||||
imageClx,
|
||||
imageSize,
|
||||
rightJustified=false,
|
||||
label,
|
||||
...rest
|
||||
}: NetworkTriggerProps,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>
|
||||
) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
{...rest}
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
className={cn(
|
||||
'flex gap-1.5 rounded-lg h-auto py-1',
|
||||
rightJustified ? 'justify-start flex-row-reverse' : 'justify-start',
|
||||
current ? '' : 'pointer-events-none !p-0', // so skeleton will look right
|
||||
buttonClx
|
||||
)}
|
||||
>
|
||||
{current ? (<>
|
||||
<img
|
||||
src={imageUrl!}
|
||||
alt={currentLabel + ' image'}
|
||||
height={imageSize}
|
||||
width={imageSize}
|
||||
loading="eager"
|
||||
className={cn('block rounded-md ', imageClx)}
|
||||
/>
|
||||
<div className={cn(
|
||||
'flex flex-col gap-0 ',
|
||||
rightJustified ? 'justify-end items-end' : 'justify-start items-start'
|
||||
)}>
|
||||
<span className='text-sm block text-muted-2'>{label}</span>
|
||||
<span className={cn('block font-semibold', currentLabel ? 'text-foreground' : 'text-muted')}>
|
||||
{ currentLabel ?? '(select)' }
|
||||
</span>
|
||||
</div>
|
||||
</>) : ( // height from observation
|
||||
<Skeleton className='w-full h-[64px] flex justify-center items-center' reverse={rightJustified}>
|
||||
<LoadingSpinner size={30}/>
|
||||
</Skeleton>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
|
||||
const NetworkComboboxTrigger = React.forwardRef(NetworkComboboxTriggerInner) as <Network, NetworkTriggerProps>(props: NetworkTriggerProps & { ref?: React.ForwardedRef<HTMLButtonElement> }) => React.ReactNode
|
||||
|
||||
export {
|
||||
NetworkComboboxTrigger as default,
|
||||
type NetworkTriggerProps
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
import { useSwapState } from '@/contexts/swap-state'
|
||||
|
||||
const BridgeLabel: React.FC<{
|
||||
imageSize?: number
|
||||
imageClx?: string
|
||||
className?: string
|
||||
}> = observer(({
|
||||
imageSize=24,
|
||||
imageClx='',
|
||||
className=''
|
||||
}) => {
|
||||
const swapState = useSwapState()
|
||||
const imageUrl = swapState.toNetwork?.logo ?? swapState.toNetwork?.img_url ?? null
|
||||
|
||||
return imageUrl ? (
|
||||
<div className={cn('flex justify-start items-center gap-2', className)}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={swapState.toNetwork?.display_name + ' image'}
|
||||
height={imageSize}
|
||||
width={imageSize}
|
||||
loading="eager"
|
||||
className={cn('block', imageClx)}
|
||||
/>
|
||||
<span className='block'>{swapState.bridge}</span>
|
||||
</div>
|
||||
) : null
|
||||
})
|
||||
|
||||
export default BridgeLabel
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { AnimatePresence } from 'motion/react'
|
||||
import * as motion from 'motion/react-client'
|
||||
|
||||
import { cn, formatToMaxChar } from '@hanzo/ui/util'
|
||||
|
||||
import { useSwapState } from '@/contexts/swap-state'
|
||||
|
||||
import BridgeLabel from './bridge-label'
|
||||
|
||||
const ReceiveCard: React.FC<{
|
||||
quantity: number | null // debounced
|
||||
usdFee: number
|
||||
assetGas: number
|
||||
txnTime: string // eg, '~5min'
|
||||
className?: string
|
||||
}> = observer(({
|
||||
quantity,
|
||||
usdFee,
|
||||
assetGas,
|
||||
txnTime, // eg, '~5min'
|
||||
className=''
|
||||
}) => {
|
||||
|
||||
const swapState = useSwapState()
|
||||
const visible = !!quantity // Note: both and null or === 0
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [quantityDisplay, setQuantityDisplay] = useState<string>('')
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
|
||||
const {
|
||||
result,
|
||||
change
|
||||
} = formatToMaxChar(quantity, 15)
|
||||
setQuantityDisplay(change === 'rounded' ? '~' + result : result)
|
||||
// mimic remote fetch
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
}, 400)
|
||||
}, [quantity])
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={visible} mode='wait'>
|
||||
{ loading && visible ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scaleY: 0 }}
|
||||
animate={{ opacity: 1, scaleY: 1 }}
|
||||
exit={{ opacity: 0, scaleY: 0 }}
|
||||
className={className}
|
||||
>
|
||||
loading...
|
||||
</motion.div>
|
||||
) : ( visible ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scaleY: 0 }}
|
||||
animate={{ opacity: 1, scaleY: 1 }}
|
||||
exit={{ opacity: 0, scaleY: 0 }}
|
||||
className={cn(
|
||||
'flex flex-col gap-3',
|
||||
'border border-muted-4 py-2 px-2',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className='flex justify-between items-center text-sm'>
|
||||
{swapState.toNetwork ? (
|
||||
<span className='block font-bold'>Get on {swapState.toNetwork!.display_name }</span>
|
||||
) : (
|
||||
<span className='block'/>
|
||||
)}
|
||||
<BridgeLabel/>
|
||||
</div>
|
||||
<div className='flex justify-start items-end text-xl font-bold'>
|
||||
{quantityDisplay && (<span className='block leading-none'>{quantityDisplay} </span>)}
|
||||
{swapState.toAsset && (<span className='block text-base leading-none pb-[1px]'>{swapState.toAsset?.asset ?? ''}</span>) }
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
export default ReceiveCard
|
||||
@@ -0,0 +1,64 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import NetworkCombobox from '../network-combobox'
|
||||
import ReverseButton from './reverse-button'
|
||||
import { useSwapState } from '@/contexts/swap-state'
|
||||
|
||||
const ABS_CENTERED = {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%,-50%)'
|
||||
} satisfies React.CSSProperties
|
||||
|
||||
const FromToCard: React.FC<{
|
||||
className?: string
|
||||
}> = observer(({
|
||||
className=''
|
||||
}) => {
|
||||
|
||||
const swapState = useSwapState()
|
||||
/* TODO
|
||||
const swapFromAndTo = () => {
|
||||
const tmp = swapState.from
|
||||
swapState.setFrom(swapState.to)
|
||||
swapState.setTo(tmp)
|
||||
|
||||
const tmpNetworks = swapState.fromNetworks
|
||||
swapState.setFromNetworks(swapState.toNetworks)
|
||||
swapState.setToNetworks(tmpNetworks)
|
||||
}
|
||||
*/
|
||||
|
||||
// 'flex w-full gap-2 relative'
|
||||
return (
|
||||
<div className={className}>
|
||||
<NetworkCombobox
|
||||
networks={swapState.fromNetworks}
|
||||
setNetwork={swapState.setFromNetwork}
|
||||
network={swapState.fromNetwork}
|
||||
buttonClx='grow pr-4'
|
||||
popoverClx='w-[350px]'
|
||||
popoverAlign='start'
|
||||
label='from'
|
||||
/>
|
||||
<NetworkCombobox
|
||||
networks={swapState.toNetworks}
|
||||
setNetwork={swapState.setToNetwork}
|
||||
network={swapState.toNetwork}
|
||||
buttonClx='grow pl-4'
|
||||
popoverClx='w-[350px]'
|
||||
popoverAlign='end'
|
||||
label='to'
|
||||
rightJustified
|
||||
/>
|
||||
<ReverseButton
|
||||
className='p-1 h-auto text-muted active:!bg-level-3'
|
||||
onClick={() => {}}
|
||||
style={ABS_CENTERED}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default FromToCard
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
import AssetCard from '../asset-card'
|
||||
import FromToCard from './from-to-card'
|
||||
import ReceiveCard from '../receive-card'
|
||||
import { useSwapState } from '@/contexts/swap-state'
|
||||
|
||||
const FIXTURE = {
|
||||
usdFee: 2.4,
|
||||
assetGas: .045,
|
||||
txnTime: '~5min',
|
||||
assetsAvailable: 134.56676767676,
|
||||
}
|
||||
|
||||
const SwapCard: React.FC<{
|
||||
className?: string
|
||||
}> = ({
|
||||
className='',
|
||||
}) => {
|
||||
|
||||
const swapState = useSwapState()
|
||||
// AssetCard debounces this change so use a sep state var
|
||||
const [quantity, setQuantity] = useState<number | null>(swapState.fromAssetQuantity ?? null)
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex flex-col justify-start items-center',
|
||||
'px-6 py-4 gap-4 rounded border border-muted-4',
|
||||
className
|
||||
)}>
|
||||
<FromToCard className='flex w-full gap-2 relative' />
|
||||
<AssetCard
|
||||
assetQuantityAvailable={FIXTURE.assetsAvailable}
|
||||
onQuantityChanged={setQuantity}
|
||||
className='w-full rounded-lg mt-2'
|
||||
/>
|
||||
<ReceiveCard
|
||||
quantity={quantity}
|
||||
usdFee={FIXTURE.usdFee}
|
||||
assetGas={FIXTURE.assetGas}
|
||||
txnTime={FIXTURE.txnTime}
|
||||
className='w-full rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwapCard
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
import { ChevronsLeftRight } from 'lucide-react'
|
||||
|
||||
import { Button } from '@hanzo/ui/primitives-common'
|
||||
import { cn } from '@hanzo/ui/util'
|
||||
|
||||
const ReverseButton: React.FC<{
|
||||
onClick: () => void
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}> = ({
|
||||
onClick,
|
||||
className='',
|
||||
style={}
|
||||
}) => (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className={cn(className)}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
<ChevronsLeftRight className='w-4 h-4'/>
|
||||
</Button>
|
||||
)
|
||||
|
||||
export default ReverseButton
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
|
||||
import { TooltipProvider } from '@hanzo/ui/primitives-common'
|
||||
import type { Network } from '@luxfi/core'
|
||||
|
||||
import SettingsProvider from '@/contexts/settings'
|
||||
import SwapStateProvider from '@/contexts/swap-state'
|
||||
import type { AppSettings } from '@/domain/types'
|
||||
|
||||
const Contexts: React.FC<{
|
||||
appSettings: AppSettings,
|
||||
defaultFromNetwork?: Network
|
||||
defaultToNetwork?: Network
|
||||
} & PropsWithChildren> = ({
|
||||
children,
|
||||
appSettings,
|
||||
defaultFromNetwork,
|
||||
defaultToNetwork,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<SettingsProvider appSettings={appSettings}>
|
||||
<SwapStateProvider
|
||||
appSettings={appSettings}
|
||||
defaultFromNetwork={defaultFromNetwork}
|
||||
defaultToNetwork={defaultToNetwork}
|
||||
>
|
||||
<TooltipProvider>
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</SwapStateProvider>
|
||||
</SettingsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Contexts
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createContext, useContext, useRef, type PropsWithChildren } from 'react'
|
||||
|
||||
import type AppSettings from '@/domain/types/app-settings'
|
||||
const SettingsContext = createContext<React.MutableRefObject<AppSettings> | null>(null)
|
||||
|
||||
const SettingsProvider: React.FC<{
|
||||
appSettings: AppSettings
|
||||
} & PropsWithChildren> = ({
|
||||
children,
|
||||
appSettings
|
||||
}) => {
|
||||
|
||||
const settingsRef = useRef<AppSettings>(appSettings)
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={settingsRef}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useSettings = (): AppSettings => {
|
||||
const ref = useContext(SettingsContext)
|
||||
|
||||
if (!ref || !ref.current) {
|
||||
throw new Error('SettingsProvider not ititialized!')
|
||||
}
|
||||
|
||||
return ref.current
|
||||
}
|
||||
|
||||
export {
|
||||
SettingsProvider as default,
|
||||
SettingsContext,
|
||||
useSettings,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createContext, useContext, useEffect, useRef, type PropsWithChildren } from 'react'
|
||||
|
||||
import type { Network } from '@luxfi/core'
|
||||
|
||||
import type { SwapState, AppSettings } from '@/domain/types'
|
||||
import getInstance from '@/domain/swap/get-instance'
|
||||
import SwapStore from '@/domain/swap/store'
|
||||
import { useSettings } from './settings'
|
||||
|
||||
const SwapStateContext = createContext<React.MutableRefObject<SwapState> | null>(null)
|
||||
|
||||
const SwapStateProvider: React.FC<{
|
||||
appSettings: AppSettings,
|
||||
defaultFromNetwork?: Network,
|
||||
defaultToNetwork?: Network,
|
||||
} & PropsWithChildren> = ({
|
||||
children,
|
||||
appSettings,
|
||||
defaultFromNetwork,
|
||||
defaultToNetwork
|
||||
}) => {
|
||||
|
||||
const swapStateRef = useRef<SwapState>(getInstance(
|
||||
appSettings,
|
||||
defaultFromNetwork,
|
||||
defaultToNetwork
|
||||
))
|
||||
|
||||
useEffect(() => {
|
||||
if (swapStateRef.current) {
|
||||
(swapStateRef.current as SwapStore).initialize()
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (swapStateRef.current) {
|
||||
(swapStateRef.current as SwapStore).dispose()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SwapStateContext.Provider value={swapStateRef}>
|
||||
{children}
|
||||
</SwapStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useSwapState = (): SwapState => {
|
||||
|
||||
const ref = useContext(SwapStateContext)
|
||||
|
||||
if (!ref || !ref.current) {
|
||||
throw new Error('SwapStateProvider or SwapState not ititialized!')
|
||||
}
|
||||
|
||||
return ref.current
|
||||
};
|
||||
|
||||
export {
|
||||
SwapStateProvider as default,
|
||||
SwapStateContext,
|
||||
useSwapState,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
This basically the same function as BridgeApiClient in the bridge2 code.
|
||||
Individual utilities that call the backend API.
|
||||
@@ -0,0 +1,13 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import type { Asset } from '@luxfi/core'
|
||||
|
||||
const getAssetPrice = async (a: Asset): Promise<number | undefined> => {
|
||||
|
||||
const { data } = await axios.get(
|
||||
import.meta.env.VITE_SERVER_URI + '/api/tokens/price/' + a.asset
|
||||
)
|
||||
return data?.data?.price ? Number(data?.data?.price) : undefined
|
||||
}
|
||||
|
||||
export default getAssetPrice
|
||||
@@ -0,0 +1,17 @@
|
||||
import axios from 'axios'
|
||||
import type AppSettings from '@/domain/types/app-settings'
|
||||
|
||||
import swapPairs from './swap-pairs'
|
||||
|
||||
const getSettings = async (): Promise<AppSettings | undefined> => {
|
||||
|
||||
const { data } = await axios.get(
|
||||
import.meta.env.VITE_SERVER_URI + '/api/settings?version=' + import.meta.env.VITE_NET_VERSION
|
||||
)
|
||||
|
||||
data.swapPairs = swapPairs
|
||||
|
||||
return data as AppSettings ?? undefined
|
||||
}
|
||||
|
||||
export default getSettings
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type BackendService } from '../types'
|
||||
import { default as getSettings } from './get-settings'
|
||||
import { default as getAssetPrice } from './get-asset-price'
|
||||
|
||||
export default {
|
||||
getSettings,
|
||||
getAssetPrice
|
||||
} satisfies BackendService
|
||||
@@ -0,0 +1,101 @@
|
||||
export default {
|
||||
// Lux tokens
|
||||
LUX: ['ZLUX'],
|
||||
LBTC: ['WBTC', 'ZBTC', 'cbBTC'],
|
||||
LETH: ['ETH', 'WETH', 'ZETH'],
|
||||
LUSD: ['USDT', 'USDC', 'DAI', 'ZUSD'],
|
||||
LZOO: ['ZOO'],
|
||||
LBNB: ['BNB', 'ZBNB'],
|
||||
LPOL: ['POL', 'ZPOL'],
|
||||
LCELO: ['CELO', 'ZCELO'],
|
||||
LFTM: ['FTM', 'ZFTM'],
|
||||
LXDAI: ['XDAI', 'ZXDAI'],
|
||||
LSOL: ['ZSOL'],
|
||||
LTON: ['ZTON'],
|
||||
LADA: ['ZADA'],
|
||||
LAVAX: ['AVAX', 'ZAVAX'],
|
||||
LBLAST: ['ZBLAST', 'BLAST'],
|
||||
LBONK: ['ZBONK'],
|
||||
LWIF: ['ZWIF'],
|
||||
LPOPCAT: ['ZPOPCAT'],
|
||||
LPNUT: ['ZPNUT'],
|
||||
LMEW: ['ZMEW'],
|
||||
LBOME: ['ZBOME'],
|
||||
LGIGA: ['ZGIGA'],
|
||||
LAI16Z: ['ZAI16Z'],
|
||||
LFWOG: ['ZFWOG'],
|
||||
LMOODENG: ['ZMOODENG'],
|
||||
LPONKE: ['ZPONKE'],
|
||||
LNOT: ['ZNOT'],
|
||||
LDOGS: ['ZDOGS'],
|
||||
LMRB: ['ZMRB'],
|
||||
LREDO: ['ZREDO'],
|
||||
// Zoo tokens
|
||||
ZOO: ['LZOO'],
|
||||
ZBTC: ['WBTC', 'LBTC'],
|
||||
ZETH: ['ETH', 'WETH', 'LETH'],
|
||||
ZUSD: ['USDT', 'USDC', 'DAI', 'LUSD'],
|
||||
ZLUX: ['LUX'],
|
||||
ZBNB: ['BNB', 'LBNB'],
|
||||
ZPOL: ['POL', 'LPOL'],
|
||||
ZCELO: ['CELO', 'LCELO'],
|
||||
ZFTM: ['FTM', 'LFTM'],
|
||||
ZXDAI: ['XDAI', 'LXDAI'],
|
||||
ZSOL: ['LSOL'],
|
||||
ZTON: ['LTON'],
|
||||
ZADA: ['LADA'],
|
||||
ZAVAX: ['AVAX', 'LAVAX'],
|
||||
ZBLAST: ['LBLAST', 'BLAST'],
|
||||
ZBONK: ['LBONK'],
|
||||
ZWIF: ['LWIF'],
|
||||
ZPOPCAT: ['LPOPCAT'],
|
||||
ZPNUT: ['LPNUT'],
|
||||
ZMEW: ['LMEW'],
|
||||
ZBOME: ['LBOME'],
|
||||
ZGIGA: ['LGIGA'],
|
||||
ZAI16Z: ['LAI16Z'],
|
||||
ZFWOG: ['LFWOG'],
|
||||
ZMOODENG: ['LMOODENG'],
|
||||
ZPONKE: ['LPONKE'],
|
||||
ZNOT: ['LNOT'],
|
||||
ZDOGS: ['LDOGS'],
|
||||
ZMRB: ['LMRB'],
|
||||
ZREDO: ['LREDO'],
|
||||
// Evm tokens
|
||||
ETH: ['LETH', 'ZETH'],
|
||||
WETH: ['LETH', 'ZETH'],
|
||||
cbBTC: ['LBTC', 'ZBTC'],
|
||||
WBTC: ['LBTC', 'ZBTC'],
|
||||
POL: ['LPOL', 'ZPOL'],
|
||||
BNB: ['LBNB', 'ZBNB'],
|
||||
FTM: ['LFTM', 'ZFTM'],
|
||||
DAI: ['LUSD', 'ZUSD'],
|
||||
USDT: ['LUSD', 'ZUSD'],
|
||||
USDC: ['LUSD', 'ZUSD'],
|
||||
CELO: ['LCELO', 'ZCELO'],
|
||||
XDAI: ['LXDAI', 'ZXDAI'],
|
||||
AVAX: ['LAVAX', 'ZAVAX'],
|
||||
BLAST: ['LBLAST', 'ZBLAST'],
|
||||
//// Utila tokens
|
||||
BTC: ['LBTC', 'ZBTC'],
|
||||
TON: ['LTON', 'ZTON'],
|
||||
// Solana tokens
|
||||
SOL: ['LSOL', 'ZSOL'],
|
||||
BONK: ['LBONK', 'ZBONK'],
|
||||
SIXR: ['LSIXR', 'ZSIXR'],
|
||||
WIF: ['LWIF', 'ZWIF'],
|
||||
POPCAT: ['LPOPCAT', 'ZPOPCAT'],
|
||||
PNUT: ['LPNUT', 'ZPNUT'],
|
||||
MEW: ['LMEW', 'ZMEW'],
|
||||
BOME: ['LBOME', 'ZBOME'],
|
||||
GIGA: ['LGIGA', 'ZGIGA'],
|
||||
AI16Z: ['LAI16Z', 'ZAI16Z'],
|
||||
FWOG: ['LFWOG', 'ZFWOG'],
|
||||
MOODENG: ['LMOODENG', 'ZMOODENG'],
|
||||
PONKE: ['LPONKE', 'ZPONKE'],
|
||||
// Ton tokens
|
||||
NOT: ['LNOT', 'ZNOT'],
|
||||
DOGS: ['LDOGS', 'ZDOGS'],
|
||||
MRB: ['LMRB', 'ZMRB'],
|
||||
REDO: ['LREDO', 'ZREDO']
|
||||
} satisfies Record<string, string[]>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { enableStaticRendering } from 'mobx-react-lite'
|
||||
|
||||
import type { Network } from '@luxfi/core'
|
||||
|
||||
import type { AppSettings, SwapState } from '@/domain/types'
|
||||
import SwapStore from './store'
|
||||
|
||||
// https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
|
||||
// (This also applies to remix)
|
||||
enableStaticRendering(typeof window === "undefined")
|
||||
|
||||
let instance: SwapStore | undefined = undefined
|
||||
|
||||
const getInstance = (
|
||||
settings: AppSettings,
|
||||
initialFrom?: Network,
|
||||
initialTo?: Network
|
||||
): SwapState => {
|
||||
|
||||
// Server side: create a new copy of the store each time
|
||||
if (typeof window === "undefined") {
|
||||
const inst = new SwapStore(
|
||||
settings,
|
||||
initialTo,
|
||||
initialFrom
|
||||
)
|
||||
// TODO how to call dispose()?
|
||||
return inst
|
||||
}
|
||||
|
||||
// Client side: create the store only once
|
||||
if (!instance) {
|
||||
instance = new SwapStore(
|
||||
settings,
|
||||
initialTo,
|
||||
initialFrom
|
||||
)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
export default getInstance
|
||||
@@ -0,0 +1,15 @@
|
||||
# Swap Logic
|
||||
|
||||
These functions keep the **Swap form elements** logically
|
||||
in sync with each other.
|
||||
|
||||
They are mobx reactions that consolidate and encapsulate
|
||||
various logic scattered across the ui code in bridge2. In order
|
||||
to **separate concerns** and collect related business logic
|
||||
in one please, they are all
|
||||
|
||||
They are designed to be triggered by both **user actions** and
|
||||
**effect functions** of other reactions. In the Swap store's
|
||||
`initialize()` method, they are create in **reverse order** so that
|
||||
a desired effect is always initialized before its trigger.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { reaction } from 'mobx'
|
||||
|
||||
import type { Network, Asset } from '@luxfi/core'
|
||||
import type { SwapState } from '@/domain/types'
|
||||
import backend from '@/domain/backend'
|
||||
|
||||
|
||||
const swapExists = (
|
||||
swapPairs: Record<string, string[]>,
|
||||
src: Asset,
|
||||
swapAsset: Asset
|
||||
): boolean => (
|
||||
swapPairs[src.asset].includes(swapAsset.asset)
|
||||
)
|
||||
|
||||
export default (store: SwapState) => (reaction(
|
||||
() => ({
|
||||
fromAsset: store.fromAsset,
|
||||
}),
|
||||
async ({
|
||||
fromAsset,
|
||||
}) => {
|
||||
// Networks for which at least one swap pair exists (swap is possible)
|
||||
store.setToNetworks(
|
||||
fromAsset ? (
|
||||
store.allNetworks
|
||||
.map((n: Network) => ({
|
||||
...n,
|
||||
currencies: n.currencies.filter((c: Asset) => (swapExists(store.swapPairs, fromAsset!, c))),
|
||||
}))
|
||||
.filter((n: Network) => n.currencies.length > 0)
|
||||
) : []
|
||||
)
|
||||
if (fromAsset) {
|
||||
const price = await backend.getAssetPrice(fromAsset)
|
||||
store.setFromAssetPriceUSD(price ?? null)
|
||||
}
|
||||
|
||||
}
|
||||
))
|
||||
@@ -0,0 +1,16 @@
|
||||
import { reaction } from 'mobx'
|
||||
|
||||
import type { SwapState } from '@/domain/types'
|
||||
|
||||
export default (store: SwapState) => (reaction(
|
||||
() => ({
|
||||
network: store.fromNetwork,
|
||||
}),
|
||||
({
|
||||
network,
|
||||
}) => {
|
||||
// Do not filter by status==='active' as the UI now shows those as disabled
|
||||
store.setFromAssets(network?.currencies ?? [])
|
||||
store.setFromAsset(store.fromAssets.length ? store.fromAssets[0] : null)
|
||||
}
|
||||
))
|
||||
@@ -0,0 +1,32 @@
|
||||
import { reaction } from 'mobx'
|
||||
|
||||
import type { Network, NetworkType } from '@luxfi/core'
|
||||
import type { SwapState, Bridge } from '@/domain/types'
|
||||
|
||||
const matchesRoute = (bridge: Bridge, t: NetworkType): boolean => (
|
||||
(bridge === 'teleport' ? t === 'evm' : t !== 'evm' )
|
||||
)
|
||||
|
||||
export default (store: SwapState) => (reaction(
|
||||
() => ({
|
||||
networks: store.allNetworks,
|
||||
bridge: store.bridge,
|
||||
}),
|
||||
({
|
||||
networks,
|
||||
bridge
|
||||
}) => {
|
||||
|
||||
store.setFromNetworks(networks.filter(
|
||||
(n: Network) => (n.status === 'active' && matchesRoute(bridge, n.type))
|
||||
))
|
||||
store.setFromNetwork(store.fromNetworks.length ? store.fromNetworks[0] : null)
|
||||
store.setFromAssets(store.fromNetwork?.currencies ?? [])
|
||||
store.setFromAsset(store.fromAssets.length ? store.fromAssets[0] : null)
|
||||
},
|
||||
// fire now so that all reactions will cascade based on
|
||||
// what was passed into the constructor
|
||||
{ fireImmediately: true}
|
||||
))
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { reaction } from 'mobx'
|
||||
|
||||
import type { SwapState } from '@/domain/types'
|
||||
|
||||
export default (store: SwapState) => (reaction(
|
||||
() => ({
|
||||
toNetwork: store.toNetwork,
|
||||
}),
|
||||
({
|
||||
toNetwork,
|
||||
}) => {
|
||||
store.setToAsset(toNetwork?.currencies.length ? toNetwork?.currencies[0] : null)
|
||||
}
|
||||
))
|
||||
@@ -0,0 +1,14 @@
|
||||
import { reaction } from 'mobx'
|
||||
|
||||
import type { SwapState } from '@/domain/types'
|
||||
|
||||
export default (store: SwapState) => (reaction(
|
||||
() => ({
|
||||
toNetworks: store.toNetworks,
|
||||
}),
|
||||
({
|
||||
toNetworks,
|
||||
}) => {
|
||||
store.setToNetwork(toNetworks.length ? toNetworks[0] : null)
|
||||
}
|
||||
))
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
computed,
|
||||
makeObservable,
|
||||
observable,
|
||||
action,
|
||||
type IReactionDisposer,
|
||||
} from 'mobx'
|
||||
|
||||
import type { Asset, Network } from '@luxfi/core'
|
||||
import type { AppSettings, SwapState, Bridge } from '@/domain/types'
|
||||
|
||||
import onNetworksOrRouteChange from './logic/on-networks-or-route-change'
|
||||
import onFromNetworkChange from './logic/on-from-network-change'
|
||||
import onFromAssetChange from './logic/on-from-asset-change'
|
||||
import onToNetworksChange from './logic/on-to-networks-change'
|
||||
import onToNetworkChange from './logic/on-to-network-change'
|
||||
|
||||
class SwapStore implements SwapState {
|
||||
|
||||
fromNetworks: Network[] = []
|
||||
toNetworks: Network[] = []
|
||||
fromNetwork: Network | null = null
|
||||
toNetwork: Network | null = null
|
||||
fromAssets: Asset[] = []
|
||||
fromAsset: Asset | null = null
|
||||
fromAssetPriceUSD: number | null = null
|
||||
toAsset: Asset | null = null
|
||||
fromAssetQuantity: number = 0
|
||||
bridge: Bridge = 'teleport'
|
||||
allNetworks: Network[] = []
|
||||
|
||||
swapPairs: Record<string, string[]> = {}
|
||||
|
||||
private _disposers: IReactionDisposer[] = []
|
||||
|
||||
constructor(
|
||||
settings: AppSettings,
|
||||
initialTo?: Network,
|
||||
initialFrom?: Network
|
||||
) {
|
||||
makeObservable(this, {
|
||||
fromNetworks: observable.shallow,
|
||||
toNetworks: observable.shallow,
|
||||
fromNetwork: observable.shallow,
|
||||
toNetwork: observable.shallow,
|
||||
fromAssets:observable.shallow,
|
||||
fromAsset: observable.shallow,
|
||||
fromAssetPriceUSD: observable,
|
||||
toAsset: observable.shallow,
|
||||
fromAssetQuantity: observable,
|
||||
bridge: observable,
|
||||
allNetworks: observable.shallow,
|
||||
setFromNetwork: action.bound,
|
||||
setToNetwork: action.bound,
|
||||
setFromAssets: action,
|
||||
setFromAsset: action.bound,
|
||||
setFromAssetPriceUSD: action.bound,
|
||||
setToAsset: action.bound,
|
||||
setFromAssetQuantity: action.bound,
|
||||
setBridge: action.bound,
|
||||
setSettings: action.bound,
|
||||
})
|
||||
|
||||
this.allNetworks = settings.networks
|
||||
this.swapPairs = settings.swapPairs
|
||||
this.fromNetwork = initialFrom ?? null
|
||||
this.toNetwork = initialTo ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
This should be called clients-side as early as possible
|
||||
(eg, in useEffect()).
|
||||
|
||||
Mobx reaction()s return a disposer function.
|
||||
Provider calls our dispose() when it unmounts
|
||||
via useEffect()'s return value.
|
||||
|
||||
These should be called in REVERSE order of causality
|
||||
for the effects to cascade properly the first time.
|
||||
*/
|
||||
initialize = () => {
|
||||
this._disposers.push(onToNetworkChange(this))
|
||||
this._disposers.push(onToNetworksChange(this))
|
||||
this._disposers.push(onFromAssetChange(this))
|
||||
this._disposers.push(onFromNetworkChange(this))
|
||||
this._disposers.push(onNetworksOrRouteChange(this)) // fires immediately
|
||||
}
|
||||
|
||||
dispose = () => { this._disposers.forEach((d) => {d()}) }
|
||||
|
||||
setFromNetworks = (n: Network[]): void => { this.fromNetworks = n }
|
||||
setToNetworks = (n: Network[]): void => { this.toNetworks = n }
|
||||
setFromNetwork = (n: Network | null): void => { this.fromNetwork = n }
|
||||
setToNetwork = (n: Network | null): void => { this.toNetwork = n }
|
||||
setFromAssets = (a: Asset[]): void => { this.fromAssets = a }
|
||||
setFromAsset = (a: Asset | null): void => { this.fromAsset = a }
|
||||
setFromAssetPriceUSD = (n: number | null): void => {this.fromAssetPriceUSD = n}
|
||||
setToAsset = (a: Asset | null): void => { this.toAsset = a }
|
||||
setFromAssetQuantity = (a: number): void => { this.fromAssetQuantity = a }
|
||||
setBridge = (b: Bridge): void => { this.bridge = b }
|
||||
|
||||
setSettings = (
|
||||
settings: AppSettings,
|
||||
initialTo?: Network,
|
||||
initialFrom?: Network
|
||||
) => {
|
||||
this.allNetworks = settings.networks
|
||||
this.swapPairs = settings.swapPairs
|
||||
this.fromNetwork = initialFrom ?? null
|
||||
this.toNetwork = initialTo ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export default SwapStore
|
||||
@@ -0,0 +1,11 @@
|
||||
import { type Network } from '@luxfi/core'
|
||||
|
||||
interface AppSettings {
|
||||
networks: Network[]
|
||||
swapPairs: Record<string, string[]>
|
||||
}
|
||||
|
||||
export {
|
||||
type AppSettings as default
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Asset } from '@luxfi/core'
|
||||
import type AppSettings from './app-settings'
|
||||
|
||||
interface BackendService {
|
||||
getSettings: () => Promise<AppSettings | undefined>
|
||||
getAssetPrice: (a: Asset) => Promise<number | undefined>
|
||||
}
|
||||
|
||||
export {
|
||||
type BackendService as default
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type { default as AppSettings } from './app-settings'
|
||||
export type { default as BackendService } from './backend-service'
|
||||
export type { default as SwapState, Bridge } from './swap-state'
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Network, Asset } from '@luxfi/core'
|
||||
import type AppSettings from './app-settings'
|
||||
|
||||
type Bridge = 'teleport' | 'utila'
|
||||
|
||||
interface SwapState {
|
||||
|
||||
allNetworks : Network[]
|
||||
fromNetworks: Network[]
|
||||
toNetworks: Network[]
|
||||
toNetwork : Network | null
|
||||
fromNetwork : Network | null
|
||||
fromAssets : Asset[]
|
||||
fromAsset : Asset | null
|
||||
fromAssetPriceUSD: number | null
|
||||
toAsset : Asset | null
|
||||
bridge : Bridge
|
||||
fromAssetQuantity : number
|
||||
|
||||
setFromNetwork(n: Network | null) : void
|
||||
setToNetwork(n: Network | null): void
|
||||
setFromAsset(a: Asset | null): void
|
||||
setFromAssetPriceUSD(n: number | null): void
|
||||
setToAsset(a: Asset | null): void
|
||||
setFromAssetQuantity(a: number): void
|
||||
setBridge(b: Bridge): void
|
||||
|
||||
// internal use
|
||||
swapPairs: Record<string, string[]>
|
||||
setFromNetworks(n: Network[]): void
|
||||
setToNetworks(n: Network[]): void
|
||||
setFromAssets(a: Asset[]): void
|
||||
|
||||
setSettings (
|
||||
settings: AppSettings,
|
||||
initialTo?: Network,
|
||||
initialFrom?: Network
|
||||
): void
|
||||
}
|
||||
|
||||
export {
|
||||
type SwapState as default,
|
||||
type Bridge
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 'type' must be outside the curlies!
|
||||
// https://github.com/remix-run/remix/issues/9916#issuecomment-2436405265
|
||||
import type { LinkDescriptor } from '@vercel/remix'
|
||||
|
||||
type CrossOrigin = "anonymous" | "use-credentials"
|
||||
|
||||
// https://github.com/remix-run/remix/discussions/4963#discussioncomment-4556820
|
||||
export default [
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "/fonts/Druk-Wide-Medium.ttf",
|
||||
type: "font/ttf",
|
||||
crossOrigin: "anonymous" as CrossOrigin
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "/fonts/Druk-Wide-Bold.ttf",
|
||||
type: "font/ttf",
|
||||
crossOrigin: "anonymous" as CrossOrigin,
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "/fonts/InterVariable.woff2",
|
||||
type: "font/woff2",
|
||||
crossOrigin: "anonymous" as CrossOrigin,
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "/fonts/InterVariable.ttf",
|
||||
type: "font/ttf",
|
||||
crossOrigin: "anonymous" as CrossOrigin,
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "/fonts/InterVariable-Italic.woff2",
|
||||
type: "font/woff2",
|
||||
crossOrigin: "anonymous" as CrossOrigin,
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "/fonts/InterVariable-Italic.ttf",
|
||||
type: "font/ttf",
|
||||
crossOrigin: "anonymous" as CrossOrigin,
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
href: '/lux-site-icons/favicon-16x16.png'
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
href: '/lux-site-icons/favicon-32x32.png'
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
href: '/lux-site-icons/android-chrome-192x192.png'
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '512x512',
|
||||
href: '/lux-site-icons/android-chrome-512x512.png'
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
type: 'image/png',
|
||||
sizes: "180x180",
|
||||
href: '/lux-site-icons/apple-touch-icon.png'
|
||||
},
|
||||
|
||||
] satisfies LinkDescriptor[]
|
||||
@@ -0,0 +1,2 @@
|
||||
These are "remix-safe" versions of the types used by @luxfi/ui
|
||||
TODO: remove them make luxfi/ui usable by next and remix
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
interface ChildMenu extends LinkDef {
|
||||
groupName?: string
|
||||
icon_act?: any
|
||||
}
|
||||
|
||||
export {
|
||||
type ChildMenu as default
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
import type ChildMenu from './child-menu'
|
||||
|
||||
|
||||
interface LinkDefExtended extends LinkDef {
|
||||
isAIMenu?: boolean
|
||||
icon?: any
|
||||
details?: string
|
||||
childMenu?: ChildMenu[]
|
||||
}
|
||||
|
||||
export {
|
||||
type LinkDefExtended as default,
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
interface SiteDef {
|
||||
|
||||
/** url of this site. All nav links in the system will show it in 'current' state */
|
||||
currentAs?: string
|
||||
|
||||
nav: {
|
||||
/** common elements (will auto-select currentAs if it's provide) */
|
||||
/** optional feature element. right-most after 'elements' (any min-w is ignored) */
|
||||
common: LinkDef[]
|
||||
featured?: LinkDef[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of columns, each of which itself is an array of links
|
||||
* (or "title defs" where href='' and variant='linkFG')
|
||||
* Common use case: To render a site-specific first column:
|
||||
* {
|
||||
* footer[myColumn, ...commonColumnsFromUi]
|
||||
* }
|
||||
* see: @hanzo/ui/siteDef/footer (default export)
|
||||
* or @hanzo/ui/siteDef/footer/common
|
||||
*/
|
||||
footer: LinkDef[][]
|
||||
|
||||
/**
|
||||
* optional override of default 'above copyright' horizantal links
|
||||
default (undefined / absent): links in side-def/footer/legal are rendered
|
||||
[] renders nothing above the copyright
|
||||
*/
|
||||
aboveCopyright?: LinkDef[]
|
||||
|
||||
//commerce?: CommerceConfig
|
||||
//chatbot?: ChatbotConfig
|
||||
|
||||
// Do not render login / avatar widget
|
||||
noAuth?: boolean
|
||||
}
|
||||
|
||||
export {
|
||||
type SiteDef as default,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// see https://remix.run/docs/ru/main/route/meta
|
||||
// Brand defaults — overridden at runtime via brand.json (see brand.ts)
|
||||
|
||||
import { brand } from './brand'
|
||||
|
||||
export default [
|
||||
{
|
||||
title: brand.title,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content: brand.description,
|
||||
},
|
||||
{
|
||||
name: 'application-name',
|
||||
content: brand.name,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
content: brand.legalEntity,
|
||||
},
|
||||
{
|
||||
name: 'keywords',
|
||||
content:
|
||||
'Blockchain Bridge, Multi-Chain, EVM, Solana, Bitcoin, Cross-Chain, Interoperability, Cryptocurrency, Blockchain Technology',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { type PropsWithChildren } from 'react'
|
||||
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
type MetaFunction,
|
||||
} from '@remix-run/react'
|
||||
|
||||
import { useLoaderData, useRouteLoaderData, useRouteError } from '@remix-run/react'
|
||||
// 'type' must be outside the curlies!
|
||||
// https://github.com/remix-run/remix/issues/9916#issuecomment-2436405265
|
||||
import type { LinksFunction } from '@vercel/remix'
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
import { BreakpointIndicator } from '@hanzo/ui/primitives-common'
|
||||
import type { Network } from '@luxfi/core'
|
||||
|
||||
import luxGlobalCss from '@/style/lux-global-non-next.css?url'
|
||||
import cartAnimationCss from '@/style/cart-animation.css?url'
|
||||
import checkoutAnimationCss from '@/style/checkout-animation.css?url'
|
||||
|
||||
import type { AppSettings } from '@/domain/types'
|
||||
|
||||
import Contexts from '@/contexts'
|
||||
import Main from '@/components/main'
|
||||
import Header from '@/components/header'
|
||||
|
||||
|
||||
import backend from '@/domain/backend'
|
||||
|
||||
import fontAndIconLinks from './font-and-icon-links';
|
||||
import metadata from './metadata'
|
||||
import siteDef from './site-def'
|
||||
|
||||
// export const config = { runtime: 'edge' } // not supported by vercel
|
||||
|
||||
const bodyClasses = 'bg-background text-foreground flex flex-col min-h-full '
|
||||
|
||||
export const links: LinksFunction = () => ([
|
||||
...fontAndIconLinks,
|
||||
{ rel: "stylesheet", href: luxGlobalCss },
|
||||
{ rel: "stylesheet", href: cartAnimationCss },
|
||||
{ rel: "stylesheet", href: checkoutAnimationCss }
|
||||
])
|
||||
export const meta: MetaFunction = () => (metadata)
|
||||
|
||||
interface LoaderReturnType {
|
||||
appSettings: AppSettings,
|
||||
defaultFromNetwork?: Network,
|
||||
defaultToNetwork?: Network,
|
||||
}
|
||||
|
||||
export const loader = async (): Promise<LoaderReturnType> => {
|
||||
|
||||
const appSettings = await backend.getSettings()
|
||||
|
||||
return {
|
||||
appSettings: appSettings!, // otherwise an error is thrown
|
||||
defaultFromNetwork: undefined,
|
||||
defaultToNetwork: undefined
|
||||
}
|
||||
}
|
||||
|
||||
// cf: https://remix.run/docs/en/main/route/should-revalidate#never-reloading-the-root
|
||||
export const shouldRevalidate = () => (false)
|
||||
|
||||
|
||||
const Layout: React.FC<PropsWithChildren> = ({
|
||||
children
|
||||
}) => {
|
||||
|
||||
// https://github.com/remix-run/remix/pull/8958
|
||||
// https://github.com/remix-run/remix/issues/8951
|
||||
const data = useRouteLoaderData("root") as LoaderReturnType
|
||||
const error = useRouteError()
|
||||
|
||||
const {
|
||||
appSettings,
|
||||
defaultFromNetwork,
|
||||
defaultToNetwork
|
||||
} = data
|
||||
|
||||
return (
|
||||
<html
|
||||
lang='en'
|
||||
suppressHydrationWarning
|
||||
className='hanzo-ui-dark-theme'
|
||||
style={{backgroundColor: '#000'}}
|
||||
>
|
||||
<head>
|
||||
<meta charSet='utf-8' />
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className={bodyClasses} >
|
||||
{error ? (
|
||||
<h1 className='mx-auto text-2xl mt-10'>error</h1>
|
||||
) : (<>
|
||||
<Contexts
|
||||
appSettings={appSettings}
|
||||
defaultFromNetwork={defaultFromNetwork}
|
||||
defaultToNetwork={defaultToNetwork}
|
||||
>
|
||||
<Header siteDef={siteDef}/>
|
||||
<Main className='gap-4 '>
|
||||
{children}
|
||||
</Main>
|
||||
</Contexts>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
{process.env.NODE_ENV === 'development' && <BreakpointIndicator />}
|
||||
<Analytics />
|
||||
</>)}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
const Root: React.FC = () => (
|
||||
<Outlet />
|
||||
)
|
||||
|
||||
|
||||
export {
|
||||
Layout,
|
||||
Root as default
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import SwapCard from '@/components/swap-card'
|
||||
|
||||
const Index: React.FC = () => (
|
||||
<SwapCard className='w-[500px] h-auto pb-10 mt-8' />
|
||||
)
|
||||
|
||||
export default Index
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
import { Warpcast } from '@luxfi/menu-icons'
|
||||
|
||||
|
||||
import { SocialIcon } from '@/components/icons'
|
||||
|
||||
const SOC_ICON_SIZE = 18
|
||||
|
||||
export default [
|
||||
{
|
||||
title: 'Community',
|
||||
href: '',
|
||||
variant: 'linkFG',
|
||||
},
|
||||
{
|
||||
title: 'Lux Channel',
|
||||
href: 'https://warpcast.com/~/channel/lux',
|
||||
icon: <Warpcast width={SOC_ICON_SIZE} height={SOC_ICON_SIZE} />
|
||||
},
|
||||
{
|
||||
title: 'Lux Discussions',
|
||||
href: 'https://github.com/orgs/luxfi/discussions',
|
||||
icon: <SocialIcon network='github' size={SOC_ICON_SIZE} />
|
||||
},
|
||||
{
|
||||
title: 'Discord',
|
||||
href: 'https://discord.gg/sxaS7FFHwh',
|
||||
icon: <SocialIcon network='discord' size={SOC_ICON_SIZE} />
|
||||
},
|
||||
{
|
||||
title: 'Telegram',
|
||||
href: 'https://t.me/luxdefi',
|
||||
icon: <SocialIcon network='telegram' size={SOC_ICON_SIZE} />
|
||||
},
|
||||
{
|
||||
title: '@luxdefi',
|
||||
href: 'https://twitter.com/luxdefi',
|
||||
icon: <SocialIcon network='x' size={SOC_ICON_SIZE} />
|
||||
},
|
||||
{
|
||||
title: '@luxdefi',
|
||||
href: 'https://facebook.com/luxdefi',
|
||||
icon: <SocialIcon network='facebook' size={SOC_ICON_SIZE + 2} />
|
||||
},
|
||||
{
|
||||
title: '@luxdefi',
|
||||
href: 'https://www.instagram.com/luxdefi',
|
||||
icon: <SocialIcon network='instagram' size={SOC_ICON_SIZE + 2} />
|
||||
},
|
||||
{
|
||||
title: '@luxdefi',
|
||||
href: 'https://linkedin.com/company/luxdefi',
|
||||
icon: <SocialIcon network='linkedin' size={SOC_ICON_SIZE + 2} />
|
||||
},
|
||||
{
|
||||
title: '@luxdefi',
|
||||
href: 'https://www.youtube.com/@luxdefi',
|
||||
icon: <SocialIcon network='youtube' size={SOC_ICON_SIZE + 2} />
|
||||
},
|
||||
] satisfies LinkDef[]
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
export default [
|
||||
{
|
||||
title: 'Company',
|
||||
href: "https://lux.partners/",
|
||||
variant: 'linkFG',
|
||||
newTab: false,
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
href: 'https://lux.partners',
|
||||
newTab: false,
|
||||
},
|
||||
{
|
||||
title: 'Brand',
|
||||
href: 'https://drive.google.com/drive/folders/14OJtKLVakGY6883XO9yGbiHtlFxQUUm5?usp=share_link',
|
||||
},
|
||||
{
|
||||
title: 'Careers',
|
||||
href: 'https://docs.google.com/document/d/1SCt0Hg7EIs06TootKCA1am1xo4mcXoKF/edit#heading=h.30j0zll',
|
||||
newTab: true,
|
||||
},
|
||||
{
|
||||
title: 'Partnerships',
|
||||
href: 'https://apply.lux.partners/',
|
||||
newTab: false,
|
||||
},
|
||||
{
|
||||
title: 'Press',
|
||||
href: 'mailto:[email protected]?subject=%E2%96%BC%20Press',
|
||||
},
|
||||
{
|
||||
title: 'Help',
|
||||
href: 'mailto:[email protected]?subject=%E2%96%BC%20Help',
|
||||
},
|
||||
] satisfies LinkDef[]
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
export default [
|
||||
{
|
||||
title: 'Ecosystem',
|
||||
href: 'https://lux.link',
|
||||
variant: 'linkFG'
|
||||
},
|
||||
{
|
||||
title: 'Lux AI',
|
||||
href: 'https://lux.chat',
|
||||
},
|
||||
{
|
||||
title: 'Lux Credit',
|
||||
href: 'https://lux.credit',
|
||||
},
|
||||
{
|
||||
title: 'Lux Exchange',
|
||||
href: 'https://lux.exchange',
|
||||
},
|
||||
{
|
||||
title: 'Lux Finance',
|
||||
href: 'https://lux.finance',
|
||||
},
|
||||
{
|
||||
title: 'Lux Market',
|
||||
href: 'https://lux.market',
|
||||
},
|
||||
{
|
||||
title: 'Lux Shop',
|
||||
href: 'https://lux.shop',
|
||||
},
|
||||
{
|
||||
title: 'Lux Quest',
|
||||
href: 'https://lux.quest',
|
||||
},
|
||||
] satisfies LinkDef[]
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
import ecosystem from './ecosystem'
|
||||
import network from './network'
|
||||
import company from './company'
|
||||
import community from './community'
|
||||
import { legal, legalColumn } from './legal'
|
||||
|
||||
|
||||
const standard = [
|
||||
ecosystem,
|
||||
network,
|
||||
company,
|
||||
community,
|
||||
] satisfies LinkDef[][]
|
||||
|
||||
export {
|
||||
ecosystem,
|
||||
network,
|
||||
company,
|
||||
community,
|
||||
legal,
|
||||
legalColumn,
|
||||
standard
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
/**
|
||||
* Legal links — use env vars for white-label deployments.
|
||||
* Default to lps.lux.network for regulatory (shared across all brands).
|
||||
*/
|
||||
const termsUrl = process.env.NEXT_PUBLIC_TERMS_URL ?? '/terms'
|
||||
const privacyUrl = process.env.NEXT_PUBLIC_PRIVACY_URL ?? '/privacy'
|
||||
const regulatoryUrl = process.env.NEXT_PUBLIC_REGULATORY_URL ?? 'https://lps.lux.network/legal/regulatory-status'
|
||||
|
||||
const legal: LinkDef[] = [
|
||||
{
|
||||
title: 'Terms of Service',
|
||||
href: termsUrl,
|
||||
newTab: termsUrl.startsWith('http'),
|
||||
},
|
||||
{
|
||||
title: 'Privacy Policy',
|
||||
href: privacyUrl,
|
||||
newTab: privacyUrl.startsWith('http'),
|
||||
},
|
||||
{
|
||||
title: 'Regulatory Status',
|
||||
href: regulatoryUrl,
|
||||
newTab: true,
|
||||
},
|
||||
]
|
||||
|
||||
const title: LinkDef =
|
||||
{
|
||||
title: 'Legal',
|
||||
href: '',
|
||||
variant: 'linkFG',
|
||||
}
|
||||
|
||||
const legalColumn: LinkDef[] = [title, ...legal]
|
||||
|
||||
export {
|
||||
legal,
|
||||
legalColumn
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { LinkDef } from '@hanzo/ui/types'
|
||||
|
||||
export default [
|
||||
{
|
||||
title: 'Network',
|
||||
href: "https://lux.network/",
|
||||
variant: 'linkFG'
|
||||
},
|
||||
{
|
||||
title: 'Lux Bridge',
|
||||
href: "https://bridge.lux.network/",
|
||||
},
|
||||
{
|
||||
title: 'Lux Explorer',
|
||||
href: "https://explore.lux.network/",
|
||||
},
|
||||
{
|
||||
title: 'Lux Wallet',
|
||||
href: "https://wallet.lux.network/",
|
||||
},
|
||||
{
|
||||
title: 'Lux Safe',
|
||||
href: "https://safe.lux.network/",
|
||||
},
|
||||
{
|
||||
title: 'Lux Validator',
|
||||
href: "https://lux.network/validator",
|
||||
},
|
||||
{
|
||||
title: 'Lux Coin',
|
||||
href: "https://lux.network/coin",
|
||||
},
|
||||
{
|
||||
title: 'Governance',
|
||||
href: "https://lux.vote",
|
||||
},
|
||||
{
|
||||
title: 'Open Source',
|
||||
href: 'https://github.com/luxfi',
|
||||
},
|
||||
{
|
||||
title: 'Launch Subnet',
|
||||
href: 'https://docs.lux.network/build/subnet/hello-subnet',
|
||||
},
|
||||
] satisfies LinkDef[]
|
||||
@@ -0,0 +1,11 @@
|
||||
import type SiteDef from '@/hanzo-ui-types/site-def'
|
||||
import { standard } from './footer'
|
||||
import mainNav from './main-nav'
|
||||
|
||||
export default {
|
||||
//currentAs: 'https://lux.bridge',
|
||||
nav: {
|
||||
common: mainNav,
|
||||
},
|
||||
footer: standard,
|
||||
} satisfies SiteDef
|
||||
@@ -0,0 +1,447 @@
|
||||
import type LinkDefExtended from '@/hanzo-ui-types/link-def-extended'
|
||||
|
||||
import {
|
||||
AIChatAct,
|
||||
AIChat,
|
||||
Anodized,
|
||||
BlogAct,
|
||||
Blog,
|
||||
BridgeAct,
|
||||
Bridge,
|
||||
ChangeLogAct,
|
||||
ChangeLog,
|
||||
Chrome,
|
||||
CoinsAct,
|
||||
Coins,
|
||||
CompareCardsAct,
|
||||
CompareCards,
|
||||
CreditAct,
|
||||
Credit,
|
||||
CustomersAct,
|
||||
Customers,
|
||||
CustomerSupportAct,
|
||||
CustomerSupport,
|
||||
DeveloperDocsAct,
|
||||
DeveloperDocs,
|
||||
ExchangeAct,
|
||||
Exchange,
|
||||
ExplorerAct,
|
||||
Explorer,
|
||||
FAQsAct,
|
||||
FAQs,
|
||||
GoldCard,
|
||||
GuidesAct,
|
||||
Guides,
|
||||
GunMetal,
|
||||
IntegrationAct,
|
||||
Integration,
|
||||
Irradescent,
|
||||
LaunchSubnetAct,
|
||||
LaunchSubnet,
|
||||
LuxFinanceAct,
|
||||
LuxFinance,
|
||||
LuxPassAct,
|
||||
LuxPass,
|
||||
LuxQuestsAct,
|
||||
LuxQuests,
|
||||
LuxWalletAct,
|
||||
MarketAct,
|
||||
Market,
|
||||
MirroredTitanium,
|
||||
MoreBenefitsAct,
|
||||
MoreBenefits,
|
||||
OpenSourceAct,
|
||||
OpenSource,
|
||||
SafeAct,
|
||||
Safe,
|
||||
ShopAct,
|
||||
Shop,
|
||||
Sterling,
|
||||
TemplatesAct,
|
||||
Templates,
|
||||
ValidatorsAct,
|
||||
Validators,
|
||||
ViewAllCardsAct,
|
||||
ViewAllCards,
|
||||
Wallet,
|
||||
} from '@luxfi/menu-icons'
|
||||
|
||||
export default [
|
||||
{
|
||||
title: "AI",
|
||||
icon: "",
|
||||
href: "https://lux.chat",
|
||||
newTab: false,
|
||||
isAIMenu: true,
|
||||
},
|
||||
{
|
||||
title: "Cards",
|
||||
icon: "",
|
||||
details: "Lux Credit is a part of the Lux Network ecosystem that offers a crypto credit card solution. This service allows users to easily conduct transactions using their cryptocurrency holdings. The Lux Credit card aims to bridge the gap between traditional financial systems and the growing world of digital assets, providing a seamless way to spend cryptocurrencies in everyday transactions.",
|
||||
href: "https://lux.credit",
|
||||
newTab: false,
|
||||
childMenu: [
|
||||
{
|
||||
groupName:'Black Card',
|
||||
title: "Anodized Black Titanium",
|
||||
icon: <Anodized />,
|
||||
icon_act: '',
|
||||
href: "https://lux.credit/cards/black?sku=LXM-CR-B-ABT",
|
||||
newTab: false,
|
||||
contents:"" //insert string to here
|
||||
},
|
||||
{
|
||||
groupName:'Black Card',
|
||||
title: "Black Gunmetal",
|
||||
icon: <GunMetal />,
|
||||
icon_act: '',
|
||||
href: "https://lux.credit/cards/black?sku=LXM-CR-B-GM",
|
||||
newTab: false,
|
||||
contents:"" //insert string to here
|
||||
},
|
||||
{
|
||||
groupName:'Founder Card',
|
||||
title: "Cool Chrome",
|
||||
icon: <Chrome />,
|
||||
icon_act: '',
|
||||
href: "https://lux.credit/cards/founder?sku=LXM-CR-F-CC",
|
||||
newTab: false,
|
||||
contents:"" //insert string to here
|
||||
},
|
||||
{
|
||||
groupName:'Founder Card',
|
||||
title: "Iridescent Chrome",
|
||||
icon: <Irradescent />,
|
||||
icon_act: '',
|
||||
href: "https://lux.credit/cards/founder?sku=LXM-CR-F-IC",
|
||||
newTab: false,
|
||||
contents:"" //insert string to here
|
||||
},
|
||||
{
|
||||
groupName:'Explore',
|
||||
title: "Compare cards",
|
||||
icon: <CompareCards width={25} height={25} />,
|
||||
icon_act: <CompareCardsAct width={27} height={27} />,
|
||||
href: "https://lux.credit/compare",
|
||||
newTab: false,
|
||||
contents:"Evaluate Lux cards"
|
||||
},
|
||||
{
|
||||
groupName:'Explore',
|
||||
title: "View all cards",
|
||||
icon: <ViewAllCards width={25} height={25} />,
|
||||
icon_act: <ViewAllCardsAct width={25} height={25}/>,
|
||||
href: "https://lux.credit/cards",
|
||||
newTab: false,
|
||||
contents:"General overview"
|
||||
},
|
||||
{
|
||||
groupName:'Explore',
|
||||
title: "More benefits",
|
||||
icon: <MoreBenefits width={25} height={25} />,
|
||||
icon_act: <MoreBenefitsAct width={27} height={27} />,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"See what's truly unique"
|
||||
},
|
||||
{
|
||||
groupName:'Elite Card',
|
||||
title: "24k Gold",
|
||||
icon: <GoldCard />,
|
||||
icon_act: "",
|
||||
href: "https://lux.credit/cards/elite?sku=LXM-CR-E-24G",
|
||||
newTab: false,
|
||||
contents:"" //insert string to here
|
||||
},
|
||||
{
|
||||
groupName:'Elite Card',
|
||||
title: "Sterling Silver",
|
||||
icon: <Sterling />,
|
||||
icon_act: "",
|
||||
href: "https://lux.credit/cards/elite?sku=LXM-CR-E-SS",
|
||||
newTab: false,
|
||||
contents:"" //insert string to here
|
||||
},
|
||||
{
|
||||
groupName:'Sovereign Card',
|
||||
title: "Reflective Titanium",
|
||||
icon: <MirroredTitanium/>,
|
||||
icon_act: "",
|
||||
href: "https://lux.credit/cards/sovereign?sku=LXM-CR-S-RT",
|
||||
newTab: false,
|
||||
contents:"" //insert string to here
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Ecosystem",
|
||||
icon: "",
|
||||
details: "The Lux Ecosystem is a comprehensive suite of blockchain-based services and platforms designed to offer a wide range of functionalities, from decentralized finance (DeFi) to asset management and secure transactions.",
|
||||
href: "https://lux.link",
|
||||
newTab: false,
|
||||
childMenu: [
|
||||
{
|
||||
groupName:'Blockchain',
|
||||
title: "AI Chat",
|
||||
icon: <AIChat width={25} height={25} />,
|
||||
icon_act: <AIChatAct width={27} height={27}/>,
|
||||
href: "https://lux.chat",
|
||||
newTab: false,
|
||||
contents:"Ask anything, literally"
|
||||
},
|
||||
{
|
||||
groupName:'Blockchain',
|
||||
title: "Exchange",
|
||||
icon: <Exchange width={25} height={25} />,
|
||||
icon_act: <ExchangeAct width={27} height={27} />,
|
||||
href: "https://lux.exchange",
|
||||
newTab: false,
|
||||
contents:"Buy, sell, trade, swap..."
|
||||
},
|
||||
{
|
||||
groupName:'Blockchain',
|
||||
title: "Market",
|
||||
icon: <Market width={25} height={25} />,
|
||||
icon_act: <MarketAct width={27} height={27}/>,
|
||||
href: "https://lux.market",
|
||||
newTab: false,
|
||||
contents:"Digital collectibles & more"
|
||||
},
|
||||
{
|
||||
groupName:'Blockchain',
|
||||
title: "Shop",
|
||||
icon: <Shop width={25} height={25} />,
|
||||
icon_act: <ShopAct width={27} height={27}/>,
|
||||
href: "https://lux.market",
|
||||
newTab: false,
|
||||
contents:"Find any Lux product for sale"
|
||||
},
|
||||
{
|
||||
groupName:'Real World',
|
||||
title: "Credit",
|
||||
icon: <Credit width={25} height={25} />,
|
||||
icon_act: <CreditAct width={27} height={27}/>,
|
||||
href: "https://lux.credit",
|
||||
newTab: false,
|
||||
contents:"Risk free loans"
|
||||
},
|
||||
{
|
||||
groupName:'Real World',
|
||||
title: "Lux Finance",
|
||||
icon: <LuxFinance width={25} height={25} />,
|
||||
icon_act: <LuxFinanceAct width={27} height={27}/>,
|
||||
href: "https://lux.finance",
|
||||
newTab: false,
|
||||
contents:"Financial arbitrage"
|
||||
},
|
||||
{
|
||||
groupName:'More',
|
||||
title: "Lux Quest",
|
||||
icon: <LuxQuests width={25} height={25} />,
|
||||
icon_act: <LuxQuestsAct width={27} height={27}/>,
|
||||
href: "https://lux.quest",
|
||||
newTab: false,
|
||||
contents:"Go above and beyond"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Network",
|
||||
icon: "",
|
||||
details: "Lux Network is a scalable, quantum-safe blockchain designed to offer high throughput and low latency. It is part of a broader ecosystem that includes various platforms and services aimed at integrating traditional finance (TradFi) with decentralized finance (DeFi), providing secure and efficient solutions for asset management, trading, and more.",
|
||||
href: "https://lux.network/",
|
||||
newTab: false,
|
||||
childMenu: [
|
||||
{
|
||||
groupName:'Defi Tools',
|
||||
title: "Bridge",
|
||||
icon: <Bridge width={25} height={25} />,
|
||||
icon_act: <BridgeAct width={27} height={27}/>,
|
||||
href: "https://bridge.lux.network/",
|
||||
newTab: false,
|
||||
contents:"Teleport assets"
|
||||
},
|
||||
{
|
||||
groupName:'Defi Tools',
|
||||
title: "Explorer",
|
||||
icon: <Explorer width={25} height={25} />,
|
||||
icon_act: <ExplorerAct width={27} height={27}/>,
|
||||
href: "https://explorer.lux.network/",
|
||||
newTab: false,
|
||||
contents:"All transactions"
|
||||
},
|
||||
{
|
||||
groupName:'Defi Tools',
|
||||
title: "Launch Subnet",
|
||||
icon: <LaunchSubnet width={25} height={25} />,
|
||||
icon_act: <LaunchSubnetAct width={27} height={27}/>,
|
||||
href: "https://docs.lux.network/build/subnet/hello-subnet",
|
||||
newTab: false,
|
||||
contents:"Easy, free, and superior"
|
||||
},
|
||||
{
|
||||
groupName:'Earn',
|
||||
title: "Coins",
|
||||
icon: <Coins width={25} height={25} />,
|
||||
icon_act: <CoinsAct width={27} height={27}/>,
|
||||
href: "https://lux.network/coin",
|
||||
newTab: false,
|
||||
contents:"Be part of the ecosystem"
|
||||
},
|
||||
{
|
||||
groupName:'Earn',
|
||||
title: "Safe",
|
||||
icon: <Safe width={25} height={25} />,
|
||||
icon_act: <SafeAct width={27} height={27} />,
|
||||
href: "https://safe.lux.network",
|
||||
newTab: false,
|
||||
contents:"Multi-signature wallet"
|
||||
},
|
||||
{
|
||||
groupName:'Earn',
|
||||
title: "Lux Wallet",
|
||||
icon: <Wallet width={25} height={25} />,
|
||||
icon_act: <LuxWalletAct width={27} height={27}/>,
|
||||
href: "https://wallet.lux.network",
|
||||
newTab: false,
|
||||
contents:"Safe and quantum secure"
|
||||
},
|
||||
{
|
||||
groupName:'Earn',
|
||||
title: "Validators",
|
||||
icon: <Validators width={25} height={25} />,
|
||||
icon_act: <ValidatorsAct width={27} height={27}/>,
|
||||
href: "https://lux.market/validator",
|
||||
newTab: false,
|
||||
contents:"Own a part of the network"
|
||||
},
|
||||
{
|
||||
groupName:'Get Access',
|
||||
title: "Developer docs",
|
||||
icon: <DeveloperDocs width={25} height={25} />,
|
||||
icon_act: <DeveloperDocsAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Software explained"
|
||||
},
|
||||
{
|
||||
groupName:'Get Access',
|
||||
title: "Open Source",
|
||||
icon: <OpenSource width={25} height={25} />,
|
||||
icon_act: <OpenSourceAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Accessible for everyone"
|
||||
},
|
||||
{
|
||||
groupName:'Get Access',
|
||||
title: "Lux Pass",
|
||||
icon: <LuxPass width={25} height={25} />,
|
||||
icon_act: <LuxPassAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"All access pass to network"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Resources",
|
||||
icon: '',
|
||||
href: "",
|
||||
newTab: false,
|
||||
details: "",
|
||||
childMenu: [
|
||||
{
|
||||
groupName:'Tools',
|
||||
title: "Resource Center",
|
||||
icon: <AIChat width={25} height={25} />,
|
||||
icon_act: <AIChatAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Dynamic solutions"
|
||||
},
|
||||
{
|
||||
groupName:'Tools',
|
||||
title: "Integrations",
|
||||
icon: <Integration width={25} height={25} />,
|
||||
icon_act: <IntegrationAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Simplified onboarding"
|
||||
},
|
||||
{
|
||||
groupName:'Tools',
|
||||
title: "Templates",
|
||||
icon: <Templates width={25} height={25} />,
|
||||
icon_act: <TemplatesAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Speedy app development"
|
||||
},
|
||||
{
|
||||
groupName:'Tools',
|
||||
title: "Guides",
|
||||
icon: <Guides width={25} height={25} />,
|
||||
icon_act: <GuidesAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Find help quickly"
|
||||
},
|
||||
{
|
||||
groupName:'Company',
|
||||
title: "Customers",
|
||||
icon: <Customers width={25} height={25} />,
|
||||
icon_act: <CustomersAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Trusted by the best teams"
|
||||
},
|
||||
{
|
||||
groupName:'Company',
|
||||
title: "Blog",
|
||||
icon: <Blog width={25} height={25} />,
|
||||
icon_act: <BlogAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Latest posts and changes"
|
||||
},
|
||||
{
|
||||
groupName:'Company',
|
||||
title: "Changelog",
|
||||
icon: <ChangeLog width={25} height={25} />,
|
||||
icon_act: <ChangeLogAct width={27} height={27} />,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Manage deployments"
|
||||
},
|
||||
{
|
||||
groupName:'More',
|
||||
title: "Developer docs",
|
||||
icon: <DeveloperDocs width={25} height={25} />,
|
||||
icon_act: <DeveloperDocsAct width={25} height={25}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Software explained"
|
||||
},
|
||||
{
|
||||
groupName:'More',
|
||||
title: "Customer Support",
|
||||
icon: <CustomerSupport width={25} height={25} />,
|
||||
icon_act: <CustomerSupportAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Dedicated help, 24/7"
|
||||
},
|
||||
{
|
||||
groupName:'More',
|
||||
title: "FAQs",
|
||||
icon: <FAQs width={25} height={25} />,
|
||||
icon_act: <FAQsAct width={27} height={27}/>,
|
||||
href: "#",
|
||||
newTab: false,
|
||||
contents:"Common queries"
|
||||
}
|
||||
]
|
||||
},
|
||||
] satisfies LinkDefExtended[]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@keyframes wiggle-larger {
|
||||
0% { transform: rotate(0deg); }
|
||||
18% { transform: rotate(10deg) scale(0.9); }
|
||||
32% { transform: rotate(0deg) scale(1.05);; }
|
||||
50% { transform: rotate(-10deg) scale(1.05); }
|
||||
68% { transform: rotate(0deg) scale(1.1); }
|
||||
84% { transform: rotate(10deg) scale(1); }
|
||||
100% { transform: rotate(0deg) scale(1);}
|
||||
}
|
||||
|
||||
@keyframes wiggle-smaller {
|
||||
0% { transform: rotate(0deg); }
|
||||
18% { transform: rotate(10deg) scale(1.05); }
|
||||
32% { transform: rotate(0deg) scale(0.9);; }
|
||||
50% { transform: rotate(-10deg) scale(0.9); }
|
||||
68% { transform: rotate(0deg) scale(0.85); }
|
||||
84% { transform: rotate(10deg) scale(1); }
|
||||
100% { transform: rotate(0deg) scale(1);}
|
||||
}
|
||||
|
||||
.wiggle-larger-animation,
|
||||
.item-added-to-cart-animation {
|
||||
animation: wiggle-larger 350ms;
|
||||
}
|
||||
|
||||
.wiggle-smaller-animation,
|
||||
.item-removed-from-cart-animation {
|
||||
animation: wiggle-smaller 300ms;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@keyframes checkout-widget-fade-in {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
100% { transform: scale(1); opacity: 1;}
|
||||
}
|
||||
|
||||
.checkout-widget-appears {
|
||||
animation-name: checkout-widget-fade-in;
|
||||
animation-duration: 200ms;
|
||||
}
|
||||
|
||||
.checkout-widget-disappears {
|
||||
animation-name: checkout-widget-fade-in;
|
||||
animation-duration: 200ms;
|
||||
animation-direction: reverse;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.checkout-widget-appears-after-buy-drawer-closes {
|
||||
animation-fill-mode: backwards;
|
||||
/* Drawer is hardcoded to open in 0.5s. We can start slightly sooner. */
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
@tailwind base;
|
||||
/* see https://stackoverflow.com/questions/69746121/using-nextjs-how-can-you-import-in-css-using-tailwind-css */
|
||||
|
||||
@layer base {
|
||||
|
||||
:root, .hanzo-ui-light-theme {
|
||||
--hz-ui-fg-0: hsl(0 0% 0%);
|
||||
--hz-ui-fg-body: hsl(0 0% 10%);
|
||||
--hz-ui-fg-1: hsl(0 0% 20%);
|
||||
--hz-ui-fg-2: hsl(0 0% 35%);
|
||||
--hz-ui-fg-3: hsl(0 0% 50%);
|
||||
--hz-ui-fg-4: hsl(0 0% 70%);
|
||||
--hz-ui-fg-5: hsl(0 0% 90%);
|
||||
|
||||
--hz-ui-bg-0: hsl(0 0% 100%);
|
||||
--hz-ui-bg-1: hsl(0 0% 90%);
|
||||
--hz-ui-bg-2: hsl(0 0% 75%);
|
||||
--hz-ui-bg-3: hsl(0 0% 55%);
|
||||
|
||||
--hz-ui-bg-overlay: hsla(0 0% 100% 0.60);
|
||||
|
||||
--hz-ui-bg-inverted: var(--hz-ui-fg-0);
|
||||
--hz-ui-bg-inverted-hover: hsla(0, 0%, 0%, 0.85);
|
||||
|
||||
--hz-ui-fg-inverted: var(--hz-ui-bg-0);
|
||||
|
||||
--hz-ui-secondary-0: hsl(262, 7%, 82%);
|
||||
--hz-ui-secondary-1: hsl(258, 7%, 72%);
|
||||
--hz-ui-secondary-2: hsl(254, 7%, 62%);
|
||||
--hz-ui-secondary-3: hsl(250, 5%, 53%);
|
||||
|
||||
--hz-ui-primary: var(--hz-ui-bg-inverted);
|
||||
--hz-ui-primary-hover: var(--hz-ui-bg-inverted-hover);
|
||||
--hz-ui-primary-fg: var(--hz-ui-fg-inverted);
|
||||
|
||||
--hz-ui-secondary: var(--hz-ui-secondary-0);
|
||||
--hz-ui-secondary-hover: var(--hz-ui-secondary-2);
|
||||
--hz-ui-secondary-fg: var(--hz-ui-bg-0);
|
||||
|
||||
--hz-ui-nav: var(--hz-ui-fg-4);
|
||||
--hz-ui-nav-current: var(--hz-ui-fg-0);
|
||||
--hz-ui-nav-hover: var(--hz-ui-fg-0);
|
||||
|
||||
--hz-ui-destructive: hsl(0 62.8% 45%);
|
||||
--hz-ui-destructive-hover: hsl(0 62.8% 25%);
|
||||
--hz-ui-destructive-fg: hsl(0 0% 100%);
|
||||
|
||||
--hz-ui-ring: var(--hz-ui-fg-1);
|
||||
|
||||
--hz-ui-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.hanzo-ui-dark-theme {
|
||||
|
||||
--hz-ui-fg-0: hsl(0 0% 100%);
|
||||
--hz-ui-fg-body: hsl(0, 0%, 97%);
|
||||
--hz-ui-fg-1: hsl(0 0% 85%);
|
||||
--hz-ui-fg-2: hsl(0 0% 70%);
|
||||
--hz-ui-fg-3: hsl(0 0% 55%);
|
||||
--hz-ui-fg-4: hsl(0 0% 40%);
|
||||
--hz-ui-fg-5: hsl(0 0% 25%);
|
||||
|
||||
--hz-ui-bg-inverted: var(--hz-ui-fg-0);
|
||||
--hz-ui-bg-inverted-hover: hsla(0, 0%, 100%, 0.85);
|
||||
|
||||
--hz-ui-bg-3: hsl(0 0% 25%);
|
||||
--hz-ui-bg-2: hsl(0 0% 18%);
|
||||
--hz-ui-bg-1: hsl(0 0% 12%);
|
||||
--hz-ui-bg-0: hsl(0 0% 0%);
|
||||
|
||||
--hz-ui-bg-overlay: hsla(0 0% 0% 0.60);
|
||||
|
||||
--hz-ui-fg-inverted: var(--hz-ui-bg-0);
|
||||
|
||||
--hz-ui-secondary-0: hsl(250, 5%, 53%);
|
||||
--hz-ui-secondary-1: hsl(254, 7%, 62%);
|
||||
--hz-ui-secondary-2: hsl(258, 7%, 72%);
|
||||
--hz-ui-secondary-3: hsl(262, 7%, 82%);
|
||||
|
||||
--hz-ui-secondary-fg: var(--hz-ui-fg-0);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@tailwind base;
|
||||
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: "DrukWide";
|
||||
src: url("/fonts/Druk-Wide-Medium.ttf");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "DrukWide";
|
||||
src: url("/fonts/Druk-Wide-Bold.ttf");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "inter";
|
||||
src: url("/fonts/InterVariable.woff2") format("woff2"),
|
||||
url("/fonts/InterVariable.ttf") format("ttf");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "inter";
|
||||
src: url("/fonts/InterVariable-Italic.woff2") format("woff2"),
|
||||
url("/fonts/InterVariable-Italic.ttf") format("ttf");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/* Note, preflight is applied first:
|
||||
https://unpkg.com/[email protected]/src/css/preflight.css
|
||||
*/
|
||||
@import "tailwindcss/base";
|
||||
|
||||
/* see https://stackoverflow.com/questions/69746121/using-nextjs-how-can-you-import-in-css-using-tailwind-css */
|
||||
/* @import "@hanzo/ui/style/hanzo-common"; */
|
||||
@import "lux-colors";
|
||||
@import "lux-fonts";
|
||||
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
|
||||
@layer base {
|
||||
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
using this approach:
|
||||
https://stackoverflow.com/a/76066443/11645689
|
||||
see src/app/(home)/scroll-snap
|
||||
*/
|
||||
|
||||
html {
|
||||
scroll-snap-type: y mandatory;
|
||||
height: 100vh;
|
||||
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
}
|
||||
|
||||
div.nextjs-toast-errors-parent[data-nextjs-toast="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.font-heading {
|
||||
@apply uppercase;
|
||||
}
|
||||
|
||||
/* Specific style fixes for react-square-web-payments-sdk -
|
||||
* reduce gap between card input and pay button
|
||||
*/
|
||||
#rswps-card-container {
|
||||
min-height: unset !important;
|
||||
}
|
||||
|
||||
.sq-card-wrapper .sq-card-message {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.element-container:hover .icon-container-nor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon-container-hov {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.element-container:hover .icon-container-hov {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.element-container:hover .text-container {
|
||||
background: transparent;
|
||||
}
|
||||
.overflow-scroll::-webkit-scrollbar{
|
||||
display:none;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { TwFontDesc } from '@hanzo/ui/tailwind'
|
||||
|
||||
import luxTwFonts from './lux-tw-fonts'
|
||||
|
||||
export const fontFamily = (ignoreTheme: any): {
|
||||
[key: string]: string[]
|
||||
} => {
|
||||
|
||||
let result: any = {}
|
||||
luxTwFonts.forEach((font: TwFontDesc) => {
|
||||
result[font.twName] = font.fontFamily
|
||||
// eg: heading: ['var(--font-druk-text-wide)']
|
||||
})
|
||||
|
||||
return result as {
|
||||
[key: string]: string[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
export { default as preset } from './tailwind.config.lux-preset'
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { TwFontDesc } from '@hanzo/ui/tailwind'
|
||||
|
||||
|
||||
/* NOTE: /next/load-and-return....ts depends on this file! */
|
||||
|
||||
export default [
|
||||
{
|
||||
fontFamily: ['var(--font-inter)'], // do not provide fall-backs due to next bug
|
||||
cssVar: '--font-inter',
|
||||
twName: 'sans'
|
||||
},
|
||||
{
|
||||
fontFamily: ['var(--font-druk-text-wide)'], // do not provide fall-backs due to next bug
|
||||
cssVar: '--font-druk-text-wide',
|
||||
twName: 'nav'
|
||||
},
|
||||
{
|
||||
fontFamily: ['var(--font-druk-wide)'], // do not provide fall-backs due to next bug
|
||||
cssVar: '--font-druk-wide',
|
||||
twName: 'heading'
|
||||
},
|
||||
{
|
||||
twName: 'serif',
|
||||
fontFamily: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times']
|
||||
},
|
||||
{
|
||||
twName: 'mono',
|
||||
fontFamily: [
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace',
|
||||
]
|
||||
}
|
||||
|
||||
] as TwFontDesc[]
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
import { preset } from '@hanzo/ui/tailwind'
|
||||
import { fontFamily } from './fontFamily.tailwind.lux'
|
||||
|
||||
export default {
|
||||
presets: [preset],
|
||||
content: [],
|
||||
theme: { fontFamily }
|
||||
} satisfies Config
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@luxbridge/app-v3",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "remix vite:build",
|
||||
"dev": "remix vite:dev",
|
||||
"start": "remix-serve ./build/server/index.js",
|
||||
"tc": "tsc",
|
||||
"typecheck": "tsc",
|
||||
"clean": "rm -rf ./node_modules && rm -rf ./vercel",
|
||||
"clean:all": "pnpm -w clean:all && rm -rf ./vercel"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@hanzo/ui": "catalog:",
|
||||
"@luxfi/core": "catalog:",
|
||||
"@luxfi/menu-icons": "1.0.2",
|
||||
"@remix-run/node": "^2.15.1",
|
||||
"@remix-run/react": "^2.15.1",
|
||||
"@remix-run/route-config": "^2.15.2",
|
||||
"@remix-run/serve": "^2.15.1",
|
||||
"@vercel/analytics": "^1.4.1",
|
||||
"@vercel/remix": "^2.15.1",
|
||||
"axios": "^1.7.9",
|
||||
"isbot": "^4",
|
||||
"lucide-react": "catalog:",
|
||||
"mobx": "^6.12.3",
|
||||
"mobx-react-lite": "^4.0.7",
|
||||
"mobx-utils": "^6.1.0",
|
||||
"motion": "^11.15.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-social-icons": "^6.18.0",
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@remix-run/dev": "^2.15.1",
|
||||
"@remix-run/server-runtime": "^2.15.1",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "5.4.11",
|
||||
"vite-tsconfig-paths": "5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 426 B |
Binary file not shown.
|
After Width: | Height: | Size: 831 B |
@@ -0,0 +1,39 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
import { preset } from './app/tailwind'
|
||||
|
||||
const fontFamily = {
|
||||
sans: 'inter',
|
||||
heading: 'DrukWide',
|
||||
nav: 'DrukWide',
|
||||
serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times'],
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace',
|
||||
]
|
||||
}
|
||||
|
||||
export default {
|
||||
presets: [preset],
|
||||
content: {
|
||||
files: [
|
||||
'app/**/*.tsx',
|
||||
'./node_modules/@hanzo/**/*.{ts,tsx}'
|
||||
]
|
||||
},
|
||||
theme: {
|
||||
fontFamily,
|
||||
extend: {
|
||||
colors: {
|
||||
white: "#ffffff",
|
||||
black: "#000000",
|
||||
gray: "#e5e7eb",
|
||||
}
|
||||
},
|
||||
}
|
||||
} satisfies Config
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
// All settings are as per remix rec's
|
||||
// https://remix.run/docs/hi/main/guides/typescript
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ES2022"
|
||||
],
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [ "./app/*" ]
|
||||
},
|
||||
"types": [
|
||||
"@vercel/remix",
|
||||
"vite/client",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/.server/**/*.ts",
|
||||
"**/.server/**/*.tsx",
|
||||
"**/.client/**/*.ts",
|
||||
"**/.client/**/*.tsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { vitePlugin as remix } from '@remix-run/dev'
|
||||
import { vercelPreset } from '@vercel/remix/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
'process.env': {}
|
||||
},
|
||||
plugins: [
|
||||
remix({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
v3_singleFetch: true, // cf: https://github.com/remix-run/react-router/pull/12441
|
||||
v3_lazyRouteDiscovery: false // silences warning
|
||||
},
|
||||
presets: [vercelPreset()],
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
viteCommonjs(),
|
||||
],
|
||||
optimizeDeps: {
|
||||
include: ['react-dom'],
|
||||
// Not excluding these seem to:
|
||||
// a) always force a refresh after initial load: https://github.com/vitejs/vite/discussions/14801)
|
||||
// b) optimize an old version!
|
||||
exclude: [
|
||||
'@hanzo/ui/primitives-common',
|
||||
'@hanzo/ui/util',
|
||||
]
|
||||
},
|
||||
// https://github.com/remix-run/remix/issues/10156#issuecomment-2440234744
|
||||
server: {
|
||||
warmup: {
|
||||
clientFiles: ['app/**/*.tsx'],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: true, // Enable source maps in production build
|
||||
rollupOptions: {
|
||||
output: {
|
||||
sourcemapExcludeSources: false, // Include source content in the map
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
+32
-2
@@ -5,7 +5,34 @@
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public",
|
||||
"scope": "@luxfi"
|
||||
"scope": "@luxfi",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./Bridge": {
|
||||
"types": "./dist/Bridge.d.ts",
|
||||
"import": "./dist/Bridge.js",
|
||||
"default": "./dist/Bridge.js"
|
||||
},
|
||||
"./config": {
|
||||
"types": "./dist/config.d.ts",
|
||||
"import": "./dist/config.js",
|
||||
"default": "./dist/config.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.ts",
|
||||
"import": "./dist/types.js",
|
||||
"default": "./dist/types.js"
|
||||
},
|
||||
"./styles.css": "./dist/app/styles/theme.css",
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
},
|
||||
"author": "Lux Industries, Inc.",
|
||||
"license": "MIT",
|
||||
@@ -33,17 +60,20 @@
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"public",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"build": "pnpm run clean && tsc -p tsconfig.build.json && cp -r src/app/styles dist/app/styles",
|
||||
"tc": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"lat": "npm show @luxfi/bridge version",
|
||||
"prepublishOnly": "pnpm run build",
|
||||
"pub": "npm publish"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -30,6 +30,8 @@ import * as gui from '@hanzo/gui'
|
||||
import { applyBrandMetadata, setConfig } from './config'
|
||||
import type { BridgeConfig } from './types'
|
||||
import { BridgeApp } from './app/BridgeApp'
|
||||
import { setRpcClient } from './app/lib/bridge-api'
|
||||
import { BridgeRPCClient } from './app/lib/bridge-rpc'
|
||||
|
||||
const GuiProvider: FC<{
|
||||
children: ReactNode
|
||||
@@ -54,6 +56,40 @@ export const Bridge: FC<BridgeProps> = ({ config }) => {
|
||||
applyBrandMetadata(config.brand)
|
||||
}, [config.brand])
|
||||
|
||||
// Install (or tear down) the direct-RPC client based on cfg.rpc. When
|
||||
// bchainUrl is set, the bridge-api dispatcher routes quote/submit/status
|
||||
// through BridgeVM JSON-RPC and falls back to REST on failure (default
|
||||
// policy). When bchainUrl is absent the SDK runs in pure-REST mode.
|
||||
useEffect(() => {
|
||||
const bchain = config.rpc?.bchainUrl
|
||||
if (!bchain) {
|
||||
setRpcClient(null)
|
||||
return
|
||||
}
|
||||
const init = {
|
||||
bridgeRpcUrl: bchain,
|
||||
...(config.rpc?.tchainUrl
|
||||
? { thresholdRpcUrl: config.rpc.tchainUrl }
|
||||
: config.mpc?.publicUrl
|
||||
? { thresholdRpcUrl: config.mpc.publicUrl }
|
||||
: {}),
|
||||
...(config.rpc?.timeoutMs !== undefined
|
||||
? { timeoutMs: config.rpc.timeoutMs }
|
||||
: {}),
|
||||
}
|
||||
const client = new BridgeRPCClient(init)
|
||||
setRpcClient(client, config.rpc?.fallback ?? 'rest')
|
||||
return () => {
|
||||
setRpcClient(null)
|
||||
}
|
||||
}, [
|
||||
config.rpc?.bchainUrl,
|
||||
config.rpc?.tchainUrl,
|
||||
config.rpc?.fallback,
|
||||
config.rpc?.timeoutMs,
|
||||
config.mpc?.publicUrl,
|
||||
])
|
||||
|
||||
// `GuiProvider` is required for @hanzo/gui's `Button` / `Input` primitives
|
||||
// (used in SwapForm, WalletConnect, AssetInput) to find their theme. The
|
||||
// SDK owns this wrapper so consumers don't have to know it exists — the
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Shared React-Query / Wagmi wrapper for renderHook tests.
|
||||
//
|
||||
// useNetworks (consumed transitively by useSwap and useTransfers) calls
|
||||
// useQuery, which requires a QueryClientProvider in the render tree.
|
||||
// Production wiring lives in BridgeApp.tsx; tests need their own.
|
||||
//
|
||||
// retry: false + gcTime: 0 so failed fetches don't spend the test budget
|
||||
// retrying, and the cache is dropped between tests for isolation.
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
export function createTestQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
staleTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fresh wrapper per test. Important: the returned component is a
|
||||
* new closure every call, so React-Query won't share cache across tests
|
||||
* unless the caller deliberately reuses one instance.
|
||||
*/
|
||||
export function makeTestWrapper(): FC<{ children: ReactNode }> {
|
||||
const queryClient = createTestQueryClient()
|
||||
const Wrapper: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
return Wrapper
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user